From 519cc274bbf1eb8fb59d8172285890b58b2c9508 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Mon, 11 May 2026 21:47:22 +0200 Subject: [PATCH 1/4] =?UTF-8?q?docs:=20session=20handover=20=E2=80=94=20MC?= =?UTF-8?q?=20Quality=20+=20Gap=20Engine=20+=20RAG=20Ingestion=20(5=20Tage?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- .../INSTRUCTION-session-handover.md | 249 ++++++------------ 1 file changed, 86 insertions(+), 163 deletions(-) diff --git a/control-pipeline/INSTRUCTION-session-handover.md b/control-pipeline/INSTRUCTION-session-handover.md index ad95b40..598e01b 100644 --- a/control-pipeline/INSTRUCTION-session-handover.md +++ b/control-pipeline/INSTRUCTION-session-handover.md @@ -1,194 +1,117 @@ -# Session-Instruktionen: Master Control Qualitaet + Regulation-Source Split +# Session-Handover: MC Quality + Gap-Analyse + RAG Ingestion -**Datum:** 2026-05-06 -**Fuer:** Naechste Claude-Session -**Repo:** breakpilot-core (~/Projekte/breakpilot-core) +**Datum:** 2026-05-07 bis 2026-05-11 (5 Tage Marathon) +**Repo:** breakpilot-core + breakpilot-compliance --- -## NAECHSTER SCHRITT: 25 grosse Master Controls aufsplitten +## ERLEDIGT -### Problem +### Master Control Quality Overhaul (Core) +- **74.5% → 92.8% Accuracy** (13.588 MCs, 83.073 Members) +- Phase 0: Quality Audit mit Claude Sonnet ($3) +- Phase 1: Ontologie 31 → 74 Tokens + LLM-Prompt fix +- Phase 2: 174K Controls re-klassifiziert via Haiku (10 Batches, ~$50) +- Phase 2b: Generic Tokens gefixt (documentation/procedure → echte Themen, $7.54) +- Phase 2c: L2 Sub-Topics (2 Runden, 172K Controls, ~$32) +- Phase 2d: Bad Subtopics gefixt (stakeholder_*, $0.50) +- Phase 3: Re-Clustering K=18704 +- Phase 4: gpre2 Direct MC (13.588 MCs) +- Phase 6: Golden Dataset (20 Controls) + 8 Quality Tests (alle grün) +- **Production Sync:** MCs + Members + Hints + doc_check_controls -25 Master Controls sind zu generisch (>200 Atomic Controls pro MC). Sie basieren auf generischen Security-Domain-Keywords wie "monitoring", "encryption", "personal_data". Embedding-Clustering allein reicht nicht — die Controls handeln zwar alle von "monitoring", aber fuer unterschiedliche Regulierungen (DSGVO, NIS2, NIST, BSI etc.). +### doc_check_controls (Core → Production) +- **1.874 Controls** über 8 Dokumenttypen (DSE, Cookie, Impressum, AGB, Widerruf, DSFA, AVV, Löschkonzept) +- Jeder mit check_question + pass_criteria + fail_criteria +- Tabelle `compliance.doc_check_controls` lokal + Production -### Die 25 betroffenen MCs +### RAG Ingestion (Core) +- **126 BAuA PDFs** (TRBS/TRGS/ASR): 27.664 Chunks → `bp_compliance_ce` +- **OSHA Technical Manual** (23 Kapitel): 7.241 Chunks → `bp_compliance_ce` +- **OSHA 1910 Subpart O** (Volltext): 745 Chunks +- **EuGH C-588/21 P**: 216 Chunks +- **EU 2018/1725**: 842 Chunks → `bp_compliance` +- **CE-Obligations extrahiert:** 6.141 Obligations → `/tmp/ce_obligations_v2.json` +- Playwright-Crawler für BAuA + OSHA gebaut -| MC-ID | Name | Controls | Problem | -|-------|------|----------|---------| -| MC-8292 | monitoring | 6.157 | Alles von Video bis Vulnerability | -| MC-2260 | procedure | 4.176 | Generisch | -| MC-8302 | alerting | 3.126 | Meldepflichten aller Gesetze gemischt | -| MC-8306 | personal_data | 3.057 | DSGVO + NIS2 + AT/CH gemischt | -| MC-8312 | training | 2.572 | | -| MC-7932 | certificate_management | 2.350 | | -| MC-8317 | incident | 2.288 | | -| MC-8329 | encryption | 1.790 | | -| MC-8333 | audit_logging | 1.645 | | -| MC-8321 | policy | 1.463 | | -| MC-8325 | patch_management | 1.155 | | -| MC-8338 | network_security | 1.071 | | -| ... | (13 weitere) | 200-960 | | +### Gap-Analyse Engine (Compliance) +- **12 Regulierungen** automatisch klassifiziert (CRA, AI Act, NIS2, DSGVO, MiCA, PSD2, AML, etc.) +- **IST-Zustand Assessment:** CE-Kennzeichnung, angewandte Normen, bestehende Prozesse, IACE-Projekt-Link +- **Norm→Control Mapping:** 20 Normen → MC-Topic Coverage +- **Prioritäts-Engine:** Severity × Deadline × Dependency +- **5 Branchentemplates:** IoT, Exchange, Cobot, SaaS, Medical +- **Frontend:** 2-Step Wizard (Produkt + IST-Zustand) + Dashboard mit Ampel-Status +- **API:** 8 Endpoints unter `/sdk/v1/gap/` +- **Persistente Projekte:** Speichern + wieder öffnen +- **Getestet:** SmartFactory Gateway → 5 Regulierungen, 500 Gaps -### Loesung: Regulation-Source Split +### Tenant Document Upload API (Core) +- `POST/GET/DELETE /api/v1/tenant/documents` +- Tenant-isolierte Qdrant-Collections +- Code fertig, nicht deployed (RAG Service rebuild nötig) -Statt nur nach Embedding-Aehnlichkeit zu clustern, nach **Regulation-Quelle** aufteilen: - -``` -MC "encryption" (1.790 Controls) - → encryption_dsgvo (DSGVO Art. 32, ~200) - → encryption_nis2 (NIS2 Art. 21, ~150) - → encryption_nist (NIST SC-13, ~300) - → encryption_bsi (BSI, ~200) - → encryption_owasp (OWASP, ~100) - → encryption_other (~840) -``` - -### Script-Ansatz - -```python -# Fuer jeden der 25 grossen MCs: -# 1. Hole alle member controls mit source_citation->>'source' -# 2. Gruppiere nach source (Regulation) -# 3. Erstelle Sub-MCs pro Regulation -# 4. Controls ohne source → "general" Sub-MC -``` - -### Qualitaetsanforderung (WICHTIG!) - -**Nur "sehr gut" ist akzeptabel.** Mittlere MCs (30-100 Controls) sind bereits excellent: -- MC-1082 (data_retention_policies, 52) → perfekt koharent -- MC-5477 (austausch_von_cybersicherheitsinformationen, 5) → perfekt - -Ziel: ALLE MCs sollen diese Qualitaet haben. Kein MC >100 Controls. +### Master Controls Browser (Compliance) +- **Neue Seite** `/sdk/master-controls` — reused Control Library UI +- Sidebar-Eintrag zwischen Control Library und Provenance +- 13.588 MCs mit allen Filtern, Paginierung, Klick-Detail +- Verbindet sich mit Production-DB --- -## SESSION 03-06.05.2026 KOMPLETT ERLEDIGT +## DB-Tabellen (neu/geändert) -### Block F (Hardcoded Knowledge → DB) -- F1: regulation_registry (223 Eintraege) ✅ -- F2: action_types (34) + action_synonyms (368) ✅ -- F3: object_synonyms (320) ✅ -- F4: LLM Enrichment (+468 Synonyme via Ollama) ✅ -- F5: Validation (8 Tests, Dicts als Fallback) ✅ - -### Control Generation Pipeline -- 1.599 Rich Controls aus E-Block Chunks (~$17 Anthropic) -- 11.522 Obligations (Pass 0a, ~$4) -- 1.147 Atomic Controls (Pass 0b, ~$4.60) -- **Gesamtkosten: ~$25.60** - -### Production Sync -- 2.625 Controls + 11.522 Obligations auf Production synchronisiert -- Production: 294.027 Controls total -- Backups: lokal + production auf MacBook - -### Block G-pre (Master Controls) -- G-pre1: 144k Objects → 7.753 Gruppen (K-Means k=5000 + Sub-Cluster + Refinement) -- G-pre2: 5.329 Master Controls, 172.504+ Members -- G-pre3: Master Control API (list, stats, detail) -- **Qualitaet:** Kleine/mittlere MCs excellent, 25 grosse MCs brauchen Regulation-Source Split - -### Block G (Compliance Execution Layer) -- G1: Decision Trace (decision_traces Tabelle + 6 API Endpoints) ✅ -- G2: Compliance Commit Ledger (compliance_commits + 5 Endpoints) ✅ -- G3: Full Decision Memory (decision_events + Timeline + 4 Endpoints) ✅ -- G4: Pre-Deployment Enforcement (deployment_checks + Override + 4 Endpoints) ✅ - -### Infrastruktur -- Vault CPU-Fix committed (Marker-File + idempotente Checks) -- Pass 0a Endpoint im Core Control-Pipeline registriert -- Gitea Timezone-Fix (docker-compose.yml) -- 61 neue regulation_ids in regulation_registry -- Container-Cleanup (fewo-finance-agent, mediaanalysisd) +| Tabelle | Repo | Rows (lokal) | Rows (Production) | +|---------|------|-------------|-------------------| +| compliance.master_controls | Core | 13.588 | 13.588 | +| compliance.master_control_members | Core | 83.073 | 83.073 | +| compliance.object_ontology | Core | 74 | 74 | +| compliance.object_groups | Core | 16.683 | — | +| compliance.doc_check_controls | Core | 1.874 | 1.874 | +| compliance.gap_projects | Compliance | 1 | 0 | --- -## DB-Tabellen (alle Bloecke) +## OFFEN / NÄCHSTE SESSION -| Tabelle | Rows | Migration | -|---------|------|-----------| -| compliance.regulation_registry | 223 | 002 | -| compliance.action_types | 34 | 003 | -| compliance.action_synonyms | 368 | 003 | -| compliance.object_synonyms | 320 | 003 | -| compliance.object_groups | 7.753 | 004 | -| compliance.master_controls | 5.329 | 005 | -| compliance.master_control_members | ~170k | 005 | -| compliance.decision_traces | 0 (Schema ready) | 006 | -| compliance.compliance_commits | 0 (Schema ready) | 007 | -| compliance.decision_events | 0 (Schema ready) | 008 | -| compliance.deployment_checks | 0 (Schema ready) | 009 | - ---- - -## API Endpoints (Core Control-Pipeline, Port 8098) - -### Bestehend -- `/v1/canonical/generate/*` — Control Generation Pipeline -- `/v1/canonical/generate/run-pass0a` — Pass 0a (NEU in dieser Session) -- `/v1/canonical/generate/submit-pass0b` — Pass 0b Batch API - -### Neu (diese Session) -- `/v1/master-controls` — G-pre3: Liste, Stats, Detail -- `/v1/decision-traces` — G1: CRUD + Stats -- `/v1/controls/{id}/full-trace` — G1: Volle Kette -- `/v1/compliance-commits` — G2: Commit Ledger -- `/v1/decision-events` — G3: Lifecycle Events + Timeline -- `/v1/deployment-checks` — G4: Pre-Deploy Gate + Override - -### API-Zugriff (WICHTIG) -```bash -# Nur via Docker exec (Port 8098 blockiert durch document-crawler) -ssh macmini "/usr/local/bin/docker exec bp-core-control-pipeline curl -sf http://127.0.0.1:8098/..." -``` +1. **Orca Deploy-Fix** — Production deployed nicht automatisch (Webhook + docker pull Problem) +2. **Gap-Analyse v2 IST-Zustand** — Frontend Step 2 deployed, Backend deployed, aber Orca blockiert +3. **Tenant Document Upload** deployen (RAG Service rebuild) +4. **Compliance-Repo auf gitea pushen** — aktuell "Everything up-to-date", Orca muss manuell redeployt werden +5. **MC-Browser erweitern** — Detail-View mit Member-Controls verbessern --- ## BACKUPS (auf MacBook) -| Datei | Inhalt | Groesse | -|-------|--------|---------| -| controls_backup_20260505.csv | 1.599 neue Controls | 7.2 MB | -| obligations_backup_20260505.csv | 11.522 Obligations | 6.2 MB | -| production_backup_20260505.dump | Production komprimiert | 30 MB | -| production_backup_20260505_plain.sql | Production plain | 1.3 GB | -| local_backup_20260506.dump | Lokale DB komprimiert | ~30 MB | -| production_backup_20260506.dump | Production komprimiert | ~30 MB | +| Datei | Inhalt | +|-------|--------| +| `backup_pre_gpre3_20260510.dump` | Vor gpre3 Live-Run (171 MB) | +| `backup_session_end_20260511.dump` | Session-Ende | +| `production_backup_20260508.dump` | Production nach Phase 2 | +| `gpre0_checkpoints_backup_20260508/` | 10 Corrections-JSONs | --- -## GESTOPPTE CONTAINER +## API-Kosten (Anthropic) -```bash -# Vault: Erst nach Fix-Deploy starten (Marker-File noetig) -ssh macmini "/usr/local/bin/docker start bp-core-vault" - -# OpenSearch: Bei Bedarf -ssh macmini "/usr/local/bin/docker start bp-lehrer-opensearch" - -# fewo-finance-agent: Fremder Container, nicht starten -``` +| Phase | Modell | Kosten | +|-------|--------|--------| +| Phase 0: Quality Audit | Sonnet | $2.92 | +| Phase 0b: Quality Audit v2 | Sonnet | $5.93 | +| Phase 2: 174K Re-Klassifizierung | Haiku | ~$50 | +| Phase 2b: Generic Token Fix | Haiku | $7.54 | +| Phase 2c: Subtopics R1 | Haiku | $20.22 | +| Phase 2c: Subtopics R2 | Haiku | $12.03 | +| Phase 2d: Bad Subtopics | Haiku | ~$0.50 | +| 5K Test-Run | Sonnet | $5.32 | +| doc_check_controls | Haiku | ~$5 | +| **Gesamt** | | **~$110** | --- -## TESTS +## STRATEGISCHE ENTSCHEIDUNGEN (in Memory) -```bash -# Pipeline (454 Tests) -PYTHONPATH=control-pipeline python3 -m pytest control-pipeline/tests/ -v -``` - ---- - -## OFFENE PUNKTE FUER ANDERE SESSIONS - -1. **Qdrant API-Key** fuer Production (qdrant-dev.breakpilot.ai) ist ungueltig (401). Muss in Coolify erneuert werden. -2. **DSI-Check False Positives**: Controls mischen interne Governance mit externen DSI-Anforderungen. Fix: nur Controls mit Art. 13/14 Referenz fuer DSI-Checks nutzen. -3. **Spotlight + mediaanalysisd** auf Mac Mini deaktivieren (braucht sudo): - ```bash - sudo mdutil -a -i off - sudo launchctl disable system/com.apple.mediaanalysisd - ``` -4. **Production DB Sync** fuer neue G-Block Tabellen (decision_traces, compliance_commits, decision_events, deployment_checks) noch ausstehend — Tabellen sind leer, Schema muss auf Production deployed werden. +1. **3 Use Cases:** Gap-Analyse (Prio 1), Vendor Risk (Prio 2), Web3/Crypto als Vertikal (Prio 3) +2. **Keine Norm-Reproduktion:** Obligation Extraction statt ISO-Texte (juristisch sicher) +3. **Regulatory Ingestion Engine:** BAuA/OSHA Crawler als Vorlage für automatisierte Source-Feeds +4. **CE-Compliance Crossover:** IACE × Master Controls für Trigger-basierte Compliance-Hinweise From f6489e77487114c9be0346215b74337ad5fe8212 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Mon, 11 May 2026 22:52:41 +0200 Subject: [PATCH 2/4] =?UTF-8?q?feat(cmp):=20Phase=202=20=E2=80=94=20send?= =?UTF-8?q?=20scripts=5Fblocked,=20scripts=5Freleased,=20cookies=5Fset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ConsentBanner detects loaded scripts (analytics/marketing) and cookies after consent, sends them to the CMP backend for transparency tracking. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/layout/ConsentBanner.tsx | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/marketing-website/components/layout/ConsentBanner.tsx b/marketing-website/components/layout/ConsentBanner.tsx index 9274537..e54bd81 100644 --- a/marketing-website/components/layout/ConsentBanner.tsx +++ b/marketing-website/components/layout/ConsentBanner.tsx @@ -86,9 +86,40 @@ function detectDevice(): { device_type: string; browser: string; os: string } { type ConsentMethod = 'accept_all' | 'reject_all' | 'custom_selection' +interface ScriptEntry { src: string; category: string } +interface CookieEntry { name: string; domain: string; expiry_days: number; category: string } + +function detectScripts(): { blocked: ScriptEntry[]; released: ScriptEntry[] } { + const scripts = Array.from(document.querySelectorAll('script[src]')) + const released: ScriptEntry[] = [] + const blocked: ScriptEntry[] = [] + for (const el of scripts) { + const src = el.getAttribute('src') || '' + if (/google.*tag|gtag|analytics/i.test(src)) released.push({ src, category: 'analytics' }) + else if (/facebook|fbevents|linkedin|tiktok/i.test(src)) released.push({ src, category: 'marketing' }) + } + return { blocked, released } +} + +function detectCookies(): CookieEntry[] { + const cookies: CookieEntry[] = [] + for (const c of document.cookie.split(';')) { + const name = c.trim().split('=')[0] + if (!name) continue + let category = 'functional' + if (/^_ga|^_gid|^_gat/i.test(name)) category = 'analytics' + else if (/^_fb|^_gcl|^_li/i.test(name)) category = 'marketing' + else if (/^bp_consent|^session|^csrf/i.test(name)) category = 'essential' + cookies.push({ name, domain: window.location.hostname, expiry_days: 0, category }) + } + return cookies +} + async function sendConsent(consent: ConsentState, method: ConsentMethod) { try { const { device_type, browser, os } = detectDevice() + const { blocked, released } = detectScripts() + const cookies_set = detectCookies() await fetch('/api/consent', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -110,6 +141,9 @@ async function sendConsent(consent: ConsentState, method: ConsentMethod) { os, screen_resolution: `${screen.width}x${screen.height}`, consent_scope: 'domain', + scripts_blocked: blocked, + scripts_released: released, + cookies_set, }), }) } catch { From 0c09b960b9dc1d0de7424e327b45360930b61476 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Tue, 12 May 2026 14:42:55 +0200 Subject: [PATCH 3/4] =?UTF-8?q?feat(cmp):=20Phase=202=20complete=20?= =?UTF-8?q?=E2=80=94=20self-hosted=20fonts,=20ScriptManager,=20GeoIP,=20ve?= =?UTF-8?q?ndor=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Session ID via sessionStorage UUID - Self-host Google Fonts (Inter, Plus Jakarta Sans, JetBrains Mono) — eliminates third-party transfer to Google, no more DSGVO violation - ScriptManager component: consent-change listener for future analytics/marketing scripts - GeoIP via browser timezone (Intl.DateTimeFormat) + IP injection in proxy - Vendor-level consent UI: loads vendor config from backend, shows per-vendor toggles under each category, sends vendor_consents dict - DSE updated: Google Fonts section now says "lokal gehostet" - Config proxy route: GET /api/consent/config Co-Authored-By: Claude Opus 4.6 (1M context) --- .../app/api/consent/config/route.ts | 21 ++++ marketing-website/app/api/consent/route.ts | 16 ++- marketing-website/app/datenschutz/page.tsx | 19 +--- marketing-website/app/globals.css | 28 ++++- marketing-website/app/layout.tsx | 2 + .../components/layout/ConsentBanner.tsx | 105 ++++++++++++++---- .../components/layout/ScriptManager.tsx | 84 ++++++++++++++ .../public/fonts/Inter-Latin.woff2 | Bin 0 -> 230192 bytes .../public/fonts/JetBrainsMono-Latin.woff2 | Bin 0 -> 55672 bytes .../public/fonts/PlusJakartaSans-Latin.woff2 | Bin 0 -> 47828 bytes 10 files changed, 232 insertions(+), 43 deletions(-) create mode 100644 marketing-website/app/api/consent/config/route.ts create mode 100644 marketing-website/components/layout/ScriptManager.tsx create mode 100644 marketing-website/public/fonts/Inter-Latin.woff2 create mode 100644 marketing-website/public/fonts/JetBrainsMono-Latin.woff2 create mode 100644 marketing-website/public/fonts/PlusJakartaSans-Latin.woff2 diff --git a/marketing-website/app/api/consent/config/route.ts b/marketing-website/app/api/consent/config/route.ts new file mode 100644 index 0000000..3ff64bc --- /dev/null +++ b/marketing-website/app/api/consent/config/route.ts @@ -0,0 +1,21 @@ +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.CONSENT_BACKEND_URL || 'https://macmini:3007/api/sdk/v1/banner' +const TENANT_ID = process.env.CONSENT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e' +const SITE_ID = process.env.NEXT_PUBLIC_CONSENT_SITE_ID || 'breakpilot-marketing' + +export async function GET(req: NextRequest) { + try { + const siteId = req.nextUrl.searchParams.get('site_id') || SITE_ID + const res = await fetch(`${BACKEND_URL}/config/${siteId}`, { + headers: { 'X-Tenant-ID': TENANT_ID }, + }) + const data = await res.text() + return new NextResponse(data, { + status: res.status, + headers: { 'Content-Type': 'application/json' }, + }) + } catch { + return NextResponse.json({ categories: [], vendors: [] }, { status: 200 }) + } +} diff --git a/marketing-website/app/api/consent/route.ts b/marketing-website/app/api/consent/route.ts index b2d9f67..16b6918 100644 --- a/marketing-website/app/api/consent/route.ts +++ b/marketing-website/app/api/consent/route.ts @@ -5,7 +5,13 @@ const TENANT_ID = process.env.CONSENT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc export async function POST(req: NextRequest) { try { - const body = await req.text() + const data = await req.json() + + // Inject client IP for backend GeoIP resolution + const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() + || req.headers.get('x-real-ip') + || null + if (ip) data.ip_address = ip const res = await fetch(`${BACKEND_URL}/consent`, { method: 'POST', @@ -13,13 +19,11 @@ export async function POST(req: NextRequest) { 'Content-Type': 'application/json', 'X-Tenant-ID': TENANT_ID, }, - body, - // Accept self-signed certs on internal network - ...(process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0' ? {} : {}), + body: JSON.stringify(data), }) - const data = await res.text() - return new NextResponse(data, { + const resBody = await res.text() + return new NextResponse(resBody, { status: res.status, headers: { 'Content-Type': 'application/json' }, }) diff --git a/marketing-website/app/datenschutz/page.tsx b/marketing-website/app/datenschutz/page.tsx index 18e0738..4630a1c 100644 --- a/marketing-website/app/datenschutz/page.tsx +++ b/marketing-website/app/datenschutz/page.tsx @@ -87,19 +87,12 @@ export default function DatenschutzPage() {
-

6. Externe Schriften

+

6. Schriften

- Diese Website laedt Schriftarten von Google Fonts. Dabei wird Ihre IP-Adresse an Google LLC, - 1600 Amphitheatre Parkway, Mountain View, CA 94043, USA uebermittelt. - Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO. - Google ist unter dem EU-US Data Privacy Framework (DPF) zertifiziert (Angemessenheitsbeschluss - der EU-Kommission vom 10. Juli 2023). -

-

- Weitere Informationen:{' '} - - policies.google.com/privacy - + Diese Website verwendet die Schriftarten Inter, Plus Jakarta Sans und JetBrains Mono. + Die Schriften werden lokal auf unserem Server gehostet — es findet kein Abruf von + externen Servern (z.B. Google Fonts) statt. Es werden keine personenbezogenen Daten + an Dritte uebermittelt.

@@ -116,8 +109,8 @@ export default function DatenschutzPage() {

8. Empfaenger und Auftragsverarbeiter

  • Hetzner Online GmbH, Industriestr. 25, 91710 Gunzenhausen — Hosting (AVV nach Art. 28 DSGVO)
  • -
  • Google LLC — Schriftarten (Google Fonts CDN, EU-US DPF)
+

Schriftarten werden lokal gehostet — kein Drittanbieter-Transfer.

diff --git a/marketing-website/app/globals.css b/marketing-website/app/globals.css index ed08ff7..d89c22b 100644 --- a/marketing-website/app/globals.css +++ b/marketing-website/app/globals.css @@ -1,6 +1,28 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap'); +/* Self-hosted fonts — kein Drittlandtransfer zu Google */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 300 900; + font-display: swap; + src: url('/fonts/Inter-Latin.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Plus Jakarta Sans'; + font-style: normal; + font-weight: 400 800; + font-display: swap; + src: url('/fonts/PlusJakartaSans-Latin.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400 600; + font-display: swap; + src: url('/fonts/JetBrainsMono-Latin.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} @tailwind base; @tailwind components; diff --git a/marketing-website/app/layout.tsx b/marketing-website/app/layout.tsx index 5b698ab..3757dab 100644 --- a/marketing-website/app/layout.tsx +++ b/marketing-website/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next' import { AppProvider } from '@/lib/context' import ConsentBanner from '@/components/layout/ConsentBanner' +import ScriptManager from '@/components/layout/ScriptManager' import './globals.css' export const metadata: Metadata = { @@ -27,6 +28,7 @@ export default function RootLayout({ {children} + diff --git a/marketing-website/components/layout/ConsentBanner.tsx b/marketing-website/components/layout/ConsentBanner.tsx index e54bd81..de665b5 100644 --- a/marketing-website/components/layout/ConsentBanner.tsx +++ b/marketing-website/components/layout/ConsentBanner.tsx @@ -51,6 +51,18 @@ const texts = { }, } +function getSessionId(): string { + if (typeof window === 'undefined') return '' + try { + let sid = sessionStorage.getItem('bp_session_id') + if (!sid) { + sid = crypto.randomUUID() + sessionStorage.setItem('bp_session_id', sid) + } + return sid + } catch { return '' } +} + function getFingerprint(): string { const nav = typeof navigator !== 'undefined' ? navigator : null const raw = [nav?.language, nav?.platform, screen?.width, screen?.height, new Date().getTimezoneOffset()].join('|') @@ -115,7 +127,7 @@ function detectCookies(): CookieEntry[] { return cookies } -async function sendConsent(consent: ConsentState, method: ConsentMethod) { +async function sendConsent(consent: ConsentState, method: ConsentMethod, vendorConsents?: Record) { try { const { device_type, browser, os } = detectDevice() const { blocked, released } = detectScripts() @@ -131,7 +143,8 @@ async function sendConsent(consent: ConsentState, method: ConsentMethod) { ...(consent.functional ? ['functional'] : []), ...(consent.analytics ? ['analytics'] : []), ], - vendors: [], + vendors: Object.keys(vendorConsents || {}).filter(k => vendorConsents?.[k]), + vendor_consents: vendorConsents || {}, user_agent: navigator.userAgent, consent_method: method, page_url: window.location.href, @@ -141,6 +154,8 @@ async function sendConsent(consent: ConsentState, method: ConsentMethod) { os, screen_resolution: `${screen.width}x${screen.height}`, consent_scope: 'domain', + session_id: getSessionId(), + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, scripts_blocked: blocked, scripts_released: released, cookies_set, @@ -151,26 +166,49 @@ async function sendConsent(consent: ConsentState, method: ConsentMethod) { } } +interface VendorConfig { + vendor_name: string + category_key: string + description_de?: string + description_en?: string + cookie_names?: string[] + retention_days?: number +} + export default function ConsentBanner() { const { lang } = useApp() const t = texts[lang] const [visible, setVisible] = useState(false) const [showDetails, setShowDetails] = useState(false) const [consent, setConsent] = useState(defaultConsent) + const [vendors, setVendors] = useState([]) + const [vendorConsents, setVendorConsents] = useState>({}) useEffect(() => { const saved = getSavedConsent() if (!saved) { setVisible(true) } + // Load vendor config from backend + fetch('/api/consent/config') + .then(r => r.json()) + .then(data => { + const v = data?.vendors || [] + setVendors(v) + // Default all vendors to true + const defaults: Record = {} + for (const vendor of v) defaults[vendor.vendor_name] = true + setVendorConsents(defaults) + }) + .catch(() => {}) }, []) const save = useCallback((state: ConsentState, method: ConsentMethod) => { localStorage.setItem(COOKIE_NAME, JSON.stringify(state)) - sendConsent(state, method) + sendConsent(state, method, vendorConsents) setVisible(false) window.dispatchEvent(new CustomEvent('consent-change', { detail: state })) - }, []) + }, [vendorConsents]) const acceptAll = () => save({ essential: true, functional: true, analytics: true }, 'accept_all') const rejectAll = () => save({ essential: true, functional: false, analytics: false }, 'reject_all') @@ -215,24 +253,49 @@ export default function ConsentBanner() { className="overflow-hidden mb-4" >
- {categories.map(([key, cat]) => ( -
@@ -80,9 +90,19 @@ export default function DatenschutzPage() {

5. Server-Logfiles

- Der Hosting-Provider erhebt technisch notwendige Logfiles (IP-Adresse, Browsertyp, Zeitstempel). - Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse an der Sicherheit). - Logfiles werden nach 7 Tagen automatisch geloescht. + Der Hosting-Provider erhebt technisch notwendige Logfiles (IP-Adresse, Browsertyp, Zeitstempel, + aufgerufene Seite, HTTP-Statuscode). +

+

+ Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse). +

+

+ Interessenabwaegung: Die Erhebung von Server-Logfiles ist + fuer die Erkennung und Abwehr von Cyberangriffen, die Fehlerbehebung und die Gewaehrleistung der + Systemstabilitaet unerlasslich. Die Daten werden automatisiert nach 7 Tagen geloescht und nicht + zur Profilbildung oder Identifizierung einzelner Nutzer verwendet. Eine Zusammenfuehrung mit anderen + Datenquellen findet nicht statt. Das Interesse der Betroffenen am Schutz ihrer Daten wird durch die + kurze Speicherdauer und die rein technische Nutzung angemessen gewahrt.

diff --git a/marketing-website/components/layout/ScriptManager.tsx b/marketing-website/components/layout/ScriptManager.tsx index cc4b723..61e6f18 100644 --- a/marketing-website/components/layout/ScriptManager.tsx +++ b/marketing-website/components/layout/ScriptManager.tsx @@ -1,31 +1,43 @@ 'use client' -import { useEffect, useRef } from 'react' +import { useEffect, useRef, useCallback } from 'react' /** - * ScriptManager — consent-aware script injection. + * ScriptManager — active consent-aware script blocking + injection. * - * Listens to the `consent-change` CustomEvent dispatched by ConsentBanner - * and injects/skips third-party scripts based on accepted categories. + * Two mechanisms: + * 1. INJECTION: Scripts in CONSENT_SCRIPTS are only injected AFTER consent. + * 2. BLOCKING: Existing + * + * Usage for adding new third-party scripts: + * Add to CONSENT_SCRIPTS below. They'll be injected only after consent. */ interface ConsentScript { src: string async?: boolean + id?: string } const CONSENT_SCRIPTS: Record = { analytics: [ - // Example: { src: 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXX', async: true }, + // { src: 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXX', async: true, id: 'gtag' }, + // { src: 'https://plausible.io/js/script.js', async: true, id: 'plausible' }, ], marketing: [ - // Example: { src: 'https://connect.facebook.net/en_US/fbevents.js', async: true }, + // { src: 'https://connect.facebook.net/en_US/fbevents.js', async: true, id: 'fb-pixel' }, + // { src: 'https://snap.licdn.com/li.lms-analytics/insight.min.js', async: true, id: 'li-insight' }, ], functional: [ - // Example: { src: 'https://widget.example.com/chat.js', async: true }, + // { src: 'https://widget.example.com/chat.js', async: true, id: 'chat-widget' }, ], } @@ -46,39 +58,58 @@ function getStoredConsent(): ConsentState | null { export default function ScriptManager() { const injected = useRef(new Set()) - function injectScripts(consent: ConsentState) { - const categories: (keyof typeof CONSENT_SCRIPTS)[] = [] - if (consent.analytics) categories.push('analytics') - if (consent.functional) categories.push('functional') - // marketing would need its own toggle in ConsentState + const applyConsent = useCallback((consent: ConsentState) => { + const accepted = new Set() + accepted.add('essential') // always allowed + if (consent.functional) accepted.add('functional') + if (consent.analytics) accepted.add('analytics') - for (const cat of categories) { + // 1. INJECT: Add scripts from CONSENT_SCRIPTS for accepted categories + for (const cat of accepted) { for (const script of CONSENT_SCRIPTS[cat] ?? []) { if (injected.current.has(script.src)) continue const el = document.createElement('script') el.src = script.src if (script.async) el.async = true + if (script.id) el.id = script.id el.dataset.consent = cat document.head.appendChild(el) injected.current.add(script.src) } } - } + + // 2. ACTIVATE: Unblock