Incident-Response: sources_json nur noch via Lazy-Endpunkt, Sidebar schlank

Backend:
- IncidentResponse: sources_json-Feld entfernt (Detail-GET liefert es
  nicht mehr mit).
- Neues Schema IncidentListItem fuer GET /incidents (Sidebar):
  Ohne summary, ohne sources_json. Ein has_summary-Bit fuer
  Erster-Refresh-Erkennung, description bleibt fuer das Edit-Modal.
- list_incidents selektiert nur die noetigen Spalten (kein SELECT *)
  — spart bei grossen Lagen Speicher + Serialisierung.
- Neuer Endpunkt GET /incidents/{id}/sources liefert geparstes
  Sources-Array fuer Zitate-Lookups (Lazy).

Frontend:
- api.js: getIncidentSources(id).
- app.js: loadIncidentDetail laedt /sources parallel, speichert Array
  in _currentSources. Alle renderSummary/Zusammenfassung/
  LatestDevelopments-Aufrufe bekommen jetzt _currentSources statt
  incident.sources_json. inc.summary-Checks -> inc.has_summary.
- components.js: _parseSources(input) akzeptiert Array ODER String
  (Rueckwaertskompatibilitaet). renderZusammenfassung, renderSummary,
  renderLatestDevelopments nutzen den Helper.

Hintergrund: Die Sidebar-Liste lieferte bei 17 Lagen 1,23 MB
(Iran allein 386 KB wegen sources_json + summary). Detail-Endpunkt
lieferte sources_json (324 KB bei Iran) bei jedem Oeffnen mit.
Beides jetzt radikal kleiner — die 324 KB Sources gibt's nur
einmalig auf Anfrage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
2026-04-20 00:07:46 +02:00
Ursprung a302790777
Commit 0d6ad8ea90
5 geänderte Dateien mit 118 neuen und 22 gelöschten Zeilen

Datei anzeigen

@@ -91,6 +91,10 @@ const API = {
return this._request('GET', `/incidents/${id}`);
},
getIncidentSources(id) {
return this._request('GET', `/incidents/${id}/sources`);
},
updateIncident(id, data) {
return this._request('PUT', `/incidents/${id}`, data);
},

Datei anzeigen

