From 662327e8b4b72e23f37d0d06c1e24927d2c039ef Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Mon, 18 May 2026 18:30:08 +0200 Subject: [PATCH] feat(compliance-check): MC-Classification + Embedding + Vendor-Redundanz + Action-Recipes + Borlabs-Features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Massiv-Update auf Basis BMW-Test-Iterationen (v1→v9): Core Compliance-Check - Sonnet check_type Klassifikation: text/process/review fuer alle 1874 MCs in compliance.doc_check_controls (script + Sidecar /data/mc_classification.db). rag_document_checker filtert auf check_type='text' fuer doc_check. Plus fits_doc_type-Audit (v2) + ui_only-Audit fuer DSA/E-Commerce-MCs in falscher doc_type-Schublade. - scope_requires-Filter: biometric/ai_decision/child_targeting MCs werden per business_profile gefiltert (FRT skipped fuer BMW etc.). - Embedding-Match (BGE-M3) als Phase-3 nach Regex-Match: Per-doc_type-Threshold-Override (impressum 0.50, dse/cookie 0.60), Short-Field-Rescue (15-Wort-Chunks) fuer Pflichtfelder im Impressum. Title+check_question als Embedding-Input fuer mehr Kontext. - Cookie-Text-Routing: consent-tester gibt cmp_cookie_text aus dem CMP-Reconstruct zurueck, Backend bevorzugt das gegen DOM-Extraction wenn richer (BMW 1824 vs 600 Worte). Vendor-Redundanz + EU-Alternativen + Cost-Saving - vendor_redundancy.analyze() — funktionale Kategorisierung der CMP-Vendors, Detektion von Mehrfach-Anbietern pro Kategorie, EU-Alternative-Lookup (Matomo, IONOS, HERE, Friendly Captcha, Smart AdServer, ...). - vendor_cost_estimator: Tier-Inferenz aus Cookie-Footprint (Cookie-Anzahl + Premium-Feature-Cookies + Third-Party-Quote → starter/professional/ enterprise/premier). - Self-Service-Werbung (Google/Meta/Pinterest/...) = 0 Lizenz-Kosten (nur Media-Spend, separat). DSP-Plattformen behalten enge Range. - Tier-aware Saving-Range: bei Enterprise/Premier nutzen wir den oberen 40-100%-Band der Listpreise, nicht starter→premier. - Multi-Function-Tools (Matomo Pro, SAP CX, IONOS Cloud, Userlike, Smart AdServer, HERE Maps, Vimeo Pro, LamaPoll) — ein Tool ersetzt mehrere Kategorien gleichzeitig. Cookie-Wissens-DB + Funktionale Klassifikation - cookie_knowledge_db: 50 kuratierte Top-Cookies (Google/Meta/Adobe/MS/...) mit vendor, exact_purpose, data_collected, IAB-TCF-IDs, reid_risk, schrems_ii_status, EuGH-Urteile, EU-Alternative. - cookie_function_classifier: pro Cookie funktionale Rolle (tracking_id, ad_pixel, session_id, ab_test, csrf, ...) + blocking_impact. Country-Inferenz aus Rechtsform - cookie_link_validator: Country-Field wird aus Vendor-Name abgeleitet (A/S=DK, GmbH=DE, Inc=US, B.V.=NL, ...) plus Vendor-Lookup-Table. Reduziert false-positive no_country-Flags bei eindeutig-EU-Vendors (Adform DK, Pinterest IE). Action-Recipes + Doc-Anchor-Locator - finding_action_recipes: pro Finding-Typ (no_cookies_listed, no_country, broken_opt_out, "Auftragsverarbeiter erwaehnen", "Art. 22 Profiling", ...) eine strukturierte Anweisung mit what/why/fix_text/where/example. Zum 1:1-Einfuegen in Kunden-Dokumente. - doc_anchor_locator: Embedding-basiert (BGE-M3 cosine) — sucht den passenden Absatz im existierenden Kundendokument fuer jeden Finding. Per-Run Thread-Local-Cache. Fallback: keyword-Match. - Email-Rendering integriert Recipe + Anchor pro Doc-Pruefungs-Fail + Vendor-Flag-Liste mit aufklappbarer Action-Liste. - Score-Erklaerung pro Vendor-Zeile (3/5-Untertitel + Tooltip). Migration-Pipeline (Compliance-Check -> Customer Banner/Documents) - migration_to_banner.py: Vendor-Liste -> CookieBannerConfig mit 4 Kategorien + Review-Flags. - migration_to_document.py: Vendor-Liste -> Cookie-Policy + VVT-Register + Privacy-Policy-Pre-Fills. - agent_migration_routes: 3 Preview-Endpoints (banner-preview, document-preview, summary). Persistierung der cmp_vendors in /data/compliance_audits.db check_payloads-Tabelle. Borlabs-Parity Cookie-Banner-Features - Consent-Historie im Banner: window.bpShowConsentHistory() + localStorage. - Content-Blocker: cookie-banner-content-blocker.ts — YouTube/Maps/Video Placeholder bis Einwilligung. - Google Consent Mode v2 erweitert: wait_for_update + region=EEA/CH/GB. - Consent-Log Export (CSV/JSON) per einwilligungen_export_routes. Bug-Fixes - canonical_control_routes: _jsonish-Helper fuer string-typed jsonb, similar-controls-Endpoint mit _has_embedding_col()-Cache (kein 500 mehr). - Control-Library Frontend: defensive .map-Coercer in 2 Detail-Views. - Embedding-Service-Batching (32er Batches statt 165 in einem Call). - KeyError 'control_id' in MC-Result-Aggregation (defensive .get). - Master-Controls-Klick-Through von /sdk/master-controls auf /sdk/control-library?control= mit URL-Param-Auto-Open. - Dockerfile: /data pre-chowned auf appuser (Audit-DB-Schreibrecht). - Cookie-Text-Routing-Bug (cmp_reconstructed > DOM-extraction). - doc_type-aware MC-Filter (statt all-text-MCs). - Master-Contract-Dedup (60 BMW-Internal-Eintraege = 1 Adobe-Vertrag). - A3-v2-Audit hat 24 UI-Sprache-MCs als 'process' reklassifiziert. Tests - test_migration_mappers.py (9 Tests) - test_migration_endpoints.py (4 Tests) Skripte (one-shot) - classify_mc_check_type.py (v1) + _v2 (PK=control_id,doc_type) - audit_mc_doctype_fit.py (v1 fits) + _v2 (ui_only + scope_requires) BMW-Run-Bilanz v1 (broken) -> v9 (alle Fixes): DSE 7,5% -> 81-83% Impressum 4% -> 100% (6 echte MCs alle erfuellt) Cookie 0% -> 79-83% (CMP-Text-Routing + Embedding) Plus: 10 Konsolidierungs-Kategorien, geschaetzte Saving 200k-3M / Jahr Plus: Action-Recipes + Doc-Anchors fuer jeden Fail Co-Authored-By: Claude Opus 4.7 (1M context) --- .../api/sdk/v1/einwilligungen/export/route.ts | 55 ++ .../_components/ControlDetailView.tsx | 53 +- .../components/ControlDetail.tsx | 42 +- .../app/sdk/control-library/page.tsx | 20 + .../app/sdk/einwilligungen/page.tsx | 36 +- .../app/sdk/master-controls/page.tsx | 61 +- .../cookie-banner-content-blocker.ts | 156 ++++ .../generator/cookie-banner-embed.ts | 80 +- backend-compliance/Dockerfile | 5 +- backend-compliance/compliance/api/__init__.py | 1 + .../api/agent_compliance_check_routes.py | 109 ++- .../compliance/api/agent_doc_check_extras.py | 58 +- .../api/agent_doc_check_redundancy.py | 141 ++++ .../compliance/api/agent_doc_check_report.py | 114 ++- .../api/canonical_control_routes.py | 62 +- .../api/einwilligungen_export_routes.py | 181 +++++ .../services/cookie_function_classifier.py | 167 ++++ .../services/cookie_knowledge_db.py | 608 +++++++++++++++ .../services/cookie_link_validator.py | 156 ++++ .../compliance/services/doc_anchor_locator.py | 350 +++++++++ .../services/finding_action_recipes.py | 353 +++++++++ .../services/mc_embedding_matcher.py | 309 ++++++++ .../compliance/services/mc_scorecard.py | 37 +- .../services/rag_document_checker.py | 104 ++- .../services/vendor_cost_estimator.py | 407 ++++++++++ .../compliance/services/vendor_redundancy.py | 727 ++++++++++++++++++ .../scripts/audit_mc_doctype_fit.py | 229 ++++++ .../scripts/audit_mc_doctype_fit_v2.py | 216 ++++++ .../scripts/classify_mc_check_type.py | 222 ++++++ .../scripts/classify_mc_check_type_v2.py | 241 ++++++ consent-tester/services/dsi_discovery.py | 18 +- 31 files changed, 5214 insertions(+), 104 deletions(-) create mode 100644 admin-compliance/app/api/sdk/v1/einwilligungen/export/route.ts create mode 100644 admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner-content-blocker.ts create mode 100644 backend-compliance/compliance/api/agent_doc_check_redundancy.py create mode 100644 backend-compliance/compliance/api/einwilligungen_export_routes.py create mode 100644 backend-compliance/compliance/services/cookie_function_classifier.py create mode 100644 backend-compliance/compliance/services/cookie_knowledge_db.py create mode 100644 backend-compliance/compliance/services/doc_anchor_locator.py create mode 100644 backend-compliance/compliance/services/finding_action_recipes.py create mode 100644 backend-compliance/compliance/services/mc_embedding_matcher.py create mode 100644 backend-compliance/compliance/services/vendor_cost_estimator.py create mode 100644 backend-compliance/compliance/services/vendor_redundancy.py create mode 100644 backend-compliance/scripts/audit_mc_doctype_fit.py create mode 100644 backend-compliance/scripts/audit_mc_doctype_fit_v2.py create mode 100644 backend-compliance/scripts/classify_mc_check_type.py create mode 100644 backend-compliance/scripts/classify_mc_check_type_v2.py diff --git a/admin-compliance/app/api/sdk/v1/einwilligungen/export/route.ts b/admin-compliance/app/api/sdk/v1/einwilligungen/export/route.ts new file mode 100644 index 00000000..d1574502 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/einwilligungen/export/route.ts @@ -0,0 +1,55 @@ +/** + * Proxy: GET /api/sdk/v1/einwilligungen/export?format=csv|json&kind=consents|history + * -> backend /api/compliance/einwilligungen/export/ + * + * Streams the backend response straight through (CSV or JSON download). + */ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +function getTenantHeader(request: NextRequest): HeadersInit { + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + const clientTenantId = request.headers.get('x-tenant-id') || request.headers.get('X-Tenant-ID') + const tenantId = (clientTenantId && uuidRegex.test(clientTenantId)) + ? clientTenantId + : (process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e') + return { 'X-Tenant-ID': tenantId } +} + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url) + const fmt = (searchParams.get('format') || 'csv').toLowerCase() + const kind = (searchParams.get('kind') || 'consents').toLowerCase() + + const filename = `${kind}.${fmt === 'json' ? 'json' : 'csv'}` + const upstreamPath = `/api/compliance/einwilligungen/export/${filename}` + + const passthroughParams = new URLSearchParams() + for (const k of ['user_id', 'granted', 'since', 'consent_id']) { + const v = searchParams.get(k) + if (v) passthroughParams.set(k, v) + } + const qs = passthroughParams.toString() + const url = `${BACKEND_URL}${upstreamPath}${qs ? `?${qs}` : ''}` + + try { + const r = await fetch(url, { headers: getTenantHeader(request) }) + if (!r.ok) { + const text = await r.text() + return NextResponse.json({ error: text || `HTTP ${r.status}` }, { status: r.status }) + } + return new NextResponse(r.body, { + status: 200, + headers: { + 'Content-Type': r.headers.get('content-type') || 'application/octet-stream', + 'Content-Disposition': r.headers.get('content-disposition') || `attachment; filename=${filename}`, + }, + }) + } catch (e) { + return NextResponse.json( + { error: 'Export-Proxy fehlgeschlagen', detail: String(e) }, + { status: 503 }, + ) + } +} diff --git a/admin-compliance/app/sdk/control-library/_components/ControlDetailView.tsx b/admin-compliance/app/sdk/control-library/_components/ControlDetailView.tsx index 28e924e9..86a5ddc5 100644 --- a/admin-compliance/app/sdk/control-library/_components/ControlDetailView.tsx +++ b/admin-compliance/app/sdk/control-library/_components/ControlDetailView.tsx @@ -8,6 +8,23 @@ import type { CanonicalControl } from '../_types' import { EFFORT_LABELS } from '../_types' import { SeverityBadge, StateBadge, LicenseRuleBadge } from './Badges' +// Defensive coercers: backend has rows where evidence/requirements/test_procedure/open_anchors +// are JSON-encoded strings instead of arrays. .map() on a string throws — coerce here. +function asArray(v: unknown): T[] { + if (Array.isArray(v)) return v as T[] + if (typeof v === 'string' && v.trim().startsWith('[')) { + try { const p = JSON.parse(v); return Array.isArray(p) ? p : [] } catch { return [] } + } + return [] +} +function asStringArray(v: unknown): string[] { + return asArray(v).map(x => typeof x === 'string' ? x : JSON.stringify(x)) +} +type EvidenceItem = string | { type?: string; description?: string } +function asEvidenceArray(v: unknown): EvidenceItem[] { + return asArray(v) +} + export function ControlDetailView({ ctrl, onBack, @@ -72,31 +89,31 @@ export function ControlDetailView({

Geltungsbereich

- {ctrl.scope.platforms && ctrl.scope.platforms.length > 0 && ( + {asStringArray(ctrl.scope?.platforms).length > 0 && (

Plattformen

- {ctrl.scope.platforms.map(p => ( + {asStringArray(ctrl.scope?.platforms).map(p => ( {p} ))}
)} - {ctrl.scope.components && ctrl.scope.components.length > 0 && ( + {asStringArray(ctrl.scope?.components).length > 0 && (

Komponenten

- {ctrl.scope.components.map(c => ( + {asStringArray(ctrl.scope?.components).map(c => ( {c} ))}
)} - {ctrl.scope.data_classes && ctrl.scope.data_classes.length > 0 && ( + {asStringArray(ctrl.scope?.data_classes).length > 0 && (

Datenklassen

- {ctrl.scope.data_classes.map(d => ( + {asStringArray(ctrl.scope?.data_classes).map(d => ( {d} ))}
@@ -109,7 +126,7 @@ export function ControlDetailView({

Anforderungen

    - {ctrl.requirements.map((req, i) => ( + {asStringArray(ctrl.requirements).map((req, i) => (
  1. {i + 1} {req} @@ -122,7 +139,7 @@ export function ControlDetailView({

    Pruefverfahren

      - {ctrl.test_procedure.map((step, i) => ( + {asStringArray(ctrl.test_procedure).map((step, i) => (
    1. {step} @@ -135,12 +152,18 @@ export function ControlDetailView({

      Nachweisanforderungen

      - {ctrl.evidence.map((ev, i) => ( + {asEvidenceArray(ctrl.evidence).map((ev, i) => (
      - {ev.type} -

      {ev.description}

      + {typeof ev === 'string' ? ( +

      {ev}

      + ) : ( + <> + {ev.type && {ev.type}} +

      {ev.description ?? JSON.stringify(ev)}

      + + )}
      ))} @@ -152,13 +175,13 @@ export function ControlDetailView({

      Open-Source-Referenzen

      - ({ctrl.open_anchors.length} Quellen) + ({asArray(ctrl.open_anchors).length} Quellen)

      Dieses Control basiert auf frei verfuegbarem Wissen. Alle Referenzen sind offen und oeffentlich zugaenglich.

      - {ctrl.open_anchors.map((anchor, i) => ( + {asArray<{ framework?: string; ref?: string; url?: string }>(ctrl.open_anchors).map((anchor, i) => (
      @@ -180,11 +203,11 @@ export function ControlDetailView({
      {/* Tags */} - {ctrl.tags.length > 0 && ( + {asStringArray(ctrl.tags).length > 0 && (

      Tags

      - {ctrl.tags.map(tag => ( + {asStringArray(ctrl.tags).map(tag => ( {tag} ))}
      diff --git a/admin-compliance/app/sdk/control-library/components/ControlDetail.tsx b/admin-compliance/app/sdk/control-library/components/ControlDetail.tsx index 731d2e13..9ef6806f 100644 --- a/admin-compliance/app/sdk/control-library/components/ControlDetail.tsx +++ b/admin-compliance/app/sdk/control-library/components/ControlDetail.tsx @@ -18,6 +18,16 @@ import { ControlRegulatorySection } from './ControlRegulatorySection' import { ControlSimilarControls } from './ControlSimilarControls' import { ControlReviewActions } from './ControlReviewActions' +// Defensive coercer: some canonical_controls rows have evidence/tags/etc. +// as JSON-encoded strings instead of arrays. .map() on a string throws. +function toArray(v: unknown): T[] { + if (Array.isArray(v)) return v as T[] + if (typeof v === 'string' && v.trim().startsWith('[')) { + try { const p = JSON.parse(v); return Array.isArray(p) ? p : [] } catch { return [] } + } + return [] +} + interface SimilarControl { control_id: string; title: string; severity: string; release_state: string; tags: string[]; license_rule: number | null; verification_method: string | null; @@ -186,7 +196,7 @@ export function ControlDetail({ - {!ctrl.source_citation && ctrl.open_anchors.length > 0 && ( + {!ctrl.source_citation && toArray(ctrl.open_anchors).length > 0 && (
      @@ -201,36 +211,36 @@ export function ControlDetail({
      )} - {(ctrl.scope.platforms?.length || ctrl.scope.components?.length || ctrl.scope.data_classes?.length) ? ( + {(toArray(ctrl.scope?.platforms).length || toArray(ctrl.scope?.components).length || toArray(ctrl.scope?.data_classes).length) ? (

      Geltungsbereich

      - {ctrl.scope.platforms?.length ?
      Plattformen: {ctrl.scope.platforms.join(', ')}
      : null} - {ctrl.scope.components?.length ?
      Komponenten: {ctrl.scope.components.join(', ')}
      : null} - {ctrl.scope.data_classes?.length ?
      Datenklassen: {ctrl.scope.data_classes.join(', ')}
      : null} + {toArray(ctrl.scope?.platforms).length ?
      Plattformen: {toArray(ctrl.scope?.platforms).join(', ')}
      : null} + {toArray(ctrl.scope?.components).length ?
      Komponenten: {toArray(ctrl.scope?.components).join(', ')}
      : null} + {toArray(ctrl.scope?.data_classes).length ?
      Datenklassen: {toArray(ctrl.scope?.data_classes).join(', ')}
      : null}
      ) : null} - {Array.isArray(ctrl.requirements) && ctrl.requirements.length > 0 && ( + {toArray(ctrl.requirements).length > 0 && (

      Anforderungen

      -
        {ctrl.requirements.map((r, i) =>
      1. {r}
      2. )}
      +
        {toArray(ctrl.requirements).map((r, i) =>
      1. {r}
      2. )}
      )} - {Array.isArray(ctrl.test_procedure) && ctrl.test_procedure.length > 0 && ( + {toArray(ctrl.test_procedure).length > 0 && (

      Pruefverfahren

      -
        {ctrl.test_procedure.map((s, i) =>
      1. {s}
      2. )}
      +
        {toArray(ctrl.test_procedure).map((s, i) =>
      1. {s}
      2. )}
      )} - {ctrl.evidence.length > 0 && ( + {toArray(ctrl.evidence).length > 0 && (

      Nachweise

      - {ctrl.evidence.map((ev, i) => ( + {toArray(ctrl.evidence).map((ev, i) => (
      {typeof ev === 'string' ?
      {ev}
      :
      {ev.type}: {ev.description}
      } @@ -243,9 +253,9 @@ export function ControlDetail({
      {ctrl.risk_score !== null &&
      Risiko-Score: {ctrl.risk_score}
      } {ctrl.implementation_effort &&
      Aufwand: {EFFORT_LABELS[ctrl.implementation_effort] || ctrl.implementation_effort}
      } - {ctrl.tags.length > 0 && ( + {toArray(ctrl.tags).length > 0 && (
      - {ctrl.tags.map(t => {t})} + {toArray(ctrl.tags).map(t => {t})}
      )}
      @@ -253,11 +263,11 @@ export function ControlDetail({
      -

      Open-Source-Referenzen ({ctrl.open_anchors.length})

      +

      Open-Source-Referenzen ({toArray(ctrl.open_anchors).length})

      - {ctrl.open_anchors.length > 0 ? ( + {toArray(ctrl.open_anchors).length > 0 ? (
      - {ctrl.open_anchors.map((anchor, i) => ( + {toArray<{ framework?: string; ref?: string; url?: string }>(ctrl.open_anchors).map((anchor, i) => (
      {anchor.framework} diff --git a/admin-compliance/app/sdk/control-library/page.tsx b/admin-compliance/app/sdk/control-library/page.tsx index 94adc36f..cdc7bd4c 100644 --- a/admin-compliance/app/sdk/control-library/page.tsx +++ b/admin-compliance/app/sdk/control-library/page.tsx @@ -1,5 +1,7 @@ 'use client' +import { useEffect } from 'react' +import { useSearchParams } from 'next/navigation' import { EMPTY_CONTROL } from './components/helpers' import { ControlForm } from './components/ControlForm' import { ControlDetail } from './components/ControlDetail' @@ -12,6 +14,24 @@ import { BACKEND_URL } from './components/helpers' export default function ControlLibraryPage() { const state = useControlLibraryState() + const searchParams = useSearchParams() + + // Deep-link via /sdk/control-library?control= + // — e.g. from /sdk/master-controls member list. + useEffect(() => { + const cid = searchParams?.get('control') + if (!cid || state.selectedControl?.control_id === cid) return + fetch(`${BACKEND_URL}?endpoint=control&id=${encodeURIComponent(cid)}`) + .then(r => r.ok ? r.json() : null) + .then(ctrl => { + if (ctrl?.control_id) { + state.setSelectedControl(ctrl) + state.setMode('detail') + } + }) + .catch(() => { /* user just sees the list */ }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchParams]) const { handleCreate, handleUpdate, handleDelete, handleReview, handleBulkReject, diff --git a/admin-compliance/app/sdk/einwilligungen/page.tsx b/admin-compliance/app/sdk/einwilligungen/page.tsx index b4ae05e2..72cd65fc 100644 --- a/admin-compliance/app/sdk/einwilligungen/page.tsx +++ b/admin-compliance/app/sdk/einwilligungen/page.tsx @@ -57,12 +57,7 @@ export default function EinwilligungenPage() { explanation={stepInfo.explanation} tips={stepInfo.tips} > - + {/* Navigation Tabs */} @@ -150,3 +145,32 @@ export default function EinwilligungenPage() {
      ) } + +// Export-Dropdown im Step-Header. Streamt CSV/JSON direkt aus dem +// Backend via /api/sdk/v1/einwilligungen/export-Proxy. +function ConsentExportButton() { + return ( + + ) +} diff --git a/admin-compliance/app/sdk/master-controls/page.tsx b/admin-compliance/app/sdk/master-controls/page.tsx index 84bdf9c1..7b24aaf2 100644 --- a/admin-compliance/app/sdk/master-controls/page.tsx +++ b/admin-compliance/app/sdk/master-controls/page.tsx @@ -199,32 +199,43 @@ function MCDetail({ mc, onBack }: { mc: Record; onBack: () => v
      ) : (
      - {filtered.map((m, i) => ( -
      -
      - {m.control_id} - {m.severity && ( - - {m.severity} - + {filtered.map((m, i) => { + const inner = ( + <> +
      + {m.control_id} + {m.severity && ( + + {m.severity} + + )} + {m.phase && ( + + {m.phase} + + )} + {m.action && ( + {m.action} + )} +
      +

      {m.title}

      + {m.regulation_source && ( +

      + {m.regulation_source} {m.regulation_article} +

      )} - {m.phase && ( - - {m.phase} - - )} - {m.action && ( - {m.action} - )} -
      -

      {m.title}

      - {m.regulation_source && ( -

      - {m.regulation_source} {m.regulation_article} -

      - )} -
      - ))} + + ) + return m.control_id ? ( + + {inner} + + ) : ( +
      {inner}
      + ) + })} {filtered.length === 0 && !loading && (
      Keine Controls gefunden
      )} diff --git a/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner-content-blocker.ts b/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner-content-blocker.ts new file mode 100644 index 00000000..42aa6b0d --- /dev/null +++ b/admin-compliance/lib/sdk/einwilligungen/generator/cookie-banner-content-blocker.ts @@ -0,0 +1,156 @@ +/** + * Content-Blocker Generator (Borlabs-Parity). + * + * Returns a small JS snippet that scans the page for blockable third-party + * embeds (YouTube, Vimeo, Google Maps, Spotify, Twitter, Facebook) and + * replaces them with a click-to-consent placeholder until the user agrees + * to the relevant cookie category. + * + * The customer drops a SECOND script tag next to the banner: + * + * + * + * Author writes content as either: + * + * + * + * + * OR auto-detect: any