Files
breakpilot-compliance/backend-compliance/compliance/api/agent_doc_check_exec_summary.py
T
Benjamin Admin 6c223c7c9b
CI / detect-changes (push) Successful in 10s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / validate-canonical-controls (push) Successful in 14s
CI / loc-budget (push) Failing after 15s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m43s
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 37s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
feat(compliance-check): exec-summary + voll-audit + TDM-respect + cookie-KB-extended + saving-scan-funnel
P1 — Exec-Summary oben im Email-Report (4 KPIs + 2 CTAs, dunkler Gradient)
P3 — no_direct_sales-Flag fuer OEM-Konfigurator-Sites; AGB/Widerruf/AGB als
     "NICHT ANWENDBAR" (grau) statt "NICHT GEFUNDEN" (rot)
P5 — Voll-Audit Unification: alle Findings (MC + Pflichtangaben + Vendor +
     Redundanz) in /data/compliance_audits.db.unified_findings; neuer
     /api/compliance/agent/findings/<id> Endpoint + FindingsTab im Audit-UI
     mit Filter + CSV-Export
P7 — Crawl-Hardening: TDM-Reservation-Check (robots.txt / ai.txt / Header /
     Meta) vor jedem Run mit 24h-Cache; HeadlessChrome-UA (Firma noch nicht
     gegruendet — Switch via BREAKPILOT_BRANDED_UA env); per-Domain
     Rate-Limit 1 req/s + max 2 concurrent
P2 — Cookie-Knowledge-DB additiv erweitert (35 -> 74 Cookies): Adobe, Meta,
     Microsoft, LinkedIn, TikTok, HubSpot, Marketo, Salesforce, Hotjar,
     FullStory, Mouseflow, Intercom, Drift, Zendesk, Cloudflare, Stripe,
     OneTrust/Cookiebot/Usercentrics, Matomo, Pinterest, Snapchat, X/Twitter,
     YouTube, Vimeo, Klaviyo, Mailchimp, Mixpanel, Segment, Amplitude,
     Optimizely, Datadog; Wire-in in cookie_function_classifier liefert
     compliance_risk-Label (kritisch/hoch/mittel/gering) pro Vendor
A  — k-Anonymitaets-Helper (benchmark_k_anonymity) fuer P6-Vorbereitung
B  — Cross-Tenant-Domain-Assertion im /findings-Endpoint (expected_domain
     Query-Param -> 403 bei Mismatch)
C  — Saving-Scan-Funnel: /api/compliance/agent/saving-scan/start mit
     Validierung + 24h-Rate-Limit pro Domain + Lead-Persistenz in
     saving_scan_leads + Auto-Discovery via _run_compliance_check; 6 Tests
D  — Risk-Badge im Email-Vendor-Row

Rechtliche Leitplanken (Memory feedback_oem_data_legal.md): nur eigene
Knapp-Bewertungen + Source-Pointer, keine 1:1-Kopien fremder CMP-Texte.
TDM-Opt-Out-Respect nach § 44b UrhG. KEINE Schema-Aenderungen — alles in
Sidecar-SQLite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:48:34 +02:00

136 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Executive-Summary-Block — der oberste Email-Abschnitt.
Zeigt CFO / GF in 4 Zahlen den Gesamt-Mehrwert des Compliance-Checks:
1) Compliance-Score (Trend vs Vorlauf)
2) Anzahl analysierter Anbieter
3) Geschaetztes jaehrliches Sparpotenzial (Range)
4) Konsolidierungs-Potenzial (Anbieter koennen reduziert werden)
Plus zwei Big-CTA-Buttons:
- "Compliance-Maengel im Detail" → springt zum Doc-Pruefungs-Block
- "Konsolidierungs-Plan ansehen" → springt zum Redundanz-Block
Ziel: in 5 Sekunden sieht der Vorstand den ROI. Wenn neugierig, scrollt
er weiter in die Detail-Bloecke (die UNTER dieser Summary liegen).
"""
from __future__ import annotations
def _fmt_eur_range(low: int, high: int) -> str:
if not low and not high:
return ""
if low == high:
return f"~{low:,}".replace(",", ".")
return f"{low:,}{high:,}".replace(",", ".")
def build_exec_summary_html(
scorecard: dict | None,
previous_scorecard: dict | None,
cmp_vendors: list[dict] | None,
redundancy_report: dict | None,
site_name: str = "",
) -> str:
"""Build the top-of-email Executive Summary with 4 KPIs + 2 CTAs."""
# 1) Compliance-Score
pct = 0
delta_str = ""
score_color = "#94a3b8"
if scorecard:
totals = scorecard.get("totals") or {}
pct = int(totals.get("pct", 0))
score_color = ("#16a34a" if pct >= 80 else
"#d97706" if pct >= 50 else "#dc2626")
if previous_scorecard:
prev_pct = int((previous_scorecard.get("totals") or {}).get("pct", 0))
d = pct - prev_pct
if d:
trend_color = "#16a34a" if d > 0 else "#dc2626"
delta_str = (
f'<span style="font-size:14px;color:{trend_color};margin-left:6px">'
f'{"+" if d > 0 else ""}{d} pp</span>'
)
# 2) Vendor-Count
n_vendors = len(cmp_vendors or [])
# 3+4) Saving + Konsolidierung
s = (redundancy_report or {}).get("summary") or {}
sav_low, sav_high = s.get("estimated_saving_year_eur", [0, 0])
n_consolidation = s.get("consolidation_potential", 0)
sav_pct = s.get("estimated_saving_pct", "")
parts = [
'<div style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;'
'max-width:700px;margin:0 auto 18px;padding:18px 22px;'
'background:linear-gradient(135deg,#1e293b 0%,#0f172a 100%);'
'border-radius:10px;color:white">',
f'<div style="font-size:11px;color:#94a3b8;text-transform:uppercase;'
f'letter-spacing:1.5px;margin-bottom:6px">Executive Summary</div>',
f'<h2 style="margin:0 0 16px;font-size:18px;color:white">'
f'Compliance-Check {site_name}</h2>',
# 2x2 KPI grid
'<table style="width:100%;border-collapse:separate;border-spacing:8px">',
# Row 1: Compliance + Vendor count
'<tr>',
f'<td style="width:50%;padding:12px 14px;background:rgba(255,255,255,0.05);'
f'border-radius:6px;border:1px solid rgba(255,255,255,0.08)">'
f'<div style="font-size:10px;color:#94a3b8;text-transform:uppercase;'
f'letter-spacing:1px;margin-bottom:4px">DSGVO / TDDDG / TMG Score</div>'
f'<div style="font-size:28px;font-weight:700;color:{score_color}">'
f'{pct}%{delta_str}</div>'
f'<div style="font-size:11px;color:#cbd5e1;margin-top:2px">'
f'aus {int((scorecard or {}).get("totals", {}).get("total", 0))} Pflicht-Pruefungen</div>'
f'</td>',
f'<td style="width:50%;padding:12px 14px;background:rgba(255,255,255,0.05);'
f'border-radius:6px;border:1px solid rgba(255,255,255,0.08)">'
f'<div style="font-size:10px;color:#94a3b8;text-transform:uppercase;'
f'letter-spacing:1px;margin-bottom:4px">Identifizierte Anbieter</div>'
f'<div style="font-size:28px;font-weight:700;color:white">{n_vendors}</div>'
f'<div style="font-size:11px;color:#cbd5e1;margin-top:2px">'
f'davon {n_consolidation} konsolidierbar</div>'
f'</td>',
'</tr>',
# Row 2: Saving + CTA-Hinweis
'<tr>',
f'<td colspan="2" style="padding:14px 16px;background:linear-gradient(90deg,'
f'rgba(16,185,129,0.15) 0%,rgba(16,185,129,0.05) 100%);'
f'border-radius:6px;border:1px solid rgba(16,185,129,0.3)">'
f'<div style="font-size:10px;color:#86efac;text-transform:uppercase;'
f'letter-spacing:1px;margin-bottom:4px">'
f'Geschaetztes Sparpotenzial pro Jahr (Tool-Lizenzen, ohne Media-Spend)</div>'
f'<div style="font-size:24px;font-weight:700;color:#34d399">'
f'{_fmt_eur_range(sav_low, sav_high)}'
f'<span style="font-size:14px;color:#86efac;margin-left:8px">({sav_pct})</span></div>'
f'<div style="font-size:11px;color:#cbd5e1;margin-top:4px">'
f'durch Konsolidierung redundanter Anbieter auf je 1 EU-Tool pro '
f'Funktions-Kategorie. <em>Schaetzbereich, mit dem Einkauf zu verifizieren.</em>'
f'</div></td>',
'</tr>',
'</table>',
# CTAs
'<div style="margin-top:14px;padding-top:12px;border-top:1px solid '
'rgba(255,255,255,0.1);text-align:center">',
'<a href="#mc-scorecard" style="display:inline-block;padding:8px 16px;'
'background:#7c3aed;color:white;text-decoration:none;border-radius:6px;'
'font-size:12px;font-weight:600;margin-right:8px">'
'Compliance-Maengel im Detail &rarr;</a>',
'<a href="#optimierungspotenzial" style="display:inline-block;padding:8px 16px;'
'background:#10b981;color:white;text-decoration:none;border-radius:6px;'
'font-size:12px;font-weight:600">'
'Konsolidierungs-Plan &rarr;</a>',
'</div>',
'</div>',
]
return "".join(parts)