Files
breakpilot-compliance/backend-compliance/compliance/api/agent_doc_check_redundancy.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

143 lines
6.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.
"""
Email-Renderer fuer den Vendor-Redundanz + EU-Alternativen + Cost-/Savings-Block.
Wird im Email-Body unter dem VVT eingebaut.
"""
from __future__ import annotations
def _fmt_eur(low: int, high: int) -> str:
if not low and not high:
return "im Listpreis bundled"
if low == high:
return f"~{low:,}".replace(",", ".")
return f"{low:,}{high:,}".replace(",", ".")
def build_redundancy_html(report: dict | None) -> str:
if not report:
return ""
s = report.get("summary") or {}
redundancies = report.get("redundancies") or []
eu_alts = report.get("eu_alternatives") or []
multi = report.get("multi_function_tools") or []
cur = s.get("estimated_current_year_eur") or [0, 0]
sav = s.get("estimated_saving_year_eur") or [0, 0]
pct = s.get("estimated_saving_pct") or "n/a"
parts = [
'<div id="optimierungspotenzial" style="font-family:-apple-system,'
'BlinkMacSystemFont,sans-serif;max-width:700px;margin:0 auto 16px;'
'padding:14px 18px;background:#fef3c7;border:1px solid #fcd34d;'
'border-radius:8px">',
'<h3 style="margin:0 0 6px;font-size:14px;color:#92400e">'
'Optimierungspotenzial: Redundanzen + EU-Alternativen</h3>',
f'<p style="margin:0 0 10px;font-size:11px;color:#78350f">'
f'<strong>{s.get("redundancy_count", 0)}</strong> Kategorien mit '
f'mehreren Anbietern · <strong>{s.get("consolidation_potential", 0)}</strong> '
f'Anbieter konsolidierbar · '
f'<strong>{s.get("eu_alternative_count", 0)}</strong> EU-Alternativen verfuegbar</p>',
'<div style="background:#fff;border:1px solid #fcd34d;border-radius:6px;'
'padding:10px 12px;margin-bottom:10px">',
'<div style="font-size:10px;color:#94a3b8;margin-bottom:6px;text-transform:uppercase;letter-spacing:0.5px">'
'Diese Schaetzung umfasst NUR die als redundant erkannten Tools — '
'nicht den Gesamt-Stack der Website</div>',
f'<div style="font-size:11px;color:#78350f">'
f'Listpreis-Schaetzung der <strong>redundanten</strong> Tools '
f'(Mehrfach-Anbieter in derselben Funktions-Kategorie):'
f' <strong>{_fmt_eur(*cur)}/Jahr</strong></div>',
f'<div style="font-size:11px;color:#16a34a;margin-top:4px">'
f'Sparpotenzial durch Konsolidierung auf je 1 EU-Tool pro Kategorie:'
f' <strong>{_fmt_eur(*sav)}/Jahr</strong> ({pct})</div>',
'<div style="font-size:10px;color:#94a3b8;margin-top:8px;font-style:italic">'
'<strong>Wichtige Einschraenkungen:</strong><br/>'
'• Konzern-Konditionen liegen ueblicherweise 3050% unter Listpreis — '
'realistisches Saving entsprechend €X·0,5 bis €X·0,7.<br/>'
'• Eintraege "<em>Eigene Marke — Tool</em>" (z.B. "BMW AG — Adobe Analytics") '
'gehoeren oft zu einem einzigen Master-Vertrag, nicht zu mehreren Lizenzen.<br/>'
'• Media-Spend (Google Ads, Meta Ads) ist NICHT enthalten — nur Tooling-Lizenzen.<br/>'
'• Quelle: Gartner/Forrester 2025 + oeffentliche Listpreise.'
'</div></div>',
]
if redundancies:
parts.append(
'<table style="width:100%;border-collapse:collapse;font-size:11px;'
'margin-bottom:10px">'
'<thead><tr style="background:#fde68a;color:#78350f;text-align:left">'
'<th style="padding:6px 8px">Kategorie</th>'
'<th style="padding:6px 8px">#</th>'
'<th style="padding:6px 8px">Anbieter</th>'
'<th style="padding:6px 8px">EU-Empfehlung</th>'
'<th style="padding:6px 8px;text-align:right">Saving / Jahr</th>'
'</tr></thead><tbody>'
)
for r in redundancies[:12]:
vendors_str = ", ".join(r.get("vendors", [])[:6])
if len(r.get("vendors", [])) > 6:
vendors_str += f" (+{len(r['vendors']) - 6} weitere)"
sav_r = r.get("estimated_saving_year_eur") or [0, 0]
parts.append(
f'<tr style="border-top:1px solid #fde68a;vertical-align:top">'
f'<td style="padding:5px 8px;color:#78350f;font-weight:600">{r["category_label"]}</td>'
f'<td style="padding:5px 8px;text-align:center">{r["count"]}</td>'
f'<td style="padding:5px 8px;color:#1e293b;font-size:10px">{vendors_str}</td>'
f'<td style="padding:5px 8px;color:#16a34a;font-size:10px">{r.get("suggested_eu_tool") or ""}</td>'
f'<td style="padding:5px 8px;text-align:right;color:#16a34a;font-weight:600">'
f'{_fmt_eur(*sav_r)}</td></tr>'
)
hint = r.get("consolidation_hint")
if hint:
parts.append(
f'<tr><td colspan="5" style="padding:0 8px 8px;color:#94a3b8;font-size:10px;font-style:italic">'
f'Hinweis: {hint}</td></tr>'
)
caveats = r.get("caveats") or []
if caveats:
parts.append(
f'<tr><td colspan="5" style="padding:0 8px 8px;color:#94a3b8;font-size:10px">'
f'<strong>Moegliche Gruende fuer Mehrfach-Einsatz:</strong> '
+ "; ".join(caveats) + '</td></tr>'
)
parts.append('</tbody></table>')
if multi:
parts.append(
'<div style="margin-top:8px"><strong style="font-size:11px;color:#78350f">'
'Multi-Funktions-Tools (1 Tool ersetzt mehrere Kategorien):</strong>'
'<ul style="margin:6px 0 0 18px;padding:0;font-size:11px;color:#78350f">'
)
for t in multi[:4]:
cats = ", ".join(t.get("replaces_categories", []))
parts.append(
f'<li style="margin-bottom:3px"><strong>{t["name"]}</strong>'
f' ({t["country"]}) — ersetzt <em>{cats}</em>'
f' ({t.get("potential_replacements", 0)} Anbieter heute)</li>'
)
parts.append('</ul></div>')
if eu_alts:
parts.append(
'<details style="margin-top:8px"><summary style="font-size:11px;color:#78350f;'
'cursor:pointer">EU-Alternativen pro Anbieter (Details)</summary>'
'<ul style="margin:6px 0 0 18px;padding:0;font-size:10px;color:#475569">'
)
for e in eu_alts[:20]:
first_alt = (e.get("alternatives") or [{}])[0]
parts.append(
f'<li style="margin-bottom:3px"><strong>{e["current_vendor"]}</strong>'
f'{first_alt.get("name", "")} ({first_alt.get("country", "")})'
f' <span style="color:#94a3b8">— {first_alt.get("notes", "")}</span></li>'
)
parts.append('</ul></details>')
parts.append('</div>')
return "".join(parts)