From 68c4e2a9c96d4def93505fd287732b46c7172bc5 Mon Sep 17 00:00:00 2001 From: claude-dev Date: Sat, 11 Apr 2026 11:09:05 +0000 Subject: [PATCH] =?UTF-8?q?Generischen=20Lagebild-API-Endpunkt=20hinzuf?= =?UTF-8?q?=C3=BCgen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shared-Logik extrahiert (_build_lagebild_response, _get_snapshot_response). Neue Endpunkte: - GET /api/public/lagebild/{incident_id} für beliebige öffentliche Lagen - GET /api/public/lagebild/{incident_id}/snapshot/{snapshot_id} Bestehende Iran-Endpunkte bleiben abwärtskompatibel. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/routers/public_api.py | 99 +++++++++++++++++++++++++++++---------- 1 file changed, 74 insertions(+), 25 deletions(-) diff --git a/src/routers/public_api.py b/src/routers/public_api.py index d91ea78..f315d0b 100644 --- a/src/routers/public_api.py +++ b/src/routers/public_api.py @@ -1,7 +1,7 @@ """Öffentliche API für die Lagebild-Seite auf aegissight.de. Authentifizierung via X-API-Key Header (getrennt von der JWT-Auth). -Exponiert den Irankonflikt (alle zugehörigen Incidents) als read-only. +Exponiert öffentliche Lagen als read-only. """ import json import logging @@ -50,14 +50,23 @@ def _in_clause(ids): return ",".join(str(int(i)) for i in ids) -@router.get("/lagebild", dependencies=[Depends(verify_api_key)]) -async def get_lagebild(db=Depends(db_dependency)): - """Liefert das aktuelle Lagebild (Irankonflikt) mit allen Daten.""" - ids = _in_clause(IRAN_INCIDENT_IDS) +# ────────────────────────────────────────────────────────────────── +# Shared-Logik für Lagebild-Responses +# ────────────────────────────────────────────────────────────────── + +async def _build_lagebild_response(db, incident_ids: list, primary_id: int) -> dict: + """Baut die Lagebild-Response für beliebige Incidents. + + Args: + db: Datenbankverbindung + incident_ids: Liste der Incident-IDs (für Iran: [6,18,19,20], sonst: [55]) + primary_id: ID des Haupt-Incidents für Metadaten + """ + ids = _in_clause(incident_ids) # Haupt-Incident laden (für Summary, Sources) cursor = await db.execute( - "SELECT * FROM incidents WHERE id = ?", (PRIMARY_INCIDENT_ID,) + "SELECT * FROM incidents WHERE id = ?", (primary_id,) ) incident = await cursor.fetchone() if not incident: @@ -72,7 +81,7 @@ async def get_lagebild(db=Depends(db_dependency)): except (json.JSONDecodeError, TypeError): pass - # Alle Artikel aus allen Iran-Incidents laden + # Alle Artikel laden cursor = await db.execute( f"""SELECT id, headline, headline_de, source, source_url, language, published_at, collected_at, verification_status, incident_id @@ -81,7 +90,7 @@ async def get_lagebild(db=Depends(db_dependency)): ) articles = [dict(r) for r in await cursor.fetchall()] - # Alle Faktenchecks aus allen Iran-Incidents laden + # Alle Faktenchecks laden cursor = await db.execute( f"""SELECT id, claim, status, sources_count, evidence, status_history, checked_at, incident_id FROM fact_checks WHERE incident_id IN ({ids}) @@ -102,7 +111,7 @@ async def get_lagebild(db=Depends(db_dependency)): ) source_count = (await cursor.fetchone())["cnt"] - # Snapshots aus allen Iran-Incidents + # Snapshots cursor = await db.execute( f"""SELECT id, incident_id, article_count, fact_check_count, created_at FROM incident_snapshots WHERE incident_id IN ({ids}) @@ -160,8 +169,39 @@ async def get_lagebild(db=Depends(db_dependency)): } +async def _get_snapshot_response(db, snapshot_id: int, incident_ids: list) -> dict: + """Liefert einen historischen Snapshot für die angegebenen Incidents.""" + ids = _in_clause(incident_ids) + cursor = await db.execute( + f"""SELECT id, summary, sources_json, article_count, fact_check_count, created_at + FROM incident_snapshots + WHERE id = ? AND incident_id IN ({ids})""", + (snapshot_id,), + ) + snap = await cursor.fetchone() + if not snap: + raise HTTPException(status_code=404, detail="Snapshot not found") + + snap = dict(snap) + try: + snap["sources_json"] = json.loads(snap.get("sources_json") or "[]") + except (json.JSONDecodeError, TypeError): + snap["sources_json"] = [] + + return snap +# ────────────────────────────────────────────────────────────────── +# Endpunkte +# ────────────────────────────────────────────────────────────────── + +@router.get("/lagebild", dependencies=[Depends(verify_api_key)]) +async def get_lagebild(db=Depends(db_dependency)): + """Liefert das aktuelle Lagebild (Irankonflikt) mit allen Daten. + + Abwärtskompatibel — aggregiert die Iran-Incidents 6, 18, 19, 20. + """ + return await _build_lagebild_response(db, IRAN_INCIDENT_IDS, PRIMARY_INCIDENT_ID) @router.post("/globe-ingest", dependencies=[Depends(verify_api_key)]) @@ -230,7 +270,6 @@ async def globe_ingest( return {"ok": True, "inserted": inserted, "total_sent": len(events)} - @router.get("/globe-incidents", dependencies=[Depends(verify_api_key)]) async def get_globe_incidents(db=Depends(db_dependency)): """Liste aller oeffentlichen aktiven Lagen fuer Globe-Auswahl.""" @@ -349,24 +388,34 @@ async def get_globe_feed( } +# WICHTIG: Snapshot-Routen VOR der generischen /{incident_id}-Route, +# damit /lagebild/snapshot/123 nicht als incident_id="snapshot" gematcht wird. + @router.get("/lagebild/snapshot/{snapshot_id}", dependencies=[Depends(verify_api_key)]) async def get_snapshot(snapshot_id: int, db=Depends(db_dependency)): - """Liefert einen historischen Snapshot.""" - ids = _in_clause(IRAN_INCIDENT_IDS) + """Liefert einen historischen Snapshot (Irankonflikt, abwärtskompatibel).""" + return await _get_snapshot_response(db, snapshot_id, IRAN_INCIDENT_IDS) + + +@router.get("/lagebild/{incident_id}/snapshot/{snapshot_id}", dependencies=[Depends(verify_api_key)]) +async def get_snapshot_by_incident(incident_id: int, snapshot_id: int, db=Depends(db_dependency)): + """Liefert einen historischen Snapshot für eine beliebige öffentliche Lage.""" cursor = await db.execute( - f"""SELECT id, summary, sources_json, article_count, fact_check_count, created_at - FROM incident_snapshots - WHERE id = ? AND incident_id IN ({ids})""", - (snapshot_id,), + "SELECT id FROM incidents WHERE id = ? AND visibility = 'public'", + (incident_id,), ) - snap = await cursor.fetchone() - if not snap: - raise HTTPException(status_code=404, detail="Snapshot not found") + if not await cursor.fetchone(): + raise HTTPException(status_code=404, detail="Lage nicht gefunden oder nicht öffentlich") + return await _get_snapshot_response(db, snapshot_id, [incident_id]) - snap = dict(snap) - try: - snap["sources_json"] = json.loads(snap.get("sources_json") or "[]") - except (json.JSONDecodeError, TypeError): - snap["sources_json"] = [] - return snap +@router.get("/lagebild/{incident_id}", dependencies=[Depends(verify_api_key)]) +async def get_lagebild_by_id(incident_id: int, db=Depends(db_dependency)): + """Liefert das Lagebild für eine beliebige öffentliche Lage.""" + cursor = await db.execute( + "SELECT id FROM incidents WHERE id = ? AND visibility = 'public'", + (incident_id,), + ) + if not await cursor.fetchone(): + raise HTTPException(status_code=404, detail="Lage nicht gefunden oder nicht öffentlich") + return await _build_lagebild_response(db, [incident_id], incident_id)