Commits vergleichen
9 Commits
b02578e48b
...
main
| Autor | SHA1 | Datum | |
|---|---|---|---|
| 7d9bca12ee | |||
|
|
3e64539aa3 | ||
|
|
1647a6f50a | ||
|
|
c53e260c6c | ||
| c3a0ee4538 | |||
|
|
e20b3de0fa | ||
| aa36a9a38f | |||
|
|
d570e13dc6 | ||
|
|
7777b77abd |
@@ -124,7 +124,7 @@ BISHERIGE QUELLEN:
|
|||||||
AUFTRAG:
|
AUFTRAG:
|
||||||
1. Aktualisiere das Lagebild basierend auf den neuen Meldungen. Das Lagebild soll so ausführlich wie nötig sein, um alle wesentlichen Themenstränge abzudecken
|
1. Aktualisiere das Lagebild basierend auf den neuen Meldungen. Das Lagebild soll so ausführlich wie nötig sein, um alle wesentlichen Themenstränge abzudecken
|
||||||
2. Behalte bestätigte Fakten aus dem bisherigen Lagebild bei
|
2. Behalte bestätigte Fakten aus dem bisherigen Lagebild bei
|
||||||
3. Ergänze neue Erkenntnisse und markiere wichtige neue Entwicklungen
|
3. Arbeite neue Erkenntnisse direkt in den thematisch passenden Abschnitt ein. Erzeuge KEINE datierten Verlaufsblöcke wie "Neu am DD.MM." oder "Neu seit ...". Das Lagebild ist eine zusammenhängende thematische Darstellung des AKTUELLEN Stands, kein chronologisches Änderungsprotokoll. Die zeitliche Abfolge der jüngsten Ereignisse wird separat in der Kachel "Neueste Entwicklungen" gepflegt und darf hier NICHT als Datums-Changelog dupliziert werden
|
||||||
4. Aktualisiere die Quellenverweise — neue Quellen bekommen fortlaufende Nummern nach den bisherigen
|
4. Aktualisiere die Quellenverweise — neue Quellen bekommen fortlaufende Nummern nach den bisherigen
|
||||||
5. Entferne nur nachweislich widerlegte Informationen. Behalte alle thematischen Abschnitte bei, auch wenn sie nicht durch neue Meldungen aktualisiert werden
|
5. Entferne nur nachweislich widerlegte Informationen. Behalte alle thematischen Abschnitte bei, auch wenn sie nicht durch neue Meldungen aktualisiert werden
|
||||||
|
|
||||||
@@ -133,6 +133,8 @@ STRUKTUR:
|
|||||||
- Wenn sich Daten strukturiert vergleichen lassen (z.B. Produkte, Unternehmen, Kennzahlen, Modelle), verwende eine Markdown-Tabelle (| Spalte1 | Spalte2 | ... mit Trennzeile |---|---|)
|
- Wenn sich Daten strukturiert vergleichen lassen (z.B. Produkte, Unternehmen, Kennzahlen, Modelle), verwende eine Markdown-Tabelle (| Spalte1 | Spalte2 | ... mit Trennzeile |---|---|)
|
||||||
- KEIN Fettdruck (**) verwenden
|
- 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.
|
- 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.
|
||||||
|
- KEINE datierten Verlaufsmarker im Lagebild. Einleitungen wie "Neu am 31.05./01.06.:", "Neu seit gestern:" oder vergleichbare Datums-Changelog-Phrasen sind nicht erlaubt. Falls das BISHERIGE LAGEBILD solche Blöcke enthält, LÖSE SIE AUF: integriere ihren Inhalt in den thematisch passenden Abschnitt und ENTFERNE die "Neu am"-Einleitung samt reiner Datumsgruppierung restlos. Innerhalb eines Abschnitts steht der aktuelle Stand vorne, ältere Belege werden im Fließtext zeitlich eingeordnet (z.B. "Ende Mai berichtete ...").
|
||||||
|
- KEINE stichwortartigen Fragmente und KEINE blanken Quellennummern-Sammlungen. Verboten sind Telegramm-Verkürzungen wie "Teheran-Bluff-Vorwurf [2897]. NYT-Abraham-Accords [2890]." sowie Auffangblöcke ohne Aussage wie "Frühere Belege [2806][2807]...". Jede Quellennummer muss an einem vollständigen, eigenständigen Satz hängen. Falls das BISHERIGE LAGEBILD solche Fragment- oder Sammelblöcke enthält, formuliere sie zu vollständigen Sätzen aus oder lass die betreffende Quellennummer weg. Am Ende eines Abschnitts oder des Lagebildes darf KEINE reine Aufzählung von Quellennummern stehen.
|
||||||
|
|
||||||
REGELN:
|
REGELN:
|
||||||
- Neutral und sachlich - keine Wertungen oder Spekulationen
|
- Neutral und sachlich - keine Wertungen oder Spekulationen
|
||||||
|
|||||||
@@ -1753,6 +1753,7 @@ class AgentOrchestrator:
|
|||||||
# Idempotent: nur Artikel ohne headline_de/content_de werden geholt.
|
# Idempotent: nur Artikel ohne headline_de/content_de werden geholt.
|
||||||
# Lauft nach der Analyse (Lagebild ist schon committed) und vor QC
|
# Lauft nach der Analyse (Lagebild ist schon committed) und vor QC
|
||||||
# (damit normalize_umlaut_articles auch die frischen DE-Texte fasst).
|
# (damit normalize_umlaut_articles auch die frischen DE-Texte fasst).
|
||||||
|
_translate_step_started = False
|
||||||
try:
|
try:
|
||||||
tr_cursor = await db.execute(
|
tr_cursor = await db.execute(
|
||||||
"""SELECT id, headline, content_original, language
|
"""SELECT id, headline, content_original, language
|
||||||
@@ -1764,7 +1765,10 @@ class AgentOrchestrator:
|
|||||||
(incident_id,),
|
(incident_id,),
|
||||||
)
|
)
|
||||||
pending_translations = [dict(r) for r in await tr_cursor.fetchall()]
|
pending_translations = [dict(r) for r in await tr_cursor.fetchall()]
|
||||||
if pending_translations:
|
if pending_translations and translator_enabled:
|
||||||
|
# Pipeline-Schritt 9: Artikel uebersetzen (nur sichtbar wenn was zu uebersetzen)
|
||||||
|
await _pipe_start("translate")
|
||||||
|
_translate_step_started = True
|
||||||
logger.info(
|
logger.info(
|
||||||
"Translator fuer Incident %d: %d Artikel ohne DE-Uebersetzung",
|
"Translator fuer Incident %d: %d Artikel ohne DE-Uebersetzung",
|
||||||
incident_id, len(pending_translations),
|
incident_id, len(pending_translations),
|
||||||
@@ -1795,8 +1799,11 @@ class AgentOrchestrator:
|
|||||||
"Translator fuer Incident %d: %d/%d Artikel uebersetzt",
|
"Translator fuer Incident %d: %d/%d Artikel uebersetzt",
|
||||||
incident_id, len(translations), len(pending_translations),
|
incident_id, len(translations), len(pending_translations),
|
||||||
)
|
)
|
||||||
|
await _pipe_done("translate", count_value=len(translations), count_secondary=len(pending_translations))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Translator-Fehler fuer Incident %d: %s", incident_id, e, exc_info=True)
|
logger.error("Translator-Fehler fuer Incident %d: %s", incident_id, e, exc_info=True)
|
||||||
|
if _translate_step_started:
|
||||||
|
await _pipe_done("translate", count_value=0, count_secondary=0)
|
||||||
# Refresh trotz Translator-Fehler weiterlaufen lassen
|
# Refresh trotz Translator-Fehler weiterlaufen lassen
|
||||||
|
|
||||||
# --- Neueste Entwicklungen (nur Live-Monitoring / adhoc) ---
|
# --- Neueste Entwicklungen (nur Live-Monitoring / adhoc) ---
|
||||||
|
|||||||
@@ -252,8 +252,8 @@ async def cleanup_expired():
|
|||||||
# vorzeitig gekillt werden. Ein Refresh gilt als verwaist, wenn entweder
|
# vorzeitig gekillt werden. Ein Refresh gilt als verwaist, wenn entweder
|
||||||
# (a) seit ORPHAN_IDLE_LIMIT Min kein Pipeline-Step Fortschritt zeigte,
|
# (a) seit ORPHAN_IDLE_LIMIT Min kein Pipeline-Step Fortschritt zeigte,
|
||||||
# oder (b) das harte Limit ORPHAN_HARD_LIMIT Min ueberschritten wurde.
|
# oder (b) das harte Limit ORPHAN_HARD_LIMIT Min ueberschritten wurde.
|
||||||
ORPHAN_IDLE_LIMIT = 30
|
ORPHAN_IDLE_LIMIT = 60
|
||||||
ORPHAN_HARD_LIMIT = 90
|
ORPHAN_HARD_LIMIT = 120
|
||||||
cursor = await db.execute(
|
cursor = await db.execute(
|
||||||
"SELECT id, incident_id, started_at FROM refresh_log WHERE status = 'running'"
|
"SELECT id, incident_id, started_at FROM refresh_log WHERE status = 'running'"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import re
|
|||||||
import uuid
|
import uuid
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from html import escape as _html_escape
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pikepdf
|
import pikepdf
|
||||||
@@ -153,6 +154,66 @@ def _markdown_to_html(text: str) -> str:
|
|||||||
return '\n'.join(result)
|
return '\n'.join(result)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_developments_for_export(text: str) -> list[tuple[str, str]]:
|
||||||
|
"""Parst die 'Neuesten Entwicklungen' (latest_developments) fuer den Export.
|
||||||
|
|
||||||
|
Eingabeformat je Eintrag: '- [DD.MM. HH:MM] Text {Quelle|URL, ...}'.
|
||||||
|
Liefert (datum_label, body) je Eintrag in gespeicherter Reihenfolge.
|
||||||
|
Quellen-Klammern und [N]-Zitate werden entfernt — der Export zeigt bewusst
|
||||||
|
KEINE Links. Das gespeicherte Format enthaelt kein Jahr; fehlt es, wird das
|
||||||
|
aktuelle Jahr ergaenzt (Live-Monitoring-Berichte sind tagesaktuell).
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return []
|
||||||
|
year2 = datetime.now(TIMEZONE).strftime("%y")
|
||||||
|
bullet_re = re.compile(
|
||||||
|
r"^\s*(?:[-*•]\s*)?\[\s*(\d{1,2})\.(\d{1,2})\.?(?:(\d{2,4}))?\s+(\d{1,2}:\d{2})\s*\]\s*(.+?)\s*$"
|
||||||
|
)
|
||||||
|
trailing_braces = re.compile(r"\s*\{[^{}]*\}\s*\.?\s*$")
|
||||||
|
citation_re = re.compile(r"\s*\[\d{1,5}[a-z]?\]")
|
||||||
|
result: list[tuple[str, str]] = []
|
||||||
|
for raw in text.splitlines():
|
||||||
|
line = raw.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
m = bullet_re.match(line)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
day, month, year, time = m.group(1), m.group(2), m.group(3), m.group(4)
|
||||||
|
body = m.group(5).strip()
|
||||||
|
# Quellen-Klammer am Ende und Inline-[N]-Zitate entfernen (keine Links)
|
||||||
|
body = trailing_braces.sub("", body).strip()
|
||||||
|
body = citation_re.sub("", body).strip()
|
||||||
|
if not body:
|
||||||
|
continue
|
||||||
|
yy = year[-2:] if year else year2
|
||||||
|
label = f"{int(day):02d}.{int(month):02d}.{yy}, {time} Uhr"
|
||||||
|
result.append((label, body))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _format_latest_developments_html(text: str) -> str:
|
||||||
|
"""Rendert die 'Neuesten Entwicklungen' als HTML-Block fuer den PDF-Export.
|
||||||
|
|
||||||
|
Pro Eintrag: Datum/Uhrzeit-Zeile, darunter (eigener Absatz) der Meldungstext.
|
||||||
|
Keine Quellen-Links. Faellt bei nicht-parsebarem Text auf _markdown_to_html zurueck.
|
||||||
|
"""
|
||||||
|
pairs = _parse_developments_for_export(text)
|
||||||
|
if not pairs:
|
||||||
|
return _markdown_to_html(text)
|
||||||
|
blocks = []
|
||||||
|
for label, body in pairs:
|
||||||
|
body_html = _html_escape(body)
|
||||||
|
body_html = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', body_html)
|
||||||
|
blocks.append(
|
||||||
|
'<div class="dev-entry">'
|
||||||
|
f'<div class="dev-entry-date">{_html_escape(label)}</div>'
|
||||||
|
f'<div class="dev-entry-body">{body_html}</div>'
|
||||||
|
'</div>'
|
||||||
|
)
|
||||||
|
return "\n".join(blocks)
|
||||||
|
|
||||||
|
|
||||||
def _truncate_lagebild(summary_text: str, max_chars: int = 4000) -> str:
|
def _truncate_lagebild(summary_text: str, max_chars: int = 4000) -> str:
|
||||||
"""Lagebild für den Lagebericht auf die Zusammenfassung kürzen.
|
"""Lagebild für den Lagebericht auf die Zusammenfassung kürzen.
|
||||||
|
|
||||||
@@ -479,6 +540,10 @@ def _build_export_metadata(
|
|||||||
subject = (incident.get("description") or "").strip()
|
subject = (incident.get("description") or "").strip()
|
||||||
if not subject:
|
if not subject:
|
||||||
subject = f"{type_label} zu: {title_raw}"
|
subject = f"{type_label} zu: {title_raw}"
|
||||||
|
# DOCX-Core-Property "subject" erzwingt ein 255-Zeichen-Limit; laengere
|
||||||
|
# Beschreibungen wuerden den Word-Export sonst mit ValueError abbrechen.
|
||||||
|
if len(subject) > 255:
|
||||||
|
subject = subject[:255]
|
||||||
|
|
||||||
# Keywords sammeln (Reihenfolge relevant für Anzeige, Dedup mit dict.fromkeys)
|
# Keywords sammeln (Reihenfolge relevant für Anzeige, Dedup mit dict.fromkeys)
|
||||||
keywords: list[str] = ["OSINT", type_label]
|
keywords: list[str] = ["OSINT", type_label]
|
||||||
@@ -711,22 +776,33 @@ async def generate_pdf(
|
|||||||
else: # full
|
else: # full
|
||||||
sections = {"zusammenfassung", "bericht", "faktencheck", "quellen", "timeline"}
|
sections = {"zusammenfassung", "bericht", "faktencheck", "quellen", "timeline"}
|
||||||
|
|
||||||
# Fuer Research-Lagen: Zusammenfassung aus dem Bericht extrahieren
|
# Zusammenfassungs-Quelle bestimmen:
|
||||||
|
# - Research: ZUSAMMENFASSUNG/UEBERBLICK aus dem Bericht extrahieren.
|
||||||
|
# - Live-Monitoring (adhoc): "Neueste Entwicklungen" aus latest_developments,
|
||||||
|
# ohne Quellen-Links, Datum/Uhrzeit als eigene Zeile.
|
||||||
|
# - sonst: KI-Executive-Summary (executive_summary_html).
|
||||||
is_research = incident.get("type") == "research"
|
is_research = incident.get("type") == "research"
|
||||||
all_sources = _prepare_sources(incident)
|
all_sources = _prepare_sources(incident)
|
||||||
|
latest_dev = (incident.get("latest_developments") or "").strip()
|
||||||
zusammenfassung_html = executive_summary_html
|
zusammenfassung_html = executive_summary_html
|
||||||
bericht_summary = incident.get("summary", "")
|
bericht_summary = incident.get("summary", "")
|
||||||
zusammenfassung_title = "Zusammenfassung"
|
zusammenfassung_title = "Zusammenfassung"
|
||||||
|
summary_has_links = True
|
||||||
|
|
||||||
if is_research and bericht_summary:
|
if is_research and bericht_summary:
|
||||||
extracted_html, remaining = _extract_zusammenfassung(bericht_summary, all_sources)
|
extracted_html, remaining = _extract_zusammenfassung(bericht_summary, all_sources)
|
||||||
if extracted_html:
|
if extracted_html:
|
||||||
zusammenfassung_html = extracted_html
|
zusammenfassung_html = extracted_html
|
||||||
zusammenfassung_title = "Zusammenfassung"
|
|
||||||
bericht_summary = remaining
|
bericht_summary = remaining
|
||||||
|
elif not is_research and latest_dev:
|
||||||
|
dev_html = _format_latest_developments_html(latest_dev)
|
||||||
|
if dev_html:
|
||||||
|
zusammenfassung_html = dev_html
|
||||||
|
zusammenfassung_title = "Neueste Entwicklungen"
|
||||||
|
summary_has_links = False # Quellen bewusst entfernt
|
||||||
|
|
||||||
# Auch das (nicht-research) Executive Summary linkifizieren — ggf. enthaelt es Zitate
|
# KI-/Research-Zusammenfassung linkifizieren; Developments bleiben linkfrei
|
||||||
if not is_research and zusammenfassung_html:
|
if not is_research and summary_has_links and zusammenfassung_html:
|
||||||
zusammenfassung_html = _linkify_citations_html(zusammenfassung_html, all_sources)
|
zusammenfassung_html = _linkify_citations_html(zusammenfassung_html, all_sources)
|
||||||
|
|
||||||
meta = _build_export_metadata(
|
meta = _build_export_metadata(
|
||||||
@@ -799,20 +875,27 @@ async def generate_docx(
|
|||||||
else: # full
|
else: # full
|
||||||
sections = {"zusammenfassung", "bericht", "faktencheck", "quellen", "timeline"}
|
sections = {"zusammenfassung", "bericht", "faktencheck", "quellen", "timeline"}
|
||||||
|
|
||||||
# Fuer Research-Lagen: Zusammenfassung aus dem Bericht extrahieren
|
# Zusammenfassungs-Quelle bestimmen (analog generate_pdf):
|
||||||
|
# Research -> Bericht-Extrakt, Live-Monitoring -> "Neueste Entwicklungen", sonst KI.
|
||||||
is_research = incident.get("type") == "research"
|
is_research = incident.get("type") == "research"
|
||||||
all_sources = _prepare_sources(incident)
|
all_sources = _prepare_sources(incident)
|
||||||
|
latest_dev = (incident.get("latest_developments") or "").strip()
|
||||||
zusammenfassung_text = executive_summary_text
|
zusammenfassung_text = executive_summary_text
|
||||||
bericht_summary = incident.get("summary") or "Keine Zusammenfassung verfügbar."
|
bericht_summary = incident.get("summary") or "Keine Zusammenfassung verfügbar."
|
||||||
zusammenfassung_title = "Zusammenfassung"
|
zusammenfassung_title = "Zusammenfassung"
|
||||||
zusammenfassung_lines: list[str] = []
|
zusammenfassung_lines: list[str] = []
|
||||||
|
zusammenfassung_developments: list[tuple[str, str]] = []
|
||||||
|
|
||||||
if is_research and bericht_summary:
|
if is_research and bericht_summary:
|
||||||
extracted_lines, remaining = _extract_zusammenfassung_lines(bericht_summary)
|
extracted_lines, remaining = _extract_zusammenfassung_lines(bericht_summary)
|
||||||
if extracted_lines:
|
if extracted_lines:
|
||||||
zusammenfassung_lines = extracted_lines
|
zusammenfassung_lines = extracted_lines
|
||||||
zusammenfassung_title = "Zusammenfassung"
|
|
||||||
bericht_summary = remaining
|
bericht_summary = remaining
|
||||||
|
elif not is_research and latest_dev:
|
||||||
|
dev_pairs = _parse_developments_for_export(latest_dev)
|
||||||
|
if dev_pairs:
|
||||||
|
zusammenfassung_developments = dev_pairs
|
||||||
|
zusammenfassung_title = "Neueste Entwicklungen"
|
||||||
|
|
||||||
meta = _build_export_metadata(
|
meta = _build_export_metadata(
|
||||||
incident, articles, fact_checks, all_sources, creator, scope, sections,
|
incident, articles, fact_checks, all_sources, creator, scope, sections,
|
||||||
@@ -890,11 +973,23 @@ async def generate_docx(
|
|||||||
|
|
||||||
doc.add_page_break()
|
doc.add_page_break()
|
||||||
|
|
||||||
# --- Zusammenfassung / Executive Summary ---
|
# --- Zusammenfassung / Neueste Entwicklungen ---
|
||||||
if "zusammenfassung" in sections:
|
if "zusammenfassung" in sections:
|
||||||
doc.add_heading(zusammenfassung_title, level=1)
|
doc.add_heading(zusammenfassung_title, level=1)
|
||||||
|
|
||||||
if zusammenfassung_lines:
|
if zusammenfassung_developments:
|
||||||
|
# Live-Monitoring: pro Eintrag Datum/Uhrzeit-Zeile + Absatz mit Text, ohne Links
|
||||||
|
for label, body in zusammenfassung_developments:
|
||||||
|
date_para = doc.add_paragraph()
|
||||||
|
date_para.paragraph_format.space_after = Pt(1)
|
||||||
|
run = date_para.add_run(label)
|
||||||
|
run.bold = True
|
||||||
|
run.font.size = Pt(9)
|
||||||
|
run.font.color.rgb = RGBColor(0x0a, 0x18, 0x32)
|
||||||
|
body_para = doc.add_paragraph()
|
||||||
|
body_para.paragraph_format.space_after = Pt(8)
|
||||||
|
body_para.add_run(re.sub(r'\*\*(.+?)\*\*', r'\1', body))
|
||||||
|
elif zusammenfassung_lines:
|
||||||
for line in zusammenfassung_lines:
|
for line in zusammenfassung_lines:
|
||||||
_add_docx_paragraph_with_citations(doc, line, all_sources, style='List Bullet')
|
_add_docx_paragraph_with_citations(doc, line, all_sources, style='List Bullet')
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ body { font-family: -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-se
|
|||||||
.exec-summary ul { margin: 8px 0 0 18px; }
|
.exec-summary ul { margin: 8px 0 0 18px; }
|
||||||
.exec-summary li { margin-bottom: 6px; line-height: 1.6; }
|
.exec-summary li { margin-bottom: 6px; line-height: 1.6; }
|
||||||
|
|
||||||
|
/* Neueste Entwicklungen (Live-Monitoring) */
|
||||||
|
.dev-entry { margin-bottom: 12px; }
|
||||||
|
.dev-entry:last-child { margin-bottom: 0; }
|
||||||
|
.dev-entry-date { font-size: 9pt; font-weight: 600; color: #0a1832; margin-bottom: 2px; }
|
||||||
|
.dev-entry-body { font-size: 10.5pt; line-height: 1.5; }
|
||||||
|
|
||||||
/* Lagebild */
|
/* Lagebild */
|
||||||
.lagebild-content { line-height: 1.7; }
|
.lagebild-content { line-height: 1.7; }
|
||||||
.lagebild-content p { margin-bottom: 8px; }
|
.lagebild-content p { margin-bottom: 8px; }
|
||||||
@@ -99,7 +105,7 @@ tr:nth-child(even) { background: #f8f9fa; }
|
|||||||
<div class="toc">
|
<div class="toc">
|
||||||
<h2>Inhaltsverzeichnis</h2>
|
<h2>Inhaltsverzeichnis</h2>
|
||||||
<ul class="toc-list">
|
<ul class="toc-list">
|
||||||
{% if 'zusammenfassung' in sections %}<li><a href="#sec-zusammenfassung">Zusammenfassung</a></li>{% endif %}
|
{% if 'zusammenfassung' in sections %}<li><a href="#sec-zusammenfassung">{{ zusammenfassung_title }}</a></li>{% endif %}
|
||||||
{% if 'bericht' in sections %}<li><a href="#sec-bericht">{% if incident.type == "research" %}Recherchebericht{% else %}Lagebild{% endif %}</a></li>{% endif %}
|
{% if 'bericht' in sections %}<li><a href="#sec-bericht">{% if incident.type == "research" %}Recherchebericht{% else %}Lagebild{% endif %}</a></li>{% endif %}
|
||||||
{% if 'faktencheck' in sections and fact_checks %}<li><a href="#sec-faktencheck">Faktencheck</a></li>{% endif %}
|
{% if 'faktencheck' in sections and fact_checks %}<li><a href="#sec-faktencheck">Faktencheck</a></li>{% endif %}
|
||||||
{% if 'quellen' in sections and sources %}<li><a href="#sec-quellen">Quellenverzeichnis</a></li>{% endif %}
|
{% if 'quellen' in sections and sources %}<li><a href="#sec-quellen">Quellenverzeichnis</a></li>{% endif %}
|
||||||
|
|||||||
@@ -1232,18 +1232,14 @@ async def export_incident(
|
|||||||
snapshots = [dict(r) for r in await cursor.fetchall()]
|
snapshots = [dict(r) for r in await cursor.fetchall()]
|
||||||
|
|
||||||
# Zusammenfassung fuer den Export:
|
# Zusammenfassung fuer den Export:
|
||||||
# - Bei Adhoc-Lagen primaer "Neueste Entwicklungen" (latest_developments) als Markdown-Bullets,
|
# - Live-Monitoring (adhoc) zeigt primaer "Neueste Entwicklungen" (latest_developments).
|
||||||
# weil Live-Monitoring von Aktualitaet lebt.
|
# Das Rendering (Datum/Uhrzeit als eigene Zeile, ohne Links) uebernimmt der
|
||||||
# - Fallback (oder bei Research): Executive Summary (KI-generiert, gecacht).
|
# Report-Generator direkt aus incident["latest_developments"].
|
||||||
|
# - Executive Summary (KI, gecacht) dient nur als Fallback (oder bei Research-Lagen).
|
||||||
is_adhoc = (incident.get("type") or "adhoc") != "research"
|
is_adhoc = (incident.get("type") or "adhoc") != "research"
|
||||||
latest_dev = (incident.get("latest_developments") or "").strip()
|
latest_dev = (incident.get("latest_developments") or "").strip()
|
||||||
exec_summary = None
|
|
||||||
if is_adhoc and latest_dev:
|
|
||||||
from report_generator import _markdown_to_html as _md_to_html
|
|
||||||
exec_summary = _md_to_html(latest_dev)
|
|
||||||
if not exec_summary:
|
|
||||||
exec_summary = incident.get("executive_summary")
|
exec_summary = incident.get("executive_summary")
|
||||||
if not exec_summary:
|
if not exec_summary and not (is_adhoc and latest_dev):
|
||||||
summary_text = incident.get("summary") or ""
|
summary_text = incident.get("summary") or ""
|
||||||
exec_summary = await generate_executive_summary(summary_text)
|
exec_summary = await generate_executive_summary(summary_text)
|
||||||
await db.execute(
|
await db.execute(
|
||||||
@@ -1251,6 +1247,7 @@ async def export_incident(
|
|||||||
(exec_summary, incident_id),
|
(exec_summary, incident_id),
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
exec_summary = exec_summary or ""
|
||||||
|
|
||||||
date_str = datetime.now(TIMEZONE).strftime("%Y%m%d")
|
date_str = datetime.now(TIMEZONE).strftime("%Y%m%d")
|
||||||
slug = _slugify(incident["title"])
|
slug = _slugify(incident["title"])
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ _PIPELINE_STEPS_DE = [
|
|||||||
"tooltip": "Aus Foren-Quellen (z.B. 5ch, Hatena, Note) wird ein Stimmungsbild der öffentlichen Diskussion extrahiert. Keine Faktenlage, sondern dominante Themen und Bruchlinien."},
|
"tooltip": "Aus Foren-Quellen (z.B. 5ch, Hatena, Note) wird ein Stimmungsbild der öffentlichen Diskussion extrahiert. Keine Faktenlage, sondern dominante Themen und Bruchlinien."},
|
||||||
{"key": "summary", "label": "Lagebild verfassen", "icon": "file-text",
|
{"key": "summary", "label": "Lagebild verfassen", "icon": "file-text",
|
||||||
"tooltip": "Aus allen geprüften Meldungen wird ein zusammenhängendes Lagebild geschrieben, mit Quellenangaben am Text."},
|
"tooltip": "Aus allen geprüften Meldungen wird ein zusammenhängendes Lagebild geschrieben, mit Quellenangaben am Text."},
|
||||||
|
{"key": "translate", "label": "Artikel uebersetzen", "icon": "languages",
|
||||||
|
"tooltip": "Fremdsprachige Meldungen (z.B. japanisch) werden ins Lagebild-Output uebersetzt. Laeuft nur fuer Quellen-Pools mit nicht-deutschen Sprachen und kann bei vielen neuen Artikeln einige Minuten dauern."},
|
||||||
{"key": "qc", "label": "Qualitätscheck", "icon": "check-circle",
|
{"key": "qc", "label": "Qualitätscheck", "icon": "check-circle",
|
||||||
"tooltip": "Eine letzte Kontrollprüfung am Ergebnis: Doppelte Fakten zusammenführen, Karten-Verortung prüfen, bevor du benachrichtigt wirst."},
|
"tooltip": "Eine letzte Kontrollprüfung am Ergebnis: Doppelte Fakten zusammenführen, Karten-Verortung prüfen, bevor du benachrichtigt wirst."},
|
||||||
{"key": "notify", "label": "Benachrichtigen", "icon": "bell",
|
{"key": "notify", "label": "Benachrichtigen", "icon": "bell",
|
||||||
@@ -59,6 +61,8 @@ _PIPELINE_STEPS_EN = [
|
|||||||
"tooltip": "Forum sources (5ch, Hatena, Note, etc.) are summarised into a public-mood overview. Not factual, but dominant themes and fault lines."},
|
"tooltip": "Forum sources (5ch, Hatena, Note, etc.) are summarised into a public-mood overview. Not factual, but dominant themes and fault lines."},
|
||||||
{"key": "summary", "label": "Writing the briefing", "icon": "file-text",
|
{"key": "summary", "label": "Writing the briefing", "icon": "file-text",
|
||||||
"tooltip": "All verified articles are combined into a coherent briefing with inline citations."},
|
"tooltip": "All verified articles are combined into a coherent briefing with inline citations."},
|
||||||
|
{"key": "translate", "label": "Translating articles", "icon": "languages",
|
||||||
|
"tooltip": "Foreign-language articles (e.g. Japanese) are translated into the briefing output language. Runs only when the source pool contains non-target-language items and can take several minutes for large incoming batches."},
|
||||||
{"key": "qc", "label": "Quality check", "icon": "check-circle",
|
{"key": "qc", "label": "Quality check", "icon": "check-circle",
|
||||||
"tooltip": "A final review: consolidate duplicate facts, verify map locations, before you get notified."},
|
"tooltip": "A final review: consolidate duplicate facts, verify map locations, before you get notified."},
|
||||||
{"key": "notify", "label": "Notifying", "icon": "bell",
|
{"key": "notify", "label": "Notifying", "icon": "bell",
|
||||||
|
|||||||
@@ -352,6 +352,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<form id="new-incident-form">
|
<form id="new-incident-form">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="inc-type" data-i18n="modal.field.type">Art der Lage</label>
|
||||||
|
<select id="inc-type" onchange="toggleTypeDefaults()">
|
||||||
|
<option value="adhoc" data-i18n="modal.option.type_adhoc">Live-Monitoring : Ereignis beobachten</option>
|
||||||
|
<option value="research" data-i18n="modal.option.type_research">Recherche : Thema analysieren</option>
|
||||||
|
</select>
|
||||||
|
<div class="form-hint" id="type-hint" data-i18n="modal.hint.type_adhoc">
|
||||||
|
Durchsucht laufend hunderte Nachrichtenquellen nach neuen Meldungen. Empfohlen: Automatische Aktualisierung.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="inc-title" data-i18n="modal.new_incident.title_field">Titel des Vorfalls</label>
|
<label for="inc-title" data-i18n="modal.new_incident.title_field">Titel des Vorfalls</label>
|
||||||
<input type="text" id="inc-title" required aria-required="true" placeholder="z.B. Explosion in Madrid" data-i18n-attr="placeholder:modal.placeholder.title">
|
<input type="text" id="inc-title" required aria-required="true" placeholder="z.B. Explosion in Madrid" data-i18n-attr="placeholder:modal.placeholder.title">
|
||||||
@@ -367,16 +377,6 @@
|
|||||||
<textarea id="inc-description" placeholder="Weitere Details zum Vorfall (optional)" data-i18n-attr="placeholder:modal.placeholder.description"></textarea>
|
<textarea id="inc-description" placeholder="Weitere Details zum Vorfall (optional)" data-i18n-attr="placeholder:modal.placeholder.description"></textarea>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label for="inc-type" data-i18n="modal.field.type">Art der Lage</label>
|
|
||||||
<select id="inc-type" onchange="toggleTypeDefaults()">
|
|
||||||
<option value="adhoc" data-i18n="modal.option.type_adhoc">Live-Monitoring : Ereignis beobachten</option>
|
|
||||||
<option value="research" data-i18n="modal.option.type_research">Recherche : Thema analysieren</option>
|
|
||||||
</select>
|
|
||||||
<div class="form-hint" id="type-hint" data-i18n="modal.hint.type_adhoc">
|
|
||||||
Durchsucht laufend hunderte Nachrichtenquellen nach neuen Meldungen. Empfohlen: Automatische Aktualisierung.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label data-i18n="modal.field.sources">Quellen</label>
|
<label data-i18n="modal.field.sources">Quellen</label>
|
||||||
<div class="toggle-group">
|
<div class="toggle-group">
|
||||||
@@ -420,7 +420,7 @@
|
|||||||
<div class="form-group conditional-field" id="refresh-interval-field">
|
<div class="form-group conditional-field" id="refresh-interval-field">
|
||||||
<label for="inc-refresh-value" data-i18n="modal.field.interval">Intervall</label>
|
<label for="inc-refresh-value" data-i18n="modal.field.interval">Intervall</label>
|
||||||
<div class="interval-input-group">
|
<div class="interval-input-group">
|
||||||
<input type="number" id="inc-refresh-value" min="10" value="15">
|
<input type="number" id="inc-refresh-value" min="30" value="30">
|
||||||
<select id="inc-refresh-unit" onchange="updateIntervalMin()">
|
<select id="inc-refresh-unit" onchange="updateIntervalMin()">
|
||||||
<option value="1" selected data-i18n="modal.unit.minutes">Minuten</option>
|
<option value="1" selected data-i18n="modal.unit.minutes">Minuten</option>
|
||||||
<option value="60" data-i18n="modal.unit.hours">Stunden</option>
|
<option value="60" data-i18n="modal.unit.hours">Stunden</option>
|
||||||
@@ -428,6 +428,7 @@
|
|||||||
<option value="10080" data-i18n="modal.unit.weeks">Wochen</option>
|
<option value="10080" data-i18n="modal.unit.weeks">Wochen</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-hint" id="interval-min-hint" style="display:none;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group conditional-field" id="refresh-starttime-field">
|
<div class="form-group conditional-field" id="refresh-starttime-field">
|
||||||
<label for="inc-refresh-starttime"><span data-i18n="modal.field.start_time">Erste Aktualisierung um</span> <span class="info-icon tooltip-below" data-tooltip="Legt den Startzeitpunkt fest. Danach wird im eingestellten Intervall aktualisiert."><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg></span></label>
|
<label for="inc-refresh-starttime"><span data-i18n="modal.field.start_time">Erste Aktualisierung um</span> <span class="info-icon tooltip-below" data-tooltip="Legt den Startzeitpunkt fest. Danach wird im eingestellten Intervall aktualisiert."><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg></span></label>
|
||||||
|
|||||||
@@ -578,8 +578,16 @@ const App = {
|
|||||||
// Telegram-Kategorien Toggle
|
// Telegram-Kategorien Toggle
|
||||||
const tgCheckbox = document.getElementById('inc-telegram');
|
const tgCheckbox = document.getElementById('inc-telegram');
|
||||||
if (tgCheckbox) {
|
if (tgCheckbox) {
|
||||||
|
tgCheckbox.addEventListener('change', () => updateIntervalMin());
|
||||||
}
|
}
|
||||||
|
{ const xCheckbox = document.getElementById('inc-x');
|
||||||
|
if (xCheckbox) xCheckbox.addEventListener('change', () => updateIntervalMin()); }
|
||||||
|
{ const ivInput = document.getElementById('inc-refresh-value');
|
||||||
|
if (ivInput) ivInput.addEventListener('change', () => {
|
||||||
|
const u = parseInt(document.getElementById('inc-refresh-unit').value);
|
||||||
|
const m = (u === 1) ? _getMinIntervalMinutes() : 1;
|
||||||
|
if (isNaN(parseInt(ivInput.value)) || parseInt(ivInput.value) < m) ivInput.value = m;
|
||||||
|
}); }
|
||||||
|
|
||||||
|
|
||||||
// Feedback
|
// Feedback
|
||||||
@@ -1836,9 +1844,9 @@ const App = {
|
|||||||
// === Event Handlers ===
|
// === Event Handlers ===
|
||||||
|
|
||||||
_getFormData() {
|
_getFormData() {
|
||||||
const value = parseInt(document.getElementById('inc-refresh-value').value) || 15;
|
const value = parseInt(document.getElementById('inc-refresh-value').value) || 30;
|
||||||
const unit = parseInt(document.getElementById('inc-refresh-unit').value) || 1;
|
const unit = parseInt(document.getElementById('inc-refresh-unit').value) || 1;
|
||||||
const interval = Math.max(10, Math.min(10080, value * unit));
|
const interval = Math.max(_getMinIntervalMinutes(), Math.min(10080, value * unit));
|
||||||
return {
|
return {
|
||||||
title: document.getElementById('inc-title').value.trim(),
|
title: document.getElementById('inc-title').value.trim(),
|
||||||
description: document.getElementById('inc-description').value.trim() || null,
|
description: document.getElementById('inc-description').value.trim() || null,
|
||||||
@@ -2294,6 +2302,7 @@ async handleRefresh() {
|
|||||||
updateSourcesHint();
|
updateSourcesHint();
|
||||||
toggleTypeDefaults(true);
|
toggleTypeDefaults(true);
|
||||||
toggleRefreshInterval();
|
toggleRefreshInterval();
|
||||||
|
updateIntervalMin();
|
||||||
|
|
||||||
// Modal-Titel und Submit ändern
|
// Modal-Titel und Submit ändern
|
||||||
{ const _e = document.getElementById('modal-new-title'); if (_e) _e.textContent = (typeof T === 'function') ? T('modal.new_incident.edit_title', 'Lage bearbeiten') : 'Lage bearbeiten'; }
|
{ const _e = document.getElementById('modal-new-title'); if (_e) _e.textContent = (typeof T === 'function') ? T('modal.new_incident.edit_title', 'Lage bearbeiten') : 'Lage bearbeiten'; }
|
||||||
@@ -3633,6 +3642,7 @@ function openModal(id) {
|
|||||||
document.getElementById('inc-notify-status-change').checked = false;
|
document.getElementById('inc-notify-status-change').checked = false;
|
||||||
toggleTypeDefaults();
|
toggleTypeDefaults();
|
||||||
toggleRefreshInterval();
|
toggleRefreshInterval();
|
||||||
|
updateIntervalMin();
|
||||||
}
|
}
|
||||||
const modal = document.getElementById(id);
|
const modal = document.getElementById(id);
|
||||||
modal._previousFocus = document.activeElement;
|
modal._previousFocus = document.activeElement;
|
||||||
@@ -3814,17 +3824,38 @@ function toggleRefreshInterval() {
|
|||||||
if (startField) startField.classList.toggle('visible', mode === 'auto');
|
if (startField) startField.classList.toggle('visible', mode === 'auto');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _getMinIntervalMinutes() {
|
||||||
|
// Mindest-Intervall (Minuten) je nach Quellen: 30 Basis, 45 bei X oder Telegram, 60 bei beiden. International zaehlt nicht.
|
||||||
|
const tg = document.getElementById('inc-telegram');
|
||||||
|
const x = document.getElementById('inc-x');
|
||||||
|
const tgOn = !!(tg && tg.checked);
|
||||||
|
const xOn = !!(x && x.checked);
|
||||||
|
if (tgOn && xOn) return 60;
|
||||||
|
if (tgOn || xOn) return 45;
|
||||||
|
return 30;
|
||||||
|
}
|
||||||
|
|
||||||
function updateIntervalMin() {
|
function updateIntervalMin() {
|
||||||
const unit = parseInt(document.getElementById('inc-refresh-unit').value);
|
const unit = parseInt(document.getElementById('inc-refresh-unit').value);
|
||||||
const input = document.getElementById('inc-refresh-value');
|
const input = document.getElementById('inc-refresh-value');
|
||||||
|
const minMinutes = _getMinIntervalMinutes();
|
||||||
|
const hint = document.getElementById('interval-min-hint');
|
||||||
if (unit === 1) {
|
if (unit === 1) {
|
||||||
// Minuten: Minimum 10
|
// Minuten: dynamisches Minimum (30 / 45 bei X oder Telegram / 60 bei beiden)
|
||||||
input.min = 10;
|
input.min = minMinutes;
|
||||||
if (parseInt(input.value) < 10) input.value = 10;
|
if (isNaN(parseInt(input.value)) || parseInt(input.value) < minMinutes) input.value = minMinutes;
|
||||||
|
if (hint) {
|
||||||
|
let zusatz = '';
|
||||||
|
if (minMinutes === 45) zusatz = ' (X oder Telegram aktiv)';
|
||||||
|
else if (minMinutes === 60) zusatz = ' (X und Telegram aktiv)';
|
||||||
|
hint.textContent = 'Mindestens ' + minMinutes + ' Minuten' + zusatz;
|
||||||
|
hint.style.display = '';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Stunden/Tage/Wochen: Minimum 1
|
// Stunden/Tage/Wochen: eine Einheit liegt ueber jedem Minuten-Minimum
|
||||||
input.min = 1;
|
input.min = 1;
|
||||||
if (parseInt(input.value) < 1) input.value = 1;
|
if (isNaN(parseInt(input.value)) || parseInt(input.value) < 1) input.value = 1;
|
||||||
|
if (hint) hint.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren