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:
@@ -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"'})
|
||||
|
||||
Reference in New Issue
Block a user