diff --git a/src/routers/incidents.py b/src/routers/incidents.py index a2d2fc9..5585b15 100644 --- a/src/routers/incidents.py +++ b/src/routers/incidents.py @@ -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, diff --git a/src/static/js/api.js b/src/static/js/api.js index ae26f92..1dbdfa5 100644 --- a/src/static/js/api.js +++ b/src/static/js/api.js @@ -111,6 +111,14 @@ const API = { return this._request('GET', `/incidents/${incidentId}/snapshots`); }, + getSnapshot(incidentId, snapshotId) { + return this._request('GET', `/incidents/${incidentId}/snapshots/${snapshotId}`); + }, + + searchSnapshots(incidentId, query) { + return this._request('GET', `/incidents/${incidentId}/snapshots/search?q=${encodeURIComponent(query)}`); + }, + getLocations(incidentId) { return this._request('GET', `/incidents/${incidentId}/locations`); }, diff --git a/src/static/js/app.js b/src/static/js/app.js index 2040592..9f3d1e1 100644 --- a/src/static/js/app.js +++ b/src/static/js/app.js @@ -420,6 +420,8 @@ const App = { _refreshingIncidents: new Set(), _editingIncidentId: null, _currentArticles: [], + _currentSnapshots: [], + _snapshotFullCache: new Map(), _currentIncidentType: 'adhoc', _sidebarFilter: 'all', _currentUsername: '', @@ -954,6 +956,7 @@ const App = { // Timeline - Artikel + Snapshots zwischenspeichern und rendern this._currentArticles = articles; this._currentSnapshots = snapshots || []; + this._snapshotFullCache = new Map(); this._currentIncidentType = incident.type; // Tab-Auswahl: gemerkt pro Lage (localStorage), Default = erster Tab @@ -1001,7 +1004,9 @@ const App = { if (filterType === 'all' || filterType === 'snapshots') { let snapshots = this._currentSnapshots || []; if (searchTerm) { - snapshots = snapshots.filter(s => (s.summary || '').toLowerCase().includes(searchTerm)); + // Suche erfolgt clientseitig ueber Preview (Snapshots-Liste enthaelt keinen Volltext mehr). + // Die asynchrone Volltext-Server-Suche wird separat ausgeloest (rerenderTimeline). + snapshots = snapshots.filter(s => (s.summary_preview || s.summary || '').toLowerCase().includes(searchTerm)); } snapshots.forEach(s => entries.push({ kind: 'snapshot', data: s, timestamp: s.created_at || '' })); } @@ -1503,6 +1508,7 @@ const App = { /** * Snapshot/Lagebericht-Eintrag für den Zeitstrahl rendern. + * Volltext + sources_json werden erst beim Aufklappen lazy nachgeladen. */ _renderSnapshotEntry(snapshot) { const time = snapshot.created_at @@ -1514,24 +1520,58 @@ const App = { if (snapshot.fact_check_count) stats.push(`${snapshot.fact_check_count} Fakten`); const statsText = stats.join(', '); - // Vorschau: erste 200 Zeichen der Zusammenfassung - const summaryText = snapshot.summary || ''; - const preview = summaryText.length > 200 ? summaryText.substring(0, 200) + '...' : summaryText; + // Vorschau: erste 200 Zeichen aus summary_preview (vom Server gekuerzt) oder Fallback summary + const previewText = snapshot.summary_preview || snapshot.summary || ''; + const preview = previewText.length > 200 ? previewText.substring(0, 200) + '...' : previewText; - // Vollständige Zusammenfassung via UI.renderSummary - const fullSummary = UI.renderSummary(snapshot.summary, snapshot.sources_json, this._currentIncidentType); + // Volltext aus Cache (falls bereits geladen), sonst Platzhalter fuer Lazy-Load + const cached = this._snapshotFullCache && this._snapshotFullCache.get(snapshot.id); + const detailHtml = cached + ? UI.renderSummary(cached.summary, cached.sources_json, this._currentIncidentType) + : '