diff --git a/src/models.py b/src/models.py
index 708a4e2..5a09b2b 100644
--- a/src/models.py
+++ b/src/models.py
@@ -78,6 +78,11 @@ class DescriptionEnhanceRequest(BaseModel):
class IncidentResponse(BaseModel):
+ """Vollstaendige Lage-Details (fuer GET /incidents/{id}).
+
+ Enthaelt summary + latest_developments, aber NICHT mehr sources_json —
+ das wird separat per GET /incidents/{id}/sources geladen (Lazy-Load).
+ """
id: int
title: str
description: Optional[str]
@@ -90,7 +95,6 @@ class IncidentResponse(BaseModel):
visibility: str = "public"
summary: Optional[str]
latest_developments: Optional[str] = None
- sources_json: Optional[str] = None
international_sources: bool = True
include_telegram: bool = False
created_by: int
@@ -101,6 +105,35 @@ class IncidentResponse(BaseModel):
source_count: int = 0
+class IncidentListItem(BaseModel):
+ """Schlankes Sidebar-Item (fuer GET /incidents).
+
+ Enthaelt, was Sidebar und Edit-Dialog brauchen — kein summary,
+ kein sources_json. Statt summary-Volltext ein ``has_summary``-Bit,
+ damit das Frontend "erster Refresh"-Zustand erkennen kann.
+ description bleibt drin (kurz, vom Edit-Modal direkt genutzt).
+ """
+ id: int
+ title: str
+ description: Optional[str] = None
+ type: str
+ status: str
+ refresh_mode: str
+ refresh_interval: int
+ refresh_start_time: Optional[str] = None
+ retention_days: int
+ visibility: str = "public"
+ international_sources: bool = True
+ include_telegram: bool = False
+ created_by: int
+ created_by_username: str = ""
+ created_at: str
+ updated_at: str
+ article_count: int = 0
+ source_count: int = 0
+ has_summary: bool = False
+
+
# Sources (Quellenverwaltung)
diff --git a/src/routers/incidents.py b/src/routers/incidents.py
index 377d03c..511818a 100644
--- a/src/routers/incidents.py
+++ b/src/routers/incidents.py
@@ -1,7 +1,7 @@
"""Incidents-Router: Lagen verwalten (Multi-Tenant)."""
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, status
from fastapi.responses import StreamingResponse
-from models import IncidentCreate, IncidentUpdate, IncidentResponse, SubscriptionUpdate, SubscriptionResponse, DescriptionEnhanceRequest
+from models import IncidentCreate, IncidentUpdate, IncidentResponse, IncidentListItem, SubscriptionUpdate, SubscriptionResponse, DescriptionEnhanceRequest
from auth import get_current_user
from middleware.license_check import require_writable_license
from database import db_dependency, get_db
@@ -69,17 +69,30 @@ async def _enrich_incident(db: aiosqlite.Connection, row: aiosqlite.Row) -> dict
return incident
-@router.get("", response_model=list[IncidentResponse])
+@router.get("", response_model=list[IncidentListItem])
async def list_incidents(
status_filter: str = None,
current_user: dict = Depends(get_current_user),
db: aiosqlite.Connection = Depends(db_dependency),
):
- """Alle Lagen des Tenants auflisten (oeffentliche + eigene private)."""
+ """Alle Lagen des Tenants auflisten (oeffentliche + eigene private).
+
+ Liefert schlanke Sidebar-Items — ohne summary, description, sources_json.
+ Volltexte kommen erst beim Oeffnen der Lage per GET /incidents/{id}.
+ """
tenant_id = current_user.get("tenant_id")
user_id = current_user["id"]
- query = "SELECT * FROM incidents WHERE tenant_id = ? AND (visibility = 'public' OR created_by = ?)"
+ # Nur die fuer Sidebar + Edit-Dialog noetigen Spalten selektieren
+ # (spart bei Iran: 324 KB sources_json + 32 KB summary).
+ # has_summary als Bit — Frontend nutzt es zur Erkennung "erster Refresh".
+ query = (
+ "SELECT id, title, description, type, status, refresh_mode, refresh_interval, "
+ "refresh_start_time, retention_days, visibility, "
+ "international_sources, include_telegram, created_by, created_at, updated_at, "
+ "CASE WHEN summary IS NOT NULL AND summary != '' THEN 1 ELSE 0 END AS has_summary "
+ "FROM incidents WHERE tenant_id = ? AND (visibility = 'public' OR created_by = ?)"
+ )
params = [tenant_id, user_id]
if status_filter:
@@ -239,12 +252,41 @@ async def get_incident(
current_user: dict = Depends(get_current_user),
db: aiosqlite.Connection = Depends(db_dependency),
):
- """Einzelne Lage abrufen."""
+ """Einzelne Lage abrufen.
+
+ sources_json wird NICHT mitgeliefert — fuer Zitate-Lookups
+ stattdessen GET /incidents/{id}/sources verwenden (lazy).
+ """
tenant_id = current_user.get("tenant_id")
row = await _check_incident_access(db, incident_id, current_user["id"], tenant_id)
return await _enrich_incident(db, row)
+@router.get("/{incident_id}/sources")
+async def get_incident_sources(
+ incident_id: int,
+ current_user: dict = Depends(get_current_user),
+ db: aiosqlite.Connection = Depends(db_dependency),
+):
+ """Sources-Array einer Lage (geparst aus sources_json) fuer Zitate-Lookups."""
+ tenant_id = current_user.get("tenant_id")
+ await _check_incident_access(db, incident_id, current_user["id"], tenant_id)
+ cursor = await db.execute(
+ "SELECT sources_json FROM incidents WHERE id = ?",
+ (incident_id,),
+ )
+ row = await cursor.fetchone()
+ sources: list = []
+ if row and row["sources_json"]:
+ try:
+ parsed = json.loads(row["sources_json"])
+ if isinstance(parsed, list):
+ sources = parsed
+ except (json.JSONDecodeError, TypeError):
+ sources = []
+ return {"incident_id": incident_id, "sources": sources}
+
+
@router.put("/{incident_id}", response_model=IncidentResponse)
async def update_incident(
incident_id: int,
diff --git a/src/static/js/api.js b/src/static/js/api.js
index 4df5018..eb8e1a1 100644
--- a/src/static/js/api.js
+++ b/src/static/js/api.js
@@ -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);
},
diff --git a/src/static/js/app.js b/src/static/js/app.js
index 7e8450a..402878d 100644
--- a/src/static/js/app.js
+++ b/src/static/js/app.js
@@ -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 = 'Zusammenfassung wird beim n\u00e4chsten Refresh generiert.';
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 = 'Noch keine Entwicklungen erfasst. Wird beim n\u00e4chsten Refresh generiert.';
}
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 = 'Noch kein Lagebild. Klicke auf "Aktualisieren" um die Recherche zu starten.';
}
@@ -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)
diff --git a/src/static/js/components.js b/src/static/js/components.js
index cf482a5..a58fb21 100644
--- a/src/static/js/components.js
+++ b/src/static/js/components.js
@@ -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 'Noch keine Zusammenfassung.';
- 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 'Noch keine Entwicklungen erfasst.';
- 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 'Noch keine Zusammenfassung.';
- let sources = [];
- try { sources = JSON.parse(sourcesJson || '[]'); } catch(e) {}
+ const sources = this._parseSources(sourcesJson);
// Markdown-Rendering
let html = this.escape(summary);