feat(audit-report): deterministischer Textreport je Audit (MD + PDF) + Bericht-Tab

Firmen-tauglicher Bericht aus den Snapshot-Modulergebnissen (kein Re-Crawl, kein
LLM): Einleitung, Testumfang+Methodik, Management-Summary (4-Status), Detail-
befunde je Modul, Maßnahmen, Rechtlicher Hinweis. Co-Pilot-Tonalität, Tracking-
statt Cookie-Rohzahl, Norm nur referenziert (kein Normtext).
- audit_report.py: assemble_report (pur) + render_markdown + render_pdf (reportlab)
- snapshot_check_routes: GET /report (struktur+md) + GET /report.pdf
- Frontend: AuditReportTab + Proxys (report, report/pdf) + "Bericht"-Tab
- Tests: 5 Assembler (compliance/tests → CI-geprüft) + 1 Vitest

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-13 14:50:45 +02:00
parent 7273245054
commit d720db07dd
8 changed files with 602 additions and 0 deletions
@@ -215,3 +215,74 @@ async def snapshot_browser_behavior(snapshot_id: str):
return {"browser_matrix": matrix}
finally:
db.close()
async def _gather_report(snapshot_id: str):
"""Lädt den Snapshot + sammelt ALLE Modul-Ergebnisse (kein Re-Crawl) für den
Audit-Report. Gibt (meta, modules) zurück."""
from database import SessionLocal
from compliance.services.check_snapshot import (
load_snapshot, load_browser_matrix,
)
from compliance.services.browser_cross_finding import build_cross_findings
db = SessionLocal()
try:
snap = load_snapshot(db, snapshot_id)
if not snap:
raise HTTPException(status_code=404, detail="snapshot not found")
meta = {
"site_label": snap.get("site_label"),
"site_domain": snap.get("site_domain"),
"created_at": snap.get("created_at"),
"check_id": snap.get("check_id"),
"scan_context": snap.get("scan_context"),
}
bm = load_browser_matrix(db, snapshot_id)
finally:
db.close()
docs = snap.get("doc_entries") or []
def _has(dt: str) -> bool:
return any(e.get("doc_type") == dt
and len(e.get("text") or e.get("content") or "") > 100
for e in docs)
modules: dict = {}
if snap.get("cmp_vendors"):
try:
modules["cookie"] = await snapshot_cookie_check(snapshot_id)
except Exception as e:
logger.warning("report cookie failed: %s", e)
for dt, agent in (("impressum", "impressum"), ("dse", "dse"), ("agb", "agb")):
if _has(dt):
try:
modules[dt] = await _run_doc_agent(snapshot_id, dt, agent)
except Exception as e:
logger.warning("report %s failed: %s", dt, e)
if bm:
modules["browser"] = {"browser_matrix": bm,
"cross_findings": build_cross_findings(bm)}
return meta, modules
@router.get("/snapshots/{snapshot_id}/report")
async def snapshot_report(snapshot_id: str):
"""Deterministischer Audit-Textreport (strukturiert + Markdown), aus den
Modul-Ergebnissen des Snapshots — kein Re-Crawl, kein LLM."""
from compliance.services.audit_report import assemble_report, render_markdown
meta, modules = await _gather_report(snapshot_id)
report = assemble_report(meta, modules)
return {"report": report, "markdown": render_markdown(report)}
@router.get("/snapshots/{snapshot_id}/report.pdf")
async def snapshot_report_pdf(snapshot_id: str):
"""Druckfertiges PDF des Audit-Reports (reportlab)."""
from fastapi import Response
from compliance.services.audit_report import assemble_report, render_pdf
meta, modules = await _gather_report(snapshot_id)
pdf = render_pdf(assemble_report(meta, modules))
dom = (meta.get("site_domain") or "report").replace("/", "_")
return Response(
content=pdf, media_type="application/pdf",
headers={"Content-Disposition": f'attachment; filename="audit-{dom}.pdf"'})