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

@@ -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,