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 }}
+
+
+
+
+
+
+
+ {{ 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
+
+
+ | Kriterium | Status | Wert |
+
+
+ {% for c in checklist %}
+
+ | {{ c.label }} |
+
+ {% if c.status == "ok" %}
+ OK
+ {% else %}
+ Fehlt
+ {% endif %}
+ |
+ {{ c.value }} |
+
+ {% endfor %}
+
+
+
+
+
+ Extrahierte Daten
+ Bilder: {{ article.extraction.images|length if article.extraction.images else 0 }}
+ {% if article.extraction.images %}
+
+ {% for img in article.extraction.images %}
+ - {{ img }}
+ {% endfor %}
+
+ {% 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 "-" }}
+
+
+
+
+
+
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()