Generischen Lagebild-API-Endpunkt hinzufügen

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) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-04-11 11:09:05 +00:00
Ursprung f2469093ee
Commit 68c4e2a9c9

Datei anzeigen

@@ -1,7 +1,7 @@
"""Öffentliche API für die Lagebild-Seite auf aegissight.de. """Öffentliche API für die Lagebild-Seite auf aegissight.de.
Authentifizierung via X-API-Key Header (getrennt von der JWT-Auth). 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 json
import logging import logging
@@ -50,14 +50,23 @@ def _in_clause(ids):
return ",".join(str(int(i)) for i in 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)): # Shared-Logik für Lagebild-Responses
"""Liefert das aktuelle Lagebild (Irankonflikt) mit allen Daten.""" # ──────────────────────────────────────────────────────────────────
ids = _in_clause(IRAN_INCIDENT_IDS)
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) # Haupt-Incident laden (für Summary, Sources)
cursor = await db.execute( cursor = await db.execute(
"SELECT * FROM incidents WHERE id = ?", (PRIMARY_INCIDENT_ID,) "SELECT * FROM incidents WHERE id = ?", (primary_id,)
) )
incident = await cursor.fetchone() incident = await cursor.fetchone()
if not incident: if not incident:
@@ -72,7 +81,7 @@ async def get_lagebild(db=Depends(db_dependency)):
except (json.JSONDecodeError, TypeError): except (json.JSONDecodeError, TypeError):
pass pass
# Alle Artikel aus allen Iran-Incidents laden # Alle Artikel laden
cursor = await db.execute( cursor = await db.execute(
f"""SELECT id, headline, headline_de, source, source_url, language, f"""SELECT id, headline, headline_de, source, source_url, language,
published_at, collected_at, verification_status, incident_id 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()] articles = [dict(r) for r in await cursor.fetchall()]
# Alle Faktenchecks aus allen Iran-Incidents laden # Alle Faktenchecks laden
cursor = await db.execute( cursor = await db.execute(
f"""SELECT id, claim, status, sources_count, evidence, status_history, checked_at, incident_id f"""SELECT id, claim, status, sources_count, evidence, status_history, checked_at, incident_id
FROM fact_checks WHERE incident_id IN ({ids}) 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"] source_count = (await cursor.fetchone())["cnt"]
# Snapshots aus allen Iran-Incidents # Snapshots
cursor = await db.execute( cursor = await db.execute(
f"""SELECT id, incident_id, article_count, fact_check_count, created_at f"""SELECT id, incident_id, article_count, fact_check_count, created_at
FROM incident_snapshots WHERE incident_id IN ({ids}) 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)]) @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)} return {"ok": True, "inserted": inserted, "total_sent": len(events)}
@router.get("/globe-incidents", dependencies=[Depends(verify_api_key)]) @router.get("/globe-incidents", dependencies=[Depends(verify_api_key)])
async def get_globe_incidents(db=Depends(db_dependency)): async def get_globe_incidents(db=Depends(db_dependency)):
"""Liste aller oeffentlichen aktiven Lagen fuer Globe-Auswahl.""" """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)]) @router.get("/lagebild/snapshot/{snapshot_id}", dependencies=[Depends(verify_api_key)])
async def get_snapshot(snapshot_id: int, db=Depends(db_dependency)): async def get_snapshot(snapshot_id: int, db=Depends(db_dependency)):
"""Liefert einen historischen Snapshot.""" """Liefert einen historischen Snapshot (Irankonflikt, abwärtskompatibel)."""
ids = _in_clause(IRAN_INCIDENT_IDS) 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( cursor = await db.execute(
f"""SELECT id, summary, sources_json, article_count, fact_check_count, created_at "SELECT id FROM incidents WHERE id = ? AND visibility = 'public'",
FROM incident_snapshots (incident_id,),
WHERE id = ? AND incident_id IN ({ids})""",
(snapshot_id,),
) )
snap = await cursor.fetchone() if not await cursor.fetchone():
if not snap: raise HTTPException(status_code=404, detail="Lage nicht gefunden oder nicht öffentlich")
raise HTTPException(status_code=404, detail="Snapshot not found") 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)