Files
breakpilot-compliance/backend-compliance/compliance/api/agent_doc_check_exec_summary.py
T
Benjamin Admin 338e03d3b0
CI / detect-changes (push) Successful in 10s
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 16s
CI / go-lint (push) Has been skipped
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m46s
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / test-python-backend (push) Successful in 43s
CI / test-python-document-crawler (push) Has been skipped
feat(audit): P34 Exec-Summary Score-Einordnung — 'wo Sie stehen sollten'
_score_band_explanation: vier Baender (Sehr gut/Akzeptabel/Handlungs-
bedarf/Erhoehtes Risiko) liefern Label + erwartete Handlung. Wird als
neue Zeile unter den KPIs in der Exec-Summary gerendert (mit
score-farbiger Linkmark).

Sachlicher Ton — kein 'Vorstand muss sofort handeln', sondern
realistische Empfehlung (z.B. '70-84: Branchen-Median, einmaliges
Aufraeumen + Halbjahres-Check').

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

180 lines
7.3 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_score_band_block(pct: int, color: str) -> list[str]:
"""P34 — eine Zeile unter den KPIs: Score-Einordnung."""
band, hint = _score_band_explanation(pct)
return [
f'<div style="margin-top:10px;padding:10px 14px;'
f'background:rgba(255,255,255,0.04);border-left:3px solid {color};'
f'border-radius:4px">'
f'<div style="font-size:11px;color:#cbd5e1">'
f'<strong style="color:{color}">{band} ({pct}%)</strong> — {hint}'
f'</div></div>',
]
def _score_band_explanation(pct: int) -> tuple[str, str]:
"""P34 — Was bedeutet der Score: wo MUESSTE man stehen.
Returns (label, what_to_expect)."""
if pct >= 85:
return (
"Sehr gut", "Praxis-uebliche DSGVO-Risikolage. "
"Standard-Pflege reicht — jaehrliche Pruefung empfohlen.",
)
if pct >= 70:
return (
"Akzeptabel", "Branchen-Median. Verbleibende Findings sind "
"meist Formalia — Empfehlung: einmaliges Aufraeumen, dann "
"Halbjahres-Check.",
)
if pct >= 50:
return (
"Handlungsbedarf", "Mehrere wesentliche Themen offen. "
"Empfehlung: priorisierte Abarbeitung der HIGH-Findings "
"binnen 4-8 Wochen mit DSB + Web-Team.",
)
return (
"Erhoehtes Risiko", "Mehrere Kern-Pflichten fehlen oder sind "
"veraltet. Empfehlung: kurzfristiger Termin mit DSB / Rechtsabteilung "
"und Web-Team zur Priorisierung.",
)
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>',
# P34 — Score-Einordnung "wer wo stehen muss"
*(_build_score_band_block(pct, score_color) if scorecard else []),
# 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)