diff --git a/backend/app/admin_ui.py b/backend/app/admin_ui.py index 9587664..bc6d9d9 100644 --- a/backend/app/admin_ui.py +++ b/backend/app/admin_ui.py @@ -18,6 +18,7 @@ from .repositories import ( create_feed, create_source, get_article_by_id, + get_feed_by_id, list_articles, list_feeds, list_runs, @@ -80,6 +81,57 @@ def _parse_meta_json(raw: str | None) -> dict: return {} +def _legal_checklist(article: dict, feed: dict | None) -> list[dict[str, str]]: + meta = article.get("meta", {}) + extraction = meta.get("extraction") if isinstance(meta.get("extraction"), dict) else {} + attribution = meta.get("attribution") if isinstance(meta.get("attribution"), dict) else {} + + checks: list[dict[str, str]] = [] + checks.append( + { + "label": "Original-Link vorhanden", + "status": "ok" if article.get("source_url") else "missing", + "value": article.get("source_url") or "-", + } + ) + checks.append( + { + "label": "Autor vorhanden", + "status": "ok" if article.get("author") else "missing", + "value": article.get("author") or "-", + } + ) + checks.append( + { + "label": "Bilder extrahiert", + "status": "ok" if extraction.get("images") else "missing", + "value": str(len(extraction.get("images", []))) if isinstance(extraction.get("images"), list) else "0", + } + ) + checks.append( + { + "label": "Pressekontakt", + "status": "ok" if extraction.get("press_contact") else "missing", + "value": extraction.get("press_contact") or "-", + } + ) + checks.append( + { + "label": "Lizenz/Terms", + "status": "ok" if attribution.get("source_license_name") and attribution.get("source_terms_url") else "missing", + "value": f"{attribution.get('source_license_name') or '-'} | {attribution.get('source_terms_url') or '-'}", + } + ) + checks.append( + { + "label": "Risiko-Status Quelle", + "status": "ok" if (feed and feed.get("source_risk_level") == "green") else "missing", + "value": feed.get("source_risk_level") if feed else "-", + } + ) + return checks + + @router.get("/admin", response_class=HTMLResponse) def admin_index(request: Request): user = _admin_user(request) @@ -167,6 +219,38 @@ def admin_dashboard(request: Request): ) +@router.get("/admin/articles/{article_id}", response_class=HTMLResponse) +def admin_article_detail(request: Request, article_id: int): + user = _admin_user(request) + if not user: + return RedirectResponse(url="/admin/login", status_code=303) + + article = get_article_by_id(article_id) + if not article: + return _dashboard_redirect(msg=f"Artikel #{article_id} nicht gefunden", msg_type="error") + + meta = _parse_meta_json(article.get("meta_json")) + article["meta"] = meta + extraction = meta.get("extraction") if isinstance(meta.get("extraction"), dict) else {} + article["extraction"] = extraction + feed = get_feed_by_id(int(article["feed_id"])) if article.get("feed_id") else None + checklist = _legal_checklist(article, feed) + + return templates.TemplateResponse( + request, + "admin_article_detail.html", + { + "request": request, + "title": f"Artikel #{article_id}", + "user": user, + "article": article, + "feed": feed, + "checklist": checklist, + "allowed_transitions": ALLOWED_TRANSITIONS.get(article.get("status"), ()), + }, + ) + + @router.post("/admin/sources/create") def admin_create_source( request: Request, diff --git a/backend/templates/admin_article_detail.html b/backend/templates/admin_article_detail.html new file mode 100644 index 0000000..098c273 --- /dev/null +++ b/backend/templates/admin_article_detail.html @@ -0,0 +1,100 @@ + + + + + + {{ title }} + + + +
+
+

Artikel-Detail #{{ article.id }}

+

Angemeldet als {{ user }}

+
+
+ Zurück +
+ +
+
+
+ +
+
+

{{ article.title }}

+

Status: {{ article.status }}

+

Autor: {{ article.author or "-" }}

+

Feed: {{ feed.name if feed else "-" }}

+

Quelle: {{ article.source_url }}

+ {% if article.canonical_url %} +

Canonical: {{ article.canonical_url }}

+ {% endif %} + {% if article.summary %} +

Summary: {{ article.summary }}

+ {% endif %} +
+ +
+

Rechts-Checkliste

+ + + + + + {% for c in checklist %} + + + + + + {% endfor %} + +
KriteriumStatusWert
{{ c.label }} + {% if c.status == "ok" %} + OK + {% else %} + Fehlt + {% endif %} + {{ c.value }}
+
+ +
+

Extrahierte Daten

+

Bilder: {{ article.extraction.images|length if article.extraction.images else 0 }}

+ {% if article.extraction.images %} + + {% endif %} + {% if article.extraction.press_contact %} +

Pressekontakt

+
{{ article.extraction.press_contact }}
+ {% endif %} + {% if article.extraction.extraction_error %} +

Extraktionsfehler: {{ article.extraction.extraction_error }}

+ {% endif %} +
+ +
+

Volltext

+
{{ article.content_raw or "-" }}
+
+ +
+

Status ändern

+
+ + + +
+
+
+ + diff --git a/backend/templates/admin_dashboard.html b/backend/templates/admin_dashboard.html index 36b30a7..d416d76 100644 --- a/backend/templates/admin_dashboard.html +++ b/backend/templates/admin_dashboard.html @@ -144,6 +144,7 @@ {{ a.title }}
Autor: {{ a.author or "-" }}
Original öffnen +
Details anzeigen {% if a.canonical_url and a.canonical_url != a.source_url %}
Canonical öffnen {% endif %} diff --git a/backend/tests/test_admin_ui.py b/backend/tests/test_admin_ui.py index c6b2188..a65cbfc 100644 --- a/backend/tests/test_admin_ui.py +++ b/backend/tests/test_admin_ui.py @@ -8,6 +8,7 @@ from fastapi.testclient import TestClient from backend.app import config as config_module from backend.app.db import init_db from backend.app.main import app +from backend.app.repositories import ArticleUpsert, FeedCreate, SourceCreate, create_feed, create_source, upsert_article class TestAdminUi(unittest.TestCase): @@ -60,6 +61,56 @@ class TestAdminUi(unittest.TestCase): self.assertEqual(res.status_code, 303) self.assertTrue(res.headers.get("location", "").startswith("/admin/dashboard")) + def test_article_detail_page_renders(self) -> None: + source_id = create_source( + SourceCreate( + name="Test Source", + base_url="https://example.org", + terms_url="https://example.org/terms", + license_name="cc-by", + risk_level="green", + is_enabled=True, + notes=None, + last_reviewed_at="2026-02-18T00:00:00Z", + ) + ) + feed_id = create_feed( + FeedCreate( + name="Test Feed", + url="https://example.org/feed.xml", + source_id=source_id, + is_enabled=True, + ) + ) + article_id = upsert_article( + ArticleUpsert( + feed_id=feed_id, + source_article_id="id-1", + source_hash="hash-1", + title="Titel A", + source_url="https://example.org/a", + canonical_url="https://example.org/a", + published_at=None, + author="Autor A", + summary="Summary A", + content_raw="Volltext A", + content_rewritten=None, + word_count=2, + status="new", + meta_json='{"extraction":{"images":["https://example.org/img.jpg"],"press_contact":"Kontakt"}}', + ) + ) + + self.client.post( + "/admin/login", + data={"username": "admin", "password": "secret"}, + follow_redirects=True, + ) + res = self.client.get(f"/admin/articles/{article_id}", follow_redirects=True) + self.assertEqual(res.status_code, 200) + self.assertIn("Artikel-Detail", res.text) + self.assertIn("Rechts-Checkliste", res.text) + if __name__ == "__main__": unittest.main()