@@ -422,6 +422,7 @@ const App = {
_currentArticles: [],
_currentSnapshots: [],
_snapshotFullCache: new Map(),
_currentSources: [],
_currentIncidentType: 'adhoc',
_sidebarFilter: 'all',
_currentUsername: '',
@@ -586,7 +587,7 @@ const App = {
this._refreshingIncidents.add(id);
const d = details[String(id)] || {};
const inc = this.incidents.find(i => i.id === id);
const isFirst = inc && !inc.summary;
const isFirst = inc && !inc.has_summary;
const isCurrent = (id === currentTask);
// Use 'researching' as default step for the actively running task
UI.showProgress(isCurrent ? 'researching' : 'queued', { started_at: d.started_at }, id, isFirst);
@@ -598,7 +599,7 @@ const App = {
queuedIds.forEach((id, idx) => {
this._refreshingIncidents.add(id);
const inc = this.incidents.find(i => i.id === id);
const isFirst = inc && !inc.summary;
const isFirst = inc && !inc.has_summary;
UI.showProgress('queued', { queue_position: idx + 1 }, id, isFirst);
});
}
@@ -787,14 +788,18 @@ const App = {
async loadIncidentDetail(id) {
try {
const [incident, articlesResponse, factchecks, snapshots, locationsResponse] = await Promise.all([
const [incident, articlesResponse, factchecks, snapshots, locationsResponse, sourcesResponse] = await Promise.all([
API.getIncident(id),
API.getArticles(id, { limit: 500, offset: 0 }),
API.getFactChecks(id),
API.getSnapshots(id),
API.getLocations(id).catch(() => []),
API.getIncidentSources(id).catch(() => ({ sources: [] })),
]);
// Sources-Array (ersetzt frueheres incident.sources_json — lazy via /sources-Endpunkt)
this._currentSources = (sourcesResponse && sourcesResponse.sources) || [];
// Articles: neue Shape {total, articles} oder alter nackter Array (Rueckwaertskompatibel)
let articles, articlesTotal;
if (Array.isArray(articlesResponse)) {
@@ -921,13 +926,13 @@ const App = {
if (incident.summary) {
const { zusammenfassung, remaining } = UI.extractZusammenfassung(incident.summary);
if (zusammenfassung) {
if (zusammenfassungText) zusammenfassungText.innerHTML = UI.renderZusammenfassung(zusammenfassung, incident.sources_json);
if (zusammenfassungText) zusammenfassungText.innerHTML = UI.renderZusammenfassung(zusammenfassung, this._currentSources);
if (zusammenfassungCard) zusammenfassungCard.style.display = '';
summaryText.innerHTML = UI.renderSummary(remaining, incident.sources_json, incident.type);
summaryText.innerHTML = UI.renderSummary(remaining, this._currentSources, incident.type);
} else {
if (zusammenfassungText) zusammenfassungText.innerHTML = '<span style="color:var(--text-disabled);">Zusammenfassung wird beim n\u00e4chsten Refresh generiert.</span>';
if (zusammenfassungCard) zusammenfassungCard.style.display = '';
summaryText.innerHTML = UI.renderSummary(incident.summary, incident.sources_json, incident.type);
summaryText.innerHTML = UI.renderSummary(incident.summary, this._currentSources, incident.type);
}
} else {
if (zusammenfassungCard) zusammenfassungCard.style.display = 'none';
@@ -939,12 +944,12 @@ const App = {
if (zusammenfassungCard) zusammenfassungCard.style.display = '';
const devText = (incident.latest_developments || '').trim();
if (devText) {
if (zusammenfassungText) zusammenfassungText.innerHTML = UI.renderLatestDevelopments(devText, incident.sources_json);
if (zusammenfassungText) zusammenfassungText.innerHTML = UI.renderLatestDevelopments(devText, this._currentSources);
} else if (zusammenfassungText) {
zusammenfassungText.innerHTML = '<span style="color:var(--text-disabled);">Noch keine Entwicklungen erfasst. Wird beim n\u00e4chsten Refresh generiert.</span>';
}
if (incident.summary) {
summaryText.innerHTML = UI.renderSummary(incident.summary, incident.sources_json, incident.type);
summaryText.innerHTML = UI.renderSummary(incident.summary, this._currentSources, incident.type);
} else {
summaryText.innerHTML = '<span style="color:var(--text-disabled);">Noch kein Lagebild. Klicke auf "Aktualisieren" um die Recherche zu starten.</span>';
}
@@ -1833,7 +1838,7 @@ async handleRefresh() {
} else {
UI.showToast('Aktualisierung gestartet.', 'success');
var _inc2 = this.incidents.find(function(i) { return i.id === this.currentIncidentId; }.bind(this));
UI.showProgress('queued', {}, this.currentIncidentId, _inc2 && !_inc2.summary);
UI.showProgress('queued', {}, this.currentIncidentId, _inc2 && !_inc2.has_summary);
}
} catch (err) {
this._refreshingIncidents.delete(this.currentIncidentId);
@@ -2176,7 +2181,7 @@ async handleRefresh() {
this._updateSidebarDot(msg.incident_id);
// Detect first refresh: no summary means first run
const inc = this.incidents.find(i => i.id === msg.incident_id);
const isFirst = inc && !inc.summary;
const isFirst = inc && !inc.has_summary;
// Update progress state for ALL incidents (sidebar + popup if current)
UI.showProgress(status, msg.data, msg.incident_id, isFirst);
// Re-render sidebar so status is baked into HTML (survives future re-renders)

Datei anzeigen

@@ -709,13 +709,27 @@ const UI = {
return { zusammenfassung, remaining: remaining.trim() };
},
/**
* Parst sources: akzeptiert Array (neu, vom /sources-Endpunkt) ODER
* JSON-String (alt, aus sources_json) fuer Rueckwaertskompatibilitaet.
*/
_parseSources(input) {
if (!input) return [];
if (Array.isArray(input)) return input;
try {
const parsed = JSON.parse(input);
return Array.isArray(parsed) ? parsed : [];
} catch (e) {
return [];
}
},
/**
* Rendert die Zusammenfassung als HTML (Bullet Points).
*/
renderZusammenfassung(text, sourcesJson) {
if (!text) return '<span style="color:var(--text-disabled);">Noch keine Zusammenfassung.</span>';
let sources = [];
try { sources = JSON.parse(sourcesJson || '[]'); } catch(e) {}
const sources = this._parseSources(sourcesJson);
// Nur Bullet-Point-Zeilen behalten, Fliesstext herausfiltern
const bulletLines = text.split("\n").filter(line => line.trim().startsWith("- "));
const bulletText = bulletLines.length > 0 ? bulletLines.join("\n") : text;
@@ -751,8 +765,7 @@ const UI = {
*/
renderLatestDevelopments(text, sourcesJson) {
if (!text) return '<span style="color:var(--text-disabled);">Noch keine Entwicklungen erfasst.</span>';
let sources = [];
try { sources = JSON.parse(sourcesJson || '[]'); } catch(e) {}
const sources = this._parseSources(sourcesJson);
const bulletLines = text.split("\n").map(l => l.trim()).filter(l => l && (l.startsWith("- ") || l.startsWith("[")));
if (bulletLines.length === 0) {
@@ -869,8 +882,7 @@ const UI = {
renderSummary(summary, sourcesJson, incidentType) {
if (!summary) return '<span style="color:var(--text-tertiary);">Noch keine Zusammenfassung.</span>';
let sources = [];
try { sources = JSON.parse(sourcesJson || '[]'); } catch(e) {}
const sources = this._parseSources(sourcesJson);
// Markdown-Rendering
let html = this.escape(summary);