Export-Metadaten: Umlaut-Fix, xmpMM:VersionID + History

- dc:rights und xmpRights:UsageTerms: Empfaenger -> Empfänger (echte Umlaute)
- Scope-Labels: Vollstaendiger Bericht -> Vollständiger Bericht (zwei Stellen)
- DOCX-Fallback-Text: verfuegbar -> verfügbar
- xmpMM:VersionID: Snapshot-Count der Lage (Proxy fuer Berichts-Revision).
  Router laedt COUNT(*) FROM incident_snapshots und reicht es durch.
- xmpMM:History: Audit-Event pro Export als rdf:Seq-Eintrag mit Timestamp,
  softwareAgent, InstanceID, Scope und Version. Single-Event-Format aus
  pragmatischem Grund (pikepdf-API unterstuetzt keine nativen stEvt-
  Strukturen; Raw-XML-Injection waere dafuer noetig).
Dieser Commit ist enthalten in:
claude-dev
2026-04-20 19:33:18 +00:00
Ursprung 5add8d9d59
Commit 285df86c7b
2 geänderte Dateien mit 31 neuen und 6 gelöschten Zeilen

Datei anzeigen

@@ -417,9 +417,9 @@ def _slug_scope_label(scope: str, sections: set[str] | None) -> str:
if sections == {"zusammenfassung"}:
return "Zusammenfassung"
if "timeline" in sections:
return "Vollstaendiger Bericht"
return "Vollständiger Bericht"
return "Lagebericht"
return {"summary": "Zusammenfassung", "report": "Lagebericht", "full": "Vollstaendiger Bericht"}.get(
return {"summary": "Zusammenfassung", "report": "Lagebericht", "full": "Vollständiger Bericht"}.get(
scope, "Lagebericht"
)
@@ -434,6 +434,7 @@ def _build_export_metadata(
sections: set[str] | None,
organization_name: str | None,
top_locations: list[str] | None,
snapshot_count: int = 0,
) -> dict:
"""Einheitlicher Metadaten-Dict fuer PDF (HTML-Meta-Tags) und DOCX (core_properties)."""
is_research = incident.get("type") == "research"
@@ -522,7 +523,7 @@ def _build_export_metadata(
identifier = f"urn:aegissight:incident:{incident.get('id', '0')}:{now.strftime('%Y%m%dT%H%M%S')}"
rights = (
"Vertrauliche Lageanalyse — AegisSight Monitor. "
"Weitergabe nur an autorisierte Empfaenger."
"Weitergabe nur an autorisierte Empfänger."
)
return {
@@ -546,6 +547,7 @@ def _build_export_metadata(
"identifier": identifier,
"rights": rights,
"doc_type": "Report",
"version_id": str(max(1, snapshot_count)),
}
@@ -624,6 +626,18 @@ def _enrich_pdf_metadata(pdf_bytes: bytes, meta: dict) -> bytes:
# xmpMM: Document- und Instance-ID fuer DMS-Versionierung
xmp["xmpMM:DocumentID"] = doc_uuid
xmp["xmpMM:InstanceID"] = instance_uuid
xmp["xmpMM:VersionID"] = meta.get("version_id", "1")
# xmpMM:History — Audit-Event fuer diesen Export (einzeiliger Eintrag je Seq-Item)
history_when = (modified or datetime.now(TIMEZONE)).strftime("%Y-%m-%dT%H:%M:%S%z")
history_entry = (
f"action=published; when={history_when}; "
f"softwareAgent={meta.get('creator_app', 'AegisSight Monitor')}; "
f"instanceID={instance_uuid}; "
f"scope={meta.get('scope_label', '')}; "
f"version={meta.get('version_id', '1')}"
)
xmp["xmpMM:History"] = [history_entry]
buf_out = io.BytesIO()
pdf.save(buf_out)
@@ -639,6 +653,7 @@ async def generate_pdf(
sections: set[str] | None = None,
organization_name: str | None = None,
top_locations: list[str] | None = None,
snapshot_count: int = 0,
) -> bytes:
"""PDF-Report via WeasyPrint generieren."""
# Sections aus scope ableiten wenn nicht explizit angegeben
@@ -670,7 +685,7 @@ async def generate_pdf(
meta = _build_export_metadata(
incident, articles, fact_checks, all_sources, creator, scope, sections,
organization_name, top_locations,
organization_name, top_locations, snapshot_count=snapshot_count,
)
env = Environment(loader=FileSystemLoader(str(TEMPLATE_DIR)))
@@ -721,6 +736,7 @@ async def generate_docx(
sections: set[str] | None = None,
organization_name: str | None = None,
top_locations: list[str] | None = None,
snapshot_count: int = 0,
) -> bytes:
"""Word-Report via python-docx generieren."""
doc = Document()
@@ -738,7 +754,7 @@ async def generate_docx(
is_research = incident.get("type") == "research"
all_sources = _prepare_sources(incident)
zusammenfassung_text = executive_summary_text
bericht_summary = incident.get("summary") or "Keine Zusammenfassung verfuegbar."
bericht_summary = incident.get("summary") or "Keine Zusammenfassung verfügbar."
zusammenfassung_title = "Zusammenfassung"
zusammenfassung_lines: list[str] = []
@@ -751,7 +767,7 @@ async def generate_docx(
meta = _build_export_metadata(
incident, articles, fact_checks, all_sources, creator, scope, sections,
organization_name, top_locations,
organization_name, top_locations, snapshot_count=snapshot_count,
)
# Dateimetadaten setzen (sichtbar in Explorer/Finder, DMS-Systemen)

Datei anzeigen

@@ -1009,6 +1009,13 @@ async def export_incident(
)
top_locations = [r["location_name"] for r in await cursor.fetchall() if r["location_name"]]
# Snapshot-Count (als xmpMM:VersionID im PDF)
cursor = await db.execute(
"SELECT COUNT(*) AS cnt FROM incident_snapshots WHERE incident_id = ?",
(incident_id,),
)
snapshot_count = (await cursor.fetchone())["cnt"] or 0
# Artikel
cursor = await db.execute(
"SELECT * FROM articles WHERE incident_id = ? ORDER BY collected_at DESC",
@@ -1063,6 +1070,7 @@ async def export_incident(
sections=sections_set,
organization_name=organization_name,
top_locations=top_locations,
snapshot_count=snapshot_count,
)
filename = f"{slug}_{scope_labels_key}_{date_str}.pdf"
return StreamingResponse(
@@ -1076,6 +1084,7 @@ async def export_incident(
sections=sections_set,
organization_name=organization_name,
top_locations=top_locations,
snapshot_count=snapshot_count,
)
filename = f"{slug}_{scope_labels_key}_{date_str}.docx"
return StreamingResponse(