Neuer API-Endpoint: /api/public/globe-feed fuer Globe-Integration
Liefert Locations + Artikel-Headlines + Summaries als GeoJSON. Flexible Lage-Auswahl per incident_id oder alle oeffentlichen. Farbkodiert nach Kategorie (primary/secondary/tertiary/mentioned).
Dieser Commit ist enthalten in:
@@ -160,6 +160,102 @@ async def get_lagebild(db=Depends(db_dependency)):
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@router.get("/globe-feed", dependencies=[Depends(verify_api_key)])
|
||||
async def get_globe_feed(
|
||||
incident_id: int = None,
|
||||
db=Depends(db_dependency),
|
||||
):
|
||||
"""Globe-Feed: Locations + Artikel + Summary fuer beliebige Lage(n) als GeoJSON."""
|
||||
import json as _json
|
||||
|
||||
# Wenn keine ID: alle oeffentlichen Lagen
|
||||
if incident_id:
|
||||
cursor = await db.execute(
|
||||
"SELECT id, title, description, summary, updated_at, type, status, category_labels "
|
||||
"FROM incidents WHERE id = ? AND status != 'deleted'", (incident_id,)
|
||||
)
|
||||
else:
|
||||
cursor = await db.execute(
|
||||
"SELECT id, title, description, summary, updated_at, type, status, category_labels "
|
||||
"FROM incidents WHERE visibility = 'public' AND status IN ('active','archived') "
|
||||
"ORDER BY updated_at DESC LIMIT 20"
|
||||
)
|
||||
incidents = [dict(r) for r in await cursor.fetchall()]
|
||||
if not incidents:
|
||||
return {"type": "FeatureCollection", "features": [], "incidents": []}
|
||||
|
||||
inc_ids = [i["id"] for i in incidents]
|
||||
ids_sql = ",".join(str(i) for i in inc_ids)
|
||||
|
||||
# Locations mit Artikel-Headlines
|
||||
cursor = await db.execute(
|
||||
f"""SELECT
|
||||
al.location_name_normalized as name,
|
||||
ROUND(al.latitude, 4) as lat,
|
||||
ROUND(al.longitude, 4) as lon,
|
||||
al.country_code,
|
||||
al.category,
|
||||
al.incident_id,
|
||||
COUNT(*) as article_count,
|
||||
MAX(al.confidence) as confidence,
|
||||
GROUP_CONCAT(DISTINCT a.headline_de) as headlines
|
||||
FROM article_locations al
|
||||
LEFT JOIN articles a ON al.article_id = a.id
|
||||
WHERE al.incident_id IN ({ids_sql})
|
||||
GROUP BY al.location_name_normalized, al.incident_id
|
||||
ORDER BY article_count DESC
|
||||
LIMIT 500"""
|
||||
)
|
||||
locations = [dict(r) for r in await cursor.fetchall()]
|
||||
|
||||
# Als GeoJSON
|
||||
features = []
|
||||
for loc in locations:
|
||||
inc = next((i for i in incidents if i["id"] == loc["incident_id"]), None)
|
||||
headlines = (loc.get("headlines") or "").split(",")[:5]
|
||||
features.append({
|
||||
"type": "Feature",
|
||||
"geometry": {"type": "Point", "coordinates": [loc["lon"], loc["lat"]]},
|
||||
"properties": {
|
||||
"name": loc["name"],
|
||||
"country": loc["country_code"],
|
||||
"category": loc["category"],
|
||||
"article_count": loc["article_count"],
|
||||
"confidence": loc["confidence"],
|
||||
"incident_id": loc["incident_id"],
|
||||
"incident_title": inc["title"] if inc else "",
|
||||
"headlines": headlines,
|
||||
},
|
||||
})
|
||||
|
||||
# Incident-Summaries
|
||||
inc_summaries = []
|
||||
for i in incidents:
|
||||
cat_labels = None
|
||||
if i.get("category_labels"):
|
||||
try:
|
||||
cat_labels = _json.loads(i["category_labels"])
|
||||
except Exception:
|
||||
pass
|
||||
inc_summaries.append({
|
||||
"id": i["id"],
|
||||
"title": i["title"],
|
||||
"type": i["type"],
|
||||
"status": i["status"],
|
||||
"summary": (i.get("summary") or "")[:1000],
|
||||
"updated_at": i["updated_at"],
|
||||
"category_labels": cat_labels,
|
||||
})
|
||||
|
||||
return {
|
||||
"type": "FeatureCollection",
|
||||
"features": features,
|
||||
"incidents": inc_summaries,
|
||||
"generated_at": datetime.now(TIMEZONE).isoformat(),
|
||||
}
|
||||
|
||||
@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."""
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren