Snapshots: Liste ohne Volltext, Lazy-Load + serverseitige Suche

Backend:
- GET /{id}/snapshots liefert nur noch schlanke Shape (Metadaten +
  SUBSTR(summary,1,300) AS summary_preview), kein Volltext, kein sources_json.
- Neuer Endpunkt GET /{id}/snapshots/{snapshot_id} fuer Volltext-Lazy-Load.
- Neuer Endpunkt GET /{id}/snapshots/search?q=... fuer serverseitige
  Volltextsuche ueber alle Snapshots einer Lage.

Frontend:
- api.js: getSnapshot() und searchSnapshots() ergaenzt.
- app.js: _snapshotFullCache, Volltext wird beim Aufklappen eines
  Snapshot-Eintrags per lazyLoadSnapshotDetail() nachgeladen und gecacht.
- Suche ueber Snapshots filtert weiterhin clientseitig ueber summary_preview.

Hintergrund: Bei grossen Lagen (Iran-Lage: 347 Snapshots) fiel die
Snapshots-Listenantwort mit Volltext-Summaries auf ~54 MB. Die Liste
faellt damit auf ~150 KB; Volltexte werden nur on-demand geladen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
2026-04-19 23:42:08 +02:00
Ursprung 34be98edaf
Commit 194790899c
3 geänderte Dateien mit 117 neuen und 11 gelöschten Zeilen

Datei anzeigen

@@ -337,12 +337,17 @@ async def get_snapshots(
current_user: dict = Depends(get_current_user),
db: aiosqlite.Connection = Depends(db_dependency),
):
"""Lageberichte (Snapshots) einer Lage abrufen."""
"""Lageberichte (Snapshots) einer Lage abrufen — schlanke Liste.
Liefert nur Metadaten und einen 300-Zeichen-Preview des Summary.
Der Volltext (summary + sources_json) wird per Einzel-Endpunkt
``GET /{incident_id}/snapshots/{snapshot_id}`` bei Bedarf geladen.
"""
tenant_id = current_user.get("tenant_id")
await _check_incident_access(db, incident_id, current_user["id"], tenant_id)
cursor = await db.execute(
"""SELECT id, incident_id, summary, sources_json,
article_count, fact_check_count, created_at
"""SELECT id, incident_id, article_count, fact_check_count, created_at,
SUBSTR(summary, 1, 300) AS summary_preview
FROM incident_snapshots WHERE incident_id = ?
ORDER BY created_at DESC""",
(incident_id,),
@@ -351,6 +356,55 @@ async def get_snapshots(
return [dict(row) for row in rows]
@router.get("/{incident_id}/snapshots/search")
async def search_snapshots(
incident_id: int,
q: str = Query(..., min_length=2, max_length=200),
current_user: dict = Depends(get_current_user),
db: aiosqlite.Connection = Depends(db_dependency),
):
"""Volltextsuche über alle Snapshots einer Lage.
Liefert dieselbe schlanke Shape wie der Listen-Endpunkt,
gefiltert per ``summary LIKE '%q%'``.
"""
tenant_id = current_user.get("tenant_id")
await _check_incident_access(db, incident_id, current_user["id"], tenant_id)
like = f"%{q}%"
cursor = await db.execute(
"""SELECT id, incident_id, article_count, fact_check_count, created_at,
SUBSTR(summary, 1, 300) AS summary_preview
FROM incident_snapshots
WHERE incident_id = ? AND summary LIKE ?
ORDER BY created_at DESC""",
(incident_id, like),
)
rows = await cursor.fetchall()
return [dict(row) for row in rows]
@router.get("/{incident_id}/snapshots/{snapshot_id}")
async def get_snapshot(
incident_id: int,
snapshot_id: int,
current_user: dict = Depends(get_current_user),
db: aiosqlite.Connection = Depends(db_dependency),
):
"""Einzelnen Snapshot mit vollem Summary + sources_json abrufen (Lazy-Load)."""
tenant_id = current_user.get("tenant_id")
await _check_incident_access(db, incident_id, current_user["id"], tenant_id)
cursor = await db.execute(
"""SELECT id, incident_id, summary, sources_json,
article_count, fact_check_count, created_at
FROM incident_snapshots WHERE id = ? AND incident_id = ?""",
(snapshot_id, incident_id),
)
row = await cursor.fetchone()
if not row:
raise HTTPException(status_code=404, detail="Snapshot nicht gefunden")
return dict(row)
@router.get("/{incident_id}/factchecks")
async def get_factchecks(
incident_id: int,