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

@@ -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`);
},

Datei anzeigen

@@ -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)
: '<div class="vt-snapshot-loading">Lagebericht wird geladen…</div>';
const loadedAttr = cached ? ' data-loaded="yes"' : '';
return `<div class="vt-entry vt-snapshot expandable" onclick="App.toggleTimelineEntry(this)">
return `<div class="vt-entry vt-snapshot expandable" data-snapshot-id="${snapshot.id}"${loadedAttr} onclick="App.toggleTimelineEntry(this)">
<div class="vt-snapshot-header">
<span class="vt-snapshot-badge">Lagebericht</span>
<span class="vt-snapshot-time">${time}</span>
<span class="vt-snapshot-stats">${UI.escape(statsText)}</span>
</div>
<div class="vt-snapshot-preview">${UI.escape(preview)}</div>
<div class="vt-snapshot-detail">${fullSummary}</div>
<div class="vt-snapshot-detail">${detailHtml}</div>
</div>`;
},
/**
* Volltext eines Snapshots bei Bedarf nachladen und in das DOM einsetzen.
* Ergebnis wird in _snapshotFullCache gecacht.
*/
async lazyLoadSnapshotDetail(el) {
if (!el || el.dataset.loaded === 'yes' || el.dataset.loaded === 'loading') return;
const snapId = parseInt(el.dataset.snapshotId || '0', 10);
if (!snapId || !this.currentIncidentId) return;
el.dataset.loaded = 'loading';
try {
let snap = this._snapshotFullCache.get(snapId);
if (!snap) {
snap = await API.getSnapshot(this.currentIncidentId, snapId);
this._snapshotFullCache.set(snapId, snap);
}
const detailEl = el.querySelector('.vt-snapshot-detail');
if (detailEl) {
detailEl.innerHTML = UI.renderSummary(snap.summary, snap.sources_json, this._currentIncidentType);
}
el.dataset.loaded = 'yes';
// Nach dem Laden die Timeline-Kachel an neue Hoehe anpassen
if (el.classList.contains('expanded')) this._resizeTimelineTile();
} catch (err) {
console.error('Snapshot-Volltext laden fehlgeschlagen:', err);
el.dataset.loaded = '';
const detailEl = el.querySelector('.vt-snapshot-detail');
if (detailEl) detailEl.innerHTML = '<div class="vt-snapshot-error">Fehler beim Laden des Lageberichts.</div>';
}
},
/**
* Timeline-Eintrag auf-/zuklappen (mutual-exclusive pro Zeitgruppe).
*/
@@ -1544,6 +1584,10 @@ const App = {
}
el.classList.toggle('expanded');
if (el.classList.contains('expanded')) {
// Snapshots: Volltext lazy nachladen (nur wenn noch nicht geladen)
if (el.classList.contains('vt-snapshot') && el.dataset.snapshotId) {
this.lazyLoadSnapshotDetail(el);
}
requestAnimationFrame(() => {
var scrollParent = el.closest('.ht-detail-content');
if (scrollParent && el.classList.contains('vt-snapshot')) {