feat(export): neutrale Export-Variante ohne Firmenbranding
Beim Bericht-Export lässt sich im Modal nun zwischen "Mit AegisSight-Branding" und "Ohne Firmen-Branding" wählen. Im neutralen Modus entfallen Logo, AegisSight-Zeile auf dem Deckblatt und Branding-Footer; die Datei-Metadaten werden neutralisiert. Das Deckblatt mit Titel, Stand und Ersteller bleibt erhalten. Betrifft PDF und DOCX. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -462,8 +462,12 @@ def _build_export_metadata(
|
|||||||
organization_name: str | None,
|
organization_name: str | None,
|
||||||
top_locations: list[str] | None,
|
top_locations: list[str] | None,
|
||||||
snapshot_count: int = 0,
|
snapshot_count: int = 0,
|
||||||
|
include_branding: bool = True,
|
||||||
) -> dict:
|
) -> 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"
|
is_research = incident.get("type") == "research"
|
||||||
type_label = "Hintergrundrecherche" if is_research else "Live-Monitoring"
|
type_label = "Hintergrundrecherche" if is_research else "Live-Monitoring"
|
||||||
category = "OSINT-Hintergrundrecherche" if is_research else "OSINT-Lagebericht"
|
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_lines.append("Orte: " + ", ".join(top_locations[:5]))
|
||||||
comments = "\n".join(comments_lines)
|
comments = "\n".join(comments_lines)
|
||||||
|
|
||||||
publisher = organization_name or "AegisSight"
|
# Branding-abhaengige Felder: bei include_branding=False neutralisiert (White-Label-Export)
|
||||||
identifier = f"urn:aegissight:incident:{incident.get('id', '0')}:{now.strftime('%Y%m%dT%H%M%S')}"
|
if include_branding:
|
||||||
rights = (
|
publisher = organization_name or "AegisSight"
|
||||||
"Vertrauliche Lageanalyse — AegisSight Monitor. "
|
author = creator or "AegisSight Monitor"
|
||||||
"Weitergabe nur an autorisierte Empfänger."
|
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 {
|
return {
|
||||||
"title": title,
|
"title": title,
|
||||||
"author": creator or "AegisSight Monitor",
|
"author": author,
|
||||||
"subject": subject,
|
"subject": subject,
|
||||||
"keywords": unique_keywords,
|
"keywords": unique_keywords,
|
||||||
"keywords_comma": ", ".join(unique_keywords),
|
"keywords_comma": ", ".join(unique_keywords),
|
||||||
"keywords_semicolon": "; ".join(unique_keywords),
|
"keywords_semicolon": "; ".join(unique_keywords),
|
||||||
"category": category,
|
"category": category,
|
||||||
"comments": comments,
|
"comments": comments,
|
||||||
"creator_app": "AegisSight Monitor",
|
"creator_app": creator_app,
|
||||||
|
"producer": producer,
|
||||||
"language": "de-DE",
|
"language": "de-DE",
|
||||||
"created": created,
|
"created": created,
|
||||||
"modified": modified,
|
"modified": modified,
|
||||||
@@ -634,7 +652,7 @@ def _enrich_pdf_metadata(pdf_bytes: bytes, meta: dict) -> bytes:
|
|||||||
|
|
||||||
# PDF Namespace
|
# PDF Namespace
|
||||||
xmp["pdf:Keywords"] = meta.get("keywords_comma", "")
|
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 Namespace
|
||||||
xmp["xmp:CreatorTool"] = meta.get("creator_app", "AegisSight Monitor")
|
xmp["xmp:CreatorTool"] = meta.get("creator_app", "AegisSight Monitor")
|
||||||
@@ -681,6 +699,7 @@ async def generate_pdf(
|
|||||||
organization_name: str | None = None,
|
organization_name: str | None = None,
|
||||||
top_locations: list[str] | None = None,
|
top_locations: list[str] | None = None,
|
||||||
snapshot_count: int = 0,
|
snapshot_count: int = 0,
|
||||||
|
include_branding: bool = True,
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
"""PDF-Report via WeasyPrint generieren."""
|
"""PDF-Report via WeasyPrint generieren."""
|
||||||
# Sections aus scope ableiten wenn nicht explizit angegeben
|
# Sections aus scope ableiten wenn nicht explizit angegeben
|
||||||
@@ -713,6 +732,7 @@ async def generate_pdf(
|
|||||||
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,
|
||||||
organization_name, top_locations, snapshot_count=snapshot_count,
|
organization_name, top_locations, snapshot_count=snapshot_count,
|
||||||
|
include_branding=include_branding,
|
||||||
)
|
)
|
||||||
|
|
||||||
env = Environment(loader=FileSystemLoader(str(TEMPLATE_DIR)))
|
env = Environment(loader=FileSystemLoader(str(TEMPLATE_DIR)))
|
||||||
@@ -741,6 +761,7 @@ async def generate_pdf(
|
|||||||
timeline=_prepare_timeline(articles) if scope == "full" else [],
|
timeline=_prepare_timeline(articles) if scope == "full" else [],
|
||||||
articles=articles if scope == "full" else [],
|
articles=articles if scope == "full" else [],
|
||||||
meta=meta,
|
meta=meta,
|
||||||
|
include_branding=include_branding,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Artikel pub_date aufbereiten
|
# Artikel pub_date aufbereiten
|
||||||
@@ -764,6 +785,7 @@ async def generate_docx(
|
|||||||
organization_name: str | None = None,
|
organization_name: str | None = None,
|
||||||
top_locations: list[str] | None = None,
|
top_locations: list[str] | None = None,
|
||||||
snapshot_count: int = 0,
|
snapshot_count: int = 0,
|
||||||
|
include_branding: bool = True,
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
"""Word-Report via python-docx generieren."""
|
"""Word-Report via python-docx generieren."""
|
||||||
doc = Document()
|
doc = Document()
|
||||||
@@ -795,6 +817,7 @@ async def generate_docx(
|
|||||||
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,
|
||||||
organization_name, top_locations, snapshot_count=snapshot_count,
|
organization_name, top_locations, snapshot_count=snapshot_count,
|
||||||
|
include_branding=include_branding,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Dateimetadaten setzen (sichtbar in Explorer/Finder, DMS-Systemen)
|
# Dateimetadaten setzen (sichtbar in Explorer/Finder, DMS-Systemen)
|
||||||
@@ -823,13 +846,15 @@ async def generate_docx(
|
|||||||
for _ in range(6):
|
for _ in range(6):
|
||||||
doc.add_paragraph()
|
doc.add_paragraph()
|
||||||
|
|
||||||
title_para = doc.add_paragraph()
|
# Firmenname-Zeile nur im gebrandeten Export
|
||||||
title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
if include_branding:
|
||||||
run = title_para.add_run("AegisSight Monitor")
|
title_para = doc.add_paragraph()
|
||||||
run.font.size = Pt(12)
|
title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
run.font.color.rgb = RGBColor(0x0a, 0x18, 0x32)
|
run = title_para.add_run("AegisSight Monitor")
|
||||||
|
run.font.size = Pt(12)
|
||||||
|
run.font.color.rgb = RGBColor(0x0a, 0x18, 0x32)
|
||||||
|
|
||||||
doc.add_paragraph()
|
doc.add_paragraph()
|
||||||
|
|
||||||
type_label = "Hintergrundrecherche" if incident.get("type") == "research" else "Live-Monitoring"
|
type_label = "Hintergrundrecherche" if incident.get("type") == "research" else "Live-Monitoring"
|
||||||
type_para = doc.add_paragraph()
|
type_para = doc.add_paragraph()
|
||||||
@@ -978,7 +1003,11 @@ async def generate_docx(
|
|||||||
doc.add_paragraph()
|
doc.add_paragraph()
|
||||||
footer = doc.add_paragraph()
|
footer = doc.add_paragraph()
|
||||||
footer.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
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.size = Pt(8)
|
||||||
run.font.color.rgb = RGBColor(0x0a, 0x18, 0x32)
|
run.font.color.rgb = RGBColor(0x0a, 0x18, 0x32)
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ tr:nth-child(even) { background: #f8f9fa; }
|
|||||||
<body>
|
<body>
|
||||||
<!-- Deckblatt -->
|
<!-- Deckblatt -->
|
||||||
<div class="cover">
|
<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-type">{{ incident_type_label }}</div>
|
||||||
<div class="cover-title">{{ incident.title }}</div>
|
<div class="cover-title">{{ incident.title }}</div>
|
||||||
<div class="cover-meta">
|
<div class="cover-meta">
|
||||||
@@ -92,7 +92,7 @@ tr:nth-child(even) { background: #f8f9fa; }
|
|||||||
<div>Erstellt von: {{ creator }}</div>
|
<div>Erstellt von: {{ creator }}</div>
|
||||||
{% if incident.organization_name %}<div>Organisation: {{ incident.organization_name }}</div>{% endif %}
|
{% if incident.organization_name %}<div>Organisation: {{ incident.organization_name }}</div>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="cover-brand">AegisSight Monitor</div>
|
{% if include_branding %}<div class="cover-brand">AegisSight Monitor</div>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Inhaltsverzeichnis -->
|
<!-- Inhaltsverzeichnis -->
|
||||||
@@ -208,7 +208,7 @@ tr:nth-child(even) { background: #f8f9fa; }
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="report-footer">
|
<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>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1152,6 +1152,7 @@ async def export_incident(
|
|||||||
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),
|
sections: str = Query(None),
|
||||||
|
branding: str = Query("on", pattern="^(on|off)$"),
|
||||||
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),
|
||||||
):
|
):
|
||||||
@@ -1268,6 +1269,7 @@ async def export_incident(
|
|||||||
organization_name=organization_name,
|
organization_name=organization_name,
|
||||||
top_locations=top_locations,
|
top_locations=top_locations,
|
||||||
snapshot_count=snapshot_count,
|
snapshot_count=snapshot_count,
|
||||||
|
include_branding=(branding == "on"),
|
||||||
)
|
)
|
||||||
filename = f"{slug}_{scope_labels_key}_{date_str}.pdf"
|
filename = f"{slug}_{scope_labels_key}_{date_str}.pdf"
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
@@ -1282,6 +1284,7 @@ async def export_incident(
|
|||||||
organization_name=organization_name,
|
organization_name=organization_name,
|
||||||
top_locations=top_locations,
|
top_locations=top_locations,
|
||||||
snapshot_count=snapshot_count,
|
snapshot_count=snapshot_count,
|
||||||
|
include_branding=(branding == "on"),
|
||||||
)
|
)
|
||||||
filename = f"{slug}_{scope_labels_key}_{date_str}.docx"
|
filename = f"{slug}_{scope_labels_key}_{date_str}.docx"
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
|
|||||||
@@ -850,6 +850,11 @@
|
|||||||
<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="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>
|
<label class="export-radio"><input type="radio" name="export-format" value="docx"><span data-i18n="export.format.docx">Word (DOCX)</span></label>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<div class="modal-footer" style="padding:12px 20px;display:flex;justify-content:flex-end;gap:8px;border-top:1px solid var(--border);">
|
<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>
|
<button class="btn btn-secondary" onclick="closeModal('modal-export')" data-i18n="common.cancel">Abbrechen</button>
|
||||||
|
|||||||
@@ -210,6 +210,9 @@
|
|||||||
"export.format": "Format",
|
"export.format": "Format",
|
||||||
"export.format.pdf": "PDF",
|
"export.format.pdf": "PDF",
|
||||||
"export.format.docx": "Word (DOCX)",
|
"export.format.docx": "Word (DOCX)",
|
||||||
|
"export.branding": "Branding",
|
||||||
|
"export.branding.on": "Mit AegisSight-Branding",
|
||||||
|
"export.branding.off": "Ohne Firmen-Branding",
|
||||||
"export.submit": "Exportieren",
|
"export.submit": "Exportieren",
|
||||||
"sources_modal.title": "Quellenverwaltung",
|
"sources_modal.title": "Quellenverwaltung",
|
||||||
"sources_modal.stats.rss": "RSS-Feeds",
|
"sources_modal.stats.rss": "RSS-Feeds",
|
||||||
|
|||||||
@@ -210,6 +210,9 @@
|
|||||||
"export.format": "Format",
|
"export.format": "Format",
|
||||||
"export.format.pdf": "PDF",
|
"export.format.pdf": "PDF",
|
||||||
"export.format.docx": "Word (DOCX)",
|
"export.format.docx": "Word (DOCX)",
|
||||||
|
"export.branding": "Branding",
|
||||||
|
"export.branding.on": "With AegisSight branding",
|
||||||
|
"export.branding.off": "Without company branding",
|
||||||
"export.submit": "Export",
|
"export.submit": "Export",
|
||||||
"sources_modal.title": "Source management",
|
"sources_modal.title": "Source management",
|
||||||
"sources_modal.stats.rss": "RSS feeds",
|
"sources_modal.stats.rss": "RSS feeds",
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ const API = {
|
|||||||
resetTutorialState() {
|
resetTutorialState() {
|
||||||
return this._request('DELETE', '/tutorial/state');
|
return this._request('DELETE', '/tutorial/state');
|
||||||
},
|
},
|
||||||
exportReport(id, format, scope, sections) {
|
exportReport(id, format, scope, sections, includeBranding) {
|
||||||
const token = localStorage.getItem('osint_token');
|
const token = localStorage.getItem('osint_token');
|
||||||
let url = `${this.baseUrl}/incidents/${id}/export?format=${format}`;
|
let url = `${this.baseUrl}/incidents/${id}/export?format=${format}`;
|
||||||
if (sections && sections.length > 0) {
|
if (sections && sections.length > 0) {
|
||||||
@@ -338,6 +338,9 @@ const API = {
|
|||||||
} else if (scope) {
|
} else if (scope) {
|
||||||
url += `&scope=${scope}`;
|
url += `&scope=${scope}`;
|
||||||
}
|
}
|
||||||
|
if (includeBranding === false) {
|
||||||
|
url += `&branding=off`;
|
||||||
|
}
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
headers: { 'Authorization': `Bearer ${token}` },
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2637,6 +2637,8 @@ async handleRefresh() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const format = document.querySelector('input[name="export-format"]:checked').value;
|
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 btn = document.getElementById('export-submit-btn');
|
const btn = document.getElementById('export-submit-btn');
|
||||||
const origText = btn.textContent;
|
const origText = btn.textContent;
|
||||||
@@ -2644,7 +2646,7 @@ async handleRefresh() {
|
|||||||
btn.textContent = (typeof T === 'function' ? T('action.creating', 'Wird erstellt...') : 'Wird erstellt...');
|
btn.textContent = (typeof T === 'function' ? T('action.creating', 'Wird erstellt...') : 'Wird erstellt...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await API.exportReport(this.currentIncidentId, format, null, sections);
|
const response = await API.exportReport(this.currentIncidentId, format, null, sections, includeBranding);
|
||||||
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