Merge branch 'main' of ssh://coolify.meghsakha.com:22222/Benjamin_Boenisch/breakpilot-core
Build pitch-deck / build-push-deploy (push) Successful in 1m48s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 30s
CI / test-python-voice (push) Successful in 29s
CI / test-bqas (push) Successful in 29s
Build pitch-deck / build-push-deploy (push) Successful in 1m48s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 30s
CI / test-python-voice (push) Successful in 29s
CI / test-bqas (push) Successful in 29s
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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' },
|
||||
})
|
||||
|
||||
@@ -31,7 +31,17 @@ export default function DatenschutzPage() {
|
||||
<p>
|
||||
Diese Website wird auf Servern der Hetzner Online GmbH in Deutschland gehostet.
|
||||
Es findet kein Drittlandtransfer fuer das Hosting statt.
|
||||
Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse an zuverlaessigem Betrieb).
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
<span className="text-white font-medium">Rechtsgrundlage:</span> Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse).
|
||||
</p>
|
||||
<p className="mt-1">
|
||||
<span className="text-white font-medium">Interessenabwaegung:</span> Unser berechtigtes Interesse liegt im
|
||||
zuverlaessigen und sicheren Betrieb der Website. Ohne Hosting-Infrastruktur koennen wir unser Angebot
|
||||
nicht bereitstellen. Die Verarbeitung beschraenkt sich auf technisch notwendige Verbindungsdaten
|
||||
(IP-Adresse, Zeitstempel). Entgegenstehende Interessen der Betroffenen ueberwiegen nicht, da die
|
||||
Daten nur kurzzeitig (7 Tage) gespeichert, nicht mit anderen Datenquellen zusammengefuehrt und
|
||||
ausschliesslich zur Sicherstellung des Betriebs und der IT-Sicherheit verwendet werden.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -80,26 +90,29 @@ export default function DatenschutzPage() {
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white mb-2">5. Server-Logfiles</h2>
|
||||
<p>
|
||||
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).
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
<span className="text-white font-medium">Rechtsgrundlage:</span> Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse).
|
||||
</p>
|
||||
<p className="mt-1">
|
||||
<span className="text-white font-medium">Interessenabwaegung:</span> 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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white mb-2">6. Externe Schriften</h2>
|
||||
<h2 className="text-lg font-semibold text-white mb-2">6. Schriften</h2>
|
||||
<p>
|
||||
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).
|
||||
</p>
|
||||
<p className="mt-1">
|
||||
Weitere Informationen:{' '}
|
||||
<a href="https://policies.google.com/privacy" className="text-accent-electric hover:underline" target="_blank" rel="noopener noreferrer">
|
||||
policies.google.com/privacy
|
||||
</a>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -116,8 +129,8 @@ export default function DatenschutzPage() {
|
||||
<h2 className="text-lg font-semibold text-white mb-2">8. Empfaenger und Auftragsverarbeiter</h2>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
<li>Hetzner Online GmbH, Industriestr. 25, 91710 Gunzenhausen — Hosting (AVV nach Art. 28 DSGVO)</li>
|
||||
<li>Google LLC — Schriftarten (Google Fonts CDN, EU-US DPF)</li>
|
||||
</ul>
|
||||
<p className="mt-1">Schriftarten werden lokal gehostet — kein Drittanbieter-Transfer.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
<AppProvider>
|
||||
{children}
|
||||
<ConsentBanner />
|
||||
<ScriptManager />
|
||||
</AppProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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('|')
|
||||
@@ -86,9 +98,40 @@ function detectDevice(): { device_type: string; browser: string; os: string } {
|
||||
|
||||
type ConsentMethod = 'accept_all' | 'reject_all' | 'custom_selection'
|
||||
|
||||
async function sendConsent(consent: ConsentState, method: ConsentMethod) {
|
||||
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, vendorConsents?: Record<string, boolean>) {
|
||||
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' },
|
||||
@@ -100,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,
|
||||
@@ -110,6 +154,11 @@ 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,
|
||||
}),
|
||||
})
|
||||
} catch {
|
||||
@@ -117,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<ConsentState>(defaultConsent)
|
||||
const [vendors, setVendors] = useState<VendorConfig[]>([])
|
||||
const [vendorConsents, setVendorConsents] = useState<Record<string, boolean>>({})
|
||||
|
||||
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<string, boolean> = {}
|
||||
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')
|
||||
@@ -181,24 +253,49 @@ export default function ConsentBanner() {
|
||||
className="overflow-hidden mb-4"
|
||||
>
|
||||
<div className="space-y-2 pt-2 border-t border-white/[0.06]">
|
||||
{categories.map(([key, cat]) => (
|
||||
<label
|
||||
key={key}
|
||||
className="flex items-center justify-between p-3 rounded-xl bg-white/[0.03] border border-white/[0.06] cursor-pointer hover:bg-white/[0.05] transition-colors"
|
||||
>
|
||||
<div className="flex-1 mr-4">
|
||||
<span className="text-xs font-semibold text-white">{cat.name}</span>
|
||||
<p className="text-xs text-white/40 mt-0.5">{cat.description}</p>
|
||||
{categories.map(([key, cat]) => {
|
||||
const catVendors = vendors.filter(v => v.category_key === key)
|
||||
return (
|
||||
<div key={key} className="rounded-xl bg-white/[0.03] border border-white/[0.06]">
|
||||
<label className="flex items-center justify-between p-3 cursor-pointer hover:bg-white/[0.05] transition-colors">
|
||||
<div className="flex-1 mr-4">
|
||||
<span className="text-xs font-semibold text-white">{cat.name}</span>
|
||||
<p className="text-xs text-white/40 mt-0.5">{cat.description}</p>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={cat.required || consent[key as keyof ConsentState]}
|
||||
disabled={cat.required}
|
||||
onChange={(e) => {
|
||||
setConsent(prev => ({ ...prev, [key]: e.target.checked }))
|
||||
for (const v of catVendors) {
|
||||
setVendorConsents(prev => ({ ...prev, [v.vendor_name]: e.target.checked }))
|
||||
}
|
||||
}}
|
||||
className="w-4 h-4 rounded accent-accent-electric"
|
||||
/>
|
||||
</label>
|
||||
{catVendors.length > 0 && consent[key as keyof ConsentState] && (
|
||||
<div className="px-3 pb-3 space-y-1">
|
||||
{catVendors.map(v => (
|
||||
<label key={v.vendor_name} className="flex items-center justify-between pl-4 py-1 text-xs cursor-pointer">
|
||||
<div className="flex-1 mr-2">
|
||||
<span className="text-white/60">{v.vendor_name}</span>
|
||||
{v.retention_days && <span className="text-white/30 ml-1">({v.retention_days}d)</span>}
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={vendorConsents[v.vendor_name] ?? true}
|
||||
onChange={(e) => setVendorConsents(prev => ({ ...prev, [v.vendor_name]: e.target.checked }))}
|
||||
className="w-3 h-3 rounded accent-accent-electric"
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={cat.required || consent[key as keyof ConsentState]}
|
||||
disabled={cat.required}
|
||||
onChange={(e) => setConsent(prev => ({ ...prev, [key]: e.target.checked }))}
|
||||
className="w-4 h-4 rounded accent-accent-electric"
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useCallback } from 'react'
|
||||
|
||||
/**
|
||||
* ScriptManager — active consent-aware script blocking + injection.
|
||||
*
|
||||
* Two mechanisms:
|
||||
* 1. INJECTION: Scripts in CONSENT_SCRIPTS are only injected AFTER consent.
|
||||
* 2. BLOCKING: Existing <script data-consent="category" type="text/plain">
|
||||
* elements in the page are activated after consent by changing type to
|
||||
* "text/javascript". This is the standard CMP blocking pattern.
|
||||
*
|
||||
* Usage for inline scripts in pages:
|
||||
* <script type="text/plain" data-consent="analytics">
|
||||
* // This won't execute until analytics consent is given
|
||||
* gtag('config', 'G-XXXXXX');
|
||||
* </script>
|
||||
*
|
||||
* 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<string, ConsentScript[]> = {
|
||||
analytics: [
|
||||
// { 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: [
|
||||
// { 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: [
|
||||
// { src: 'https://widget.example.com/chat.js', async: true, id: 'chat-widget' },
|
||||
],
|
||||
}
|
||||
|
||||
interface ConsentState {
|
||||
essential: boolean
|
||||
functional: boolean
|
||||
analytics: boolean
|
||||
}
|
||||
|
||||
function getStoredConsent(): ConsentState | null {
|
||||
if (typeof window === 'undefined') return null
|
||||
try {
|
||||
const raw = localStorage.getItem('bp_consent')
|
||||
return raw ? JSON.parse(raw) : null
|
||||
} catch { return null }
|
||||
}
|
||||
|
||||
export default function ScriptManager() {
|
||||
const injected = useRef(new Set<string>())
|
||||
|
||||
const applyConsent = useCallback((consent: ConsentState) => {
|
||||
const accepted = new Set<string>()
|
||||
accepted.add('essential') // always allowed
|
||||
if (consent.functional) accepted.add('functional')
|
||||
if (consent.analytics) accepted.add('analytics')
|
||||
|
||||
// 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 <script type="text/plain" data-consent="...">
|
||||
const blocked = document.querySelectorAll('script[type="text/plain"][data-consent]')
|
||||
for (const el of blocked) {
|
||||
const cat = el.getAttribute('data-consent') || ''
|
||||
if (accepted.has(cat)) {
|
||||
const clone = document.createElement('script')
|
||||
// Copy attributes
|
||||
for (const attr of el.attributes) {
|
||||
if (attr.name === 'type') continue // skip type="text/plain"
|
||||
clone.setAttribute(attr.name, attr.value)
|
||||
}
|
||||
clone.type = 'text/javascript'
|
||||
clone.textContent = el.textContent
|
||||
el.parentNode?.replaceChild(clone, el)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// On mount: apply saved consent (return visitors)
|
||||
const saved = getStoredConsent()
|
||||
if (saved) applyConsent(saved)
|
||||
|
||||
// Listen for new consent decisions
|
||||
function onConsentChange(e: Event) {
|
||||
const detail = (e as CustomEvent<ConsentState>).detail
|
||||
if (detail) applyConsent(detail)
|
||||
}
|
||||
|
||||
window.addEventListener('consent-change', onConsentChange)
|
||||
return () => window.removeEventListener('consent-change', onConsentChange)
|
||||
}, [applyConsent])
|
||||
|
||||
return null
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user