diff --git a/src/agents/analyzer.py b/src/agents/analyzer.py
index 8cd3172..e961426 100644
--- a/src/agents/analyzer.py
+++ b/src/agents/analyzer.py
@@ -30,6 +30,7 @@ STRUKTUR:
- Wenn verschiedene Aspekte oder Themenfelder aufkommen (z.B. Ereignis + Reaktionen + Hintergrund): Gliedere mit kurzen Markdown-Zwischenüberschriften (##)
- Wenn sich Daten strukturiert vergleichen lassen (z.B. Produkte, Unternehmen, Kennzahlen, Modelle), verwende eine Markdown-Tabelle (| Spalte1 | Spalte2 | ... mit Trennzeile |---|---|)
- Die Entscheidung liegt bei dir — Überschriften und Tabellen nur wenn sie dem Leser helfen
+- ERZEUGE KEINE Sektion "## ZUSAMMENFASSUNG", "## ÜBERBLICK" oder "## KERNPUNKTE". Die neuesten Entwicklungen werden separat als eigene Kachel aufbereitet und dürfen im Lagebild NICHT dupliziert werden. Steige direkt mit dem Fließtext oder der ersten inhaltlichen Zwischenüberschrift ein.
REGELN:
- Neutral und sachlich - keine Wertungen oder Spekulationen
@@ -43,7 +44,7 @@ REGELN:
- Ältere Quellen zeitlich einordnen (z.B. "laut einem Bericht vom Januar", "Anfang Februar berichtete...")
Antworte AUSSCHLIESSLICH als JSON-Objekt mit diesen Feldern:
-- "summary": Zusammenfassung auf {output_language} mit Quellenverweisen [1], [2] etc. im Text (Markdown-Überschriften ## erlaubt wenn sinnvoll)
+- "summary": Zusammenfassung auf {output_language} mit Quellenverweisen [1], [2] etc. im Text (Markdown-Überschriften ## erlaubt wenn sinnvoll, aber KEINE "## ZUSAMMENFASSUNG"/"## ÜBERBLICK"-Sektion)
- "sources": Array von Quellenobjekten, je: {{"nr": 1, "name": "Quellenname", "url": "https://..."}}
- "key_facts": Array von bestätigten Kernfakten (Strings, in Ausgabesprache)
- "translations": Array von Objekten mit "article_id", "headline_de", "content_de" (nur für fremdsprachige Artikel)
@@ -133,6 +134,7 @@ STRUKTUR:
- Fließtext oder mit Markdown-Zwischenüberschriften (##) — je nach Komplexität
- Wenn sich Daten strukturiert vergleichen lassen (z.B. Produkte, Unternehmen, Kennzahlen, Modelle), verwende eine Markdown-Tabelle (| Spalte1 | Spalte2 | ... mit Trennzeile |---|---|)
- KEIN Fettdruck (**) verwenden
+- ERZEUGE KEINE Sektion "## ZUSAMMENFASSUNG", "## ÜBERBLICK" oder "## KERNPUNKTE". Falls das BISHERIGE LAGEBILD eine solche Sektion enthält, ENTFERNE sie vollständig beim Aktualisieren. Die neuesten Entwicklungen werden separat als eigene Kachel gepflegt und dürfen im Lagebild NICHT dupliziert werden.
REGELN:
- Neutral und sachlich - keine Wertungen oder Spekulationen
@@ -202,6 +204,41 @@ Antworte AUSSCHLIESSLICH als JSON-Objekt mit diesen Feldern:
Antworte NUR mit dem JSON-Objekt. Keine Einleitung, keine Erklärung."""
+LATEST_DEVELOPMENTS_PROMPT_TEMPLATE = """Du pflegst eine Kachel "Neueste Entwicklungen" für eine Live-Monitoring-Lage.
+HEUTIGES DATUM: {today}
+AUSGABESPRACHE: {output_language}
+WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss).
+
+LAGE: {title}
+KONTEXT: {description}
+
+BISHERIGE ENTWICKLUNGEN (chronologisch absteigend, neueste oben):
+{previous_developments}
+
+NEUE MELDUNGEN SEIT DEM LETZTEN UPDATE:
+{new_articles_text}
+
+AUFTRAG:
+Extrahiere aus den NEUEN Meldungen konkrete Ereignisse und aktualisiere die Liste. Fasse die bisherigen und neuen Ereignisse zu EINER Liste zusammen (max. 8 Bullets, neueste oben).
+
+REGELN:
+- Jedes Bullet = EIN konkretes Ereignis (1-2 Sätze, faktenbasiert). Keine Themen-Zusammenfassungen.
+- Jedes Bullet beginnt mit dem Zeitstempel der frühesten belegenden Quelle im Format "[DD.MM. HH:MM]".
+- Wenn mehrere Meldungen dasselbe Ereignis betreffen: EIN Bullet, Zeitstempel = frühester Zeitpunkt.
+- Bestehende Bullets aus BISHERIGE ENTWICKLUNGEN sinngemäß übernehmen, NICHT umformulieren. Nur entfernen, wenn sie durch neue Meldungen nachweislich überholt sind oder die 8-Bullet-Grenze überschritten wird (dann älteste fallen raus).
+- Wenn eine Quelle eine erkennbare politische Ausrichtung hat (z.B. pro-russisch, staatsnah, rechtsextrem), im Bullet-Text erwähnen ("laut pro-russischem Telegram-Kanal Rybar...").
+- Neutral und sachlich — keine Wertungen oder Spekulationen.
+- KEINE Gedankenstriche (—, –) — stattdessen Kommas, Doppelpunkte oder neue Sätze.
+- Bei widersprüchlichen Angaben beide Seiten knapp nennen.
+- KEINE Einleitung, KEINE Überschrift, KEINE Nachbemerkungen.
+- Wenn aus den neuen Meldungen kein neues Ereignis extrahierbar ist: BISHERIGE ENTWICKLUNGEN unverändert zurückgeben.
+
+OUTPUT-FORMAT (ausschliesslich, keine Anführungszeichen, kein Code-Fence):
+- [DD.MM. HH:MM] Erster Ereignistext.
+- [DD.MM. HH:MM] Zweiter Ereignistext.
+..."""
+
+
class AnalyzerAgent:
"""Analysiert und übersetzt Meldungen über Claude CLI."""
@@ -336,6 +373,70 @@ class AnalyzerAgent:
logger.error(f"Inkrementelle Analyse-Fehler: {e}")
return None, None
+ async def generate_latest_developments(
+ self,
+ title: str,
+ description: str,
+ new_articles: list[dict],
+ previous_developments: str | None,
+ ) -> tuple[str | None, ClaudeUsage | None]:
+ """Pflegt die Kachel 'Neueste Entwicklungen' für Live-Monitoring-Lagen.
+
+ Gibt Markdown-Bullets mit Zeitstempel zurück (max 8, neueste oben).
+ Wenn keine neuen Artikel vorliegen, werden die bisherigen Bullets unverändert zurückgegeben.
+ """
+ prev = (previous_developments or "").strip()
+ if not new_articles:
+ return (prev or None), None
+
+ from config import OUTPUT_LANGUAGE, CLAUDE_MODEL_FAST
+ today = datetime.now(TIMEZONE).strftime("%d.%m.%Y")
+ new_articles_text = self._format_articles_text(new_articles, max_articles=25)
+ prev_block = prev if prev else "(noch keine Einträge)"
+
+ prompt = LATEST_DEVELOPMENTS_PROMPT_TEMPLATE.format(
+ title=title,
+ description=description or "Keine weiteren Details",
+ previous_developments=prev_block,
+ new_articles_text=new_articles_text,
+ today=today,
+ output_language=OUTPUT_LANGUAGE,
+ )
+
+ try:
+ result, usage = await call_claude(prompt, tools=None, model=CLAUDE_MODEL_FAST, raw_text=True)
+ except Exception as e:
+ logger.error(f"Latest-Developments-Fehler: {e}")
+ return (prev or None), None
+
+ bullets = self._parse_latest_developments(result)
+ if not bullets:
+ logger.info("Latest-Developments: keine Bullets geparst, behalte bisherigen Stand")
+ return (prev or None), usage
+
+ bullets = bullets[:8]
+ output = "\n".join(bullets)
+ logger.info(f"Latest-Developments: {len(bullets)} Bullets generiert")
+ return output, usage
+
+ @staticmethod
+ def _parse_latest_developments(text: str) -> list[str]:
+ """Extrahiert '- [DD.MM. HH:MM] ...'-Zeilen aus der Claude-Antwort."""
+ if not text:
+ return []
+ bullets: list[str] = []
+ bullet_re = re.compile(r"^\s*[-*•]\s*\[(\d{1,2}\.\d{1,2}\.(?:\d{2,4})?\s+\d{1,2}:\d{2})\]\s*(.+?)\s*$")
+ for raw_line in text.splitlines():
+ line = raw_line.strip()
+ if not line:
+ continue
+ m = bullet_re.match(line)
+ if m:
+ ts = m.group(1)
+ body = m.group(2).rstrip()
+ bullets.append(f"- [{ts}] {body}")
+ return bullets
+
def _sanitize_sources(self, analysis: dict) -> dict:
"""Entfernt Buchstaben-Suffixe aus Quellennummern (z.B. '1383a' -> 1383).
diff --git a/src/agents/orchestrator.py b/src/agents/orchestrator.py
index 74e4f8e..3b9183c 100644
--- a/src/agents/orchestrator.py
+++ b/src/agents/orchestrator.py
@@ -674,6 +674,7 @@ class AgentOrchestrator:
tenant_id = incident["tenant_id"] if "tenant_id" in incident.keys() else None
previous_summary = incident["summary"] or ""
previous_sources_json = incident["sources_json"] if "sources_json" in incident.keys() else None
+ previous_developments = incident["latest_developments"] if "latest_developments" in incident.keys() else None
# Bei Retry: vorherigen running-Eintrag als error markieren
if retry_count > 0:
@@ -1233,6 +1234,25 @@ class AgentOrchestrator:
# Cancel-Check nach paralleler Verarbeitung
self._check_cancelled(incident_id)
+ # --- Neueste Entwicklungen (nur Live-Monitoring / adhoc) ---
+ if incident_type == "adhoc" and new_articles_for_analysis:
+ try:
+ dev_analyzer = AnalyzerAgent()
+ dev_text, dev_usage = await dev_analyzer.generate_latest_developments(
+ title, description, new_articles_for_analysis, previous_developments,
+ )
+ if dev_usage:
+ usage_acc.add(dev_usage)
+ if dev_text is not None:
+ await db.execute(
+ "UPDATE incidents SET latest_developments = ? WHERE id = ?",
+ (dev_text, incident_id),
+ )
+ await db.commit()
+ previous_developments = dev_text
+ except Exception as e:
+ logger.warning(f"Latest-Developments-Generator fehlgeschlagen: {e}")
+
# Cancel-Check nach Analyse+Faktencheck
self._check_cancelled(incident_id)
diff --git a/src/database.py b/src/database.py
index a22891b..a6937d0 100644
--- a/src/database.py
+++ b/src/database.py
@@ -369,6 +369,11 @@ async def init_db():
await db.commit()
logger.info("Migration: refresh_start_time zu incidents hinzugefuegt (bestehende Auto-Lagen auf 07:00)")
+ if "latest_developments" not in columns:
+ await db.execute("ALTER TABLE incidents ADD COLUMN latest_developments TEXT")
+ await db.commit()
+ logger.info("Migration: latest_developments zu incidents hinzugefuegt")
+
# Migration: Token-Spalten fuer refresh_log
cursor = await db.execute("PRAGMA table_info(refresh_log)")
rl_columns = [row[1] for row in await cursor.fetchall()]
diff --git a/src/models.py b/src/models.py
index d7bbc58..9ec638b 100644
--- a/src/models.py
+++ b/src/models.py
@@ -89,6 +89,7 @@ class IncidentResponse(BaseModel):
retention_days: int
visibility: str = "public"
summary: Optional[str]
+ latest_developments: Optional[str] = None
sources_json: Optional[str] = None
international_sources: bool = True
include_telegram: bool = False
diff --git a/src/static/js/app.js b/src/static/js/app.js
index 7eece18..ff38356 100644
--- a/src/static/js/app.js
+++ b/src/static/js/app.js
@@ -850,25 +850,43 @@ const App = {
const zusammenfassungText = document.getElementById('zusammenfassung-text');
const summaryText = document.getElementById('summary-text');
const zusammenfassungCard = document.getElementById('zusammenfassung-card');
+ const zusammenfassungTitle = zusammenfassungCard ? zusammenfassungCard.querySelector('.card-title') : null;
- if (incident.summary && incident.type === 'research') {
- const { zusammenfassung, remaining } = UI.extractZusammenfassung(incident.summary);
- if (zusammenfassung) {
- if (zusammenfassungText) zusammenfassungText.innerHTML = UI.renderZusammenfassung(zusammenfassung, incident.sources_json);
- if (zusammenfassungCard) zusammenfassungCard.style.display = '';
- summaryText.innerHTML = UI.renderSummary(remaining, incident.sources_json, incident.type);
+ if (incident.type === 'research') {
+ // Recherche: ZUSAMMENFASSUNG-Sektion aus Briefing extrahieren
+ if (zusammenfassungTitle) zusammenfassungTitle.textContent = 'Zusammenfassung';
+ if (zusammenfassungTitle) zusammenfassungTitle.setAttribute('onclick', "openContentModal('Zusammenfassung', 'zusammenfassung-content')");
+ if (incident.summary) {
+ const { zusammenfassung, remaining } = UI.extractZusammenfassung(incident.summary);
+ if (zusammenfassung) {
+ if (zusammenfassungText) zusammenfassungText.innerHTML = UI.renderZusammenfassung(zusammenfassung, incident.sources_json);
+ if (zusammenfassungCard) zusammenfassungCard.style.display = '';
+ summaryText.innerHTML = UI.renderSummary(remaining, incident.sources_json, 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);
+ }
} 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);
+ if (zusammenfassungCard) zusammenfassungCard.style.display = 'none';
+ summaryText.innerHTML = 'Noch keine Zusammenfassung. Klicke auf "Aktualisieren" um die Recherche zu starten.';
}
- } else if (incident.summary) {
- // Adhoc/Live: Keine Zusammenfassung-Kachel
- if (zusammenfassungCard) zusammenfassungCard.style.display = 'none';
- summaryText.innerHTML = UI.renderSummary(incident.summary, incident.sources_json, incident.type);
} else {
- if (zusammenfassungCard) zusammenfassungCard.style.display = 'none';
- summaryText.innerHTML = 'Noch keine Zusammenfassung. Klicke auf "Aktualisieren" um die Recherche zu starten.';
+ // Live-Monitoring (adhoc): Kachel zeigt "Neueste Entwicklungen" (max 8 Bullets mit Zeitstempel)
+ if (zusammenfassungTitle) zusammenfassungTitle.textContent = 'Neueste Entwicklungen';
+ if (zusammenfassungTitle) zusammenfassungTitle.setAttribute('onclick', "openContentModal('Neueste Entwicklungen', 'zusammenfassung-content')");
+ if (zusammenfassungCard) zusammenfassungCard.style.display = '';
+ const devText = (incident.latest_developments || '').trim();
+ if (devText) {
+ if (zusammenfassungText) zusammenfassungText.innerHTML = UI.renderZusammenfassung(devText, incident.sources_json);
+ } 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);
+ } else {
+ summaryText.innerHTML = 'Noch kein Lagebild. Klicke auf "Aktualisieren" um die Recherche zu starten.';
+ }
}
// Meta (im Header-Strip) — relative Zeitangabe mit vollem Datum als Tooltip