Export: Zusammenfassung-Sektion, Checkbox-Auswahl, neue Reihenfolge
Research-Briefings: - Neue Sektion ZUSAMMENFASSUNG mit Bullet-Points als erstes Element - UEBERBLICK entfernt, durch ZUSAMMENFASSUNG ersetzt - Inkrementelles Briefing ebenfalls angepasst Export-System: - Zusammenfassung wird direkt aus dem Bericht extrahiert (kein separater KI-Aufruf mehr fuer Research-Lagen) - Reihenfolge: Zusammenfassung > Recherchebericht > Faktencheck > Quellen > Timeline - Sections-basiert statt scope-basiert (rueckwaertskompatibel) - Checkbox-Dialog statt Radio-Buttons im Frontend - Bereiche: Zusammenfassung, Recherchebericht, Faktencheck, Quellen, Timeline, Karte - PDF und DOCX Templates angepasst - Backend akzeptiert sections-Parameter (kommagetrennt)
Dieser Commit ist enthalten in:
@@ -66,8 +66,8 @@ AUFTRAG:
|
|||||||
Erstelle ein strukturiertes Briefing auf {output_language} mit folgenden Abschnitten. Sei so ausführlich wie nötig, um alle Aspekte gründlich abzudecken.
|
Erstelle ein strukturiertes Briefing auf {output_language} mit folgenden Abschnitten. Sei so ausführlich wie nötig, um alle Aspekte gründlich abzudecken.
|
||||||
Verwende durchgehend Inline-Quellenverweise [1], [2], [3] etc. im Text.
|
Verwende durchgehend Inline-Quellenverweise [1], [2], [3] etc. im Text.
|
||||||
|
|
||||||
## ÜBERBLICK
|
## ZUSAMMENFASSUNG
|
||||||
Kurze Einordnung des Themas (2-3 Sätze)
|
Kompakte Übersicht als Aufzählung (4-8 Bullet Points mit "- "). Jeder Punkt fasst einen Kernaspekt des Themas in 1-2 Sätzen zusammen. Der Leser soll nach dieser Sektion das Wesentliche erfasst haben, ohne den Rest lesen zu müssen.
|
||||||
|
|
||||||
## HINTERGRUND
|
## HINTERGRUND
|
||||||
Historischer Kontext, relevante Vorgeschichte
|
Historischer Kontext, relevante Vorgeschichte
|
||||||
@@ -171,7 +171,7 @@ NEUE QUELLEN SEIT DEM LETZTEN UPDATE:
|
|||||||
AUFTRAG:
|
AUFTRAG:
|
||||||
Aktualisiere das Briefing mit den neuen Erkenntnissen. Sei so ausführlich wie nötig. Behalte die Struktur bei:
|
Aktualisiere das Briefing mit den neuen Erkenntnissen. Sei so ausführlich wie nötig. Behalte die Struktur bei:
|
||||||
|
|
||||||
## ÜBERBLICK
|
## ZUSAMMENFASSUNG
|
||||||
## HINTERGRUND
|
## HINTERGRUND
|
||||||
## AKTEURE
|
## AKTEURE
|
||||||
## AKTUELLE LAGE
|
## AKTUELLE LAGE
|
||||||
|
|||||||
@@ -171,6 +171,55 @@ def _strip_citation_numbers(text: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_zusammenfassung(summary_text: str) -> tuple[str, str]:
|
||||||
|
"""Extrahiert die ZUSAMMENFASSUNG-Sektion aus einem Research-Briefing.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(zusammenfassung_html, remaining_summary)
|
||||||
|
zusammenfassung_html: HTML-formatierte Bullet Points
|
||||||
|
remaining_summary: Der Rest des Berichts ohne die Zusammenfassung
|
||||||
|
"""
|
||||||
|
if not summary_text:
|
||||||
|
return "", summary_text
|
||||||
|
|
||||||
|
# Suche nach ## ZUSAMMENFASSUNG ... bis zur naechsten ## Ueberschrift
|
||||||
|
pattern = r"(## ZUSAMMENFASSUNG\s*\n)(.*?)(?=\n## |\Z)"
|
||||||
|
match = re.search(pattern, summary_text, re.DOTALL)
|
||||||
|
if not match:
|
||||||
|
return "", summary_text
|
||||||
|
|
||||||
|
zusammenfassung_raw = match.group(2).strip()
|
||||||
|
# Rest des Berichts ohne die Zusammenfassung-Sektion
|
||||||
|
remaining = summary_text[:match.start()] + summary_text[match.end():]
|
||||||
|
remaining = remaining.strip()
|
||||||
|
|
||||||
|
# Bullet Points als HTML formatieren
|
||||||
|
lines = []
|
||||||
|
for line in zusammenfassung_raw.split("\n"):
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped.startswith("- "):
|
||||||
|
clean = _strip_citation_numbers(stripped[2:].strip())
|
||||||
|
if clean:
|
||||||
|
lines.append(clean)
|
||||||
|
elif stripped.startswith("* "):
|
||||||
|
clean = _strip_citation_numbers(stripped[2:].strip())
|
||||||
|
if clean:
|
||||||
|
lines.append(clean)
|
||||||
|
elif stripped and not stripped.startswith("#"):
|
||||||
|
clean = _strip_citation_numbers(stripped)
|
||||||
|
if clean:
|
||||||
|
lines.append(clean)
|
||||||
|
|
||||||
|
if lines:
|
||||||
|
html = "<ul>\n" + "\n".join(f"<li>{line}</li>" for line in lines) + "\n</ul>"
|
||||||
|
else:
|
||||||
|
html = f"<p>{_strip_citation_numbers(zusammenfassung_raw)}</p>"
|
||||||
|
|
||||||
|
return html, remaining
|
||||||
|
|
||||||
|
|
||||||
async def generate_executive_summary(summary_text: str) -> str:
|
async def generate_executive_summary(summary_text: str) -> str:
|
||||||
"""KI-verdichtetes Executive Summary aus dem Lagebild."""
|
"""KI-verdichtetes Executive Summary aus dem Lagebild."""
|
||||||
if not summary_text or len(summary_text.strip()) < 50:
|
if not summary_text or len(summary_text.strip()) < 50:
|
||||||
@@ -246,8 +295,31 @@ LAGEBILD:
|
|||||||
async def generate_pdf(
|
async def generate_pdf(
|
||||||
incident: dict, articles: list, fact_checks: list, snapshots: list,
|
incident: dict, articles: list, fact_checks: list, snapshots: list,
|
||||||
scope: str, creator: str, executive_summary_html: str,
|
scope: str, creator: str, executive_summary_html: str,
|
||||||
|
sections: set[str] | None = None,
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
"""PDF-Report via WeasyPrint generieren."""
|
"""PDF-Report via WeasyPrint generieren."""
|
||||||
|
# Sections aus scope ableiten wenn nicht explizit angegeben
|
||||||
|
if sections is None:
|
||||||
|
if scope == "summary":
|
||||||
|
sections = {"zusammenfassung"}
|
||||||
|
elif scope == "report":
|
||||||
|
sections = {"zusammenfassung", "bericht", "faktencheck", "quellen"}
|
||||||
|
else: # full
|
||||||
|
sections = {"zusammenfassung", "bericht", "faktencheck", "quellen", "timeline"}
|
||||||
|
|
||||||
|
# Fuer Research-Lagen: Zusammenfassung aus dem Bericht extrahieren
|
||||||
|
is_research = incident.get("type") == "research"
|
||||||
|
zusammenfassung_html = executive_summary_html
|
||||||
|
bericht_summary = incident.get("summary", "")
|
||||||
|
zusammenfassung_title = "Executive Summary"
|
||||||
|
|
||||||
|
if is_research and bericht_summary:
|
||||||
|
extracted_html, remaining = _extract_zusammenfassung(bericht_summary)
|
||||||
|
if extracted_html:
|
||||||
|
zusammenfassung_html = extracted_html
|
||||||
|
zusammenfassung_title = "Zusammenfassung"
|
||||||
|
bericht_summary = remaining
|
||||||
|
|
||||||
env = Environment(loader=FileSystemLoader(str(TEMPLATE_DIR)))
|
env = Environment(loader=FileSystemLoader(str(TEMPLATE_DIR)))
|
||||||
template = env.get_template("report.html")
|
template = env.get_template("report.html")
|
||||||
|
|
||||||
@@ -260,13 +332,12 @@ async def generate_pdf(
|
|||||||
report_date=now.strftime("%d.%m.%Y, %H:%M Uhr"),
|
report_date=now.strftime("%d.%m.%Y, %H:%M Uhr"),
|
||||||
creator=creator,
|
creator=creator,
|
||||||
logo_base64=_get_logo_base64(),
|
logo_base64=_get_logo_base64(),
|
||||||
executive_summary=executive_summary_html,
|
executive_summary=zusammenfassung_html,
|
||||||
|
zusammenfassung_title=zusammenfassung_title,
|
||||||
|
sections=sections,
|
||||||
scope=scope,
|
scope=scope,
|
||||||
lagebild_html=_markdown_to_html(
|
lagebild_html=_markdown_to_html(
|
||||||
_strip_citation_numbers(
|
_strip_citation_numbers(bericht_summary)
|
||||||
_truncate_lagebild(incident.get("summary", ""), 4000) if scope == "report"
|
|
||||||
else incident.get("summary", "")
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
lagebild_timestamp=(incident.get("updated_at") or "")[:16].replace("T", " "),
|
lagebild_timestamp=(incident.get("updated_at") or "")[:16].replace("T", " "),
|
||||||
sources=_prepare_sources(incident)[:30] if scope == "report" else _prepare_sources(incident),
|
sources=_prepare_sources(incident)[:30] if scope == "report" else _prepare_sources(incident),
|
||||||
@@ -292,10 +363,33 @@ async def generate_pdf(
|
|||||||
async def generate_docx(
|
async def generate_docx(
|
||||||
incident: dict, articles: list, fact_checks: list, snapshots: list,
|
incident: dict, articles: list, fact_checks: list, snapshots: list,
|
||||||
scope: str, creator: str, executive_summary_text: str,
|
scope: str, creator: str, executive_summary_text: str,
|
||||||
|
sections: set[str] | None = None,
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
"""Word-Report via python-docx generieren."""
|
"""Word-Report via python-docx generieren."""
|
||||||
doc = Document()
|
doc = Document()
|
||||||
|
|
||||||
|
# Sections aus scope ableiten wenn nicht explizit angegeben
|
||||||
|
if sections is None:
|
||||||
|
if scope == "summary":
|
||||||
|
sections = {"zusammenfassung"}
|
||||||
|
elif scope == "report":
|
||||||
|
sections = {"zusammenfassung", "bericht", "faktencheck", "quellen"}
|
||||||
|
else: # full
|
||||||
|
sections = {"zusammenfassung", "bericht", "faktencheck", "quellen", "timeline"}
|
||||||
|
|
||||||
|
# Fuer Research-Lagen: Zusammenfassung aus dem Bericht extrahieren
|
||||||
|
is_research = incident.get("type") == "research"
|
||||||
|
zusammenfassung_text = executive_summary_text
|
||||||
|
bericht_summary = incident.get("summary") or "Keine Zusammenfassung verfuegbar."
|
||||||
|
zusammenfassung_title = "Executive Summary"
|
||||||
|
|
||||||
|
if is_research and bericht_summary:
|
||||||
|
extracted_html, remaining = _extract_zusammenfassung(bericht_summary)
|
||||||
|
if extracted_html:
|
||||||
|
zusammenfassung_text = extracted_html
|
||||||
|
zusammenfassung_title = "Zusammenfassung"
|
||||||
|
bericht_summary = remaining
|
||||||
|
|
||||||
# Styles
|
# Styles
|
||||||
style = doc.styles['Normal']
|
style = doc.styles['Normal']
|
||||||
style.font.size = Pt(10)
|
style.font.size = Pt(10)
|
||||||
@@ -347,23 +441,21 @@ async def generate_docx(
|
|||||||
|
|
||||||
doc.add_page_break()
|
doc.add_page_break()
|
||||||
|
|
||||||
# --- Executive Summary ---
|
# --- Zusammenfassung / Executive Summary ---
|
||||||
doc.add_heading("Executive Summary", level=1)
|
if "zusammenfassung" in sections:
|
||||||
|
doc.add_heading(zusammenfassung_title, level=1)
|
||||||
|
|
||||||
# HTML-Tags entfernen und als Bullet Points
|
# HTML-Tags entfernen und als Bullet Points
|
||||||
clean_text = re.sub(r'<[^>]+>', '', executive_summary_text)
|
clean_text = re.sub(r'<[^>]+>', '', zusammenfassung_text)
|
||||||
lines = [line.strip().lstrip("- ").lstrip("* ") for line in clean_text.strip().split("\n") if line.strip()]
|
lines = [line.strip().lstrip("- ").lstrip("* ") for line in clean_text.strip().split("\n") if line.strip()]
|
||||||
for line in lines:
|
for line in lines:
|
||||||
if line:
|
if line:
|
||||||
doc.add_paragraph(line, style='List Bullet')
|
doc.add_paragraph(line, style='List Bullet')
|
||||||
|
|
||||||
if scope in ("report", "full"):
|
if "bericht" in sections:
|
||||||
# --- Lagebild ---
|
# --- Lagebild / Recherchebericht ---
|
||||||
doc.add_heading("Recherchebericht" if incident.get("type") == "research" else "Lagebild", level=1)
|
doc.add_heading("Recherchebericht" if is_research else "Lagebild", level=1)
|
||||||
raw_summary = incident.get("summary") or "Keine Zusammenfassung verfügbar."
|
summary = _strip_citation_numbers(bericht_summary)
|
||||||
summary = _strip_citation_numbers(
|
|
||||||
_truncate_lagebild(raw_summary, 4000) if scope == "report" else raw_summary
|
|
||||||
)
|
|
||||||
# Markdown-Formatierung entfernen
|
# Markdown-Formatierung entfernen
|
||||||
clean_summary = re.sub(r'\*\*(.+?)\*\*', r'\1', summary)
|
clean_summary = re.sub(r'\*\*(.+?)\*\*', r'\1', summary)
|
||||||
clean_summary = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', clean_summary)
|
clean_summary = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', clean_summary)
|
||||||
@@ -379,8 +471,9 @@ async def generate_docx(
|
|||||||
else:
|
else:
|
||||||
doc.add_paragraph(para_text)
|
doc.add_paragraph(para_text)
|
||||||
|
|
||||||
|
if "faktencheck" in sections:
|
||||||
# --- Faktencheck ---
|
# --- Faktencheck ---
|
||||||
report_fcs = fact_checks[:20] if scope == 'report' else fact_checks
|
report_fcs = fact_checks
|
||||||
if report_fcs:
|
if report_fcs:
|
||||||
doc.add_heading("Faktencheck", level=1)
|
doc.add_heading("Faktencheck", level=1)
|
||||||
table = doc.add_table(rows=1, cols=3)
|
table = doc.add_table(rows=1, cols=3)
|
||||||
@@ -400,10 +493,9 @@ async def generate_docx(
|
|||||||
row[1].text = FC_STATUS_LABELS.get(fc.get("status", ""), fc.get("status", ""))
|
row[1].text = FC_STATUS_LABELS.get(fc.get("status", ""), fc.get("status", ""))
|
||||||
row[2].text = str(fc.get("sources_count", 0))
|
row[2].text = str(fc.get("sources_count", 0))
|
||||||
|
|
||||||
|
if "quellen" in sections:
|
||||||
# --- Quellenstatistik ---
|
# --- Quellenstatistik ---
|
||||||
source_stats = _prepare_source_stats(articles)
|
source_stats = _prepare_source_stats(articles)
|
||||||
if scope == 'report':
|
|
||||||
source_stats = source_stats[:20]
|
|
||||||
if source_stats:
|
if source_stats:
|
||||||
doc.add_heading("Quellenstatistik", level=1)
|
doc.add_heading("Quellenstatistik", level=1)
|
||||||
table = doc.add_table(rows=1, cols=3)
|
table = doc.add_table(rows=1, cols=3)
|
||||||
@@ -423,7 +515,7 @@ async def generate_docx(
|
|||||||
row[1].text = str(stat["count"])
|
row[1].text = str(stat["count"])
|
||||||
row[2].text = stat["languages"]
|
row[2].text = stat["languages"]
|
||||||
|
|
||||||
if scope == "full":
|
if "timeline" in sections:
|
||||||
# --- Artikelverzeichnis ---
|
# --- Artikelverzeichnis ---
|
||||||
if articles:
|
if articles:
|
||||||
doc.add_page_break()
|
doc.add_page_break()
|
||||||
|
|||||||
@@ -78,17 +78,27 @@ tr:nth-child(even) { background: #f8f9fa; }
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Executive Summary -->
|
<!-- Zusammenfassung / Executive Summary -->
|
||||||
|
{% if 'zusammenfassung' in sections %}
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>Executive Summary</h2>
|
<h2>{{ zusammenfassung_title }}</h2>
|
||||||
<div class="exec-summary">
|
<div class="exec-summary">
|
||||||
{{ executive_summary | safe }}
|
{{ executive_summary | safe }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Recherchebericht / Lagebild -->
|
||||||
|
{% if 'bericht' in sections %}
|
||||||
|
<div class="section">
|
||||||
|
<h2>{% if incident.type == "research" %}Recherchebericht{% else %}Lagebild{% endif %}</h2>
|
||||||
|
{% if lagebild_timestamp %}<p style="font-size:9pt;color:#888;margin-bottom:10px;">Aktualisiert: {{ lagebild_timestamp }}</p>{% endif %}
|
||||||
|
<div class="lagebild-content">{{ lagebild_html | safe }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if scope in ('report', 'full') %}
|
|
||||||
<!-- Faktencheck -->
|
<!-- Faktencheck -->
|
||||||
{% if fact_checks %}
|
{% if 'faktencheck' in sections and fact_checks %}
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>Faktencheck</h2>
|
<h2>Faktencheck</h2>
|
||||||
<table>
|
<table>
|
||||||
@@ -106,10 +116,12 @@ tr:nth-child(even) { background: #f8f9fa; }
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Quellenstatistik -->
|
<!-- Quellenverzeichnis -->
|
||||||
{% if source_stats %}
|
{% if 'quellen' in sections and sources %}
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>Quellenstatistik</h2>
|
<h2>Quellenverzeichnis</h2>
|
||||||
|
{% if source_stats %}
|
||||||
|
<h3>Quellenstatistik</h3>
|
||||||
<table>
|
<table>
|
||||||
<thead><tr><th>Quelle</th><th>Artikel</th><th>Sprache</th></tr></thead>
|
<thead><tr><th>Quelle</th><th>Artikel</th><th>Sprache</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -118,18 +130,8 @@ tr:nth-child(even) { background: #f8f9fa; }
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
{% endif %}
|
||||||
<!-- Lagebild -->
|
<h3>Quellen</h3>
|
||||||
<div class="section">
|
|
||||||
<h2>{% if incident.type == "research" %}Recherchebericht{% else %}Lagebild{% endif %}</h2>
|
|
||||||
{% if lagebild_timestamp %}<p style="font-size:9pt;color:#888;margin-bottom:10px;">Aktualisiert: {{ lagebild_timestamp }}</p>{% endif %}
|
|
||||||
<div class="lagebild-content">{{ lagebild_html | safe }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Quellenverzeichnis -->
|
|
||||||
{% if sources %}
|
|
||||||
<div class="section">
|
|
||||||
<h2>Quellenverzeichnis</h2>
|
|
||||||
<table class="quellen-table">
|
<table class="quellen-table">
|
||||||
<thead><tr><th style="width:30px">#</th><th style="width:120px">Quelle</th><th>URL</th></tr></thead>
|
<thead><tr><th style="width:30px">#</th><th style="width:120px">Quelle</th><th>URL</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -141,12 +143,8 @@ tr:nth-child(even) { background: #f8f9fa; }
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if scope == 'full' %}
|
|
||||||
<!-- Timeline -->
|
<!-- Timeline -->
|
||||||
{% if timeline %}
|
{% if 'timeline' in sections and timeline %}
|
||||||
<div class="section" style="page-break-before:always;">
|
<div class="section" style="page-break-before:always;">
|
||||||
<h2>Ereignis-Timeline</h2>
|
<h2>Ereignis-Timeline</h2>
|
||||||
{% for event in timeline %}
|
{% for event in timeline %}
|
||||||
@@ -160,7 +158,7 @@ tr:nth-child(even) { background: #f8f9fa; }
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Artikelverzeichnis -->
|
<!-- Artikelverzeichnis -->
|
||||||
{% if articles %}
|
{% if 'timeline' in sections and articles %}
|
||||||
<div class="section" style="page-break-before:always;">
|
<div class="section" style="page-break-before:always;">
|
||||||
<h2>Artikelverzeichnis ({{ articles | length }} Artikel)</h2>
|
<h2>Artikelverzeichnis ({{ articles | length }} Artikel)</h2>
|
||||||
<table>
|
<table>
|
||||||
@@ -178,7 +176,6 @@ tr:nth-child(even) { background: #f8f9fa; }
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="report-footer">
|
<div class="report-footer">
|
||||||
Erstellt mit AegisSight Monitor — aegis-sight.de — {{ report_date }}
|
Erstellt mit AegisSight Monitor — aegis-sight.de — {{ report_date }}
|
||||||
|
|||||||
@@ -713,12 +713,21 @@ async def export_incident(
|
|||||||
incident_id: int,
|
incident_id: int,
|
||||||
format: str = Query("pdf", pattern="^(pdf|docx)$"),
|
format: str = Query("pdf", pattern="^(pdf|docx)$"),
|
||||||
scope: str = Query("report", pattern="^(summary|report|full)$"),
|
scope: str = Query("report", pattern="^(summary|report|full)$"),
|
||||||
|
sections: str = Query(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),
|
||||||
):
|
):
|
||||||
"""Lage als PDF oder Word exportieren."""
|
"""Lage als PDF oder Word exportieren."""
|
||||||
from report_generator import generate_pdf, generate_docx, generate_executive_summary
|
from report_generator import generate_pdf, generate_docx, generate_executive_summary
|
||||||
|
|
||||||
|
# Sections aus Komma-getrenntem String parsen
|
||||||
|
VALID_SECTIONS = {"zusammenfassung", "bericht", "faktencheck", "quellen", "timeline", "karte"}
|
||||||
|
sections_set = None
|
||||||
|
if sections:
|
||||||
|
sections_set = {s.strip() for s in sections.split(",") if s.strip() in VALID_SECTIONS}
|
||||||
|
if not sections_set:
|
||||||
|
sections_set = None
|
||||||
|
|
||||||
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)
|
||||||
incident = dict(row)
|
incident = dict(row)
|
||||||
@@ -765,18 +774,28 @@ async def export_incident(
|
|||||||
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"])
|
||||||
scope_labels = {"summary": "executive_summary", "report": "lagebericht", "full": "vollstaendig"}
|
scope_labels = {"summary": "executive_summary", "report": "lagebericht", "full": "vollstaendig"}
|
||||||
|
# Wenn sections explizit angegeben, passenden Label waehlen
|
||||||
|
if sections_set:
|
||||||
|
if sections_set == {"zusammenfassung"}:
|
||||||
|
scope_labels_key = "executive_summary"
|
||||||
|
elif "timeline" in sections_set:
|
||||||
|
scope_labels_key = "vollstaendig"
|
||||||
|
else:
|
||||||
|
scope_labels_key = "lagebericht"
|
||||||
|
else:
|
||||||
|
scope_labels_key = scope_labels.get(scope, "lagebericht")
|
||||||
|
|
||||||
if format == "pdf":
|
if format == "pdf":
|
||||||
pdf_bytes = await generate_pdf(incident, articles, fact_checks, snapshots, scope, creator, exec_summary)
|
pdf_bytes = await generate_pdf(incident, articles, fact_checks, snapshots, scope, creator, exec_summary, sections=sections_set)
|
||||||
filename = f"{slug}_{scope_labels[scope]}_{date_str}.pdf"
|
filename = f"{slug}_{scope_labels_key}_{date_str}.pdf"
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
io.BytesIO(pdf_bytes),
|
io.BytesIO(pdf_bytes),
|
||||||
media_type="application/pdf",
|
media_type="application/pdf",
|
||||||
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
docx_bytes = await generate_docx(incident, articles, fact_checks, snapshots, scope, creator, exec_summary)
|
docx_bytes = await generate_docx(incident, articles, fact_checks, snapshots, scope, creator, exec_summary, sections=sections_set)
|
||||||
filename = f"{slug}_{scope_labels[scope]}_{date_str}.docx"
|
filename = f"{slug}_{scope_labels_key}_{date_str}.docx"
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
io.BytesIO(docx_bytes),
|
io.BytesIO(docx_bytes),
|
||||||
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
|||||||
@@ -668,10 +668,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body" style="padding:20px;">
|
<div class="modal-body" style="padding:20px;">
|
||||||
<div style="margin-bottom:16px;">
|
<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;">Umfang</label>
|
<label style="font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--text-secondary);display:block;margin-bottom:8px;">Bereiche</label>
|
||||||
<label class="export-radio"><input type="radio" name="export-scope" value="summary" checked><span>Executive Summary</span><span class="export-radio-desc">1-2 Seiten, Kernpunkte</span></label>
|
<label class="export-radio"><input type="checkbox" name="export-section" value="zusammenfassung" checked><span>Zusammenfassung</span></label>
|
||||||
<label class="export-radio"><input type="radio" name="export-scope" value="report"><span>Lagebericht</span><span class="export-radio-desc">Lagebild, Faktencheck, Quellen</span></label>
|
<label class="export-radio"><input type="checkbox" name="export-section" value="bericht" checked><span>Recherchebericht / Lagebild</span></label>
|
||||||
<label class="export-radio"><input type="radio" name="export-scope" value="full"><span>Vollständiger Bericht</span><span class="export-radio-desc">+ Timeline, Artikelverzeichnis</span></label>
|
<label class="export-radio"><input type="checkbox" name="export-section" value="faktencheck" checked><span>Faktencheck</span></label>
|
||||||
|
<label class="export-radio"><input type="checkbox" name="export-section" value="quellen" checked><span>Quellen</span></label>
|
||||||
|
<label class="export-radio"><input type="checkbox" name="export-section" value="timeline"><span>Timeline</span></label>
|
||||||
|
<label class="export-radio"><input type="checkbox" name="export-section" value="karte"><span>Karte</span></label>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-bottom:16px;">
|
<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;">Format</label>
|
<label style="font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--text-secondary);display:block;margin-bottom:8px;">Format</label>
|
||||||
|
|||||||
@@ -237,9 +237,15 @@ const API = {
|
|||||||
resetTutorialState() {
|
resetTutorialState() {
|
||||||
return this._request('DELETE', '/tutorial/state');
|
return this._request('DELETE', '/tutorial/state');
|
||||||
},
|
},
|
||||||
exportReport(id, format, scope) {
|
exportReport(id, format, scope, sections) {
|
||||||
const token = localStorage.getItem('osint_token');
|
const token = localStorage.getItem('osint_token');
|
||||||
return fetch(`${this.baseUrl}/incidents/${id}/export?format=${format}&scope=${scope}`, {
|
let url = `${this.baseUrl}/incidents/${id}/export?format=${format}`;
|
||||||
|
if (sections && sections.length > 0) {
|
||||||
|
url += `§ions=${sections.join(',')}`;
|
||||||
|
} else if (scope) {
|
||||||
|
url += `&scope=${scope}`;
|
||||||
|
}
|
||||||
|
return fetch(url, {
|
||||||
headers: { 'Authorization': `Bearer ${token}` },
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2284,16 +2284,21 @@ async handleRefresh() {
|
|||||||
|
|
||||||
async submitExport() {
|
async submitExport() {
|
||||||
if (!this.currentIncidentId) return;
|
if (!this.currentIncidentId) return;
|
||||||
const scope = document.querySelector('input[name="export-scope"]:checked').value;
|
const checked = document.querySelectorAll('input[name="export-section"]:checked');
|
||||||
|
const sections = Array.from(checked).map(cb => cb.value);
|
||||||
|
if (sections.length === 0) {
|
||||||
|
UI.showToast('Bitte mindestens einen Bereich ausw\u00e4hlen.', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const format = document.querySelector('input[name="export-format"]:checked').value;
|
const format = document.querySelector('input[name="export-format"]:checked').value;
|
||||||
|
|
||||||
const btn = document.getElementById('export-submit-btn');
|
const btn = document.getElementById('export-submit-btn');
|
||||||
const origText = btn.textContent;
|
const origText = btn.textContent;
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = scope === 'summary' ? 'KI generiert Executive Summary...' : 'Wird erstellt...';
|
btn.textContent = 'Wird erstellt...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await API.exportReport(this.currentIncidentId, format, scope);
|
const response = await API.exportReport(this.currentIncidentId, format, null, sections);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const err = await response.json().catch(() => ({}));
|
const err = await response.json().catch(() => ({}));
|
||||||
throw new Error(err.detail || 'Fehler ' + response.status);
|
throw new Error(err.detail || 'Fehler ' + response.status);
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren