Commits vergleichen
17 Commits
0c34f67194
...
main
| Autor | SHA1 | Datum | |
|---|---|---|---|
|
|
1647a6f50a | ||
|
|
c53e260c6c | ||
| c3a0ee4538 | |||
|
|
e20b3de0fa | ||
| aa36a9a38f | |||
|
|
d570e13dc6 | ||
|
|
7777b77abd | ||
| b02578e48b | |||
|
|
952df87afa | ||
| 38ce26f0be | |||
| 7f7b30c1d6 | |||
|
|
d986d611cf | ||
| 7954a78964 | |||
|
|
453c505a7e | ||
| 0b335263c9 | |||
|
|
279df0f56b | ||
| 889044cc3b |
@@ -1,4 +1,12 @@
|
||||
[
|
||||
{
|
||||
"version": "2026-05-22T19:10Z",
|
||||
"date": "2026-05-22",
|
||||
"title": "Exportdialog: Ersteller manuell eintragbar",
|
||||
"items": [
|
||||
"Im Export-Dialog kann der Ersteller jetzt manuell eingegeben werden."
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "2026-05-22T07:41Z",
|
||||
"date": "2026-05-22",
|
||||
|
||||
@@ -124,7 +124,7 @@ BISHERIGE QUELLEN:
|
||||
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
|
||||
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
|
||||
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 |---|---|)
|
||||
- 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.
|
||||
- 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:
|
||||
- Neutral und sachlich - keine Wertungen oder Spekulationen
|
||||
|
||||
@@ -1753,6 +1753,7 @@ class AgentOrchestrator:
|
||||
# Idempotent: nur Artikel ohne headline_de/content_de werden geholt.
|
||||
# Lauft nach der Analyse (Lagebild ist schon committed) und vor QC
|
||||
# (damit normalize_umlaut_articles auch die frischen DE-Texte fasst).
|
||||
_translate_step_started = False
|
||||
try:
|
||||
tr_cursor = await db.execute(
|
||||
"""SELECT id, headline, content_original, language
|
||||
@@ -1764,7 +1765,10 @@ class AgentOrchestrator:
|
||||
(incident_id,),
|
||||
)
|
||||
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(
|
||||
"Translator fuer Incident %d: %d Artikel ohne DE-Uebersetzung",
|
||||
incident_id, len(pending_translations),
|
||||
@@ -1795,8 +1799,11 @@ class AgentOrchestrator:
|
||||
"Translator fuer Incident %d: %d/%d Artikel uebersetzt",
|
||||
incident_id, len(translations), len(pending_translations),
|
||||
)
|
||||
await _pipe_done("translate", count_value=len(translations), count_secondary=len(pending_translations))
|
||||
except Exception as e:
|
||||
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
|
||||
|
||||
# --- Neueste Entwicklungen (nur Live-Monitoring / adhoc) ---
|
||||
|
||||
49
src/main.py
49
src/main.py
@@ -246,7 +246,14 @@ async def cleanup_expired():
|
||||
)
|
||||
logger.info(f"Lage {incident['id']} archiviert (Aufbewahrung abgelaufen)")
|
||||
|
||||
# Verwaiste running-Einträge bereinigen (> 15 Minuten ohne Abschluss)
|
||||
# Verwaiste running-Einträge bereinigen.
|
||||
# Pruefen auf Pipeline-Fortschritt: legitime Long-Runner (z.B. Translator
|
||||
# nach summary fuer jp_demo mit 200+ Artikeln ~20 Min) duerfen nicht
|
||||
# vorzeitig gekillt werden. Ein Refresh gilt als verwaist, wenn entweder
|
||||
# (a) seit ORPHAN_IDLE_LIMIT Min kein Pipeline-Step Fortschritt zeigte,
|
||||
# oder (b) das harte Limit ORPHAN_HARD_LIMIT Min ueberschritten wurde.
|
||||
ORPHAN_IDLE_LIMIT = 60
|
||||
ORPHAN_HARD_LIMIT = 120
|
||||
cursor = await db.execute(
|
||||
"SELECT id, incident_id, started_at FROM refresh_log WHERE status = 'running'"
|
||||
)
|
||||
@@ -258,12 +265,46 @@ async def cleanup_expired():
|
||||
else:
|
||||
started = started.astimezone(TIMEZONE)
|
||||
age_minutes = (now - started).total_seconds() / 60
|
||||
if age_minutes >= 15:
|
||||
if age_minutes < ORPHAN_IDLE_LIMIT:
|
||||
continue
|
||||
|
||||
# Letzter Pipeline-Step-Fortschritt (Start ODER Ende)
|
||||
prog_cursor = await db.execute(
|
||||
"""SELECT MAX(COALESCE(completed_at, started_at)) AS last_activity
|
||||
FROM refresh_pipeline_steps WHERE refresh_log_id = ?""",
|
||||
(orphan["id"],),
|
||||
)
|
||||
prog_row = await prog_cursor.fetchone()
|
||||
last_activity_str = prog_row["last_activity"] if prog_row else None
|
||||
|
||||
is_orphan = False
|
||||
reason = None
|
||||
if age_minutes >= ORPHAN_HARD_LIMIT:
|
||||
is_orphan = True
|
||||
reason = f"Verwaist (>{int(age_minutes)} Min, hartes Limit {ORPHAN_HARD_LIMIT} Min)"
|
||||
elif last_activity_str:
|
||||
last_activity = datetime.fromisoformat(last_activity_str)
|
||||
if last_activity.tzinfo is None:
|
||||
last_activity = last_activity.replace(tzinfo=TIMEZONE)
|
||||
else:
|
||||
last_activity = last_activity.astimezone(TIMEZONE)
|
||||
idle_minutes = (now - last_activity).total_seconds() / 60
|
||||
if idle_minutes >= ORPHAN_IDLE_LIMIT:
|
||||
is_orphan = True
|
||||
reason = (
|
||||
f"Verwaist (kein Pipeline-Fortschritt seit {int(idle_minutes)} Min, "
|
||||
f"gesamt {int(age_minutes)} Min)"
|
||||
)
|
||||
else:
|
||||
is_orphan = True
|
||||
reason = f"Verwaist (keine Pipeline-Schritte nach {int(age_minutes)} Min)"
|
||||
|
||||
if is_orphan:
|
||||
await db.execute(
|
||||
"UPDATE refresh_log SET status = 'error', completed_at = ?, error_message = ? WHERE id = ?",
|
||||
(now.strftime('%Y-%m-%d %H:%M:%S'), f"Verwaist (>{int(age_minutes)} Min ohne Abschluss, automatisch bereinigt)", orphan["id"]),
|
||||
(now.strftime('%Y-%m-%d %H:%M:%S'), reason, orphan["id"]),
|
||||
)
|
||||
logger.warning(f"Verwaisten Refresh #{orphan['id']} für Lage {orphan['incident_id']} bereinigt ({int(age_minutes)} Min)")
|
||||
logger.warning(f"Verwaisten Refresh #{orphan['id']} fuer Lage {orphan['incident_id']} bereinigt: {reason}")
|
||||
|
||||
# Alte Notifications bereinigen (> 7 Tage)
|
||||
await db.execute("DELETE FROM notifications WHERE created_at < datetime('now', '-7 days')")
|
||||
|
||||
@@ -462,8 +462,12 @@ def _build_export_metadata(
|
||||
organization_name: str | None,
|
||||
top_locations: list[str] | None,
|
||||
snapshot_count: int = 0,
|
||||
include_branding: bool = True,
|
||||
) -> dict:
|
||||
"""Einheitlicher Metadaten-Dict fuer PDF (HTML-Meta-Tags) und DOCX (core_properties)."""
|
||||
"""Einheitlicher Metadaten-Dict fuer PDF (HTML-Meta-Tags) und DOCX (core_properties).
|
||||
|
||||
include_branding=False neutralisiert alle AegisSight-Firmenbezeichnungen (White-Label-Export).
|
||||
"""
|
||||
is_research = incident.get("type") == "research"
|
||||
type_label = "Hintergrundrecherche" if is_research else "Live-Monitoring"
|
||||
category = "OSINT-Hintergrundrecherche" if is_research else "OSINT-Lagebericht"
|
||||
@@ -546,23 +550,37 @@ def _build_export_metadata(
|
||||
comments_lines.append("Orte: " + ", ".join(top_locations[:5]))
|
||||
comments = "\n".join(comments_lines)
|
||||
|
||||
# Branding-abhaengige Felder: bei include_branding=False neutralisiert (White-Label-Export)
|
||||
if include_branding:
|
||||
publisher = organization_name or "AegisSight"
|
||||
identifier = f"urn:aegissight:incident:{incident.get('id', '0')}:{now.strftime('%Y%m%dT%H%M%S')}"
|
||||
author = creator or "AegisSight Monitor"
|
||||
creator_app = "AegisSight Monitor"
|
||||
producer = "WeasyPrint + AegisSight Monitor"
|
||||
urn_ns = "aegissight"
|
||||
rights = (
|
||||
"Vertrauliche Lageanalyse — AegisSight Monitor. "
|
||||
"Weitergabe nur an autorisierte Empfänger."
|
||||
)
|
||||
else:
|
||||
publisher = organization_name or ""
|
||||
author = creator or "Unbekannt"
|
||||
creator_app = ""
|
||||
producer = "WeasyPrint"
|
||||
urn_ns = "report"
|
||||
rights = "Vertrauliche Lageanalyse. Weitergabe nur an autorisierte Empfänger."
|
||||
identifier = f"urn:{urn_ns}:incident:{incident.get('id', '0')}:{now.strftime('%Y%m%dT%H%M%S')}"
|
||||
|
||||
return {
|
||||
"title": title,
|
||||
"author": creator or "AegisSight Monitor",
|
||||
"author": author,
|
||||
"subject": subject,
|
||||
"keywords": unique_keywords,
|
||||
"keywords_comma": ", ".join(unique_keywords),
|
||||
"keywords_semicolon": "; ".join(unique_keywords),
|
||||
"category": category,
|
||||
"comments": comments,
|
||||
"creator_app": "AegisSight Monitor",
|
||||
"creator_app": creator_app,
|
||||
"producer": producer,
|
||||
"language": "de-DE",
|
||||
"created": created,
|
||||
"modified": modified,
|
||||
@@ -634,7 +652,7 @@ def _enrich_pdf_metadata(pdf_bytes: bytes, meta: dict) -> bytes:
|
||||
|
||||
# PDF Namespace
|
||||
xmp["pdf:Keywords"] = meta.get("keywords_comma", "")
|
||||
xmp["pdf:Producer"] = "WeasyPrint + AegisSight Monitor"
|
||||
xmp["pdf:Producer"] = meta.get("producer", "WeasyPrint + AegisSight Monitor")
|
||||
|
||||
# XMP Namespace
|
||||
xmp["xmp:CreatorTool"] = meta.get("creator_app", "AegisSight Monitor")
|
||||
@@ -681,6 +699,7 @@ async def generate_pdf(
|
||||
organization_name: str | None = None,
|
||||
top_locations: list[str] | None = None,
|
||||
snapshot_count: int = 0,
|
||||
include_branding: bool = True,
|
||||
) -> bytes:
|
||||
"""PDF-Report via WeasyPrint generieren."""
|
||||
# Sections aus scope ableiten wenn nicht explizit angegeben
|
||||
@@ -713,6 +732,7 @@ async def generate_pdf(
|
||||
meta = _build_export_metadata(
|
||||
incident, articles, fact_checks, all_sources, creator, scope, sections,
|
||||
organization_name, top_locations, snapshot_count=snapshot_count,
|
||||
include_branding=include_branding,
|
||||
)
|
||||
|
||||
env = Environment(loader=FileSystemLoader(str(TEMPLATE_DIR)))
|
||||
@@ -741,6 +761,7 @@ async def generate_pdf(
|
||||
timeline=_prepare_timeline(articles) if scope == "full" else [],
|
||||
articles=articles if scope == "full" else [],
|
||||
meta=meta,
|
||||
include_branding=include_branding,
|
||||
)
|
||||
|
||||
# Artikel pub_date aufbereiten
|
||||
@@ -764,6 +785,7 @@ async def generate_docx(
|
||||
organization_name: str | None = None,
|
||||
top_locations: list[str] | None = None,
|
||||
snapshot_count: int = 0,
|
||||
include_branding: bool = True,
|
||||
) -> bytes:
|
||||
"""Word-Report via python-docx generieren."""
|
||||
doc = Document()
|
||||
@@ -795,6 +817,7 @@ async def generate_docx(
|
||||
meta = _build_export_metadata(
|
||||
incident, articles, fact_checks, all_sources, creator, scope, sections,
|
||||
organization_name, top_locations, snapshot_count=snapshot_count,
|
||||
include_branding=include_branding,
|
||||
)
|
||||
|
||||
# Dateimetadaten setzen (sichtbar in Explorer/Finder, DMS-Systemen)
|
||||
@@ -823,6 +846,8 @@ async def generate_docx(
|
||||
for _ in range(6):
|
||||
doc.add_paragraph()
|
||||
|
||||
# Firmenname-Zeile nur im gebrandeten Export
|
||||
if include_branding:
|
||||
title_para = doc.add_paragraph()
|
||||
title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
run = title_para.add_run("AegisSight Monitor")
|
||||
@@ -978,7 +1003,11 @@ async def generate_docx(
|
||||
doc.add_paragraph()
|
||||
footer = doc.add_paragraph()
|
||||
footer.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
run = footer.add_run(f"Erstellt mit AegisSight Monitor — aegis-sight.de — {now.strftime('%d.%m.%Y')}")
|
||||
if include_branding:
|
||||
footer_text = f"Erstellt mit AegisSight Monitor — aegis-sight.de — {now.strftime('%d.%m.%Y')}"
|
||||
else:
|
||||
footer_text = f"Stand: {now.strftime('%d.%m.%Y')}"
|
||||
run = footer.add_run(footer_text)
|
||||
run.font.size = Pt(8)
|
||||
run.font.color.rgb = RGBColor(0x0a, 0x18, 0x32)
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ tr:nth-child(even) { background: #f8f9fa; }
|
||||
<body>
|
||||
<!-- Deckblatt -->
|
||||
<div class="cover">
|
||||
<img src="data:image/svg+xml;base64,{{ logo_base64 }}" class="cover-logo" alt="AegisSight">
|
||||
{% if include_branding %}<img src="data:image/svg+xml;base64,{{ logo_base64 }}" class="cover-logo" alt="AegisSight">{% endif %}
|
||||
<div class="cover-type">{{ incident_type_label }}</div>
|
||||
<div class="cover-title">{{ incident.title }}</div>
|
||||
<div class="cover-meta">
|
||||
@@ -92,7 +92,7 @@ tr:nth-child(even) { background: #f8f9fa; }
|
||||
<div>Erstellt von: {{ creator }}</div>
|
||||
{% if incident.organization_name %}<div>Organisation: {{ incident.organization_name }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="cover-brand">AegisSight Monitor</div>
|
||||
{% if include_branding %}<div class="cover-brand">AegisSight Monitor</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Inhaltsverzeichnis -->
|
||||
@@ -208,7 +208,7 @@ tr:nth-child(even) { background: #f8f9fa; }
|
||||
{% endif %}
|
||||
|
||||
<div class="report-footer">
|
||||
Erstellt mit AegisSight Monitor — aegis-sight.de — {{ report_date }}
|
||||
{% if include_branding %}Erstellt mit AegisSight Monitor — aegis-sight.de — {{ report_date }}{% else %}Stand: {{ report_date }}{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1152,6 +1152,8 @@ async def export_incident(
|
||||
format: str = Query("pdf", pattern="^(pdf|docx)$"),
|
||||
scope: str = Query("report", pattern="^(summary|report|full)$"),
|
||||
sections: str = Query(None),
|
||||
branding: str = Query("on", pattern="^(on|off)$"),
|
||||
creator: str = Query(None, max_length=120),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
@@ -1170,7 +1172,10 @@ async def export_incident(
|
||||
row = await _check_incident_access(db, incident_id, current_user["id"], tenant_id)
|
||||
incident = dict(row)
|
||||
|
||||
# Ersteller-Name
|
||||
# Ersteller-Name: manuell uebergebener Wert hat Vorrang, sonst E-Mail des Lage-Erstellers
|
||||
if creator and creator.strip():
|
||||
creator = creator.strip()
|
||||
else:
|
||||
cursor = await db.execute("SELECT email FROM users WHERE id = ?", (incident["created_by"],))
|
||||
user_row = await cursor.fetchone()
|
||||
creator = user_row["email"] if user_row else "Unbekannt"
|
||||
@@ -1268,6 +1273,7 @@ async def export_incident(
|
||||
organization_name=organization_name,
|
||||
top_locations=top_locations,
|
||||
snapshot_count=snapshot_count,
|
||||
include_branding=(branding == "on"),
|
||||
)
|
||||
filename = f"{slug}_{scope_labels_key}_{date_str}.pdf"
|
||||
return StreamingResponse(
|
||||
@@ -1282,6 +1288,7 @@ async def export_incident(
|
||||
organization_name=organization_name,
|
||||
top_locations=top_locations,
|
||||
snapshot_count=snapshot_count,
|
||||
include_branding=(branding == "on"),
|
||||
)
|
||||
filename = f"{slug}_{scope_labels_key}_{date_str}.docx"
|
||||
return StreamingResponse(
|
||||
|
||||
@@ -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."},
|
||||
{"key": "summary", "label": "Lagebild verfassen", "icon": "file-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",
|
||||
"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",
|
||||
@@ -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."},
|
||||
{"key": "summary", "label": "Writing the briefing", "icon": "file-text",
|
||||
"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",
|
||||
"tooltip": "A final review: consolidate duplicate facts, verify map locations, before you get notified."},
|
||||
{"key": "notify", "label": "Notifying", "icon": "bell",
|
||||
|
||||
@@ -352,6 +352,16 @@
|
||||
</div>
|
||||
<form id="new-incident-form">
|
||||
<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">
|
||||
<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">
|
||||
@@ -367,16 +377,6 @@
|
||||
<textarea id="inc-description" placeholder="Weitere Details zum Vorfall (optional)" data-i18n-attr="placeholder:modal.placeholder.description"></textarea>
|
||||
|
||||
</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">
|
||||
<label data-i18n="modal.field.sources">Quellen</label>
|
||||
<div class="toggle-group">
|
||||
@@ -420,7 +420,7 @@
|
||||
<div class="form-group conditional-field" id="refresh-interval-field">
|
||||
<label for="inc-refresh-value" data-i18n="modal.field.interval">Intervall</label>
|
||||
<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()">
|
||||
<option value="1" selected data-i18n="modal.unit.minutes">Minuten</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>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-hint" id="interval-min-hint" style="display:none;"></div>
|
||||
</div>
|
||||
<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>
|
||||
@@ -805,12 +806,12 @@
|
||||
<script src="/static/vendor/leaflet.js"></script>
|
||||
<script src="/static/vendor/leaflet.markercluster.js"></script>
|
||||
<script src="/static/js/i18n.js?v=20260513a"></script>
|
||||
<script src="/static/js/api.js?v=20260423a"></script>
|
||||
<script src="/static/js/api.js?v=20260522f"></script>
|
||||
<script src="/static/js/ws.js?v=20260316b"></script>
|
||||
<script src="/static/js/components.js?v=20260522d"></script>
|
||||
<script src="/static/js/layout.js?v=20260513f"></script>
|
||||
<script src="/static/js/pipeline.js?v=20260513d"></script>
|
||||
<script src="/static/js/app.js?v=20260522c"></script>
|
||||
<script src="/static/js/app.js?v=20260522f"></script>
|
||||
<script src="/static/js/cluster-data.js?v=20260322f"></script>
|
||||
<script src="/static/js/tutorial.js?v=20260316z"></script>
|
||||
<script src="/static/js/chat.js?v=20260514e"></script>
|
||||
@@ -850,6 +851,16 @@
|
||||
<label class="export-radio"><input type="radio" name="export-format" value="pdf" checked><span>PDF</span></label>
|
||||
<label class="export-radio"><input type="radio" name="export-format" value="docx"><span data-i18n="export.format.docx">Word (DOCX)</span></label>
|
||||
</div>
|
||||
<div style="margin-bottom:16px;">
|
||||
<label style="font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--text-secondary);display:block;margin-bottom:8px;" data-i18n="export.branding">Branding</label>
|
||||
<label class="export-radio"><input type="radio" name="export-branding" value="on" checked><span data-i18n="export.branding.on">Mit AegisSight-Branding</span></label>
|
||||
<label class="export-radio"><input type="radio" name="export-branding" value="off"><span data-i18n="export.branding.off">Ohne Firmen-Branding</span></label>
|
||||
</div>
|
||||
<div style="margin-bottom:0;">
|
||||
<label for="export-ersteller" style="font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--text-secondary);display:block;margin-bottom:8px;">Ersteller</label>
|
||||
<input type="text" id="export-ersteller" maxlength="120" placeholder="Name des Erstellers (optional)" style="width:100%;box-sizing:border-box;">
|
||||
<div style="font-size:11px;color:var(--text-secondary);margin-top:6px;">Leer lassen, dann wird automatisch der Lage-Ersteller verwendet.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" style="padding:12px 20px;display:flex;justify-content:flex-end;gap:8px;border-top:1px solid var(--border);">
|
||||
<button class="btn btn-secondary" onclick="closeModal('modal-export')" data-i18n="common.cancel">Abbrechen</button>
|
||||
|
||||
@@ -210,6 +210,9 @@
|
||||
"export.format": "Format",
|
||||
"export.format.pdf": "PDF",
|
||||
"export.format.docx": "Word (DOCX)",
|
||||
"export.branding": "Branding",
|
||||
"export.branding.on": "Mit AegisSight-Branding",
|
||||
"export.branding.off": "Ohne Firmen-Branding",
|
||||
"export.submit": "Exportieren",
|
||||
"sources_modal.title": "Quellenverwaltung",
|
||||
"sources_modal.stats.rss": "RSS-Feeds",
|
||||
|
||||
@@ -210,6 +210,9 @@
|
||||
"export.format": "Format",
|
||||
"export.format.pdf": "PDF",
|
||||
"export.format.docx": "Word (DOCX)",
|
||||
"export.branding": "Branding",
|
||||
"export.branding.on": "With AegisSight branding",
|
||||
"export.branding.off": "Without company branding",
|
||||
"export.submit": "Export",
|
||||
"sources_modal.title": "Source management",
|
||||
"sources_modal.stats.rss": "RSS feeds",
|
||||
|
||||
@@ -330,7 +330,7 @@ const API = {
|
||||
resetTutorialState() {
|
||||
return this._request('DELETE', '/tutorial/state');
|
||||
},
|
||||
exportReport(id, format, scope, sections) {
|
||||
exportReport(id, format, scope, sections, includeBranding, creator) {
|
||||
const token = localStorage.getItem('osint_token');
|
||||
let url = `${this.baseUrl}/incidents/${id}/export?format=${format}`;
|
||||
if (sections && sections.length > 0) {
|
||||
@@ -338,6 +338,12 @@ const API = {
|
||||
} else if (scope) {
|
||||
url += `&scope=${scope}`;
|
||||
}
|
||||
if (includeBranding === false) {
|
||||
url += `&branding=off`;
|
||||
}
|
||||
if (creator) {
|
||||
url += `&creator=${encodeURIComponent(creator)}`;
|
||||
}
|
||||
return fetch(url, {
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
});
|
||||
|
||||
@@ -578,8 +578,16 @@ const App = {
|
||||
// Telegram-Kategorien Toggle
|
||||
const tgCheckbox = document.getElementById('inc-telegram');
|
||||
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
|
||||
@@ -1836,9 +1844,9 @@ const App = {
|
||||
// === Event Handlers ===
|
||||
|
||||
_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 interval = Math.max(10, Math.min(10080, value * unit));
|
||||
const interval = Math.max(_getMinIntervalMinutes(), Math.min(10080, value * unit));
|
||||
return {
|
||||
title: document.getElementById('inc-title').value.trim(),
|
||||
description: document.getElementById('inc-description').value.trim() || null,
|
||||
@@ -2294,6 +2302,7 @@ async handleRefresh() {
|
||||
updateSourcesHint();
|
||||
toggleTypeDefaults(true);
|
||||
toggleRefreshInterval();
|
||||
updateIntervalMin();
|
||||
|
||||
// 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'; }
|
||||
@@ -2637,6 +2646,9 @@ async handleRefresh() {
|
||||
return;
|
||||
}
|
||||
const format = document.querySelector('input[name="export-format"]:checked').value;
|
||||
const brandingEl = document.querySelector('input[name="export-branding"]:checked');
|
||||
const includeBranding = !brandingEl || brandingEl.value === 'on';
|
||||
const ersteller = (document.getElementById('export-ersteller')?.value || '').trim();
|
||||
|
||||
const btn = document.getElementById('export-submit-btn');
|
||||
const origText = btn.textContent;
|
||||
@@ -2644,7 +2656,7 @@ async handleRefresh() {
|
||||
btn.textContent = (typeof T === 'function' ? T('action.creating', 'Wird erstellt...') : 'Wird erstellt...');
|
||||
|
||||
try {
|
||||
const response = await API.exportReport(this.currentIncidentId, format, null, sections);
|
||||
const response = await API.exportReport(this.currentIncidentId, format, null, sections, includeBranding, ersteller);
|
||||
if (!response.ok) {
|
||||
const err = await response.json().catch(() => ({}));
|
||||
throw new Error(err.detail || 'Fehler ' + response.status);
|
||||
@@ -3630,6 +3642,7 @@ function openModal(id) {
|
||||
document.getElementById('inc-notify-status-change').checked = false;
|
||||
toggleTypeDefaults();
|
||||
toggleRefreshInterval();
|
||||
updateIntervalMin();
|
||||
}
|
||||
const modal = document.getElementById(id);
|
||||
modal._previousFocus = document.activeElement;
|
||||
@@ -3811,17 +3824,38 @@ function toggleRefreshInterval() {
|
||||
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() {
|
||||
const unit = parseInt(document.getElementById('inc-refresh-unit').value);
|
||||
const input = document.getElementById('inc-refresh-value');
|
||||
const minMinutes = _getMinIntervalMinutes();
|
||||
const hint = document.getElementById('interval-min-hint');
|
||||
if (unit === 1) {
|
||||
// Minuten: Minimum 10
|
||||
input.min = 10;
|
||||
if (parseInt(input.value) < 10) input.value = 10;
|
||||
// Minuten: dynamisches Minimum (30 / 45 bei X oder Telegram / 60 bei beiden)
|
||||
input.min = minMinutes;
|
||||
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 {
|
||||
// Stunden/Tage/Wochen: Minimum 1
|
||||
// Stunden/Tage/Wochen: eine Einheit liegt ueber jedem Minuten-Minimum
|
||||
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