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:
@@ -78,6 +78,11 @@ class DescriptionEnhanceRequest(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class IncidentResponse(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
|
id: int
|
||||||
title: str
|
title: str
|
||||||
description: Optional[str]
|
description: Optional[str]
|
||||||
@@ -90,7 +95,6 @@ class IncidentResponse(BaseModel):
|
|||||||
visibility: str = "public"
|
visibility: str = "public"
|
||||||
summary: Optional[str]
|
summary: Optional[str]
|
||||||
latest_developments: Optional[str] = None
|
latest_developments: Optional[str] = None
|
||||||
sources_json: Optional[str] = None
|
|
||||||
international_sources: bool = True
|
international_sources: bool = True
|
||||||
include_telegram: bool = False
|
include_telegram: bool = False
|
||||||
created_by: int
|
created_by: int
|
||||||
@@ -101,6 +105,35 @@ class IncidentResponse(BaseModel):
|
|||||||
source_count: int = 0
|
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)
|
# Sources (Quellenverwaltung)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Incidents-Router: Lagen verwalten (Multi-Tenant)."""
|
"""Incidents-Router: Lagen verwalten (Multi-Tenant)."""
|
||||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, status
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, status
|
||||||
from fastapi.responses import StreamingResponse
|
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 auth import get_current_user
|
||||||
from middleware.license_check import require_writable_license
|
from middleware.license_check import require_writable_license
|
||||||
from database import db_dependency, get_db
|
from database import db_dependency, get_db
|
||||||
@@ -69,17 +69,30 @@ async def _enrich_incident(db: aiosqlite.Connection, row: aiosqlite.Row) -> dict
|
|||||||
return incident
|
return incident
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=list[IncidentResponse])
|
@router.get("", response_model=list[IncidentListItem])
|
||||||
async def list_incidents(
|
async def list_incidents(
|
||||||
status_filter: str = None,
|
status_filter: str = None,
|
||||||
current_user: dict = Depends(get_current_user),
|
current_user: dict = Depends(get_current_user),
|
||||||
db: aiosqlite.Connection = Depends(db_dependency),
|
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")
|
tenant_id = current_user.get("tenant_id")
|
||||||
user_id = current_user["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]
|
params = [tenant_id, user_id]
|
||||||
|
|
||||||
if status_filter:
|
if status_filter:
|
||||||
@@ -239,12 +252,41 @@ async def get_incident(
|
|||||||
current_user: dict = Depends(get_current_user),
|
current_user: dict = Depends(get_current_user),
|
||||||
db: aiosqlite.Connection = Depends(db_dependency),
|
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")
|
tenant_id = current_user.get("tenant_id")
|
||||||
row = await _check_incident_access(db, incident_id, current_user["id"], tenant_id)
|
row = await _check_incident_access(db, incident_id, current_user["id"], tenant_id)
|
||||||
return await _enrich_incident(db, row)
|
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)
|
@router.put("/{incident_id}", response_model=IncidentResponse)
|
||||||
async def update_incident(
|
async def update_incident(
|
||||||
incident_id: int,
|
incident_id: int,
|
||||||
|
|||||||
@@ -91,6 +91,10 @@ const API = {
|
|||||||
return this._request('GET', `/incidents/${id}`);
|
return this._request('GET', `/incidents/${id}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getIncidentSources(id) {
|
||||||
|
return this._request('GET', `/incidents/${id}/sources`);
|
||||||
|
},
|
||||||
|
|
||||||
updateIncident(id, data) {
|
updateIncident(id, data) {
|
||||||
return this._request('PUT', `/incidents/${id}`, data);
|
return this._request('PUT', `/incidents/${id}`, data);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -422,6 +422,7 @@ const App = {
|
|||||||
_currentArticles: [],
|
_currentArticles: [],
|
||||||
_currentSnapshots: [],
|
_currentSnapshots: [],
|
||||||
_snapshotFullCache: new Map(),
|
_snapshotFullCache: new Map(),
|
||||||
|
_currentSources: [],
|
||||||
_currentIncidentType: 'adhoc',
|
_currentIncidentType: 'adhoc',
|
||||||
_sidebarFilter: 'all',
|
_sidebarFilter: 'all',
|
||||||
_currentUsername: '',
|
_currentUsername: '',
|
||||||
@@ -586,7 +587,7 @@ const App = {
|
|||||||
this._refreshingIncidents.add(id);
|
this._refreshingIncidents.add(id);
|
||||||
const d = details[String(id)] || {};
|
const d = details[String(id)] || {};
|
||||||
const inc = this.incidents.find(i => i.id === 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);
|
const isCurrent = (id === currentTask);
|
||||||
// Use 'researching' as default step for the actively running task
|
// Use 'researching' as default step for the actively running task
|
||||||
UI.showProgress(isCurrent ? 'researching' : 'queued', { started_at: d.started_at }, id, isFirst);
|
UI.showProgress(isCurrent ? 'researching' : 'queued', { started_at: d.started_at }, id, isFirst);
|
||||||
@@ -598,7 +599,7 @@ const App = {
|
|||||||
queuedIds.forEach((id, idx) => {
|
queuedIds.forEach((id, idx) => {
|
||||||
this._refreshingIncidents.add(id);
|
this._refreshingIncidents.add(id);
|
||||||
const inc = this.incidents.find(i => i.id === 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);
|
UI.showProgress('queued', { queue_position: idx + 1 }, id, isFirst);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -787,14 +788,18 @@ const App = {
|
|||||||
|
|
||||||
async loadIncidentDetail(id) {
|
async loadIncidentDetail(id) {
|
||||||
try {
|
try {
|
||||||
const [incident, articlesResponse, factchecks, snapshots, locationsResponse] = await Promise.all([
|
const [incident, articlesResponse, factchecks, snapshots, locationsResponse, sourcesResponse] = await Promise.all([
|
||||||
API.getIncident(id),
|
API.getIncident(id),
|
||||||
API.getArticles(id, { limit: 500, offset: 0 }),
|
API.getArticles(id, { limit: 500, offset: 0 }),
|
||||||
API.getFactChecks(id),
|
API.getFactChecks(id),
|
||||||
API.getSnapshots(id),
|
API.getSnapshots(id),
|
||||||
API.getLocations(id).catch(() => []),
|
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)
|
// Articles: neue Shape {total, articles} oder alter nackter Array (Rueckwaertskompatibel)
|
||||||
let articles, articlesTotal;
|
let articles, articlesTotal;
|
||||||
if (Array.isArray(articlesResponse)) {
|
if (Array.isArray(articlesResponse)) {
|
||||||
@@ -921,13 +926,13 @@ const App = {
|
|||||||
if (incident.summary) {
|
if (incident.summary) {
|
||||||
const { zusammenfassung, remaining } = UI.extractZusammenfassung(incident.summary);
|
const { zusammenfassung, remaining } = UI.extractZusammenfassung(incident.summary);
|
||||||
if (zusammenfassung) {
|
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 = '';
|
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 {
|
} else {
|
||||||
if (zusammenfassungText) zusammenfassungText.innerHTML = '<span style="color:var(--text-disabled);">Zusammenfassung wird beim n\u00e4chsten Refresh generiert.</span>';
|
if (zusammenfassungText) zusammenfassungText.innerHTML = '<span style="color:var(--text-disabled);">Zusammenfassung wird beim n\u00e4chsten Refresh generiert.</span>';
|
||||||
if (zusammenfassungCard) zusammenfassungCard.style.display = '';
|
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 {
|
} else {
|
||||||
if (zusammenfassungCard) zusammenfassungCard.style.display = 'none';
|
if (zusammenfassungCard) zusammenfassungCard.style.display = 'none';
|
||||||
@@ -939,12 +944,12 @@ const App = {
|
|||||||
if (zusammenfassungCard) zusammenfassungCard.style.display = '';
|
if (zusammenfassungCard) zusammenfassungCard.style.display = '';
|
||||||
const devText = (incident.latest_developments || '').trim();
|
const devText = (incident.latest_developments || '').trim();
|
||||||
if (devText) {
|
if (devText) {
|
||||||
if (zusammenfassungText) zusammenfassungText.innerHTML = UI.renderLatestDevelopments(devText, incident.sources_json);
|
if (zusammenfassungText) zusammenfassungText.innerHTML = UI.renderLatestDevelopments(devText, this._currentSources);
|
||||||
} else if (zusammenfassungText) {
|
} else if (zusammenfassungText) {
|
||||||
zusammenfassungText.innerHTML = '<span style="color:var(--text-disabled);">Noch keine Entwicklungen erfasst. Wird beim n\u00e4chsten Refresh generiert.</span>';
|
zusammenfassungText.innerHTML = '<span style="color:var(--text-disabled);">Noch keine Entwicklungen erfasst. Wird beim n\u00e4chsten Refresh generiert.</span>';
|
||||||
}
|
}
|
||||||
if (incident.summary) {
|
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 {
|
} else {
|
||||||
summaryText.innerHTML = '<span style="color:var(--text-disabled);">Noch kein Lagebild. Klicke auf "Aktualisieren" um die Recherche zu starten.</span>';
|
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 {
|
} else {
|
||||||
UI.showToast('Aktualisierung gestartet.', 'success');
|
UI.showToast('Aktualisierung gestartet.', 'success');
|
||||||
var _inc2 = this.incidents.find(function(i) { return i.id === this.currentIncidentId; }.bind(this));
|
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) {
|
} catch (err) {
|
||||||
this._refreshingIncidents.delete(this.currentIncidentId);
|
this._refreshingIncidents.delete(this.currentIncidentId);
|
||||||
@@ -2176,7 +2181,7 @@ async handleRefresh() {
|
|||||||
this._updateSidebarDot(msg.incident_id);
|
this._updateSidebarDot(msg.incident_id);
|
||||||
// Detect first refresh: no summary means first run
|
// Detect first refresh: no summary means first run
|
||||||
const inc = this.incidents.find(i => i.id === msg.incident_id);
|
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)
|
// Update progress state for ALL incidents (sidebar + popup if current)
|
||||||
UI.showProgress(status, msg.data, msg.incident_id, isFirst);
|
UI.showProgress(status, msg.data, msg.incident_id, isFirst);
|
||||||
// Re-render sidebar so status is baked into HTML (survives future re-renders)
|
// Re-render sidebar so status is baked into HTML (survives future re-renders)
|
||||||
|
|||||||
@@ -709,13 +709,27 @@ const UI = {
|
|||||||
return { zusammenfassung, remaining: remaining.trim() };
|
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).
|
* Rendert die Zusammenfassung als HTML (Bullet Points).
|
||||||
*/
|
*/
|
||||||
renderZusammenfassung(text, sourcesJson) {
|
renderZusammenfassung(text, sourcesJson) {
|
||||||
if (!text) return '<span style="color:var(--text-disabled);">Noch keine Zusammenfassung.</span>';
|
if (!text) return '<span style="color:var(--text-disabled);">Noch keine Zusammenfassung.</span>';
|
||||||
let sources = [];
|
const sources = this._parseSources(sourcesJson);
|
||||||
try { sources = JSON.parse(sourcesJson || '[]'); } catch(e) {}
|
|
||||||
// Nur Bullet-Point-Zeilen behalten, Fliesstext herausfiltern
|
// Nur Bullet-Point-Zeilen behalten, Fliesstext herausfiltern
|
||||||
const bulletLines = text.split("\n").filter(line => line.trim().startsWith("- "));
|
const bulletLines = text.split("\n").filter(line => line.trim().startsWith("- "));
|
||||||
const bulletText = bulletLines.length > 0 ? bulletLines.join("\n") : text;
|
const bulletText = bulletLines.length > 0 ? bulletLines.join("\n") : text;
|
||||||
@@ -751,8 +765,7 @@ const UI = {
|
|||||||
*/
|
*/
|
||||||
renderLatestDevelopments(text, sourcesJson) {
|
renderLatestDevelopments(text, sourcesJson) {
|
||||||
if (!text) return '<span style="color:var(--text-disabled);">Noch keine Entwicklungen erfasst.</span>';
|
if (!text) return '<span style="color:var(--text-disabled);">Noch keine Entwicklungen erfasst.</span>';
|
||||||
let sources = [];
|
const sources = this._parseSources(sourcesJson);
|
||||||
try { sources = JSON.parse(sourcesJson || '[]'); } catch(e) {}
|
|
||||||
|
|
||||||
const bulletLines = text.split("\n").map(l => l.trim()).filter(l => l && (l.startsWith("- ") || l.startsWith("[")));
|
const bulletLines = text.split("\n").map(l => l.trim()).filter(l => l && (l.startsWith("- ") || l.startsWith("[")));
|
||||||
if (bulletLines.length === 0) {
|
if (bulletLines.length === 0) {
|
||||||
@@ -869,8 +882,7 @@ const UI = {
|
|||||||
renderSummary(summary, sourcesJson, incidentType) {
|
renderSummary(summary, sourcesJson, incidentType) {
|
||||||
if (!summary) return '<span style="color:var(--text-tertiary);">Noch keine Zusammenfassung.</span>';
|
if (!summary) return '<span style="color:var(--text-tertiary);">Noch keine Zusammenfassung.</span>';
|
||||||
|
|
||||||
let sources = [];
|
const sources = this._parseSources(sourcesJson);
|
||||||
try { sources = JSON.parse(sourcesJson || '[]'); } catch(e) {}
|
|
||||||
|
|
||||||
// Markdown-Rendering
|
// Markdown-Rendering
|
||||||
let html = this.escape(summary);
|
let html = this.escape(summary);
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren