Compare commits
51 Commits
0bb9726ddd
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a7850a0296 | |||
| ec3b0e26fd | |||
| 19d1a56df4 | |||
| 3934bdf814 | |||
| dbd44ecc20 | |||
| 93687a32fe | |||
| 2d9fec3a6d | |||
| a6f4ca88a4 | |||
| 297eff949e | |||
| 01e2e0fc4b | |||
| b4043b20b2 | |||
| ad61fd3779 | |||
| d1b55cd65b | |||
| cb46372e52 | |||
| f1814fe8ec | |||
| 12a9fe1810 | |||
| 8b5b9905a7 | |||
| cd23ebc3ba | |||
| f30ac73b79 | |||
| bb85ee2e27 | |||
| 0d5ebcd27a | |||
| 7d721a6787 | |||
| 9a1ad87acd | |||
| 911697bab4 | |||
| 9783657da3 | |||
| 47d7beeb52 | |||
| 63b195c0aa | |||
| 77993d0ea0 | |||
| 9382d2a7a4 | |||
| b727f14011 | |||
| 084beed348 | |||
| 5510689710 | |||
| 49e594bf38 | |||
| 583e54fabc | |||
| 7f4b7da098 | |||
| f3e54180f0 | |||
| ae937a35d7 | |||
| edac3aca6c | |||
| fc4d5d8c56 | |||
| f5d4e3bd95 | |||
| 9e3604fe31 | |||
| 0c09b960b9 | |||
| cf18b1074a | |||
| 2e8cbfff3f | |||
| f6489e7748 | |||
| 519cc274bb | |||
| 79810f4eb8 | |||
| 5f193c8a72 | |||
| d13f4511cb | |||
| 937eca6b77 | |||
| 0c1561d6cc |
@@ -25,6 +25,7 @@ voice-service/bqas/** | owner=pipeline | reason=RAG Quality Assessment, produkti
|
||||
# Seed/Helper Scripts (keine Service-Logik)
|
||||
scripts/seed-demo-and-screenshot.py | owner=infra | reason=Einmaliges Seed-Script, kein Service-Code | review=permanent
|
||||
pitch-deck/scripts/import-finanzplan.py | owner=pitch-deck | reason=583 LOC, einmaliges Excel-Import-Script (9 Sheet-Importer), hardcodierte Row/Col-Mappings fuer eine Finanzplan-.xlsm-Datei, keine wiederverwendbare Logik | review=2027-01
|
||||
pitch-deck/scripts/export-finanzplan-excel.ts | owner=pitch-deck | reason=1254 LOC, Excel-Export-Script — analog zu import-finanzplan.py: 9 Sheets, ~80% Cell-Formatting/Styling-Boilerplate, keine wiederverwendbare Logik | review=2027-01
|
||||
|
||||
# PDF Templates (reine statische HTML/CSS Strings, keine Logik)
|
||||
backend-core/services/pdf_templates.py | owner=all | reason=519 LOC, rein statische Jinja2-HTML-Templates + CSS, keine Logik | review=2026-07
|
||||
@@ -33,3 +34,6 @@ backend-core/services/pdf_templates.py | owner=all | reason=519 LOC, rein statis
|
||||
pitch-deck/lib/presenter/presenter-faq.ts | owner=pitch-deck | reason=973 LOC, pure static FAQ array (questions/answers/keywords), no logic | review=2027-01
|
||||
pitch-deck/lib/presenter/presenter-script.ts | owner=pitch-deck | reason=608 LOC, pure static presenter script data + 3 trivial lookup functions | review=2027-01
|
||||
pitch-deck/lib/i18n.ts | owner=pitch-deck | reason=620 LOC, pure DE/EN translation dictionaries + 3 small format helpers | review=2027-01
|
||||
|
||||
# Marketing Website — adapted from pitch-deck USP slide (complex SVG animation, inline styles, no logic to split)
|
||||
marketing-website/components/sections/PlatformBridgeSection.tsx | owner=marketing | reason=816 LOC, adapted 1:1 from pitch-deck USPSlide with SVG animations, CSS keyframes, inline styles — splitting would break animation coherence | review=2027-01
|
||||
|
||||
@@ -41,6 +41,11 @@ backups/*.backup
|
||||
*.mp3
|
||||
*.wav
|
||||
|
||||
# Cloned external legal-source repos (gitignored; pulled fresh at ingest time)
|
||||
legal-sources/bsi-quaidal/
|
||||
legal-sources/bsi-quaidal-src/
|
||||
legal-sources/bsi-grundschutz-plus/
|
||||
|
||||
# Compiled binaries
|
||||
billing-service/billing-service
|
||||
consent-service/server
|
||||
@@ -62,3 +67,7 @@ consent-service/server
|
||||
# Coverage
|
||||
coverage/
|
||||
*.coverage
|
||||
controls_backup_*.dump
|
||||
|
||||
# Allow Finanzplan exports (generated by pitch-deck/scripts/export-finanzplan.sh)
|
||||
!pitch-deck/exports/*.xlsx
|
||||
|
||||
Generated
+2948
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.468.0",
|
||||
"next": "^15.1.0",
|
||||
"next": "^15.5.16",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"reactflow": "^11.11.4",
|
||||
|
||||
@@ -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,132 @@
|
||||
# Lessons Learned — MC `check_type` Klassifikation (KRITISCH fuer CRA + alle neuen Frameworks)
|
||||
|
||||
Datum: 2026-05-17
|
||||
Auslöser: Compliance-Check BMW lieferte 0/381 Cookie-MCs, 3/75 Impressum-MCs, 43/571 DSE-MCs — alle Doc-Typen unter 20%.
|
||||
|
||||
## TL;DR
|
||||
|
||||
**Die heutigen Master-Controls (MCs) vermischen drei strukturell unterschiedliche Klassen von Pruefungen in einer einzigen Tabelle (`compliance.doc_check_controls`). Nur eine der drei Klassen lässt sich gegen Dokument-Text matchen. Die anderen zwei werden faelschlich als "failed" gezaehlt, weil sie ueberhaupt nicht ueber Text-Matching gepruefbar sind.**
|
||||
|
||||
Bei der CRA-MC-Generierung (laeuft jetzt im Pass 0a mit Haiku) **MUSS** jeder MC ein **`check_type`-Feld** bekommen, bevor er in die Datenbank geht. Sonst wiederholt sich das Problem.
|
||||
|
||||
## Die drei Klassen
|
||||
|
||||
| `check_type` | Pruefungsfrage-Pattern | Beispiel | Wie pruefbar? |
|
||||
|---|---|---|---|
|
||||
| **`text`** | "Enthaelt das Dokument...", "Wird im X die Y benannt?", "Ist im Text aufgelistet..." | "Wird im Impressum die Aufsichtsbehoerde benannt?" | Regex / Embedding-Match gegen Doc-Text |
|
||||
| **`process`** | "Ist sichergestellt...", "Ist implementiert...", "Wird durchgefuehrt..." | "Ist sichergestellt, dass Cookies erst nach Einwilligung gespeichert werden?" | Evidence/TOM-Check — kein Doc-Text vorhanden |
|
||||
| **`review`** | "Sind ALLE / Werden ALLE / Stimmt X mit Y ueberein?" | "Sind alle Verarbeitungszwecke vollstaendig erfasst?" | Mensch (DSB) — Checkliste, nicht automatisch |
|
||||
|
||||
## Befund aus den BMW-Daten
|
||||
|
||||
| Doc-Type | TEXT (matchbar) | PROCESS | UNKLAR/REVIEW | Total | % TEXT |
|
||||
|---|---|---|---|---|---|
|
||||
| cookie | 30 | 49 | 302 | 381 | **8%** |
|
||||
| dse | 72 | 139 | 359 | 571 | **13%** |
|
||||
| impressum | 14 | 14 | 47 | 75 | **19%** |
|
||||
| agb | 24 | 20 | 69 | 113 | 21% |
|
||||
| widerruf | 29 | 26 | 96 | 153 | 19% |
|
||||
| loeschkonzept | 38 | 39 | 232 | 309 | 12% |
|
||||
|
||||
**Selbst mit perfektem Matching liegt die Obergrenze fuer doc_check bei 8-20%**, weil 80-92% der MCs nicht ueber Text-Matching pruefbar sind. Es sind keine "schlechten MCs" — sie sind in der falschen Schublade.
|
||||
|
||||
## Konsequenzen fuer CRA-Generation (Pass 0a)
|
||||
|
||||
### 1. Prompt-Aenderung (Hauptmassnahme)
|
||||
|
||||
Der Pass-0a-Prompt fuer Haiku/Sonnet MUSS pro generiertem Control ein `check_type`-Feld erzwingen. Vorschlag:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"control_id": "CRA-...-A01",
|
||||
"title": "...",
|
||||
"check_question": "...",
|
||||
"check_type": "text" | "process" | "review", // PFLICHT
|
||||
"rationale_for_check_type": "..."
|
||||
}
|
||||
```
|
||||
|
||||
Klassifikations-Regel im Prompt:
|
||||
|
||||
> Wenn deine `check_question` mit "Enthaelt", "Wird … genannt/aufgelistet/erwaehnt", "Steht im Text" beginnt -> `text`.
|
||||
> Wenn sie mit "Ist sichergestellt", "Ist implementiert", "Wird durchgefuehrt", "Existiert ein Prozess" beginnt -> `process`.
|
||||
> Wenn sie mit "Sind ALLE", "Werden ALLE", "Stimmt X mit Y ueberein" beginnt -> `review`.
|
||||
> Im Zweifel: lieber `review` als `text`.
|
||||
|
||||
### 2. Doc-Type-Zuordnung kritisch validieren
|
||||
|
||||
Bei den heutigen MCs sind viele falsch zugeordnet (z.B. "Bestellbestätigung implementieren" landet im `impressum`-doc_type, gehoert aber zu AGB/Widerruf). Fuer CRA:
|
||||
|
||||
- **`doc_type` darf nur Werte aus einer expliziten Liste annehmen** — pro Regulation festlegen.
|
||||
- Fuer CRA z.B.: `produkt_konformitaetserklaerung`, `risiko_management_dossier`, `sbom`, `cra_dse`, `meldepflichten_doku`.
|
||||
- Falsche Zuordnung im Prompt explizit verbieten: "Wenn der Control nicht eindeutig zu EINEM dieser Doc-Typen passt, setze `doc_type: 'unassigned'` und `check_type: 'review'`."
|
||||
|
||||
### 3. Zwei Tabellen statt einer
|
||||
|
||||
Heutige Architektur:
|
||||
- `compliance.doc_check_controls` <- alle 1874 MCs (mit allem vermischt)
|
||||
|
||||
Empfohlen fuer CRA + Refactor:
|
||||
- `compliance.text_check_controls` <- nur `check_type='text'`
|
||||
- `compliance.process_check_controls` <- nur `check_type='process'`, gepruefte via Evidence/TOM
|
||||
- `compliance.review_checklist_controls` <- nur `check_type='review'`, gepruefte via DSB-Workflow
|
||||
|
||||
Falls Schema-Aenderung nicht moeglich (CLAUDE.md: DB ist frozen), Sidecar-SQLite mit `mc_classification.db` oder neue Spalte als Add-only-Migration.
|
||||
|
||||
### 4. Dedupe-Phase respektieren
|
||||
|
||||
In Pass 0b (Dedup) muss `check_type` ein **Pflicht-Dedupe-Key** sein:
|
||||
- Zwei MCs mit gleicher Aussage aber unterschiedlichem `check_type` sind **nicht** Duplikate — sie pruefen verschiedene Dinge ("ist im Text genannt" vs "ist technisch implementiert").
|
||||
- Heute werden solche faelschlich gemerged → noch mehr Vermischung.
|
||||
|
||||
### 5. Matching-Engine danach umbauen
|
||||
|
||||
Das eigentliche doc-check-Match-System muss nur noch `check_type='text'`-MCs verarbeiten. Andere werden in ihre eigenen Module geroutet:
|
||||
|
||||
- `text` MCs -> `rag_document_checker` (Regex + spaeter Embedding)
|
||||
- `process` MCs -> neuer `evidence_check_runner` (Kunde lieferte Nachweise/TOM hoch)
|
||||
- `review` MCs -> neuer `review_checklist_ui` (DSB beantwortet manuell)
|
||||
|
||||
## Checkliste fuer CRA-Session
|
||||
|
||||
- [ ] Pass-0a-Prompt um `check_type`-Pflichtfeld erweitert (Wortlaut-Regel + Beispiele)
|
||||
- [ ] Pass-0a-Prompt zwingt `doc_type` aus expliziter Whitelist
|
||||
- [ ] Pass-0b-Dedup-Key enthaelt `check_type`
|
||||
- [ ] Output-Validator weist MCs ohne `check_type` zurueck
|
||||
- [ ] DB-Schema (oder Sidecar) hat `check_type`-Spalte mit Default `review` (sicherer Fallback)
|
||||
- [ ] Stichprobe von 50 generierten CRA-MCs vor Bulk-Run: TEXT-Anteil sollte 30-50% sein (mehr als bei den alten DSGVO-MCs, weil CRA stark dokument-fokussiert ist).
|
||||
|
||||
## Update 2026-05-17 — Parallel-CRA-Session-Findings
|
||||
|
||||
Die laufende CRA-Generation hat ein Feld `verification_method` (document/tool/hybrid/code_review/empty), das **NICHT identisch** mit `check_type` ist:
|
||||
|
||||
- `verification_method` fragt: **WAS schaust du dir an?** (Dokument, Tool-Output, Code)
|
||||
- `check_type` fragt: **KANN das per Text-Match geprueft werden?** (text/process/review)
|
||||
|
||||
Ein Control kann `verification_method=document` haben UND trotzdem `check_type=process` sein. Beispiel: "Wird die SBOM regelmaessig (mindestens monatlich) aktualisiert?" — Du schaust ins Dokument SBOM-Historie, prüfst aber einen Prozess. Text-Match findet das nie.
|
||||
|
||||
**Mapping-Heuristik (gut genug fuer 80% der Faelle, Rest LLM):**
|
||||
|
||||
| `verification_method` | Auto-Mapping `check_type` | LLM noetig? |
|
||||
|---|---|---|
|
||||
| `tool` | `process` | nein |
|
||||
| `code_review` | `process` | nein |
|
||||
| empty/null | `review` (sicherer Default) | nein |
|
||||
| `document` | erstmal `text`, Stichprobe pruefen | 10-20% sampling |
|
||||
| `hybrid` | LLM klassifizieren | ja, alle |
|
||||
|
||||
**Idealfall (fuer alle KUENFTIGEN Pass-0a-Generationen — auch CRA falls man nochmal generiert):** Beide Felder gleichzeitig generieren, nicht eins aus dem anderen ableiten.
|
||||
|
||||
## Backfill-Workflow fuer die laufende CRA-Generation
|
||||
|
||||
1. Aktueller Haiku-Job laeuft fertig (kein Restart, kein Verlust)
|
||||
2. Nach Job-Ende: Auto-Mapping fuer eindeutige Buckets (tool/code_review/empty)
|
||||
3. Sonnet-Klassifikation nur fuer `document`+`hybrid` Subset (~62 Calls fuer 1500 Controls, ~$0.05 statt $2)
|
||||
4. Wiederverwenden: `breakpilot-compliance/backend-compliance/scripts/classify_mc_check_type.py` — nur DB-Query anpassen (Source-Tabelle + WHERE-Filter)
|
||||
5. Validierung: TEXT-Anteil bei CRA sollte 40-60% sein (CRA ist dokument-zentrierter als DSGVO-Cookie)
|
||||
|
||||
## Quervewweise
|
||||
|
||||
- BMW-Run-Befund: `breakpilot-compliance` E-Mail vom 2026-05-17, check_id `08bcc9dd`
|
||||
- Bestehender Klassifikations-Skript fuer Retrofit der alten 1874: `backend-compliance/scripts/classify_mc_check_type.py`
|
||||
- Doc-Type-Audit-Query: dieselbe Datei, am Ende
|
||||
@@ -1553,6 +1553,7 @@ async def get_repair_backfill_status(backfill_id: str):
|
||||
class BatchDedupRequest(BaseModel):
|
||||
dry_run: bool = True
|
||||
hint_filter: Optional[str] = None # Only process groups matching this hint prefix
|
||||
since: Optional[str] = None # ISO datetime — scope to controls created at/after this
|
||||
|
||||
|
||||
_batch_dedup_status: dict = {}
|
||||
@@ -1567,7 +1568,15 @@ async def _run_batch_dedup(req: BatchDedupRequest, dedup_id: str):
|
||||
runner = BatchDedupRunner(db)
|
||||
_batch_dedup_status[dedup_id] = {"status": "running", "phase": "starting"}
|
||||
|
||||
stats = await runner.run(dry_run=req.dry_run, hint_filter=req.hint_filter)
|
||||
since_dt = None
|
||||
if req.since:
|
||||
from datetime import datetime
|
||||
since_dt = datetime.fromisoformat(req.since.replace("Z", "+00:00"))
|
||||
stats = await runner.run(
|
||||
dry_run=req.dry_run,
|
||||
hint_filter=req.hint_filter,
|
||||
since=since_dt,
|
||||
)
|
||||
|
||||
_batch_dedup_status[dedup_id] = {
|
||||
"status": "completed",
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
source: Derived from BSI QUAIDAL (Clean-Room)
|
||||
source_url: https://github.com/BSI-Bund/QUAIDAL
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
plagiarism_limit_4gram: 0.2
|
||||
generated_by_model: qwen3.5:35b-a3b
|
||||
controls:
|
||||
- id: AC-AI-DATA-QB-01-syntaktische-genauigkeit
|
||||
canonical_name: Syntaktische Genauigkeit
|
||||
description: Das KI-Trainingsset muss syntaktisch konsistent sein, wobei alle definierten
|
||||
Grammatik- und Strukturregeln strikt einzuhalten sind. Eine fehlerfreie Datenstruktur
|
||||
ist zwingend erforderlich, um eine korrekte Verarbeitung durch Parser oder Sprachmodelle
|
||||
zu gewährleisten. Die Validierung der formalen Korrektheit ist vor jedem Training
|
||||
durchzuführen, um Verarbeitungsfehler auszuschließen.
|
||||
kind: building_block
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- MA-01
|
||||
- MA-02
|
||||
- MA-03
|
||||
- MA-04
|
||||
- MA-05
|
||||
- MA-27
|
||||
external_refs:
|
||||
- framework: BSI AIC4
|
||||
citation: null
|
||||
- framework: ISO/IEC 25012
|
||||
citation: null
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QB-01
|
||||
title_original_de: QB-01 Syntaktische Genauigkeit
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0001_Qualitätsbausteine/QB-01_Syntactic%20Accuracy.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: AC-AI-DATA-QB-02-semantische-genauigkeit
|
||||
canonical_name: Semantische Genauigkeit
|
||||
description: Die KI-Trainingsdaten müssen inhaltlich korrekt sein, sodass die zugewiesenen
|
||||
Werte dem tatsächlichen Sachverhalt entsprechen und nicht nur formal valide sind.
|
||||
Es ist sicherzustellen, dass semantische Zuordnungen keine logischen Fehler aufweisen,
|
||||
wie beispielsweise die Klassifizierung von Tieren als technische Geräte. Eine
|
||||
Prüfung muss verifizieren, dass die Bedeutung der Datenpunkte im Kontext der Anwendung
|
||||
eindeutig und fehlerfrei interpretiert werden kann.
|
||||
kind: building_block
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- MA-05
|
||||
- MA-06
|
||||
- MA-07
|
||||
- MA-27
|
||||
external_refs:
|
||||
- framework: BSI AIC4
|
||||
citation: null
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QB-02
|
||||
title_original_de: QB-02 Semantische Genauigkeit
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0001_Qualitätsbausteine/QB-02_Semantic%20Accuracy.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: AC-AI-DATA-QB-03-vielfalt
|
||||
canonical_name: Vielfalt
|
||||
description: Das KI-Trainingsdatenset muss eine maximale Varianz in den relevanten
|
||||
Merkmalen aufweisen, um die Heterogenität der Eingabewerte zu gewährleisten. Es
|
||||
ist sicherzustellen, dass das Spektrum der enthaltenen Werte breit genug ist,
|
||||
um das Variationspotential der Zielgruppe vollständig abzudecken. Eine Prüfung
|
||||
der Datenverteilung ist vor dem Training durchzuführen, um eine unzureichende
|
||||
Diversität auszuschließen.
|
||||
kind: building_block
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- MA-08
|
||||
- MA-09
|
||||
- MA-10
|
||||
- MA-12
|
||||
- MA-27
|
||||
- MA-28
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QB-03
|
||||
title_original_de: QB-03 Vielfalt
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0001_Qualitätsbausteine/QB-03_Diversity.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0204
|
||||
- id: AC-AI-DATA-QB-04-ausgewogenheit
|
||||
canonical_name: Ausgewogenheit
|
||||
description: Der Trainingsdatensatz ist so zu konzipieren, dass die Verteilung aller
|
||||
relevanten Klassen proportional zur Zielrealität erfolgt, um eine einseitige Dominanz
|
||||
einzelner Kategorien zu vermeiden. Es ist sicherzustellen, dass keine Gruppe systematisch
|
||||
unter- oder überrepräsentiert wird, um Verzerrungen im Modellverhalten auszuschließen.
|
||||
Die Datenqualität muss durch eine ausgewogene Varianz aller Merkmale gewährleistet
|
||||
werden, um Overfitting und Bias wirksam zu verhindern.
|
||||
kind: building_block
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- MA-08
|
||||
- MA-09
|
||||
- MA-10
|
||||
- MA-12
|
||||
- MA-14
|
||||
- MA-27
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QB-04
|
||||
title_original_de: QB-04 Ausgewogenheit
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0001_Qualitätsbausteine/QB-04_Balance.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0182
|
||||
- id: AC-AI-DATA-QB-05-umfang
|
||||
canonical_name: Umfang
|
||||
description: Der Trainingsdatensatz muss eine quantitativ ausreichende Anzahl an
|
||||
Datenpunkten aufweisen, um statistisch signifikante Muster zu erfassen und das
|
||||
Risiko von Overfitting zu minimieren. Die Größe der Datenbasis ist so zu dimensionieren,
|
||||
dass sie eine belastbare Analyse der zugrundeliegenden Verteilungen ermöglicht
|
||||
und die Generalisierungsfähigkeit des Modells stabilisiert. Eine Prüfung ist durchzuführen,
|
||||
um sicherzustellen, dass der reine quantitative Umfang die notwendige Basis für
|
||||
eine robuste Modellbildung bildet.
|
||||
kind: building_block
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- MA-11
|
||||
- MA-12
|
||||
- MA-15
|
||||
- MA-27
|
||||
external_refs:
|
||||
- framework: BSI AIC4
|
||||
citation: null
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QB-05
|
||||
title_original_de: QB-05 Umfang
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0001_Qualitätsbausteine/QB-05_Size.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0161
|
||||
- id: AC-AI-DATA-QB-06-verzerrung
|
||||
canonical_name: Verzerrung
|
||||
description: Das KI-System muss vor dem produktiven Einsatz auf systematische Verzerrungen
|
||||
in den Trainingsdaten und den daraus resultierenden Vorhersagen untersucht werden.
|
||||
Es ist sicherzustellen, dass latente Ungleichbehandlungen quantitativ erfasst
|
||||
und dokumentiert werden, um eine transparente Bewertung der Fairness zu ermöglichen.
|
||||
Die Prüfung umfasst die Identifikation von Abweichungen, die auf unausgewogene
|
||||
Datenverteilungen zurückzuführen sind, bevor das Modell für reale Anwendungen
|
||||
freigegeben wird.
|
||||
kind: building_block
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- MA-01
|
||||
- MA-02
|
||||
- MA-03
|
||||
- MA-04
|
||||
- MA-06
|
||||
- MA-07
|
||||
- MA-08
|
||||
- MA-09
|
||||
- MA-10
|
||||
- MA-11
|
||||
- MA-12
|
||||
- MA-13
|
||||
- MA-14
|
||||
- MA-15
|
||||
- MA-16
|
||||
- MA-17
|
||||
- MA-18
|
||||
- MA-20
|
||||
- MA-23
|
||||
- MA-24
|
||||
- MA-27
|
||||
- MA-28
|
||||
- QB-15
|
||||
- QM-11
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QB-06
|
||||
title_original_de: QB-06 Verzerrung
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0001_Qualitätsbausteine/QB-06_Bias-Detektion.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: AC-AI-DATA-QB-07-gesamtheit
|
||||
canonical_name: Gesamtheit
|
||||
description: Das Trainingsdatenset muss sämtliche für das spezifische Anwendungsszenario
|
||||
definierten Attribute und Entitätsinstanzen vollständig enthalten, um die Anforderung
|
||||
der Gesamtheit zu erfüllen. Diese Vollständigkeit ist auf der Ebene des gesamten
|
||||
Datensatzes, einzelner Spalten oder einzelner Datenpunkte nachweisbar zu prüfen.
|
||||
Die Bewertung der Datenqualität erfolgt stets kontextbezogen unter Berücksichtigung
|
||||
der jeweiligen Nutzungszwecke.
|
||||
kind: building_block
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- MA-12
|
||||
- MA-13
|
||||
- MA-27
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QB-07
|
||||
title_original_de: QB-07 Gesamtheit
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0001_Qualitätsbausteine/QB-07_Totality.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: AC-AI-DATA-QB-08-konsistenzsicherung
|
||||
canonical_name: Konsistenzsicherung
|
||||
description: Die Konsistenz der KI-Trainingsdaten ist durch standardisierte Datentypen
|
||||
und formatierte Attribute über den gesamten Lebenszyklus sicherzustellen. Automatisierte
|
||||
Prüfmechanismen müssen Abweichungen in den Datenwerten sowie zeitlichen Verläufen
|
||||
frühzeitig identifizieren, um nachvollziehbare Transformations- oder Imputationsmaßnahmen
|
||||
einzuleiten. Eine einheitliche Datenstruktur ist zwingend erforderlich, um die
|
||||
Integrität der Trainingsbasis für valide Modellentscheidungen zu gewährleisten.
|
||||
kind: building_block
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- MA-01
|
||||
- MA-02
|
||||
- MA-03
|
||||
external_refs:
|
||||
- framework: ISO/IEC 25012
|
||||
citation: null
|
||||
- framework: BSI AIC4
|
||||
citation: null
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QB-08
|
||||
title_original_de: QB-08 Konsistenzsicherung
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0001_Qualitätsbausteine/QB-08_ConsistencyAssurance.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: AC-AI-DATA-QB-09-quellenmanagement
|
||||
canonical_name: Quellenmanagement
|
||||
description: Die Organisation muss einen durchgängigen Mechanismus implementieren,
|
||||
der die Herkunft und den Verarbeitungsweg jeder Trainingsdaten-Einheit lückenlos
|
||||
dokumentiert. Es ist sicherzustellen, dass jeder Datenpunkt mit seinem Ursprung
|
||||
sowie allen nachfolgenden Transformationsschritten verknüpft bleibt, um die Integrität
|
||||
der KI-Datenbasis zu gewährleisten. Zusätzlich sind alle Zugriffe und Modifikationen
|
||||
in einem unveränderlichen Protokoll chronologisch festzuhalten, um einen vollständigen
|
||||
Audit-Trail für Compliance-Prüfungen zu schaffen.
|
||||
kind: building_block
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- MA-18
|
||||
- MA-19
|
||||
- MA-20
|
||||
- MA-22
|
||||
external_refs:
|
||||
- framework: BSI AIC4
|
||||
citation: null
|
||||
- framework: AI Act
|
||||
citation: null
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QB-09
|
||||
title_original_de: QB-09 Quellenmanagement
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0001_Qualitätsbausteine/QB-09_Sourcemanagement.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0167
|
||||
- id: AC-AI-DATA-QB-10-datenpruefung
|
||||
canonical_name: _Datenprüfung
|
||||
description: Vor der Initialisierung des Trainingsprozesses ist eine systematische
|
||||
Validierung der Eingangsdaten auf Vollständigkeit, Konsistenz und Integrität durchzuführen.
|
||||
Dabei sind Unregelmäßigkeiten wie fehlende Werte, formatinkonsistenzen oder statistische
|
||||
Ausreißer zu identifizieren und zu bereinigen. Das System muss sicherstellen,
|
||||
dass keine verzerrten oder fehlerhaften Datensätze das Modelltraining beeinträchtigen
|
||||
und die Datenqualität den definierten Qualitätsstandards entspricht.
|
||||
kind: building_block
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- MA-05
|
||||
- MA-20
|
||||
- MA-26
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QB-10
|
||||
title_original_de: QB-10_Datenprüfung
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0001_Qualitätsbausteine/QB-10_DataChecks.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0204
|
||||
- id: AC-AI-DATA-QB-11-prozesse
|
||||
canonical_name: Prozesse
|
||||
description: Es ist sicherzustellen, dass jeder Schritt der Datenvorbereitung und
|
||||
-verarbeitung für KI-Trainingszwecke lückenlos protokolliert wird, um die vollständige
|
||||
Nachvollziehbarkeit der Datenherkunft und aller Transformationen zu gewährleisten.
|
||||
Diese Dokumentation muss so strukturiert sein, dass sie eine valide Reproduzierbarkeit
|
||||
der Modelle sowie eine fundierte Qualitätssicherung der zugrundeliegenden Datensätze
|
||||
ermöglicht. Durch die Erfassung aller Änderungsereignisse wird die Integrität
|
||||
der Trainingsdaten über den gesamten Lebenszyklus hinweg verifiziert.
|
||||
kind: building_block
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- MA-18
|
||||
- MA-21
|
||||
external_refs:
|
||||
- framework: BSI Grundschutz
|
||||
citation: null
|
||||
- framework: ISO/IEC 23894
|
||||
citation: null
|
||||
- framework: ISO/IEC 42001
|
||||
citation: null
|
||||
- framework: AI Act
|
||||
citation: null
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QB-11
|
||||
title_original_de: QB-11 Prozesse
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0001_Qualitätsbausteine/QB-11_Processes.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: AC-AI-DATA-QB-12-merkmalsentwicklung
|
||||
canonical_name: Merkmalsentwicklung
|
||||
description: Die Erstellung und Auswahl von Eingangsmerkmalen für KI-Modelle ist
|
||||
so zu gestalten, dass sie signifikante Korrelationen zur Zielgröße aufweisen und
|
||||
redundante Informationen eliminieren. Es ist sicherzustellen, dass die transformierten
|
||||
Daten generalisierbar sind und eine hohe Informationsdichte für neue, unbekannte
|
||||
Datensätze bieten. Eine Validierung muss nachweisen, dass die abgeleiteten Merkmale
|
||||
die Interpretierbarkeit des Modells unterstützen und keine unnötige Komplexität
|
||||
verursachen.
|
||||
kind: building_block
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- MA-01
|
||||
- MA-02
|
||||
- MA-03
|
||||
- MA-06
|
||||
- MA-12
|
||||
- MA-14
|
||||
- MA-17
|
||||
- MA-23
|
||||
- MA-24
|
||||
- MA-27
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QB-12
|
||||
title_original_de: QB-12 Merkmalsentwicklung
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0001_Qualitätsbausteine/QB-12_FeatureEngineering.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: AC-AI-DATA-QB-13-datenvorbereitung
|
||||
canonical_name: Datenvorbereitung
|
||||
description: Vor der Initialisierung des Trainingsprozesses sind alle Rohdaten durch
|
||||
definierte Transformationen in eine qualitätsgeprüfte und für das Modell verarbeitbare
|
||||
Struktur zu überführen. Es ist sicherzustellen, dass jede angewandte Datenaufbereitung
|
||||
die Integrität der Trainingsmenge gewährleistet und keine nicht validierten Artefakte
|
||||
in das Lernsystem einfließen. Die Durchführbarkeit dieser Schritte ist vor dem
|
||||
Start der Modellkonvergenz durch systematische Prüfverfahren nachzuweisen.
|
||||
kind: building_block
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- MA-02
|
||||
- MA-03
|
||||
- MA-04
|
||||
- MA-13
|
||||
- MA-14
|
||||
- MA-16
|
||||
- MA-17
|
||||
- MA-23
|
||||
- MA-24
|
||||
- MA-25
|
||||
- MA-27
|
||||
- MA-29
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QB-13
|
||||
title_original_de: QB-13 Datenvorbereitung
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0001_Qualitätsbausteine/QB-13_DataPreparation.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: AC-AI-DATA-QB-14-expertanalysis
|
||||
canonical_name: _Expertanalysis
|
||||
description: Die Qualität der KI-Trainingsdaten ist durch eine unabhängige, manuelle
|
||||
Begutachtung durch qualifiziertes Fachpersonal zu validieren. Dabei sind mehrere
|
||||
Prüfer eigenständig einzusetzen, um subjektive Verzerrungen und Gruppenkonformitätseffekte
|
||||
bei der Bewertung auszuschließen. Die Ergebnisse dieser fachlichen Analyse müssen
|
||||
anonymisiert zusammengeführt werden, um eine objektive Beurteilung der Datensatzqualität
|
||||
zu gewährleisten.
|
||||
kind: building_block
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- MA-06
|
||||
- MA-10
|
||||
- MA-14
|
||||
- MA-15
|
||||
- MA-21
|
||||
- MA-22
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QB-14
|
||||
title_original_de: QB-14_Expertanalysis
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0001_Qualitätsbausteine/QB-14_Expertanalysis.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: AC-AI-DATA-QB-15-bias-mitigation
|
||||
canonical_name: Bias-Mitigation
|
||||
description: Das System muss technische Mechanismen implementieren, um systematische
|
||||
Verzerrungen in den Trainingsdaten oder während des Lernprozesses zu identifizieren
|
||||
und zu kompensieren. Diese Maßnahmen sind unabhängig vom Entwicklungsstadium anzuwenden,
|
||||
wobei Datenanpassungen vor dem Training, Regularisierungsverfahren während des
|
||||
Lernens oder Korrekturen der Ausgabeergebnisse nach dem Training möglich sind.
|
||||
Eine Prüfung der Fairness-Kriterien ist vor der Freigabe des Modells durchzuführen,
|
||||
um sicherzustellen, dass keine diskriminierenden Muster in den Ergebnissen verbleiben.
|
||||
kind: building_block
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- MA-30
|
||||
- QM-57
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QB-15
|
||||
title_original_de: QB-15 Bias-Mitigation
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0001_Qualitätsbausteine/QB-15_Bias-Mitigation.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
@@ -0,0 +1,280 @@
|
||||
source: Derived from BSI QUAIDAL (Clean-Room)
|
||||
source_url: https://github.com/BSI-Bund/QUAIDAL
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
plagiarism_limit_4gram: 0.2
|
||||
generated_by_model: qwen3.5:35b-a3b
|
||||
controls:
|
||||
- id: MC-AI-DATA-QKB-01-repraesentativitaet
|
||||
canonical_name: Repräsentativität
|
||||
description: Der Trainingsdatensatz muss die statistische Verteilung der Zielpopulation
|
||||
exakt abbilden, um systematische Verzerrungen im Modell zu vermeiden. Es ist sicherzustellen,
|
||||
dass alle relevanten Merkmalsausprägungen in ausreichender Häufigkeit und ohne
|
||||
Über- oder Unterrepräsentation vorliegen. Die Datenmenge ist so zu dimensionieren,
|
||||
dass eine robuste Generalisierungsfähigkeit für alle Subgruppen der Gesamtpopulation
|
||||
gewährleistet wird. Eine Prüfung auf Stichprobenqualität ist vor dem Training
|
||||
durchzuführen.
|
||||
kind: criterion
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QB-03
|
||||
- QB-04
|
||||
- QB-05
|
||||
- QB-06
|
||||
- QB-15
|
||||
external_refs:
|
||||
- framework: AI Act
|
||||
citation: Artikel 10
|
||||
- framework: ISO/IEC 25012
|
||||
citation: null
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QKB-01
|
||||
title_original_de: QKB-01 Repräsentativität
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0000_Qualitätskriterien/QKB-01_Representativity.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MC-AI-DATA-QKB-02-vollstaendigkeit
|
||||
canonical_name: Vollständigkeit
|
||||
description: Der Datensatz muss sämtliche für das spezifische KI-Modell erwarteten
|
||||
Attribute und Merkmalsausprägungen lückenlos beinhalten. Es ist sicherzustellen,
|
||||
dass keine Entitätsinstanzen fehlen und alle definierten Merkmale mit Werten belegt
|
||||
sind. Eine Prüfung auf fehlende Werte oder unvollständige Attributmengen ist vor
|
||||
dem Training zwingend durchzuführen, um Verzerrungen zu vermeiden.
|
||||
kind: criterion
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QB-07
|
||||
- QB-09
|
||||
external_refs:
|
||||
- framework: AI Act
|
||||
citation: Artikel 10
|
||||
- framework: BSI AIC4
|
||||
citation: null
|
||||
- framework: ISO/IEC 25012
|
||||
citation: null
|
||||
- framework: ISO/IEC 25024
|
||||
citation: null
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QKB-02
|
||||
title_original_de: QKB-02 Vollständigkeit
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0000_Qualitätskriterien/QKB-02_Completeness.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MC-AI-DATA-QKB-03-genauigkeit
|
||||
canonical_name: Genauigkeit
|
||||
description: Die Integrität der KI-Trainingsdaten erfordert, dass jeder einzelne
|
||||
Datenelementwert eine definierte numerische oder symbolische Übereinstimmung mit
|
||||
dem referenzierten Sollwert aufweist. Es ist sicherzustellen, dass Abweichungen
|
||||
innerhalb festgelegter Toleranzgrenzen bezüglich Rundung, Formatierung und Messauflösung
|
||||
bleiben. Die Einhaltung dieser Spezifikation ist durch automatisierte Prüfverfahren
|
||||
vor jedem Trainingslauf zu verifizieren.
|
||||
kind: criterion
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QB-01
|
||||
- QB-02
|
||||
external_refs:
|
||||
- framework: ISO/IEC 25012
|
||||
citation: null
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QKB-03
|
||||
title_original_de: QKB-03 Genauigkeit
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0000_Qualitätskriterien/QKB-03_Accuracy.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MC-AI-DATA-QKB-04-konsistenz
|
||||
canonical_name: Konsistenz
|
||||
description: Das System muss sicherstellen, dass alle Eingabedaten für das KI-Training
|
||||
logisch kohärent und frei von internen Widersprüchen sind. Einheitliche Kodierungen
|
||||
für Kategorien sowie konsistente Formatierungen sind zwingend erforderlich, um
|
||||
eine fehlerfreie Generalisierung durch das Modell zu ermöglichen. Jede Abweichung
|
||||
von den definierten Datenstandards ist durch automatische Prüfmechanismen zu identifizieren
|
||||
und zu unterbinden.
|
||||
kind: criterion
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QB-02
|
||||
- QB-07
|
||||
- QB-08
|
||||
- QB-10
|
||||
- QB-11
|
||||
- QB-12
|
||||
external_refs:
|
||||
- framework: ISO/IEC 25012
|
||||
citation: null
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QKB-04
|
||||
title_original_de: QKB-04 Konsistenz
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0000_Qualitätskriterien/QKB-04_Consistency.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MC-AI-DATA-QKB-05-korrektheit
|
||||
canonical_name: Korrektheit
|
||||
description: Das KI-Modell muss ausschließlich auf Datensätzen trainiert werden,
|
||||
die inhaltlich frei von Fehlern sind und den tatsächlichen Gegebenheiten oder
|
||||
definierten Referenzstandards exakt entsprechen. Es ist sicherzustellen, dass
|
||||
jede annotierte Information den als wahr geltenden Zustand im Anwendungskontext
|
||||
fehlerfrei abbildet. Die Validierung der Trainingsdaten ist vor Beginn des Lernprozesses
|
||||
durchzuführen, um sicherzustellen, dass keine inkorrekten Werte die Modellleistung
|
||||
beeinträchtigen.
|
||||
kind: criterion
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QB-09
|
||||
- QB-10
|
||||
- QB-12
|
||||
- QB-14
|
||||
external_refs:
|
||||
- framework: ISO/IEC 25012
|
||||
citation: null
|
||||
- framework: BSI AIC4
|
||||
citation: null
|
||||
- framework: AI Act
|
||||
citation: Artikel 10
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QKB-05
|
||||
title_original_de: QKB-05 Korrektheit
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0000_Qualitätskriterien/QKB-05_Correctness.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MC-AI-DATA-QKB-06-einheitlichkeit
|
||||
canonical_name: Einheitlichkeit
|
||||
description: Die Konsistenz der KI-Trainingsdaten ist durch die strikte Einhaltung
|
||||
definierter Syntaxregeln und Datenstrukturen sicherzustellen. Jedes Datenelement
|
||||
muss vor der Verarbeitung gemäß festgelegten Standards formatiert werden, um strukturelle
|
||||
Abweichungen auszuschließen. Eine Prüfung der formalen Einheitlichkeit ist unabhängig
|
||||
von der inhaltlichen Richtigkeit der Werte durchzuführen.
|
||||
kind: criterion
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QB-02
|
||||
- QB-08
|
||||
- QB-10
|
||||
- QB-12
|
||||
- QB-14
|
||||
external_refs:
|
||||
- framework: ISO/IEC 25012
|
||||
citation: null
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QKB-06
|
||||
title_original_de: QKB-06 Einheitlichkeit
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0000_Qualitätskriterien/QKB-06_Uniformity.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MC-AI-DATA-QKB-07-gueltigkeit
|
||||
canonical_name: Gültigkeit
|
||||
description: Das System muss sicherstellen, dass die für das KI-Training verwendeten
|
||||
Daten inhaltlich exakt das intendierte Zielkonstrukt abbilden und nicht nur oberflächliche
|
||||
Korrelationen erfassen. Es ist zu prüfen, ob die erfassten Merkmale den theoretischen
|
||||
Anforderungen an den Messgegenstand entsprechen, um eine valide Grundlage für
|
||||
Ableitungen zu gewährleisten. Eine Abweichung zwischen dem gemessenen Inhalt und
|
||||
dem definierten Zielkonzept ist als Fehlerzustand zu klassifizieren und muss ausgeschlossen
|
||||
werden.
|
||||
kind: criterion
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QB-02
|
||||
- QB-05
|
||||
- QB-09
|
||||
- QB-10
|
||||
- QB-14
|
||||
external_refs:
|
||||
- framework: ISO/IEC 25012
|
||||
citation: null
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QKB-07
|
||||
title_original_de: QKB-07 Gültigkeit
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0000_Qualitätskriterien/QKB-07_Validity.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MC-AI-DATA-QKB-08-eindeutigkeit
|
||||
canonical_name: Eindeutigkeit
|
||||
description: Jeder Datensatz im Trainingskorpus muss eine eindeutige Identität besitzen,
|
||||
um die Entstehung redundanter Instanzen auszuschließen. Es ist sicherzustellen,
|
||||
dass keine doppelten oder mehrdeutigen Einträge vorliegen, da diese die Modellgeneralisierung
|
||||
beeinträchtigen und zu Overfitting führen können. Die Validierung muss nachweisen,
|
||||
dass jede Dateneinheit eindeutig identifizierbar ist und logisch von anderen unterscheidbar
|
||||
bleibt.
|
||||
kind: criterion
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QB-05
|
||||
- QB-10
|
||||
- QB-13
|
||||
external_refs:
|
||||
- framework: ISO/IEC 25012
|
||||
citation: null
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QKB-08
|
||||
title_original_de: QKB-08 Eindeutigkeit
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0000_Qualitätskriterien/QKB-08_Uniqueness.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MC-AI-DATA-QKB-09-sichere-quellen
|
||||
canonical_name: Sichere Quellen
|
||||
description: Für KI-Trainingsdaten muss eine lückenlose Provenienz-Dokumentation
|
||||
etabliert werden, die jeden Verarbeitungsschritt von der Erfassung bis zur finalen
|
||||
Nutzung nachvollziehbar macht. Es ist sicherzustellen, dass alle Transformationen
|
||||
und Herkunftsinformationen vollständig erfasst sind, um die Datenintegrität und
|
||||
-qualität kontinuierlich verifizieren zu können. Die Nachprüfbarkeit dieser Metadaten
|
||||
ist zwingend erforderlich, um potenzielle Qualitätsmängel oder Manipulationen
|
||||
in den Trainingsbeständen frühzeitig zu identifizieren.
|
||||
kind: criterion
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QB-09
|
||||
- QB-11
|
||||
external_refs:
|
||||
- framework: ISO/IEC 25012
|
||||
citation: null
|
||||
- framework: BSI AIC4
|
||||
citation: null
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QKB-09
|
||||
title_original_de: QKB-09 Sichere Quellen
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0000_Qualitätskriterien/QKB-09_SecureSource.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MC-AI-DATA-QKB-10-daten-mit-personenbezug
|
||||
canonical_name: Daten mit Personenbezug
|
||||
description: Das System muss vor der Nutzung von Trainingsdaten eine automatisierte
|
||||
Prüfung durchführen, um personenbezogene Informationen zu identifizieren. Ist
|
||||
derartige Datenbestandteil der Eingabedaten, ist deren vollständige und nachweisbare
|
||||
Entfernung sicherzustellen, bevor ein Modelltraining initiiert wird. Die Integrität
|
||||
der verbleibenden Datensätze ist durch technische Maßnahmen gegen unbeabsichtigte
|
||||
Wiederverwendung zu gewährleisten.
|
||||
kind: criterion
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QB-09
|
||||
- QB-10
|
||||
- QB-11
|
||||
- QB-14
|
||||
external_refs:
|
||||
- framework: EU GDPR
|
||||
citation: null
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: QKB-10
|
||||
title_original_de: QKB-10 Daten mit Personenbezug
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0000_Qualitätskriterien/QKB-10_PersonalDataCheck.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,753 @@
|
||||
source: Derived from BSI QUAIDAL (Clean-Room)
|
||||
source_url: https://github.com/BSI-Bund/QUAIDAL
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
plagiarism_limit_4gram: 0.2
|
||||
generated_by_model: qwen3.5:35b-a3b
|
||||
controls:
|
||||
- id: MIT-AI-DATA-MA-01-datentyp-validierung
|
||||
canonical_name: Datentyp Validierung
|
||||
description: Es ist sicherzustellen, dass alle Eingabedaten und Trainingsdatensätze
|
||||
vor der Verarbeitung auf Konformität mit den definierten Schemata und Datentypen
|
||||
des Modells geprüft werden. Abweichungen von den erwarteten Formaten sind automatisch
|
||||
zu identifizieren und müssen entweder bereinigt oder ausgeschlossen werden, um
|
||||
Inferenzfehler zu verhindern. Diese Validierung ist als automatisierter Schritt
|
||||
in den Datenpipelines zu implementieren, um die Integrität der KI-Systeme zu gewährleisten.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QM-32
|
||||
- QM-34
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-01
|
||||
title_original_de: MA-01 Datentyp Validierung
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-01_Datatype%20Validation.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-02-format-pruefung
|
||||
canonical_name: Format Prüfung
|
||||
description: Die Eingabedaten für KI-Trainingszwecke sind vor der Verarbeitung auf
|
||||
strukturelle Korrektheit zu validieren, wobei Datentypen wie Zeitstempel oder
|
||||
Textfelder exakt den definierten Schemata entsprechen müssen. Durch die erzwingung
|
||||
einer einheitlichen Formatierung wird verhindert, dass regionale Abweichungen
|
||||
oder inkonsistente Darstellungen zu Fehlinterpretationen im Modell führen. Die
|
||||
Konformität ist automatisiert zu prüfen, um sicherzustellen, dass keine nicht
|
||||
konformen Datensätze in den Lernprozess eingehen.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QM-32
|
||||
- QM-34
|
||||
- QM-43
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-02
|
||||
title_original_de: MA-02 Format Prüfung
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-02_Format%20Check.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-03-bereichspruefung
|
||||
canonical_name: Bereichsprüfung
|
||||
description: Das System muss vor dem KI-Training eine automatische Validierung aller
|
||||
Eingangsmerkmale durchführen, um Werte außerhalb definierter physikalischer oder
|
||||
logischer Grenzen zu identifizieren. Dabei sind insbesondere inkonsistente Datentypen,
|
||||
fehlerhafte Maßeinheiten und statistisch unplausible Ausreißer zu detektieren
|
||||
und zu isolieren. Die Integrität des Trainingsdatensatzes ist erst dann gewährleistet,
|
||||
wenn alle nicht konformen Einträge ausgeschlossen oder korrigiert wurden, bevor
|
||||
der Lernprozess initiiert wird.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QM-51
|
||||
- QM-52
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-03
|
||||
title_original_de: MA-03 Bereichsprüfung
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-03_Range%20Check.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-04-over-undersampling
|
||||
canonical_name: Over-Undersampling
|
||||
description: Das Daten-Set für das KI-Training ist auf ein ausgewogenes Klassenverhältnis
|
||||
zu prüfen, wobei eine künstliche Aufstockung seltener Kategorien durch synthetische
|
||||
Generierung oder Duplizierung zulässig ist. Alternativ ist eine Reduktion der
|
||||
Datenpunkte der Mehrheitsklasse nach definierten Kriterien durchzuführen, um eine
|
||||
Verzerrung des Modells zu vermeiden. Die angewandte Methode zur Erreichung dieses
|
||||
Gleichgewichts ist dokumentiert und muss reproduzierbar sein.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QM-34
|
||||
- QM-38
|
||||
- QM-57
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-04
|
||||
title_original_de: MA-04 Over-Undersampling
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-04_Over-Undersampling.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-05-automatisierte-aufgaben
|
||||
canonical_name: Automatisierte Aufgaben
|
||||
description: Wiederkehrende Prozesse der Datenvorverarbeitung und Qualitätsprüfung
|
||||
im KI-Lebenszyklus sind durch automatisierte Mechanismen zu implementieren. Die
|
||||
Ausführung dieser Aufgaben muss so konfiguriert sein, dass eine konsistente Ergebnisqualität
|
||||
über alle Durchläufe hinweg sichergestellt wird. Es ist zu prüfen, dass die eingesetzten
|
||||
Automatisierungswerkzeuge spezifische Validierungsregeln für Trainingsdaten zuverlässig
|
||||
anwenden.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- MA-02
|
||||
- MA-03
|
||||
- QM-10
|
||||
- QM-34
|
||||
- QM-64
|
||||
external_refs:
|
||||
- framework: AI Act
|
||||
citation: null
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-05
|
||||
title_original_de: MA-05 Automatisierte Aufgaben
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-05_Automated%20Tasks.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-06-experten-auswertung
|
||||
canonical_name: Experten Auswertung
|
||||
description: Für die Validierung von KI-Trainingsdaten ist eine manuelle Prüfung
|
||||
durch qualifizierte Fachexperten zwingend erforderlich. Diese Experten müssen
|
||||
die inhaltliche Gültigkeit, Relevanz und Korrektheit der Datensätze auf Basis
|
||||
domänenspezifischen Wissens systematisch evaluieren. Das Ergebnis dieser Begutachtung
|
||||
dient dazu, methodische Fehler oder qualitative Mängel frühzeitig zu identifizieren
|
||||
und konkrete Maßnahmen zur Datenbereinigung abzuleiten.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QM-16
|
||||
- QM-30
|
||||
- QM-43
|
||||
- QM-45
|
||||
- QM-59
|
||||
- QM-70
|
||||
external_refs:
|
||||
- framework: ISO/IEC 25012
|
||||
citation: null
|
||||
- framework: ISO/IEC 25024
|
||||
citation: null
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-06
|
||||
title_original_de: MA-06 Experten Auswertung
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-06_Expert%20Evaluation.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0204
|
||||
- id: MIT-AI-DATA-MA-07-massenbeteiligung
|
||||
canonical_name: Massenbeteiligung
|
||||
description: Das System muss Mechanismen implementieren, um die Qualität von Trainingsdaten
|
||||
durch dezentrale Validierung durch eine heterogene Gruppe externer Prüfer sicherzustellen.
|
||||
Es ist zwingend erforderlich, dass die Ergebnisse dieser kollektiven Überprüfung
|
||||
mit internen Qualitätsstandards abgeglichen werden, um systematische Fehler in
|
||||
den annotierten Datensätzen zu identifizieren. Die Integrität der KI-Modelle ist
|
||||
nur gewährleistet, wenn diese skalierbare Prüfprozedur für kritische Datenmengen
|
||||
routinemäßig angewendet wird.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- MA-06
|
||||
- QM-03
|
||||
- QM-16
|
||||
- QM-43
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-07
|
||||
title_original_de: MA-07 Massenbeteiligung
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-07_Crowdsourcing.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-08-verteilungsanalyse
|
||||
canonical_name: Verteilungsanalyse
|
||||
description: Es ist sicherzustellen, dass die Verteilung der Trainingsdaten über
|
||||
alle relevanten Klassen und Merkmalsbereiche systematisch auf statistische Verzerrungen
|
||||
und Anomalien geprüft wird. Diese Analyse muss nachweisen, dass das Modell auf
|
||||
einer repräsentativen und ausgewogenen Datenbasis trainiert wurde, um die Generalisierungsfähigkeit
|
||||
der Vorhersagen zu gewährleisten. Die Ergebnisse der Verteilungsprüfung sind vor
|
||||
Beginn des Trainings zu dokumentieren und bei signifikanten Abweichungen sind
|
||||
Korrekturmaßnahmen einzuleiten.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- MA-06
|
||||
- QM-10
|
||||
- QM-11
|
||||
- QM-51
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-08
|
||||
title_original_de: MA-08 Verteilungsanalyse
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-08_DistributionAnalysis.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0339
|
||||
- id: MIT-AI-DATA-MA-09-vergleichgrundgesamtheit
|
||||
canonical_name: VergleichGrundgesamtheit
|
||||
description: Das System muss eine repräsentative Referenzstichprobe aus der Zielverteilung
|
||||
bereitstellen, um die Validität von KI-Trainingsdaten zu verifizieren. Es ist
|
||||
sicherzustellen, dass diese Referenzdaten als Goldstandard dienen, um Abweichungen
|
||||
zwischen dem Trainingsset und der tatsächlichen Grundgesamtheit zu quantifizieren.
|
||||
Die Übereinstimmung ist durch einen automatisierten Abgleich mit den vorab definierten
|
||||
Verteilungsparametern zu prüfen.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- MA-9
|
||||
- QM-51
|
||||
- QM-52
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-09
|
||||
title_original_de: MA-09 VergleichGrundgesamtheit
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-09_CompareGroundtruth.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-10-gewichtung-der-daten
|
||||
canonical_name: Gewichtung der Daten
|
||||
description: Für KI-Trainingsdatensätze ist eine manuelle Gewichtung der einzelnen
|
||||
Merkmale zwingend erforderlich, um systematische Verzerrungen zu minimieren. Diese
|
||||
Maßnahme dient der Sicherstellung einer ausgewogenen Datenrepräsentation und verbessert
|
||||
die Generalisierungsfähigkeit des Modells auf spezifische Anwendungsfälle. Die
|
||||
Zuordnung der Gewichtungsfaktoren ist vor dem Training durchzuführen und muss
|
||||
dokumentiert werden, um die Nachvollziehbarkeit der Datenqualität zu gewährleisten.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QM-10
|
||||
- QM-18
|
||||
- QM-28
|
||||
- QM-29
|
||||
- QM-37
|
||||
- QM-38
|
||||
- QM-39
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-10
|
||||
title_original_de: MA-10 Gewichtung der Daten
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-10_ManualWeights.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-11-stichprobengroesse
|
||||
canonical_name: Stichprobengröße
|
||||
description: Die Menge der für das Training verwendeten Daten ist so zu dimensionieren,
|
||||
dass statistisch signifikante Ergebnisse bei definiertem Konfidenzniveau und akzeptabler
|
||||
Fehlervarianz gewährleistet sind. Die Datengröße muss iterativ angepasst werden,
|
||||
wobei sowohl die Gesamtgröße der zugrundeliegenden Population als auch die spezifische
|
||||
Art der Datenerweiterung systematisch zu berücksichtigen sind. Eine Validierung
|
||||
der Datenqualität ist zwingend erforderlich, um Verzerrungen durch unterschiedliche
|
||||
Skalierungsmethoden auszuschließen.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QM-08
|
||||
- QM-09
|
||||
- QM-39
|
||||
- QM-41
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-11
|
||||
title_original_de: MA-11 Stichprobengröße
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-11_Trainingsdataset%20Size.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-12-abdeckung-relevanter-merkmale
|
||||
canonical_name: Abdeckung relevanter Merkmale
|
||||
description: Das Trainingsdatenset muss vollständig alle für die spezifische Problemstellung
|
||||
essenziellen Eingangsvariablen enthalten, um eine lückenlose Merkmalsabdeckung
|
||||
zu gewährleisten. Es ist sicherzustellen, dass keine kritischen Einflussgrößen
|
||||
fehlen, da sonst das Modell keine verlässlichen Korrelationen erlernen kann. Die
|
||||
Vollständigkeit des Merkmalsraums ist vor Beginn des Trainingsprozesses durch
|
||||
eine formale Prüfung zu verifizieren.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- MA-06
|
||||
- MA-14
|
||||
- QM-10
|
||||
- QM-11
|
||||
- QM-13
|
||||
- QM-25
|
||||
- QM-26
|
||||
- QM-27
|
||||
- QM-28
|
||||
- QM-29
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-12
|
||||
title_original_de: MA-12 Abdeckung relevanter Merkmale
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-12_RelevantFeatureCoverage.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-13-vollstaendige-information-in-datensaetze
|
||||
canonical_name: Vollständige Information in Datensätzen
|
||||
description: Für die Validierung von KI-Trainingsdaten ist sicherzustellen, dass
|
||||
alle für die Analyse erforderlichen Attribute vollständig vorliegen und keine
|
||||
unbeabsichtigten Lücken existieren. Bei festgestellten Datenfehlern ist zwingend
|
||||
die Ursache zu ermitteln, um das passende Imputationsverfahren basierend auf dem
|
||||
spezifischen Fehlerschema auszuwählen. Eine unzureichende Datenbasis darf nicht
|
||||
zur Modellierung genutzt werden, solange die Integrität der relevanten Information
|
||||
nicht durch geeignete Maßnahmen wiederhergestellt wurde.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QM-12
|
||||
- QM-40
|
||||
- QM-53
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-13
|
||||
title_original_de: MA-13 Vollständige Information in Datensätzen
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-13_CompleteInformation.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-14-eda-explorative-daten-analyse
|
||||
canonical_name: EDA-Explorative Daten Analyse
|
||||
description: Vor Beginn des Modelltrainings ist eine explorative Datenanalyse durchzuführen,
|
||||
um Datenverteilungen, Korrelationen sowie Ausreißer und strukturelle Anomalien
|
||||
ohne vorab definierte Hypothesen zu identifizieren. Die gewonnenen Erkenntnisse
|
||||
sind systematisch zu dokumentieren, um die Qualität der Trainingsdaten zu validieren
|
||||
und fundierte Entscheidungen über notwendige Bereinigungs- oder Erweiterungsschritte
|
||||
abzuleiten. Auf Basis dieser Analyse ist der Datensatz so anzupassen, dass er
|
||||
die für die Zielfunktion erforderliche Repräsentativität und Integrität gewährleistet.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QM-10
|
||||
- QM-12
|
||||
- QM-24
|
||||
- QM-25
|
||||
- QM-26
|
||||
- QM-27
|
||||
- QM-28
|
||||
- QM-29
|
||||
- QM-36
|
||||
- QM-42
|
||||
- QM-54
|
||||
- QM-57
|
||||
- QM-61
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-14
|
||||
title_original_de: MA-14 EDA-Explorative Daten Analyse
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-14_EDA-ExplorativeDataAnalysis.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-15-empirische-evidenz
|
||||
canonical_name: Empirische Evidenz
|
||||
description: Es ist sicherzustellen, dass die Wirksamkeit von Schutzmaßnahmen gegen
|
||||
KI-gestützte Angriffe durch den systematischen Vergleich mit historischen Einsatzszenarien
|
||||
empirisch validiert wird. Dabei sind Leistungsdaten aus vergleichbaren Anwendungsfällen
|
||||
heranzuziehen, um die Angemessenheit der eingesetzten Trainingsdatensätze und
|
||||
Methoden für den spezifischen Kontext nachzuweisen. Die Analyse muss belegen,
|
||||
dass die gewählten Maßnahmen die identifizierten Risiken in der Praxis effektiv
|
||||
reduzieren und die Datenqualität den aktuellen Bedrohungsmodellen entspricht.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QM-16
|
||||
- QM-30
|
||||
- QM-61
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-15
|
||||
title_original_de: MA-15 Empirische Evidenz
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-15_EmpiricEvidence.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-16-daten-imputation
|
||||
canonical_name: Daten Imputation
|
||||
description: Für KI-Trainingsdatensätze ist eine systematische Analyse der Ursachen
|
||||
für fehlende Werte zwingend erforderlich, bevor eine Rekonstruktion erfolgt. Das
|
||||
gewählte Verfahren zur Datenergänzung muss sich strikt an den identifizierten
|
||||
Entstehungsgründen orientieren, um die statistische Integrität des Modells zu
|
||||
wahren. Eine unkritische Imputation ohne Ursachenanalyse ist unzulässig, da sie
|
||||
das Lernverhalten des Algorithmus verfälschen kann.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- MA-13
|
||||
- QM-10
|
||||
- QM-22
|
||||
- QM-44
|
||||
- QM-53
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-16
|
||||
title_original_de: MA-16 Daten Imputation
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-16_DataImputation.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-17-metadatenverwaltung
|
||||
canonical_name: Metadatenverwaltung
|
||||
description: Für den KI-Trainingsprozess ist eine vollständige Dokumentation der
|
||||
Datenherkunft, der Qualitätsmetriken sowie der rechtlichen Klassifizierung jeder
|
||||
einzelnen Trainingsinstanz sicherzustellen. Diese strukturellen Begleitinformationen
|
||||
müssen maschinenlesbar vorliegen, um eine automatisierte Validierung der Datenintegrität
|
||||
und eine nachvollziehbare Auditierung des Datensatzes zu ermöglichen. Die Erfassung
|
||||
dieser Attribute ist zwingend erforderlich, um die Eignung der Daten für den spezifischen
|
||||
Trainingszweck zu gewährleisten und regulatorische Vorgaben einzuhalten.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QM-59
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-17
|
||||
title_original_de: MA-17 Metadatenverwaltung
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-17_MetadataManagement.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-18-provenienztracking
|
||||
canonical_name: ProvenienzTracking
|
||||
description: Die Herkunft und der Verarbeitungsweg von KI-Trainingsdaten sind lückenlos
|
||||
zu dokumentieren, um deren Integrität und Nachvollziehbarkeit sicherzustellen.
|
||||
Für jeden Datensatz ist eine eindeutige Identifikation des Ursprungs sowie aller
|
||||
Transformationsschritte im Lebenszyklus zu führen. Diese Metadaten müssen so strukturiert
|
||||
sein, dass eine Rückverfolgung zur ursprünglichen Quelle jederzeit möglich ist,
|
||||
ohne dass Datenverluste oder Manipulationen unentdeckt bleiben.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QM-59
|
||||
- QM-60
|
||||
- QM-61
|
||||
- QM-65
|
||||
- QM-67
|
||||
- QM-70
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-18
|
||||
title_original_de: MA-18 ProvenienzTracking
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-18_ProvenienzTracking.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-19-audit-trails
|
||||
canonical_name: Audit Trails
|
||||
description: Für die Nachvollziehbarkeit von KI-Trainingsprozessen ist ein lückenloses
|
||||
Protokollierungssystem zu implementieren, das alle Datenmanipulationen und Modellupdates
|
||||
zeitgestempelt erfasst. Jeder Zugriff auf Trainingsdatensätze sowie jede Änderung
|
||||
der Modellparameter muss mit eindeutigen Benutzeridentitäten verknüpft werden.
|
||||
Die gespeicherten Logs müssen so strukturiert sein, dass sie eine vollständige
|
||||
Rekonstruktion des Datenflusses und eine Rückführung auf frühere Datenqualitätszustände
|
||||
ermöglichen.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- MA-22
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-19
|
||||
title_original_de: MA-19 Audit Trails
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-19_AuditTrails.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-20-prozess-dokumentation
|
||||
canonical_name: Prozess Dokumentation
|
||||
description: Für die Sicherstellung der Datenqualität im KI-Trainingsprozess ist
|
||||
eine vollständige Dokumentation aller Phasen der Datenerstellung und -aufbereitung
|
||||
zwingend erforderlich. Diese Spezifikation muss verbindlich festlegen, welche
|
||||
Aktivitäten auszuführen sind, wer hierfür verantwortlich zeichnet, welche Ressourcen
|
||||
notwendig sind und welche qualitativen Ergebnisse zu erzielen sind. Insbesondere
|
||||
ist die Nachverfolgbarkeit der Datenherkunft innerhalb des Dokumentationsprozesses
|
||||
lückenlos zu gewährleisten, um die Integrität der Trainingsdaten zu validieren.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QM-15
|
||||
- QM-31
|
||||
- QM-62
|
||||
- QM-65
|
||||
external_refs:
|
||||
- framework: ISO/IEC 42001
|
||||
citation: null
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-20
|
||||
title_original_de: MA-20 Prozess Dokumentation
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-20_ProcessDocumentation.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-21-compliance
|
||||
canonical_name: Compliance
|
||||
description: Der Einsatz von KI-Modellen erfordert eine zwingende Prüfung der Trainingsdatensätze
|
||||
auf rechtliche Konformität und ethische Integrität, bevor diese zur Modellgenerierung
|
||||
verwendet werden. Es ist sicherzustellen, dass alle verarbeiteten Informationen
|
||||
die Vorgaben der DSGVO sowie branchenspezifische Regularien vollständig erfüllen
|
||||
und keine unrechtmäßig beschafften oder personenbezogenen Daten ohne explizite
|
||||
Einwilligung enthalten. Die Validierung dieser Datenqualität muss vor jedem Trainingslauf
|
||||
durch einen automatisierten oder manuellen Compliance-Check nachgewiesen werden.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QM-12
|
||||
- QM-15
|
||||
external_refs:
|
||||
- framework: EU GDPR
|
||||
citation: null
|
||||
- framework: AI Act
|
||||
citation: null
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-21
|
||||
title_original_de: MA-21 Compliance
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-21_Compliance.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-22-vertrauenswuerdigkeit
|
||||
canonical_name: Vertrauenswürdigkeit
|
||||
description: Die Integrität und Zuverlässigkeit der für das KI-Training verwendeten
|
||||
Datensätze ist im jeweiligen Anwendungskontext nachweislich zu verifizieren. Es
|
||||
ist sicherzustellen, dass potenzielle Manipulationen oder unbeabsichtigte Korruptionen
|
||||
des Datenflusses durch technische Prüfmechanismen ausgeschlossen werden. Bei der
|
||||
Anwendung von Korrekturverfahren zur Datenbereinigung muss die ursprüngliche Glaubwürdigkeit
|
||||
der Informationen gewahrt bleiben und darf nicht durch die Maßnahme beeinträchtigt
|
||||
werden.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QM-15
|
||||
- QM-43
|
||||
- QM-65
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-22
|
||||
title_original_de: MA-22 Vertrauenswürdigkeit
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-22_Credibility.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-23-merkmalsskalierung
|
||||
canonical_name: Merkmalsskalierung
|
||||
description: Für KI-Trainingsdatensätze ist eine Normalisierung der Merkmalswerte
|
||||
auf einen einheitlichen Wertebereich zwingend erforderlich, um Dominanzeffekte
|
||||
durch unterschiedliche Größenordnungen zu vermeiden. Diese Maßnahme stellt sicher,
|
||||
dass Algorithmen, die auf Distanzberechnungen oder Gradientenverfahren basieren,
|
||||
nicht durch skalenbedingte Verzerrungen beeinträchtigt werden. Die Wirksamkeit
|
||||
der Skalierung ist vor dem Training systematisch zu prüfen, um die Vorhersagegenauigkeit
|
||||
des Modells zu garantieren.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QM-10
|
||||
- QM-56
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-23
|
||||
title_original_de: MA-23 Merkmalsskalierung
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-23_FeatureScaling.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-24-merkmalserstellung
|
||||
canonical_name: Merkmalserstellung
|
||||
description: Es ist sicherzustellen, dass bei der Erstellung neuer Eingangsmerkmale
|
||||
für KI-Modelle ausschließlich validierte Transformationsverfahren angewendet werden,
|
||||
um die Datenqualität zu gewährleisten. Die Generierung neuer Features muss auf
|
||||
nachvollziehbaren Algorithmen basieren, die eine signifikante Verbesserung der
|
||||
Modellleistung gegenüber den Rohdaten nachweisen. Jede angewandte Methode zur
|
||||
Datenanreicherung oder -bereinigung ist vor dem Training auf ihre Eignung zur
|
||||
Mustererkennung und Vorhersagegenauigkeit zu prüfen.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QM-11
|
||||
- QM-25
|
||||
- QM-26
|
||||
- QM-27
|
||||
- QM-28
|
||||
- QM-51
|
||||
- QM-71
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-24
|
||||
title_original_de: MA-24 Merkmalserstellung
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-24_FeatureCreation.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-25-differential-privacy
|
||||
canonical_name: Differential Privacy
|
||||
description: Das System muss bei der Verarbeitung von KI-Trainingsdaten differenzielle
|
||||
Privatsphäre implementieren, indem statistisch signifikante, zufällige Störgrößen
|
||||
zu den Ergebnissen hinzugefügt werden. Es ist sicherzustellen, dass die An- oder
|
||||
Abwesenheit einzelner Datensätze im Trainingsset das Ausgabeergebnis nur marginal
|
||||
beeinflusst. Durch diese Maßnahme ist zu prüfen, ob keine Rückschlüsse auf spezifische
|
||||
Personen aus den generierten Analysen gezogen werden können, während die allgemeine
|
||||
Datenqualität für das Modelltraining erhalten bleibt.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QM-58
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-25
|
||||
title_original_de: MA-25 Differential Privacy
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-25_Differential%20Privacy.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0625
|
||||
- id: MIT-AI-DATA-MA-26-federated-learning
|
||||
canonical_name: Federated Learning
|
||||
description: Für KI-Systeme, die auf verteilten Datenquellen basieren, ist ein Federated-Learning-Ansatz
|
||||
zwingend vorzusehen, um die Rohdaten dezentral zu belassen. Die lokalen Modelle
|
||||
müssen ausschließlich aggregierte Parameter an eine zentrale Instanz übermitteln,
|
||||
während die ursprünglichen Trainingsdaten niemals die lokale Umgebung verlassen.
|
||||
Eine Prüfung ist sicherzustellen, dass durch diese Architektur keine sensiblen
|
||||
Informationen während des Lernprozesses zentralisiert oder übertragen werden.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QM-63
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-26
|
||||
title_original_de: MA-26 Federated Learning
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-26_Federated%20Learning%20Approach.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-27-statistische-grundlagenthemen
|
||||
canonical_name: Statistische Grundlagenthemen
|
||||
description: Für die Sicherstellung der Datenqualität im KI-Lebenszyklus sind statistische
|
||||
Basisverfahren systematisch zu implementieren und kontinuierlich zu validieren.
|
||||
Es ist sicherzustellen, dass alle relevanten Metriken zur Verteilungsanalyse und
|
||||
Datenintegrität konsistent in die Berechnungspipelines integriert werden. Diese
|
||||
fundamentalen Analysen müssen unabhängig von spezifischen Bausteinen als übergeordnete
|
||||
Prüfkriterien für die Modellgüte dienen.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QM-01
|
||||
- QM-02
|
||||
- QM-03
|
||||
- QM-04
|
||||
- QM-06
|
||||
- QM-07
|
||||
- QM-09
|
||||
- QM-23
|
||||
- QM-51
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-27
|
||||
title_original_de: MA-27 Statistische Grundlagenthemen
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-27_StatisticalBasis.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0213
|
||||
- id: MIT-AI-DATA-MA-28-diversitaetsindizes
|
||||
canonical_name: Diversitätsindizes
|
||||
description: Das System muss quantitative Metriken zur Erfassung der Heterogenität
|
||||
von KI-Trainingsdaten implementieren, um die Verteilung verschiedener Kategorien
|
||||
zu messen. Es ist sicherzustellen, dass diese Kennzahlen sowohl die Anzahl vorhandener
|
||||
Klassen als auch deren Gleichverteilung abbilden. Die Validierung der Datenqualität
|
||||
erfolgt durch die Berechnung von Diversitätsindizes, die statistische Unsicherheit
|
||||
oder Kollisionswahrscheinlichkeiten quantifizieren.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QM-68
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-28
|
||||
title_original_de: MA-28 Diversitätsindizes
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-28_Diversity-Indices.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-29-data-splitting
|
||||
canonical_name: Data-Splitting
|
||||
description: Die Aufteilung von KI-Trainingsdaten in disjunkte Teilmengen ist zwingend
|
||||
erforderlich, um eine unvoreingenommene Validierung der Modellgüte zu gewährleisten.
|
||||
Dabei müssen mindestens drei voneinander getrennte Bereiche für das Training,
|
||||
die Hyperparameter-Optimierung sowie die abschließende Leistungsbewertung definiert
|
||||
werden. Eine zufällige oder stratifizierte Trennung ist sicherzustellen, um Datenlecks
|
||||
zwischen den Phasen auszuschließen und die Generalisierungsfähigkeit des Systems
|
||||
nachweisbar zu prüfen.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QM-69
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-29
|
||||
title_original_de: MA-29 Data-Splitting
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-29_Data%20Splitting.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
- id: MIT-AI-DATA-MA-30-fairness
|
||||
canonical_name: Fairness
|
||||
description: Das System muss sicherstellen, dass KI-Trainingsdaten keine systematischen
|
||||
Verzerrungen bezüglich sensibler demografischer Merkmale aufweisen, um diskriminierende
|
||||
Vorhersagen zu vermeiden. Bei unzureichender Repräsentation von Teilgruppen sind
|
||||
präventive Aufbereitungsverfahren oder algorithmische Transformationsmethoden
|
||||
zur Bias-Korrektur zwingend anzuwenden. Die Wirksamkeit dieser Maßnahmen ist vor
|
||||
der Modellbereitstellung durch quantitative Prüfverfahren auf Gleichbehandlungsgrundsätze
|
||||
zu validieren.
|
||||
kind: measure
|
||||
regulation_anchor: EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)
|
||||
related_quaidal_ids:
|
||||
- QM-57
|
||||
external_refs: []
|
||||
source:
|
||||
framework: BSI QUAIDAL
|
||||
section: MA-30
|
||||
title_original_de: MA-30 Fairness
|
||||
url: https://github.com/BSI-Bund/QUAIDAL/blob/main/0000_Markdown/0001_Criteria,Measurements,Metrics/0002_Maßnahmen/MA-30_Fairness.md
|
||||
commit_sha: c39b75369841b359c6bf56d6588e3768c722842f
|
||||
license_note: § 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.
|
||||
plagiarism_score_at_generation: 0.0
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,83 @@
|
||||
# Lizenzregeln der Control-Pipeline
|
||||
|
||||
> **Stand:** 2026-05-21 — Mapping festgezurrt nach DB-Inspektion und IACE-Audit.
|
||||
>
|
||||
> Die Pipeline klassifiziert jede Regulation (und damit jedes daraus extrahierte
|
||||
> Chunk und jeden atomic_control) in eine von **drei Lizenzregeln**. Die Regel
|
||||
> entscheidet, ob der Volltext aufbewahrt werden darf und welche Attribution im
|
||||
> Ausgabe-Renderer Pflicht ist.
|
||||
|
||||
## Die drei Regeln
|
||||
|
||||
| Regel | Bedeutung | Volltext speichern? | Attribution Pflicht? | Beispiele |
|
||||
|-------|-----------|---------------------|----------------------|-----------|
|
||||
| **1** | Wörtlich — Hoheitsrecht / Public Domain | ✓ | nein (empfohlen für Audit) | EU-Recht (EUR-Lex), Bundesrecht, Satzungsrecht (DGUV UVV), TRBS, TRGS, ASR, US Federal Code (OSHA), NIST SP, EU-Leitfäden |
|
||||
| **2** | Wörtlich mit Attribution — freie Lizenzen | ✓ | **ja** | OWASP (CC-BY-SA-4.0), OECD AI Principles (OECD_PUBLIC), ENISA-Dokumente (CC-BY-4.0), Apache-2.0 Werke |
|
||||
| **3** | Nur zitieren — proprietäre Standards | ✗ | nicht anwendbar (kein Volltext) | DIN, EN, ISO, ANSI, UL, IEC, IEEE, DGUV Regeln/Informationen/Grundsätze, Bitkom-Leitfäden, BSI-Bausteine (urheberrechtlich) |
|
||||
|
||||
**Wichtige Klarstellung:** Regel 3 = "nur Identifier/Abschnitt zitieren", **nicht** "umformulieren". Die ursprüngliche Bezeichnung "neu formulieren" war irreführend. Korrekt: Bei Regel-3-Quellen darf die Pipeline den Volltext nicht speichern; sie bewahrt nur die Quellenreferenz (regulation_id + article/paragraph), und der Output-Renderer zeigt diese Referenz im Frontend/PDF.
|
||||
|
||||
## Mapping `license_type` → `license_rule`
|
||||
|
||||
| license_type | license_rule | Erklärung |
|
||||
|---|---|---|
|
||||
| `EU_LAW`, `EU_PUBLIC` | 1 | EU-Verordnungen, Richtlinien, OJ-Veröffentlichungen, EU-Leitfäden |
|
||||
| `DE_LAW`, `DE_PUBLIC` | 1 | Bundesgesetze, TRBS, TRGS, ASR, DGUV-UVV (Satzungsrecht) |
|
||||
| `AT_LAW`, `CH_LAW`, `FR_LAW`, `IT_LAW`, `ES_LAW`, `NL_LAW`, `HU_LAW` | 1 | Andere EU-Mitgliedsstaaten-Recht |
|
||||
| `US_GOV_PUBLIC`, `NIST_PUBLIC_DOMAIN`, `OSHA_PUBLIC` | 1 | US Federal Code (17 U.S.C. §105 Public Domain) |
|
||||
| `CC-BY-4.0`, `CC-BY-SA-4.0`, `CC-BY-3.0`, `CC-BY-SA-3.0` | 2 | Creative-Commons mit Attribution-Pflicht |
|
||||
| `Apache-2.0`, `MIT` | 2 | Permissive OSS-Lizenzen, NOTICE-Pflicht |
|
||||
| `OECD_PUBLIC`, `ENISA_CC_BY_4.0` | 2 | Behörden-Publikationen mit Attribution-Auflage |
|
||||
| `DIN_COPYRIGHT`, `ISO_COPYRIGHT`, `ANSI_COPYRIGHT`, `UL_COPYRIGHT`, `IEC_COPYRIGHT` | 3 | Normungsorganisationen — nur Identifier-Zitat |
|
||||
| `DGUV_COPYRIGHT` | 3 | DGUV Regeln/Informationen/Grundsätze (nicht UVV) |
|
||||
| `BITKOM_COPYRIGHT`, `BSI_COPYRIGHT`, `VDMA_COPYRIGHT` | 3 | Verbands-/Behörden-Publikationen mit eigenständigem Urheberrecht |
|
||||
| `OWN_WORK` | 3 | BreakPilot-Eigentexte (Templates, eigene Patterns) — kein externes Lizenzrisiko, aber auch kein Public-Domain-Status |
|
||||
|
||||
**Sonderfall DGUV:** Die Klasse trennt sich nach Publikationstyp:
|
||||
- DGUV **Vorschriften / UVV** → `DE_LAW` → Regel 1
|
||||
- DGUV **Regeln, Informationen, Grundsätze** → `DGUV_COPYRIGHT` → Regel 3
|
||||
|
||||
## Auswirkung pro Pipeline-Stage
|
||||
|
||||
| Stage | Verhalten bei Regel 1 | Regel 2 | Regel 3 |
|
||||
|---|---|---|---|
|
||||
| Stage 6 ControlCompose (`pipeline_adapter.py:147`) | speichert `chunk_text` | speichert `chunk_text` | speichert `chunk_text = None` |
|
||||
| Atomic-Control-Bildung | Volltext als Quelle | Volltext + Attribution-Vermerk | nur regulation_id + article |
|
||||
| Output-Renderer (Frontend/PDF) | optionaler Quellen-Hinweis | **Pflicht-Attribution in Footer + Inline** | nur Identifier rendern |
|
||||
| Tech-File-Anhang | Quelle nennen | Quelle + Lizenz-URL | Identifier-Liste |
|
||||
|
||||
## Quellen ohne Klassifikation
|
||||
|
||||
Aktuell sind in `regulation_registry` **232 Regulationen** klassifiziert (Stand 2026-05-21). Die folgenden müssen noch ergänzt werden (Task #20 deckt den DGUV-Ingest):
|
||||
|
||||
| Quelle | Regel | Begründung |
|
||||
|---|---|---|
|
||||
| TRBS-Familie (24 PDFs im RAG) | 1 | Technische Regeln Betriebssicherheit — BAuA Bundesarbeitsblatt |
|
||||
| TRGS-Familie (alle Volltext-Chunks) | 1 | Technische Regeln Gefahrstoffe — BAuA |
|
||||
| ASR-Familie (17 PDFs) | 1 | Arbeitsstättenregeln — BAuA |
|
||||
| OSHA 29 CFR 1910 Subpart O + Technical Manual | 1 | US Federal Public Domain (17 U.S.C. §105) |
|
||||
| DGUV Vorschrift 1 + UVV-Familie (sobald ingest) | 1 | Satzungsrecht der BG |
|
||||
| DGUV Regel 100-500 + Information 209-072/074/073 | 3 | DGUV-Copyright, nur Identifier |
|
||||
| DIN-Identifier-Tabelle (ohne Volltext) | 3 | DIN-Beuth-Copyright |
|
||||
| ANSI B11.0 + RIA R15.06 + UL 508A Identifier | 3 | ANSI/UL-Copyright |
|
||||
| ISO 12100/13849/13857 Identifier | 3 | ISO-Copyright |
|
||||
|
||||
## Audit-Pflicht
|
||||
|
||||
Vor jedem Ingest neuer Quellen:
|
||||
1. Lizenz prüfen (publikationen.dguv.de, EUR-Lex, etc.)
|
||||
2. license_type aus obiger Tabelle wählen — wenn nicht vorhanden, hier ergänzen
|
||||
3. license_rule wird daraus deterministisch abgeleitet
|
||||
4. Attribution-Text bei Regel 2 ist Pflichtfeld
|
||||
|
||||
Vor jedem Output:
|
||||
- Wenn ein atomic_control aus einer Regel-3-Quelle stammt: prüfen dass NUR Identifier gezeigt wird, niemals Volltext
|
||||
- Wenn aus Regel-2-Quelle: Attribution muss im PDF-Footer und im Frontend-Tooltip vorhanden sein
|
||||
- Wenn aus Regel-1-Quelle: empfohlen Quelle nennen für Auditierbarkeit
|
||||
|
||||
## Verweise
|
||||
|
||||
- Schema: `migrations/002_regulation_registry.sql`
|
||||
- Code: `services/regulation_registry.py`, `services/pipeline_adapter.py`
|
||||
- Seed-Script: `scripts/f1_migrate_regulation_registry.py`
|
||||
- Tests: `tests/test_regulation_registry.py` (assert: rule IN (1,2,3))
|
||||
@@ -0,0 +1,101 @@
|
||||
# Incremental BatchDedup für nachgeschobene Dokumente
|
||||
|
||||
Eingefuehrt am 2026-05-18. Pattern fuer alle zukuenftigen Einzeldokument-Ingestionen.
|
||||
|
||||
## Problem
|
||||
|
||||
Der Default-BatchDedup-Runner lief gegen ALLE `pass0b` Atomics ohne Filter
|
||||
(WHERE decomposition_method = 'pass0b' AND release_state NOT IN ('deprecated','duplicate')).
|
||||
Das sind bei uns ~172k Controls. Pace ~5k/h → 25-40h Laufzeit. Bei jedem
|
||||
hinzugefuegten Dokument der gleiche volle Lauf — auch wenn das neue Dokument
|
||||
nur 1-2k Atomics erzeugt.
|
||||
|
||||
Zusaetzliches Risiko: Phase 1 schreibt master_controls erst am Ende. Ein
|
||||
Container-Crash mitten im Lauf (z.B. via Qdrant-Timeout) verwirft 100%
|
||||
des In-Memory-Fortschritts.
|
||||
|
||||
## Loesung — `since` Parameter
|
||||
|
||||
`POST /v1/canonical/generate/batch-dedup` akzeptiert jetzt:
|
||||
|
||||
```json
|
||||
{
|
||||
"dry_run": false,
|
||||
"since": "2026-05-18T02:53:00+00:00"
|
||||
}
|
||||
```
|
||||
|
||||
Effekt:
|
||||
- Phase 1 (intra-group dedup) laedt nur Controls mit `created_at >= since`
|
||||
- Phase 2 (cross-group dedup) filtert ebenfalls auf `created_at >= since`
|
||||
- Phase 2 Checkpoint wird vor Lauf-Start geloescht (sonst skippt stale
|
||||
`last_control_id` neu erzeugte Atomics deren control_id alphabetisch
|
||||
davor liegt)
|
||||
|
||||
Phase 2 sucht weiter im **vollen** Qdrant-Index `atomic_controls_dedup`,
|
||||
findet also Matches zu alten Master Controls und verlinkt korrekt.
|
||||
|
||||
## Wann verwenden
|
||||
|
||||
| Szenario | Empfehlung |
|
||||
|---|---|
|
||||
| Einzelnes neues Dokument ingestiert + Pass 0a + Pass 0b durchgelaufen | `since` setzen auf Zeitpunkt vor Pass 0b |
|
||||
| Mehrere kleine Updates seit letztem Full-Dedup | `since` setzen auf Zeitpunkt nach letztem Full-Dedup |
|
||||
| Initial-Setup oder Pipeline-Major-Update | KEIN `since` — full run |
|
||||
| Verdacht auf Drift / Quality-Regression | KEIN `since` — full run |
|
||||
|
||||
## Workflow nach Einzeldokument-Ingestion
|
||||
|
||||
```bash
|
||||
# 1. Pass 0a auf neue Controls (Obligations extrahieren)
|
||||
curl -X POST .../v1/canonical/generate/run-pass0a -d '{...}'
|
||||
|
||||
# 2. Pass 0b Decomposition Submit (Atomics erzeugen)
|
||||
curl -X POST .../v1/canonical/generate/submit-pass0b -d '{...}'
|
||||
|
||||
# 3. Wenn Anthropic Batch durch: process-batch
|
||||
curl -X POST .../v1/canonical/generate/process-batch -d '{
|
||||
"batch_id": "msgbatch_...",
|
||||
"pass_type": "0b"
|
||||
}'
|
||||
|
||||
# 4. Inkrementell deduppen (NEU, statt 25h full run)
|
||||
curl -X POST .../v1/canonical/generate/batch-dedup -d '{
|
||||
"dry_run": false,
|
||||
"since": "<ISO-Datetime kurz vor Pass-0b-Start>"
|
||||
}'
|
||||
```
|
||||
|
||||
## Pace-Beobachtung (CRA-Lauf 2026-05-18)
|
||||
|
||||
- Total neue Atomics: 19.423
|
||||
- Phase 1 multi-groups: 568 (Rest 18.101 sind Singletons → direkt Master)
|
||||
- Phase 2 Cross-Group: ~3-4h erwartet
|
||||
- Vergleich: Full-Run waere 25-40h gewesen, scoped 6-13x schneller.
|
||||
|
||||
## Implementation-Details (fuer Wartung)
|
||||
|
||||
Geaenderte Dateien:
|
||||
- `services/batch_dedup_runner.py` — `run()` + `_load_merge_groups()` +
|
||||
`_run_cross_group_pass()` SQL-Queries
|
||||
- `api/control_generator_routes.py` — `BatchDedupRequest.since` Feld +
|
||||
Handler reicht durch
|
||||
|
||||
Backwards-kompatibel: ohne `since` aequivalent zum alten Verhalten.
|
||||
|
||||
## Bekannte Limits
|
||||
|
||||
1. **Phase 2 Checkpoint wird beim scoped Lauf geloescht.** Wenn waehrend
|
||||
eines `since`-Laufs ein voller Run dazwischen geschoben werden soll
|
||||
(sollte nicht passieren), muss neu starten.
|
||||
2. **Phase 1 commit-Granularitaet nicht angefasst.** Bei Crash mitten in
|
||||
Phase 1 ohne `since` bleibt der Verlust gleich. Aber: scoped Phase 1
|
||||
ist so kurz (Minuten), dass das praktisch egal ist.
|
||||
3. **Singleton-Atomics werden direkt Master ohne Cross-Check.** Wenn ein
|
||||
neues Singleton-Atomic semantisch identisch zu einem alten Master
|
||||
ist, faengt das nur Phase 2 (via Qdrant). Funktioniert solange Phase 2
|
||||
nicht uebersprungen wird (dry_run=false ist Pflicht).
|
||||
|
||||
## Memory-Eintrag
|
||||
|
||||
Siehe `~/.claude/projects/-Users-benjaminadmin-Projekte-breakpilot-core/memory/feedback_incremental_dedup.md`
|
||||
@@ -0,0 +1,58 @@
|
||||
-- Migration 011: Derived Controls Library (Clean-Room MCs from external sources)
|
||||
-- Schema: compliance
|
||||
--
|
||||
-- Holds Master Controls + atomic controls + mitigations + metrics that were
|
||||
-- derived Clean-Room from external regulatory sources (BSI QUAIDAL today,
|
||||
-- Grundschutz++/CRA/NIST AI RMF next). Kept separate from the gpre2
|
||||
-- master_controls table because:
|
||||
-- 1) The shape is different (no object_group/phase concepts).
|
||||
-- 2) Source-Layer-Trennung: derivations from external IP must be cleanly
|
||||
-- separable from internally-generated artifacts.
|
||||
-- 3) Each row carries the licence + provenance for due diligence.
|
||||
--
|
||||
-- Run: ssh macmini "docker exec -i bp-core-postgres psql -U breakpilot -d breakpilot_db" \
|
||||
-- < control-pipeline/migrations/011_derived_controls.sql
|
||||
|
||||
SET search_path TO compliance, public;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS derived_controls (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
derived_id VARCHAR(200) UNIQUE NOT NULL, -- e.g. MC-AI-DATA-QKB-01-repraesentativitaet
|
||||
kind VARCHAR(30) NOT NULL, -- criterion | building_block | measure | metric
|
||||
canonical_name VARCHAR(300) NOT NULL,
|
||||
description TEXT NOT NULL, -- our own wording, never the original
|
||||
regulation_anchor TEXT, -- e.g. "EU AI Act Art. 10"
|
||||
related_quaidal_ids JSONB NOT NULL DEFAULT '[]', -- ["QB-03", "QB-04", ...]
|
||||
external_refs JSONB NOT NULL DEFAULT '[]', -- [{framework, citation}, ...]
|
||||
source_framework VARCHAR(80) NOT NULL, -- "BSI QUAIDAL"
|
||||
source_section VARCHAR(80) NOT NULL, -- "QKB-01"
|
||||
source_url TEXT,
|
||||
source_commit_sha VARCHAR(80),
|
||||
source_title_original TEXT, -- original title (label, not protected)
|
||||
source_license_note TEXT,
|
||||
plagiarism_score_at_generation NUMERIC(5,4), -- 0..1; gate was 0.20
|
||||
generated_by_model VARCHAR(80),
|
||||
yaml_path TEXT, -- pointer back to source YAML
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_derived_controls_kind ON derived_controls(kind);
|
||||
CREATE INDEX IF NOT EXISTS idx_derived_controls_source_framework ON derived_controls(source_framework);
|
||||
CREATE INDEX IF NOT EXISTS idx_derived_controls_source_section ON derived_controls(source_section);
|
||||
CREATE INDEX IF NOT EXISTS idx_derived_controls_related_quaidal_gin
|
||||
ON derived_controls USING GIN(related_quaidal_ids);
|
||||
|
||||
-- Trigger to keep updated_at fresh
|
||||
CREATE OR REPLACE FUNCTION trg_derived_controls_set_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS derived_controls_updated_at ON derived_controls;
|
||||
CREATE TRIGGER derived_controls_updated_at
|
||||
BEFORE UPDATE ON derived_controls
|
||||
FOR EACH ROW EXECUTE FUNCTION trg_derived_controls_set_updated_at();
|
||||
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Upsert derived QUAIDAL controls from YAML into compliance.derived_controls.
|
||||
|
||||
Reads:
|
||||
control-pipeline/data/quaidal/master_controls.yaml
|
||||
control-pipeline/data/quaidal/atomic_controls.yaml
|
||||
control-pipeline/data/quaidal/mitigations.yaml
|
||||
control-pipeline/data/quaidal/metrics.yaml
|
||||
|
||||
Writes: compliance.derived_controls (idempotent UPSERT by derived_id)
|
||||
|
||||
Usage:
|
||||
# Mac Mini direct:
|
||||
python3 control-pipeline/scripts/apply_quaidal_to_db.py
|
||||
|
||||
# Via SSH (locally, against macmini DB):
|
||||
DB_HOST=macmini python3 control-pipeline/scripts/apply_quaidal_to_db.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import psycopg
|
||||
import yaml
|
||||
except ImportError as e:
|
||||
print(f"ERROR: missing dependency {e.name}. Install with: pip install psycopg[binary] pyyaml", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
DATA_DIR = REPO_ROOT / "control-pipeline" / "data" / "quaidal"
|
||||
|
||||
KIND_FILES = {
|
||||
"criterion": "master_controls.yaml",
|
||||
"building_block": "atomic_controls.yaml",
|
||||
"measure": "mitigations.yaml",
|
||||
"metric": "metrics.yaml",
|
||||
}
|
||||
|
||||
UPSERT_SQL = """
|
||||
INSERT INTO compliance.derived_controls (
|
||||
derived_id, kind, canonical_name, description, regulation_anchor,
|
||||
related_quaidal_ids, external_refs,
|
||||
source_framework, source_section, source_url, source_commit_sha,
|
||||
source_title_original, source_license_note,
|
||||
plagiarism_score_at_generation, generated_by_model, yaml_path
|
||||
) VALUES (
|
||||
%(derived_id)s, %(kind)s, %(canonical_name)s, %(description)s, %(regulation_anchor)s,
|
||||
%(related_quaidal_ids)s::jsonb, %(external_refs)s::jsonb,
|
||||
%(source_framework)s, %(source_section)s, %(source_url)s, %(source_commit_sha)s,
|
||||
%(source_title_original)s, %(source_license_note)s,
|
||||
%(plagiarism_score)s, %(generated_by_model)s, %(yaml_path)s
|
||||
)
|
||||
ON CONFLICT (derived_id) DO UPDATE SET
|
||||
kind = EXCLUDED.kind,
|
||||
canonical_name = EXCLUDED.canonical_name,
|
||||
description = EXCLUDED.description,
|
||||
regulation_anchor = EXCLUDED.regulation_anchor,
|
||||
related_quaidal_ids = EXCLUDED.related_quaidal_ids,
|
||||
external_refs = EXCLUDED.external_refs,
|
||||
source_framework = EXCLUDED.source_framework,
|
||||
source_section = EXCLUDED.source_section,
|
||||
source_url = EXCLUDED.source_url,
|
||||
source_commit_sha = EXCLUDED.source_commit_sha,
|
||||
source_title_original = EXCLUDED.source_title_original,
|
||||
source_license_note = EXCLUDED.source_license_note,
|
||||
plagiarism_score_at_generation = EXCLUDED.plagiarism_score_at_generation,
|
||||
generated_by_model = EXCLUDED.generated_by_model,
|
||||
yaml_path = EXCLUDED.yaml_path
|
||||
"""
|
||||
|
||||
|
||||
def load_yaml_records(yaml_path: Path) -> tuple[list[dict], str | None, str | None]:
|
||||
if not yaml_path.exists():
|
||||
return [], None, None
|
||||
data = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
|
||||
return data.get("controls", []), data.get("commit_sha"), data.get("generated_by_model")
|
||||
|
||||
|
||||
def to_row(ctrl: dict, yaml_path: Path, default_model: str | None, default_commit: str | None) -> dict:
|
||||
source = ctrl.get("source") or {}
|
||||
return {
|
||||
"derived_id": ctrl["id"],
|
||||
"kind": ctrl["kind"],
|
||||
"canonical_name": ctrl["canonical_name"],
|
||||
"description": ctrl["description"],
|
||||
"regulation_anchor": ctrl.get("regulation_anchor"),
|
||||
"related_quaidal_ids": json.dumps(ctrl.get("related_quaidal_ids", []), ensure_ascii=False),
|
||||
"external_refs": json.dumps(ctrl.get("external_refs", []), ensure_ascii=False),
|
||||
"source_framework": source.get("framework", "BSI QUAIDAL"),
|
||||
"source_section": source.get("section", ""),
|
||||
"source_url": source.get("url"),
|
||||
"source_commit_sha": source.get("commit_sha") or default_commit,
|
||||
"source_title_original": source.get("title_original_de"),
|
||||
"source_license_note": source.get("license_note"),
|
||||
"plagiarism_score": ctrl.get("plagiarism_score_at_generation"),
|
||||
"generated_by_model": default_model,
|
||||
"yaml_path": str(yaml_path.relative_to(REPO_ROOT)),
|
||||
}
|
||||
|
||||
|
||||
def build_dsn(args: argparse.Namespace) -> str:
|
||||
if args.dsn:
|
||||
return args.dsn
|
||||
return (
|
||||
f"host={args.db_host} port={args.db_port} "
|
||||
f"dbname={args.db_name} user={args.db_user} password={args.db_password}"
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--dsn", help="Full DSN; overrides individual flags")
|
||||
ap.add_argument("--db-host", default=os.environ.get("DB_HOST", "localhost"))
|
||||
ap.add_argument("--db-port", default=os.environ.get("DB_PORT", "5432"))
|
||||
ap.add_argument("--db-name", default=os.environ.get("DB_NAME", "breakpilot_db"))
|
||||
ap.add_argument("--db-user", default=os.environ.get("DB_USER", "breakpilot"))
|
||||
ap.add_argument("--db-password", default=os.environ.get("DB_PASSWORD", "breakpilot"))
|
||||
ap.add_argument("--dry-run", action="store_true")
|
||||
args = ap.parse_args()
|
||||
|
||||
total = 0
|
||||
rows: list[dict] = []
|
||||
for kind, fname in KIND_FILES.items():
|
||||
path = DATA_DIR / fname
|
||||
records, commit, model = load_yaml_records(path)
|
||||
for rec in records:
|
||||
rows.append(to_row(rec, path, model, commit))
|
||||
if records:
|
||||
print(f" {fname}: {len(records)} entries", file=sys.stderr)
|
||||
total += len(records)
|
||||
|
||||
if not rows:
|
||||
print("ERROR: no YAML records found; run derive_quaidal_mcs.py first", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
print(f"Total rows: {total}", file=sys.stderr)
|
||||
if args.dry_run:
|
||||
print("Dry run — sample row:", file=sys.stderr)
|
||||
print(json.dumps({k: (v[:200] if isinstance(v, str) else v) for k, v in rows[0].items()}, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
dsn = build_dsn(args)
|
||||
print(f"Connecting to {args.db_host}:{args.db_port}/{args.db_name}", file=sys.stderr)
|
||||
inserted = updated = 0
|
||||
with psycopg.connect(dsn) as conn:
|
||||
with conn.cursor() as cur:
|
||||
for row in rows:
|
||||
cur.execute(
|
||||
"SELECT 1 FROM compliance.derived_controls WHERE derived_id = %s",
|
||||
(row["derived_id"],),
|
||||
)
|
||||
existed = cur.fetchone() is not None
|
||||
cur.execute(UPSERT_SQL, row)
|
||||
if existed:
|
||||
updated += 1
|
||||
else:
|
||||
inserted += 1
|
||||
conn.commit()
|
||||
print(f"Inserted: {inserted}, Updated: {updated}", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Audit script for license classification gaps in the control pipeline.
|
||||
|
||||
Reports:
|
||||
|
||||
1. **regulation_registry coverage** — how many regulations are classified, by
|
||||
rule and license_type.
|
||||
2. **atomic_controls without license_rule** — how many controls reference a
|
||||
regulation_id that has no entry (or no license_rule) in the registry.
|
||||
3. **Qdrant payload consistency** — for each indexed collection, how many
|
||||
chunks carry both ``license`` and ``license_rule`` payload fields.
|
||||
|
||||
The goal is to surface every record where the engine could in principle
|
||||
extract or emit content but the license rule is unknown — those records are
|
||||
the highest-risk material in a license audit.
|
||||
|
||||
Usage::
|
||||
|
||||
python3 scripts/audit_license_classification.py --db-host 100.80.114.48
|
||||
|
||||
Add ``--check-qdrant`` to also probe ``http://<host>:6333`` collections.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from urllib import request as urllib_request
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
DEFAULT_HOST = "100.80.114.48"
|
||||
DEFAULT_PORT = 5432
|
||||
DEFAULT_USER = "breakpilot"
|
||||
DEFAULT_DB = "breakpilot_db"
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument("--db-host", default=DEFAULT_HOST)
|
||||
p.add_argument("--db-port", type=int, default=DEFAULT_PORT)
|
||||
p.add_argument("--db-user", default=DEFAULT_USER)
|
||||
p.add_argument("--db-name", default=DEFAULT_DB)
|
||||
p.add_argument("--db-password", default="")
|
||||
p.add_argument("--check-qdrant", action="store_true")
|
||||
p.add_argument("--qdrant-host", default="100.80.114.48")
|
||||
p.add_argument("--qdrant-port", type=int, default=6333)
|
||||
p.add_argument("--json", action="store_true", help="Emit JSON result on stdout")
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
def audit_registry(conn) -> dict:
|
||||
"""Coverage of regulation_registry."""
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"SET search_path TO compliance, public; "
|
||||
"SELECT license_rule, license_type, COUNT(*) "
|
||||
"FROM regulation_registry GROUP BY license_rule, license_type "
|
||||
"ORDER BY license_rule, license_type;"
|
||||
)
|
||||
by_rule_and_type: list[tuple] = []
|
||||
by_rule: Counter = Counter()
|
||||
for rule, ltype, count in cur.fetchall():
|
||||
by_rule_and_type.append((rule, ltype or "(empty)", count))
|
||||
by_rule[rule] += count
|
||||
|
||||
cur.execute(
|
||||
"SELECT COUNT(*) FROM regulation_registry "
|
||||
"WHERE license_type IS NULL OR license_type = '';"
|
||||
)
|
||||
missing_type = cur.fetchone()[0]
|
||||
|
||||
cur.execute("SELECT COUNT(*) FROM regulation_registry;")
|
||||
total = cur.fetchone()[0]
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"by_rule": dict(by_rule),
|
||||
"by_rule_and_type": by_rule_and_type,
|
||||
"missing_license_type": missing_type,
|
||||
}
|
||||
|
||||
|
||||
def audit_atomic_controls(conn) -> dict:
|
||||
"""Controls whose source regulation has no license rule.
|
||||
|
||||
Important: the schema differs between core (bp-core) and customer
|
||||
deployments. We probe a handful of likely column names and skip if
|
||||
none are found.
|
||||
"""
|
||||
cur = conn.cursor()
|
||||
# Detect controls table
|
||||
cur.execute(
|
||||
"SELECT table_name FROM information_schema.tables "
|
||||
"WHERE table_schema='compliance' AND table_name IN "
|
||||
"('atomic_controls','atomic_controls_dedup','canonical_controls');"
|
||||
)
|
||||
tables = [r[0] for r in cur.fetchall()]
|
||||
if not tables:
|
||||
return {"skipped": True, "reason": "no controls table found"}
|
||||
|
||||
result: dict = {"tables": {}}
|
||||
for tbl in tables:
|
||||
cur.execute(
|
||||
f"SELECT column_name FROM information_schema.columns "
|
||||
f"WHERE table_schema='compliance' AND table_name='{tbl}';"
|
||||
)
|
||||
cols = {r[0] for r in cur.fetchall()}
|
||||
if "license_rule" not in cols:
|
||||
result["tables"][tbl] = {"skipped": True, "reason": "no license_rule column"}
|
||||
continue
|
||||
cur.execute(f"SELECT COUNT(*) FROM compliance.{tbl};")
|
||||
total = cur.fetchone()[0]
|
||||
cur.execute(
|
||||
f"SELECT license_rule, COUNT(*) FROM compliance.{tbl} "
|
||||
f"GROUP BY license_rule ORDER BY license_rule;"
|
||||
)
|
||||
by_rule = {str(r[0]): r[1] for r in cur.fetchall()}
|
||||
cur.execute(
|
||||
f"SELECT COUNT(*) FROM compliance.{tbl} WHERE license_rule IS NULL;"
|
||||
)
|
||||
missing = cur.fetchone()[0]
|
||||
result["tables"][tbl] = {
|
||||
"total": total,
|
||||
"by_rule": by_rule,
|
||||
"missing_license_rule": missing,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def audit_qdrant(host: str, port: int) -> dict:
|
||||
"""Probe Qdrant collections for license + license_rule payload coverage.
|
||||
|
||||
Samples 500 points per collection and reports how many have neither
|
||||
field populated.
|
||||
"""
|
||||
out: dict = {"collections": {}}
|
||||
base = f"http://{host}:{port}"
|
||||
try:
|
||||
with urllib_request.urlopen(f"{base}/collections", timeout=10) as r:
|
||||
colls = json.loads(r.read()).get("result", {}).get("collections", [])
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
for c in colls:
|
||||
name = c["name"]
|
||||
if "compliance" not in name and "atomic_controls" not in name:
|
||||
continue
|
||||
payload = {"limit": 500, "with_payload": True, "with_vector": False}
|
||||
req = urllib_request.Request(
|
||||
f"{base}/collections/{name}/points/scroll",
|
||||
data=json.dumps(payload).encode(),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
try:
|
||||
with urllib_request.urlopen(req, timeout=15) as r:
|
||||
points = json.loads(r.read()).get("result", {}).get("points", [])
|
||||
except Exception as e:
|
||||
out["collections"][name] = {"error": str(e)}
|
||||
continue
|
||||
sampled = len(points)
|
||||
both_set = 0
|
||||
only_license = 0
|
||||
only_rule = 0
|
||||
neither = 0
|
||||
for p in points:
|
||||
pl = p.get("payload", {}) or {}
|
||||
has_lic = bool(pl.get("license"))
|
||||
has_rule = pl.get("license_rule") is not None
|
||||
if has_lic and has_rule:
|
||||
both_set += 1
|
||||
elif has_lic:
|
||||
only_license += 1
|
||||
elif has_rule:
|
||||
only_rule += 1
|
||||
else:
|
||||
neither += 1
|
||||
out["collections"][name] = {
|
||||
"sampled": sampled,
|
||||
"both_set": both_set,
|
||||
"only_license_field": only_license,
|
||||
"only_license_rule_field": only_rule,
|
||||
"neither_set": neither,
|
||||
"neither_pct": round(neither / sampled * 100, 1) if sampled else 0,
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
try:
|
||||
import psycopg2
|
||||
except ImportError:
|
||||
print("error: psycopg2 not installed (pip install psycopg2-binary)", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
conn = psycopg2.connect(
|
||||
host=args.db_host,
|
||||
port=args.db_port,
|
||||
user=args.db_user,
|
||||
dbname=args.db_name,
|
||||
password=args.db_password or None,
|
||||
)
|
||||
try:
|
||||
registry = audit_registry(conn)
|
||||
controls = audit_atomic_controls(conn)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
qdrant: Optional[dict] = None
|
||||
if args.check_qdrant:
|
||||
qdrant = audit_qdrant(args.qdrant_host, args.qdrant_port)
|
||||
|
||||
result = {"registry": registry, "atomic_controls": controls, "qdrant": qdrant}
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(result, indent=2, default=str))
|
||||
return 0
|
||||
|
||||
print("=" * 60)
|
||||
print(" Audit — License Classification")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print(f"## regulation_registry ({registry['total']} rows)")
|
||||
print(f" By rule: {registry['by_rule']}")
|
||||
print(f" Missing license_type: {registry['missing_license_type']}")
|
||||
print()
|
||||
print("## atomic_controls")
|
||||
for tbl, info in controls.get("tables", {}).items():
|
||||
if info.get("skipped"):
|
||||
print(f" {tbl}: SKIPPED ({info['reason']})")
|
||||
continue
|
||||
print(f" {tbl}: {info['total']} rows")
|
||||
print(f" by_rule={info['by_rule']}")
|
||||
print(f" missing_license_rule={info['missing_license_rule']}")
|
||||
print()
|
||||
if qdrant:
|
||||
print("## qdrant")
|
||||
for name, info in qdrant.get("collections", {}).items():
|
||||
if "error" in info:
|
||||
print(f" {name}: ERROR {info['error']}")
|
||||
continue
|
||||
print(
|
||||
f" {name:30} sampled={info['sampled']:4} "
|
||||
f"both={info['both_set']:4} "
|
||||
f"neither={info['neither_set']:4} ({info['neither_pct']}%)"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Backfill license_rule on canonical_controls by inheriting from parent.
|
||||
|
||||
Background
|
||||
==========
|
||||
|
||||
Audit (audit_license_classification.py) showed that 279,384 of 314,811 rows
|
||||
in compliance.canonical_controls have NULL license_rule. Drilling in:
|
||||
|
||||
- 261,980 of those (94%) have a parent_control_uuid whose parent already
|
||||
carries a non-NULL license_rule. The pass0b decomposition pipeline did
|
||||
not propagate the rule to its child controls — this is a clear inheritance
|
||||
bug, fixable without any classification decisions.
|
||||
- 16,617 have a parent that itself has no license_rule (transitive case).
|
||||
Inheriting iteratively converges to either rule-set or root-orphan.
|
||||
- 787 have no parent at all (decomposition roots). These need cluster-based
|
||||
manual classification (see Strategy Notes at the bottom of this file).
|
||||
|
||||
This script runs the inheritance fix in three idempotent stages and
|
||||
prints per-stage counts before any write happens.
|
||||
|
||||
Usage::
|
||||
|
||||
# Always dry-run first:
|
||||
python3 scripts/backfill_license_rule.py --db-host 100.80.114.48 \\
|
||||
--db-password breakpilot123 --dry-run
|
||||
|
||||
# If counts look right:
|
||||
python3 scripts/backfill_license_rule.py --db-host 100.80.114.48 \\
|
||||
--db-password breakpilot123 --apply
|
||||
|
||||
The script is safe to rerun — it only touches rows where license_rule
|
||||
IS NULL.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument("--db-host", default="100.80.114.48")
|
||||
p.add_argument("--db-port", type=int, default=5432)
|
||||
p.add_argument("--db-user", default="breakpilot")
|
||||
p.add_argument("--db-name", default="breakpilot_db")
|
||||
p.add_argument("--db-password", required=True)
|
||||
g = p.add_mutually_exclusive_group(required=True)
|
||||
g.add_argument("--dry-run", action="store_true")
|
||||
g.add_argument("--apply", action="store_true")
|
||||
p.add_argument("--max-iterations", type=int, default=5,
|
||||
help="Cap on inheritance iterations to avoid loops")
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
# Stage 1: direct parent has license_rule — single UPDATE.
|
||||
# Stage 2: iterative — parent did not have it, but a grandparent does.
|
||||
# We loop until no more rows can be filled or max-iterations.
|
||||
# Stage 3: residual rows with no resolvable parent. Report them clustered
|
||||
# by category/pattern_id so the user can classify by family.
|
||||
|
||||
SQL_REPORT_NULLS = """
|
||||
SET search_path TO compliance, public;
|
||||
SELECT
|
||||
CASE WHEN cc.parent_control_uuid IS NULL THEN 'no_parent'
|
||||
WHEN p.license_rule IS NULL THEN 'parent_null'
|
||||
ELSE 'parent_set' END AS bucket,
|
||||
COUNT(*) AS n
|
||||
FROM canonical_controls cc
|
||||
LEFT JOIN canonical_controls p ON cc.parent_control_uuid = p.id
|
||||
WHERE cc.license_rule IS NULL
|
||||
GROUP BY 1 ORDER BY 2 DESC;
|
||||
"""
|
||||
|
||||
SQL_INHERIT_FROM_PARENT = """
|
||||
SET search_path TO compliance, public;
|
||||
UPDATE canonical_controls cc
|
||||
SET license_rule = p.license_rule, updated_at = NOW()
|
||||
FROM canonical_controls p
|
||||
WHERE cc.parent_control_uuid = p.id
|
||||
AND cc.license_rule IS NULL
|
||||
AND p.license_rule IS NOT NULL;
|
||||
"""
|
||||
|
||||
SQL_REPORT_ORPHAN_CLUSTERS = """
|
||||
SET search_path TO compliance, public;
|
||||
SELECT
|
||||
COALESCE(category, '(null)') AS category,
|
||||
COALESCE(pattern_id, '(null)') AS pattern_id,
|
||||
COALESCE(generation_strategy, '(null)') AS gen,
|
||||
COUNT(*) AS n
|
||||
FROM canonical_controls
|
||||
WHERE license_rule IS NULL AND parent_control_uuid IS NULL
|
||||
GROUP BY 1, 2, 3 ORDER BY n DESC LIMIT 25;
|
||||
"""
|
||||
|
||||
|
||||
def print_bucket(rows, label: str) -> None:
|
||||
print(f"\n## {label}")
|
||||
total = 0
|
||||
for bucket, n in rows:
|
||||
print(f" {bucket:12} {n:>8}")
|
||||
total += n
|
||||
print(f" {'TOTAL':12} {total:>8}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
try:
|
||||
import psycopg2
|
||||
except ImportError:
|
||||
print("error: psycopg2 not installed", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
conn = psycopg2.connect(
|
||||
host=args.db_host, port=args.db_port, user=args.db_user,
|
||||
dbname=args.db_name, password=args.db_password,
|
||||
)
|
||||
conn.autocommit = False
|
||||
cur = conn.cursor()
|
||||
|
||||
print("=" * 60)
|
||||
print(" Backfill — license_rule via parent inheritance")
|
||||
print(f" Mode: {'DRY-RUN' if args.dry_run else 'APPLY'}")
|
||||
print("=" * 60)
|
||||
|
||||
# Initial bucket report
|
||||
cur.execute(SQL_REPORT_NULLS)
|
||||
rows = cur.fetchall()
|
||||
print_bucket(rows, "Initial NULL distribution")
|
||||
|
||||
if args.dry_run:
|
||||
# Print what the FIRST inherit pass would resolve (without writing)
|
||||
cur.execute(
|
||||
"SET search_path TO compliance, public; "
|
||||
"SELECT p.license_rule, COUNT(*) "
|
||||
"FROM canonical_controls cc "
|
||||
"JOIN canonical_controls p ON cc.parent_control_uuid = p.id "
|
||||
"WHERE cc.license_rule IS NULL AND p.license_rule IS NOT NULL "
|
||||
"GROUP BY 1 ORDER BY 1;"
|
||||
)
|
||||
print("\n## First inherit-pass would fill:")
|
||||
for rule, n in cur.fetchall():
|
||||
print(f" rule={rule} {n:>8} rows")
|
||||
|
||||
# Show orphan clusters that would remain
|
||||
cur.execute(SQL_REPORT_ORPHAN_CLUSTERS)
|
||||
print("\n## Orphan clusters (no parent + no rule, top 25):")
|
||||
for cat, pid, gen, n in cur.fetchall():
|
||||
print(f" cat={cat[:20]:20} pat={pid[:20]:20} gen={gen[:20]:20} n={n}")
|
||||
print("\nNo writes performed. Use --apply to execute.")
|
||||
conn.rollback()
|
||||
return 0
|
||||
|
||||
# Apply mode — iterative inheritance
|
||||
total_updated = 0
|
||||
for i in range(1, args.max_iterations + 1):
|
||||
cur.execute(SQL_INHERIT_FROM_PARENT)
|
||||
updated = cur.rowcount
|
||||
total_updated += updated
|
||||
print(f"\n iteration {i}: {updated} rows updated")
|
||||
if updated == 0:
|
||||
break
|
||||
|
||||
conn.commit()
|
||||
print(f"\n✓ Total rows backfilled: {total_updated}")
|
||||
|
||||
# Final bucket report
|
||||
cur.execute(SQL_REPORT_NULLS)
|
||||
print_bucket(cur.fetchall(), "Remaining NULL distribution")
|
||||
|
||||
cur.execute(SQL_REPORT_ORPHAN_CLUSTERS)
|
||||
rows = cur.fetchall()
|
||||
if rows:
|
||||
print("\n## Orphan clusters still need classification:")
|
||||
for cat, pid, gen, n in rows:
|
||||
print(f" cat={cat[:20]:20} pat={pid[:20]:20} gen={gen[:20]:20} n={n}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Backfill ``license_rule`` payload field into Qdrant atomic_controls_dedup
|
||||
and related compliance collections, sourced from canonical_controls in Postgres.
|
||||
|
||||
The audit (audit_license_classification.py) surfaced that Qdrant collections
|
||||
holding canonical-control vectors (notably ``atomic_controls_dedup``) carry no
|
||||
license_rule payload at all, even though the underlying Postgres table is now
|
||||
fully classified. This script joins the two via ``control_uuid`` and patches the
|
||||
Qdrant payload in batches.
|
||||
|
||||
Usage::
|
||||
|
||||
python3 scripts/backfill_qdrant_license_payload.py \\
|
||||
--pg-host 100.80.114.48 --pg-password breakpilot123 \\
|
||||
--qdrant http://100.80.114.48:6333 \\
|
||||
--collection atomic_controls_dedup \\
|
||||
--dry-run
|
||||
|
||||
# apply
|
||||
python3 scripts/backfill_qdrant_license_payload.py ... --apply
|
||||
|
||||
Notes
|
||||
-----
|
||||
- ``control_uuid`` lives in the payload of atomic_controls_dedup. For other
|
||||
collections that key the canonical control by a different field, override with
|
||||
``--uuid-field``.
|
||||
- Qdrant ``set_payload`` is keyed by point id, not payload field. We resolve
|
||||
UUID → point id by a paginated scroll-and-filter pass, then issue grouped
|
||||
set_payload requests per license_rule (3 batches per collection).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from typing import Iterator
|
||||
from urllib import request as urllib_request
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument("--pg-host", default="100.80.114.48")
|
||||
p.add_argument("--pg-port", type=int, default=5432)
|
||||
p.add_argument("--pg-user", default="breakpilot")
|
||||
p.add_argument("--pg-name", default="breakpilot_db")
|
||||
p.add_argument("--pg-password", required=True)
|
||||
p.add_argument("--qdrant", default="http://100.80.114.48:6333")
|
||||
p.add_argument("--qdrant-api-key", default="",
|
||||
help="API key for managed Qdrant (Production)")
|
||||
p.add_argument("--collection", default="atomic_controls_dedup")
|
||||
p.add_argument("--uuid-field", default="control_uuid",
|
||||
help="Payload field used for lookup (control_uuid or regulation_id)")
|
||||
p.add_argument("--lookup", choices=["canonical_controls", "regulation_registry"],
|
||||
default="canonical_controls",
|
||||
help="Postgres table to resolve the lookup against")
|
||||
p.add_argument("--batch-size", type=int, default=500)
|
||||
g = p.add_mutually_exclusive_group(required=True)
|
||||
g.add_argument("--dry-run", action="store_true")
|
||||
g.add_argument("--apply", action="store_true")
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
def fetch_rule_by_uuid(args) -> dict[str, int]:
|
||||
"""Pull lookup-key → license_rule mapping from Postgres.
|
||||
|
||||
Source table is chosen by ``--lookup``:
|
||||
- canonical_controls: id (UUID) → license_rule, for atomic_controls_dedup
|
||||
- regulation_registry: regulation_id → license_rule, for document chunks
|
||||
"""
|
||||
import psycopg2
|
||||
conn = psycopg2.connect(
|
||||
host=args.pg_host, port=args.pg_port, user=args.pg_user,
|
||||
dbname=args.pg_name, password=args.pg_password,
|
||||
)
|
||||
cur = conn.cursor()
|
||||
cur.execute("SET search_path TO compliance, public;")
|
||||
if args.lookup == "regulation_registry":
|
||||
cur.execute(
|
||||
"SELECT regulation_id, license_rule FROM regulation_registry "
|
||||
"WHERE license_rule IS NOT NULL"
|
||||
)
|
||||
else:
|
||||
cur.execute(
|
||||
"SELECT id::text, license_rule FROM canonical_controls "
|
||||
"WHERE license_rule IS NOT NULL"
|
||||
)
|
||||
mapping = {row[0]: int(row[1]) for row in cur.fetchall()}
|
||||
conn.close()
|
||||
return mapping
|
||||
|
||||
|
||||
def _headers(api_key: str = "") -> dict:
|
||||
h = {"Content-Type": "application/json"}
|
||||
if api_key:
|
||||
h["api-key"] = api_key
|
||||
return h
|
||||
|
||||
|
||||
def scroll_collection(qdrant: str, collection: str, uuid_field: str, api_key: str = "") -> Iterator[dict]:
|
||||
"""Yield (point_id, uuid_value, has_rule_already) tuples."""
|
||||
next_offset = None
|
||||
while True:
|
||||
body = {"limit": 1000, "with_payload": True, "with_vector": False}
|
||||
if next_offset is not None:
|
||||
body["offset"] = next_offset
|
||||
req = urllib_request.Request(
|
||||
f"{qdrant}/collections/{collection}/points/scroll",
|
||||
data=json.dumps(body).encode(),
|
||||
headers=_headers(api_key),
|
||||
)
|
||||
with urllib_request.urlopen(req, timeout=60) as r:
|
||||
payload = json.loads(r.read())
|
||||
result = payload.get("result", {})
|
||||
for pt in result.get("points", []):
|
||||
pl = pt.get("payload", {}) or {}
|
||||
yield {
|
||||
"id": pt["id"],
|
||||
"uuid": pl.get(uuid_field),
|
||||
"has_rule": "license_rule" in pl,
|
||||
}
|
||||
next_offset = result.get("next_page_offset")
|
||||
if next_offset is None:
|
||||
break
|
||||
|
||||
|
||||
def set_payload_batch(qdrant: str, collection: str, point_ids: list, rule: int, api_key: str = "") -> int:
|
||||
"""POST set_payload for a batch of point IDs with a single license_rule."""
|
||||
body = {
|
||||
"payload": {"license_rule": rule},
|
||||
"points": point_ids,
|
||||
}
|
||||
req = urllib_request.Request(
|
||||
f"{qdrant}/collections/{collection}/points/payload?wait=true",
|
||||
data=json.dumps(body).encode(),
|
||||
headers=_headers(api_key),
|
||||
method="POST",
|
||||
)
|
||||
with urllib_request.urlopen(req, timeout=120) as r:
|
||||
resp = json.loads(r.read())
|
||||
if resp.get("status") != "ok":
|
||||
raise RuntimeError(f"set_payload failed: {resp}")
|
||||
return len(point_ids)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
print("Loading canonical_controls → license_rule mapping…")
|
||||
rule_by_uuid = fetch_rule_by_uuid(args)
|
||||
print(f" Postgres returned {len(rule_by_uuid)} classified controls")
|
||||
|
||||
print(f"Scrolling Qdrant collection {args.collection!r}…")
|
||||
by_rule: dict[int, list] = {1: [], 2: [], 3: []}
|
||||
points_total = 0
|
||||
points_with_uuid = 0
|
||||
points_already_set = 0
|
||||
points_no_match = 0
|
||||
|
||||
for pt in scroll_collection(args.qdrant, args.collection, args.uuid_field, args.qdrant_api_key):
|
||||
points_total += 1
|
||||
uuid = pt["uuid"]
|
||||
if not uuid:
|
||||
continue
|
||||
points_with_uuid += 1
|
||||
if pt["has_rule"]:
|
||||
points_already_set += 1
|
||||
continue
|
||||
rule = rule_by_uuid.get(uuid)
|
||||
if rule is None:
|
||||
points_no_match += 1
|
||||
continue
|
||||
if rule not in by_rule:
|
||||
continue
|
||||
by_rule[rule].append(pt["id"])
|
||||
|
||||
print(f" total points scanned: {points_total}")
|
||||
print(f" with {args.uuid_field}: {points_with_uuid}")
|
||||
print(f" already had license_rule: {points_already_set}")
|
||||
print(f" uuid not found in Postgres: {points_no_match}")
|
||||
print(f" to set per rule: rule1={len(by_rule[1])} rule2={len(by_rule[2])} rule3={len(by_rule[3])}")
|
||||
|
||||
if args.dry_run:
|
||||
print("\nDRY-RUN: no writes performed. Use --apply to execute.")
|
||||
return 0
|
||||
|
||||
total_written = 0
|
||||
for rule, ids in by_rule.items():
|
||||
if not ids:
|
||||
continue
|
||||
print(f"\nWriting license_rule={rule} to {len(ids)} points (batch {args.batch_size})…")
|
||||
for i in range(0, len(ids), args.batch_size):
|
||||
chunk = ids[i:i + args.batch_size]
|
||||
n = set_payload_batch(args.qdrant, args.collection, chunk, rule, args.qdrant_api_key)
|
||||
total_written += n
|
||||
print(f" batch {i // args.batch_size + 1}: {n} points (cumulative {total_written})")
|
||||
time.sleep(0.05)
|
||||
print(f"\nWrote license_rule on {total_written} Qdrant points in {args.collection}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,310 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Derive doc_check_controls from existing Master Controls.
|
||||
|
||||
Filters MCs by document-relevant regulations, then uses Claude Haiku
|
||||
to generate check_question + pass_criteria + fail_criteria per control.
|
||||
|
||||
Usage:
|
||||
python3 /app/scripts/derive_doc_check_controls.py --dry-run
|
||||
python3 /app/scripts/derive_doc_check_controls.py
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
from sqlalchemy import create_engine, text
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s"
|
||||
)
|
||||
logger = logging.getLogger("doc-check-derive")
|
||||
|
||||
DB_URL = os.getenv(
|
||||
"DATABASE_URL",
|
||||
"postgresql://breakpilot:breakpilot123@postgres:5432/breakpilot_db",
|
||||
)
|
||||
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY", "")
|
||||
ANTHROPIC_URL = "https://api.anthropic.com/v1/messages"
|
||||
|
||||
# Document types and their regulation sources
|
||||
DOC_TYPES = {
|
||||
"dse": {
|
||||
"name": "Datenschutzinformation",
|
||||
"sources": ["DSGVO (EU) 2016/679"],
|
||||
"articles": ["%13%", "%14%"],
|
||||
"extra_tokens": ["personal_data%", "data_subject_rights%", "consent%",
|
||||
"data_processing_register%", "data_transfer%"],
|
||||
},
|
||||
"cookie": {
|
||||
"name": "Cookie-Richtlinie",
|
||||
"sources": ["TDDDG", "ePrivacy-Richtlinie"],
|
||||
"articles": ["%25%", "%5%"],
|
||||
"extra_tokens": ["cookie_consent%", "consent%"],
|
||||
},
|
||||
"impressum": {
|
||||
"name": "Impressum",
|
||||
"sources": ["TMG"],
|
||||
"articles": ["%5%"],
|
||||
"extra_tokens": ["ecommerce%"],
|
||||
},
|
||||
"widerruf": {
|
||||
"name": "Widerrufsbelehrung",
|
||||
"sources": ["BGB"],
|
||||
"articles": ["%355%", "%312%"],
|
||||
"extra_tokens": ["consumer_protection%"],
|
||||
},
|
||||
"agb": {
|
||||
"name": "AGB",
|
||||
"sources": ["BGB"],
|
||||
"articles": ["%305%", "%307%", "%308%", "%309%"],
|
||||
"extra_tokens": ["consumer_protection%"],
|
||||
},
|
||||
"dsfa": {
|
||||
"name": "Datenschutz-Folgenabschaetzung",
|
||||
"sources": ["DSGVO (EU) 2016/679"],
|
||||
"articles": ["%35%"],
|
||||
"extra_tokens": ["dpia%"],
|
||||
},
|
||||
"avv": {
|
||||
"name": "Auftragsverarbeitung",
|
||||
"sources": ["DSGVO (EU) 2016/679"],
|
||||
"articles": ["%28%"],
|
||||
"extra_tokens": ["data_processing_agreement%"],
|
||||
},
|
||||
"loeschkonzept": {
|
||||
"name": "Loeschkonzept",
|
||||
"sources": ["DSGVO (EU) 2016/679"],
|
||||
"articles": ["%5%", "%17%"],
|
||||
"extra_tokens": ["data_retention%"],
|
||||
},
|
||||
}
|
||||
|
||||
SYSTEM_PROMPT = """Du erzeugst binäre Prüfkriterien für Compliance-Dokumente.
|
||||
|
||||
Für jeden Control erzeugst du:
|
||||
1. check_question: Eine JA/NEIN Frage die ein LLM anhand eines Dokuments beantworten kann
|
||||
2. pass_criteria: Konkrete Textinhalte die vorhanden sein MÜSSEN (3-5 Stück)
|
||||
3. fail_criteria: Typische Fehler/Mängel (2-3 Stück)
|
||||
4. severity: HIGH, MEDIUM oder LOW
|
||||
|
||||
REGELN:
|
||||
- check_question muss BINÄR beantwortbar sein (nicht "wie gut")
|
||||
- pass_criteria müssen KONKRET sein ("Name + Rechtsform + Anschrift", nicht "Angaben")
|
||||
- fail_criteria müssen TYPISCHE Fehler beschreiben
|
||||
- Alles auf Deutsch
|
||||
|
||||
Antworte als JSON-Array:
|
||||
[{"id":"...","check_question":"...","pass_criteria":["..."],"fail_criteria":["..."],"severity":"HIGH"}]"""
|
||||
|
||||
|
||||
def get_doc_controls(engine, doc_type: str, config: dict) -> list[dict]:
|
||||
"""Get controls relevant for a document type."""
|
||||
controls = []
|
||||
|
||||
# Strategy 1: By source + article
|
||||
for source in config["sources"]:
|
||||
for article in config["articles"]:
|
||||
with engine.connect() as c:
|
||||
rows = c.execute(text("""
|
||||
SELECT cc.id, cc.control_id, cc.title,
|
||||
COALESCE(cc.objective, '') as objective,
|
||||
pc.source_citation->>'article' as article
|
||||
FROM compliance.canonical_controls cc
|
||||
JOIN compliance.canonical_controls pc ON pc.id = cc.parent_control_uuid
|
||||
WHERE pc.source_citation->>'source' = :source
|
||||
AND pc.source_citation->>'article' LIKE :article
|
||||
AND cc.release_state NOT IN ('deprecated', 'rejected')
|
||||
LIMIT 200
|
||||
"""), {"source": source, "article": article}).fetchall()
|
||||
for r in rows:
|
||||
controls.append({
|
||||
"uuid": str(r[0]), "control_id": r[1],
|
||||
"title": r[2] or "", "objective": r[3] or "",
|
||||
"article": r[4] or "", "doc_type": doc_type,
|
||||
})
|
||||
|
||||
# Strategy 2: By MC canonical_name
|
||||
for token_pattern in config.get("extra_tokens", []):
|
||||
with engine.connect() as c:
|
||||
rows = c.execute(text("""
|
||||
SELECT cc.id, cc.control_id, cc.title,
|
||||
COALESCE(cc.objective, '') as objective
|
||||
FROM compliance.master_controls mc
|
||||
JOIN compliance.master_control_members mcm ON mcm.master_control_uuid = mc.id
|
||||
JOIN compliance.canonical_controls cc ON cc.id = mcm.control_uuid
|
||||
WHERE mc.canonical_name LIKE :pattern
|
||||
AND cc.release_state NOT IN ('deprecated', 'rejected')
|
||||
LIMIT 100
|
||||
"""), {"pattern": token_pattern}).fetchall()
|
||||
for r in rows:
|
||||
controls.append({
|
||||
"uuid": str(r[0]), "control_id": r[1],
|
||||
"title": r[2] or "", "objective": r[3] or "",
|
||||
"article": "", "doc_type": doc_type,
|
||||
})
|
||||
|
||||
# Deduplicate
|
||||
seen = set()
|
||||
unique = []
|
||||
for c in controls:
|
||||
if c["control_id"] not in seen:
|
||||
seen.add(c["control_id"])
|
||||
unique.append(c)
|
||||
|
||||
return unique
|
||||
|
||||
|
||||
def enrich_with_llm(controls: list[dict], doc_type_name: str) -> list[dict]:
|
||||
"""Add check_question, pass/fail_criteria via Haiku."""
|
||||
enriched = []
|
||||
batch_size = 5
|
||||
|
||||
for i in range(0, len(controls), batch_size):
|
||||
batch = controls[i:i + batch_size]
|
||||
items = [
|
||||
f'- id="{c["control_id"]}" doc="{doc_type_name}" '
|
||||
f't="{c["title"]}" o="{c["objective"][:100]}"'
|
||||
for c in batch
|
||||
]
|
||||
|
||||
prompt = (
|
||||
f"Dokumenttyp: {doc_type_name}\n"
|
||||
f"Erzeuge Prüfkriterien:\n" + "\n".join(items)
|
||||
)
|
||||
|
||||
try:
|
||||
resp = httpx.post(ANTHROPIC_URL, headers={
|
||||
"x-api-key": ANTHROPIC_API_KEY,
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json",
|
||||
}, json={
|
||||
"model": "claude-haiku-4-5-20251001",
|
||||
"max_tokens": 2000, "temperature": 0.1,
|
||||
"system": SYSTEM_PROMPT,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
}, timeout=45.0)
|
||||
resp.raise_for_status()
|
||||
content = resp.json().get("content", [{}])[0].get("text", "")
|
||||
start = content.find("[")
|
||||
end = content.rfind("]") + 1
|
||||
if start >= 0 and end > start:
|
||||
results = json.loads(content[start:end])
|
||||
result_map = {r.get("id", ""): r for r in results}
|
||||
for ctrl in batch:
|
||||
r = result_map.get(ctrl["control_id"], {})
|
||||
if r.get("check_question"):
|
||||
ctrl["check_question"] = r["check_question"]
|
||||
ctrl["pass_criteria"] = r.get("pass_criteria", [])
|
||||
ctrl["fail_criteria"] = r.get("fail_criteria", [])
|
||||
ctrl["severity"] = r.get("severity", "MEDIUM")
|
||||
enriched.append(ctrl)
|
||||
except Exception as e:
|
||||
logger.error("Batch %d failed: %s", i, e)
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
return enriched
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
parser.add_argument("--doc-type", choices=list(DOC_TYPES.keys()),
|
||||
help="Only one doc type")
|
||||
args = parser.parse_args()
|
||||
|
||||
engine = create_engine(
|
||||
DB_URL, connect_args={"options": "-c search_path=compliance,public"}
|
||||
)
|
||||
|
||||
# Create table
|
||||
with engine.begin() as c:
|
||||
c.execute(text("SET search_path TO compliance, public"))
|
||||
c.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS doc_check_controls (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
control_id VARCHAR(500) NOT NULL,
|
||||
control_uuid UUID,
|
||||
doc_type VARCHAR(50) NOT NULL,
|
||||
title VARCHAR(500),
|
||||
regulation VARCHAR(200),
|
||||
article VARCHAR(100),
|
||||
check_question TEXT NOT NULL,
|
||||
pass_criteria JSONB DEFAULT '[]',
|
||||
fail_criteria JSONB DEFAULT '[]',
|
||||
severity VARCHAR(20) DEFAULT 'MEDIUM',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
)
|
||||
"""))
|
||||
c.execute(text("""
|
||||
CREATE INDEX IF NOT EXISTS idx_doc_check_doc_type
|
||||
ON doc_check_controls(doc_type)
|
||||
"""))
|
||||
|
||||
doc_types = [args.doc_type] if args.doc_type else list(DOC_TYPES.keys())
|
||||
all_checks = []
|
||||
|
||||
for dt in doc_types:
|
||||
config = DOC_TYPES[dt]
|
||||
logger.info("\n=== %s (%s) ===", dt, config["name"])
|
||||
|
||||
controls = get_doc_controls(engine, dt, config)
|
||||
logger.info("Found %d relevant controls", len(controls))
|
||||
|
||||
if not controls:
|
||||
continue
|
||||
|
||||
enriched = enrich_with_llm(controls, config["name"])
|
||||
logger.info("Enriched %d with check criteria", len(enriched))
|
||||
all_checks.extend(enriched)
|
||||
|
||||
logger.info("\nTotal: %d doc_check_controls across %d doc types",
|
||||
len(all_checks), len(doc_types))
|
||||
|
||||
if args.dry_run:
|
||||
for dc in all_checks[:5]:
|
||||
logger.info(" [%s] %s: %s", dc["doc_type"], dc["control_id"],
|
||||
dc.get("check_question", "?")[:80])
|
||||
logger.info("DRY RUN — not writing")
|
||||
return
|
||||
|
||||
# Write to DB
|
||||
with engine.begin() as c:
|
||||
c.execute(text("SET search_path TO compliance, public"))
|
||||
c.execute(text("DELETE FROM doc_check_controls"))
|
||||
for dc in all_checks:
|
||||
c.execute(text("""
|
||||
INSERT INTO doc_check_controls
|
||||
(control_id, control_uuid, doc_type, title,
|
||||
check_question, pass_criteria, fail_criteria, severity)
|
||||
VALUES (:cid, CAST(:uuid AS uuid), :doc_type, :title,
|
||||
:question, CAST(:pass AS jsonb),
|
||||
CAST(:fail AS jsonb), :severity)
|
||||
"""), {
|
||||
"cid": dc["control_id"],
|
||||
"uuid": dc["uuid"],
|
||||
"doc_type": dc["doc_type"],
|
||||
"title": dc["title"],
|
||||
"question": dc.get("check_question", ""),
|
||||
"pass": json.dumps(dc.get("pass_criteria", [])),
|
||||
"fail": json.dumps(dc.get("fail_criteria", [])),
|
||||
"severity": dc.get("severity", "MEDIUM"),
|
||||
})
|
||||
|
||||
logger.info("Wrote %d doc_check_controls to DB", len(all_checks))
|
||||
|
||||
# Save as JSON too
|
||||
Path("/tmp/doc_check_controls.json").write_text(
|
||||
json.dumps(all_checks, indent=2, ensure_ascii=False)
|
||||
)
|
||||
logger.info("Saved to /tmp/doc_check_controls.json")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,400 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Clean-Room MC derivation from BSI QUAIDAL.
|
||||
|
||||
For each QUAIDAL entry in the parsed index, ask a local LLM to produce our own
|
||||
wording for a Master Control / atomic control / mitigation / metric. Reject any
|
||||
output whose 4-gram overlap with the BSI source text exceeds PLAGIARISM_LIMIT.
|
||||
|
||||
We never store the BSI prose; only our own derived wording plus structural
|
||||
references (BSI section ID + URL + commit SHA).
|
||||
|
||||
Usage:
|
||||
# Single entry, prints to stdout for review:
|
||||
python3 control-pipeline/scripts/derive_quaidal_mcs.py --only QKB-01 --dry-run
|
||||
|
||||
# Full run, writes YAML:
|
||||
python3 control-pipeline/scripts/derive_quaidal_mcs.py --ollama-host macmini
|
||||
|
||||
Output: control-pipeline/data/quaidal/{master_controls,atomic_controls,mitigations,metrics}.yaml
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import httpx
|
||||
import yaml
|
||||
except ImportError as e:
|
||||
print(f"ERROR: missing dependency {e.name}. Install with: pip install httpx pyyaml", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
SOURCE_ROOT = REPO_ROOT / "legal-sources" / "bsi-quaidal"
|
||||
INDEX_FILE = REPO_ROOT / "control-pipeline" / "data" / "quaidal" / "quaidal_index.json"
|
||||
OUTPUT_DIR = REPO_ROOT / "control-pipeline" / "data" / "quaidal"
|
||||
|
||||
PLAGIARISM_LIMIT = 0.20 # max share of 4-grams that may appear in BSI source
|
||||
N_GRAM = 4
|
||||
MAX_RETRIES = 3
|
||||
|
||||
DEFAULT_OLLAMA_URL = "http://macmini:11434"
|
||||
OLLAMA_MODEL = "qwen3.5:35b-a3b"
|
||||
QUAIDAL_REPO_URL = "https://github.com/BSI-Bund/QUAIDAL"
|
||||
|
||||
KIND_TO_PROMPT_ROLE = {
|
||||
"criterion": "Master Control",
|
||||
"building_block": "atomarer technischer Control",
|
||||
"measure": "Schutzmaßnahme",
|
||||
"metric": "messbarer Qualitäts-Indikator",
|
||||
}
|
||||
|
||||
KIND_TO_OUTPUT_FILE = {
|
||||
"criterion": "master_controls.yaml",
|
||||
"building_block": "atomic_controls.yaml",
|
||||
"measure": "mitigations.yaml",
|
||||
"metric": "metrics.yaml",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Source-side extraction (kept in memory, never written to disk)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
FRONTMATTER_RE = re.compile(r"^---\s*\n.*?\n---\s*\n", re.DOTALL)
|
||||
SECTION_RE = re.compile(r"^###?\s+(.+?)\s*$", re.MULTILINE)
|
||||
|
||||
|
||||
def load_source_extract(rel_path: str) -> dict:
|
||||
"""Load BSI source text for ONE entry. Used only for prompt + plagiarism check."""
|
||||
path = SOURCE_ROOT / rel_path
|
||||
text = path.read_text(encoding="utf-8")
|
||||
|
||||
# Strip frontmatter; capture shortdesc separately for the prompt.
|
||||
fm_match = re.match(r"^---\s*\n(.*?)\n---\s*\n", text, re.DOTALL)
|
||||
shortdesc = ""
|
||||
if fm_match:
|
||||
for line in fm_match.group(1).splitlines():
|
||||
if line.lower().startswith("shortdesc:"):
|
||||
shortdesc = line.split(":", 1)[1].strip()
|
||||
break
|
||||
body = FRONTMATTER_RE.sub("", text, count=1)
|
||||
|
||||
# Pull the first 1-2 paragraphs under "Beschreibung" (or whole body if none)
|
||||
desc_match = re.search(r"###?\s+Beschreibung\s*\n+(.+?)(?:\n###?\s|\Z)", body, re.DOTALL)
|
||||
description_excerpt = desc_match.group(1).strip() if desc_match else body[:1500].strip()
|
||||
paragraphs = [p.strip() for p in description_excerpt.split("\n\n") if p.strip()]
|
||||
description_excerpt = "\n\n".join(paragraphs[:2])
|
||||
|
||||
return {
|
||||
"shortdesc": shortdesc,
|
||||
"description_excerpt": description_excerpt,
|
||||
"full_body": body,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plagiarism gate
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
WORD_RE = re.compile(r"\b[\wäöüÄÖÜß]+\b", re.UNICODE)
|
||||
|
||||
|
||||
def _tokenize(text: str) -> list[str]:
|
||||
return [w.lower() for w in WORD_RE.findall(text)]
|
||||
|
||||
|
||||
def ngram_overlap(produced: str, source: str, n: int = N_GRAM) -> float:
|
||||
"""Share of produced n-grams that also appear in source."""
|
||||
p_tokens = _tokenize(produced)
|
||||
s_tokens = _tokenize(source)
|
||||
if len(p_tokens) < n:
|
||||
return 0.0
|
||||
s_grams = {tuple(s_tokens[i : i + n]) for i in range(len(s_tokens) - n + 1)}
|
||||
if not s_grams:
|
||||
return 0.0
|
||||
p_grams = [tuple(p_tokens[i : i + n]) for i in range(len(p_tokens) - n + 1)]
|
||||
hits = sum(1 for g in p_grams if g in s_grams)
|
||||
return hits / len(p_grams)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# LLM prompt + call
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
PROMPT_TEMPLATE = """Du bist Compliance-Engineer bei BreakPilot. Schreibe eine eigenständige Anforderung im Stil einer technischen Kontroll-Spezifikation.
|
||||
|
||||
Quelle: BSI QUAIDAL Sektion {entry_id} ("{title_de}"). Die Quelle steht unter unklarer Lizenz (BSI-Veröffentlichung, § 5 UrhG anwendbar) — wir dürfen die Idee aufgreifen, aber NICHT abschreiben.
|
||||
|
||||
Aufgabe: Formuliere eine eigenständige Anforderung im Stil eines {role}. Anforderungen:
|
||||
- Eigene Formulierung in deutscher Sprache. Kein Satz darf aus der Quelle übernommen werden, auch nicht teilweise. Synonyme verwenden, Satzbau ändern, Inhalt strukturell anders aufbauen.
|
||||
- 2-4 Sätze (max 80 Wörter).
|
||||
- Sprachstil: nüchtern, technisch, normativ ("muss", "ist sicherzustellen", "ist zu prüfen").
|
||||
- Bezug auf KI-Trainingsdaten oder KI-Datenqualität, je nach Quelle.
|
||||
- Nicht die wörtlichen BSI-Beispiele kopieren.
|
||||
|
||||
Quellauszug (NUR zur Orientierung, NICHT abschreiben):
|
||||
---
|
||||
shortdesc: {shortdesc}
|
||||
|
||||
{description_excerpt}
|
||||
---
|
||||
|
||||
Antwort: Liefere AUSSCHLIESSLICH die fertige Beschreibung als reinen Text — kein JSON, keine Überschriften, keine Anführungszeichen, keine Quellenangabe."""
|
||||
|
||||
|
||||
def call_ollama(prompt: str, ollama_url: str, model: str, retries: int = 2) -> str:
|
||||
last_err = None
|
||||
for attempt in range(retries + 1):
|
||||
try:
|
||||
resp = httpx.post(
|
||||
f"{ollama_url}/api/chat",
|
||||
json={
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"stream": False,
|
||||
"options": {"temperature": 0.4},
|
||||
"think": False,
|
||||
},
|
||||
timeout=180.0,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()["message"]["content"].strip()
|
||||
except (httpx.HTTPError, KeyError, ValueError) as e:
|
||||
last_err = e
|
||||
if attempt < retries:
|
||||
time.sleep(2 ** attempt)
|
||||
raise RuntimeError(f"Ollama call failed after {retries+1} attempts: {last_err}")
|
||||
|
||||
|
||||
def strip_llm_artifacts(text: str) -> str:
|
||||
"""Clean leading/trailing markdown and quotes from LLM output."""
|
||||
text = text.strip()
|
||||
# Strip surrounding code fences
|
||||
if text.startswith("```"):
|
||||
text = re.sub(r"^```[a-zA-Z]*\n?", "", text)
|
||||
text = re.sub(r"\n?```\s*$", "", text)
|
||||
# Strip surrounding quotes
|
||||
text = text.strip('"„"”„')
|
||||
# Drop a leading "Beschreibung:" or similar label
|
||||
text = re.sub(r"^(Beschreibung|Description|Anforderung|Control):\s*", "", text, flags=re.IGNORECASE)
|
||||
return text.strip()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Derivation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class DerivedControl:
|
||||
derived_id: str
|
||||
source_id: str
|
||||
kind: str
|
||||
canonical_name: str
|
||||
description: str
|
||||
plagiarism_score: float
|
||||
related_quaidal_ids: list[str]
|
||||
external_refs: list[dict]
|
||||
source: dict
|
||||
|
||||
|
||||
_ASCII_FOLD = str.maketrans({"ä": "ae", "ö": "oe", "ü": "ue", "Ä": "ae", "Ö": "oe", "Ü": "ue", "ß": "ss"})
|
||||
|
||||
|
||||
def slug(text: str) -> str:
|
||||
text = text.translate(_ASCII_FOLD).lower()
|
||||
text = re.sub(r"[^a-z0-9]+", "-", text)
|
||||
return text.strip("-")
|
||||
|
||||
|
||||
def derived_id_for(entry: dict) -> str:
|
||||
prefix = {
|
||||
"criterion": "MC-AI-DATA",
|
||||
"building_block": "AC-AI-DATA",
|
||||
"measure": "MIT-AI-DATA",
|
||||
"metric": "MET-AI-DATA",
|
||||
}.get(entry["kind"], "X-AI-DATA")
|
||||
title = entry["title_de"]
|
||||
title = re.sub(r"^\s*(QKB|QB|MA|QM)-\d+[a-zA-Z]?\s*", "", title)
|
||||
return f"{prefix}-{entry['id']}-{slug(title)[:40]}".rstrip("-")
|
||||
|
||||
|
||||
def derive_one(entry: dict, source_extract: dict, ollama_url: str, model: str, *, verbose: bool = False) -> DerivedControl:
|
||||
role = KIND_TO_PROMPT_ROLE.get(entry["kind"], "Control")
|
||||
prompt = PROMPT_TEMPLATE.format(
|
||||
entry_id=entry["id"],
|
||||
title_de=entry["title_de"],
|
||||
role=role,
|
||||
shortdesc=source_extract["shortdesc"] or "(keiner)",
|
||||
description_excerpt=source_extract["description_excerpt"] or "(keine Beschreibung)",
|
||||
)
|
||||
|
||||
source_corpus = "\n\n".join(filter(None, [source_extract["shortdesc"], source_extract["description_excerpt"]]))
|
||||
|
||||
best: tuple[str, float] | None = None
|
||||
for attempt in range(1, MAX_RETRIES + 1):
|
||||
output = call_ollama(prompt, ollama_url, model)
|
||||
output = strip_llm_artifacts(output)
|
||||
score = ngram_overlap(output, source_corpus)
|
||||
if verbose:
|
||||
print(f" attempt {attempt}: overlap={score:.2%} len={len(output)}", file=sys.stderr)
|
||||
if score < PLAGIARISM_LIMIT:
|
||||
best = (output, score)
|
||||
break
|
||||
if best is None or score < best[1]:
|
||||
best = (output, score)
|
||||
# Strengthen the next prompt by appending a reject notice
|
||||
prompt += f"\n\n(Vorheriger Versuch hatte {score:.0%} Wortdeckung mit der Quelle. Verwende völlig andere Begriffe und Satzstruktur.)"
|
||||
|
||||
if best is None:
|
||||
raise RuntimeError(f"Could not derive {entry['id']}: no output")
|
||||
output, score = best
|
||||
if score >= PLAGIARISM_LIMIT:
|
||||
raise RuntimeError(
|
||||
f"Plagiarism gate failed for {entry['id']}: best overlap {score:.2%} >= limit {PLAGIARISM_LIMIT:.0%}.\n"
|
||||
f"Output:\n{output}"
|
||||
)
|
||||
|
||||
title_de_clean = re.sub(r"^\s*(QKB|QB|MA|QM)-\d+[a-zA-Z]?\s*", "", entry["title_de"]).strip()
|
||||
return DerivedControl(
|
||||
derived_id=derived_id_for(entry),
|
||||
source_id=entry["id"],
|
||||
kind=entry["kind"],
|
||||
canonical_name=title_de_clean or entry["title_de"],
|
||||
description=output,
|
||||
plagiarism_score=round(score, 4),
|
||||
related_quaidal_ids=entry["referenced_ids"],
|
||||
external_refs=entry["external_refs"],
|
||||
source={
|
||||
"framework": "BSI QUAIDAL",
|
||||
"section": entry["id"],
|
||||
"title_original_de": entry["title_de"],
|
||||
"url": f"{QUAIDAL_REPO_URL}/blob/main/{entry['source_path'].replace(' ', '%20')}",
|
||||
"commit_sha": None, # filled in by main()
|
||||
"license_note": "§ 5 UrhG anwendbar; share:true im Frontmatter; Clean-Room-Ableitung.",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Output writers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def control_to_dict(c: DerivedControl) -> dict:
|
||||
d = {
|
||||
"id": c.derived_id,
|
||||
"canonical_name": c.canonical_name,
|
||||
"description": c.description,
|
||||
"kind": c.kind,
|
||||
"regulation_anchor": "EU AI Act Art. 10 (Datenqualität für Hochrisiko-KI)",
|
||||
"related_quaidal_ids": c.related_quaidal_ids,
|
||||
"external_refs": c.external_refs,
|
||||
"source": c.source,
|
||||
"plagiarism_score_at_generation": c.plagiarism_score,
|
||||
}
|
||||
return d
|
||||
|
||||
|
||||
def write_yaml_per_kind(controls: list[DerivedControl], commit_sha: str | None) -> dict[str, Path]:
|
||||
out: dict[str, list[dict]] = {}
|
||||
for c in controls:
|
||||
c.source["commit_sha"] = commit_sha
|
||||
fname = KIND_TO_OUTPUT_FILE.get(c.kind, "other.yaml")
|
||||
out.setdefault(fname, []).append(control_to_dict(c))
|
||||
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
written: dict[str, Path] = {}
|
||||
for fname, items in out.items():
|
||||
path = OUTPUT_DIR / fname
|
||||
payload = {
|
||||
"source": "Derived from BSI QUAIDAL (Clean-Room)",
|
||||
"source_url": QUAIDAL_REPO_URL,
|
||||
"commit_sha": commit_sha,
|
||||
"plagiarism_limit_4gram": PLAGIARISM_LIMIT,
|
||||
"generated_by_model": OLLAMA_MODEL,
|
||||
"controls": items,
|
||||
}
|
||||
path.write_text(yaml.safe_dump(payload, allow_unicode=True, sort_keys=False), encoding="utf-8")
|
||||
written[fname] = path
|
||||
return written
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--only", help="Derive only this QUAIDAL ID (e.g. QKB-01)")
|
||||
ap.add_argument("--kind", help="Derive only entries of this kind (criterion/building_block/measure/metric)")
|
||||
ap.add_argument("--limit", type=int, help="Process at most N entries")
|
||||
ap.add_argument("--dry-run", action="store_true", help="Print derived controls instead of writing YAML")
|
||||
ap.add_argument("--ollama-host", default="macmini", help="Ollama host (default: macmini)")
|
||||
ap.add_argument("--model", default=OLLAMA_MODEL)
|
||||
ap.add_argument("--verbose", action="store_true")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not INDEX_FILE.exists():
|
||||
print(f"ERROR: missing index. Run ingest_bsi_quaidal.py first ({INDEX_FILE})", file=sys.stderr)
|
||||
return 2
|
||||
index = json.loads(INDEX_FILE.read_text(encoding="utf-8"))
|
||||
entries = index["entries"]
|
||||
if args.only:
|
||||
entries = [e for e in entries if e["id"].upper() == args.only.upper()]
|
||||
if args.kind:
|
||||
entries = [e for e in entries if e["kind"] == args.kind]
|
||||
if args.limit:
|
||||
entries = entries[: args.limit]
|
||||
|
||||
if not entries:
|
||||
print("No entries match the filter.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
ollama_url = args.ollama_host if "://" in args.ollama_host else f"http://{args.ollama_host}:11434"
|
||||
print(f"Derivation: {len(entries)} entries, model={args.model}, ollama={ollama_url}, limit={PLAGIARISM_LIMIT:.0%}", file=sys.stderr)
|
||||
|
||||
derived: list[DerivedControl] = []
|
||||
failed: list[tuple[str, str]] = []
|
||||
for i, entry in enumerate(entries, 1):
|
||||
if args.verbose:
|
||||
print(f"[{i}/{len(entries)}] {entry['id']} ({entry['kind']}): {entry['title_de']}", file=sys.stderr)
|
||||
try:
|
||||
extract = load_source_extract(entry["source_path"])
|
||||
ctrl = derive_one(entry, extract, ollama_url, args.model, verbose=args.verbose)
|
||||
derived.append(ctrl)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
failed.append((entry["id"], str(exc)))
|
||||
print(f" FAILED {entry['id']}: {exc}", file=sys.stderr)
|
||||
|
||||
print(f"\nDerived: {len(derived)} | Failed: {len(failed)}", file=sys.stderr)
|
||||
|
||||
if args.dry_run:
|
||||
for c in derived:
|
||||
c.source["commit_sha"] = index.get("commit_sha")
|
||||
print(yaml.safe_dump(control_to_dict(c), allow_unicode=True, sort_keys=False))
|
||||
print("---")
|
||||
return 0 if not failed else 1
|
||||
|
||||
written = write_yaml_per_kind(derived, index.get("commit_sha"))
|
||||
for fname, path in written.items():
|
||||
print(f"Wrote {path.relative_to(REPO_ROOT)} ({sum(1 for c in derived if KIND_TO_OUTPUT_FILE[c.kind] == fname)} entries)", file=sys.stderr)
|
||||
|
||||
if failed:
|
||||
print("\nFailures:", file=sys.stderr)
|
||||
for fid, msg in failed:
|
||||
print(f" - {fid}: {msg.splitlines()[0]}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
G-pre1 INCREMENTAL: Append new objects to object_groups via embedding similarity.
|
||||
|
||||
Non-destructive alternative to gpre1_object_clustering.py (which DELETEs and
|
||||
rebuilds all groups via K-Means). This script:
|
||||
- Finds objects referenced in atomic controls that are NOT yet in
|
||||
object_groups.members
|
||||
- Embeds each unmatched object via bge-m3 (local embedding-service)
|
||||
- Nearest-neighbor search against existing object_groups.canonical_name
|
||||
- Cosine >= --threshold (default 0.85) → APPEND to existing group's members
|
||||
- Cosine < --threshold → CREATE new object_group with next free group_id
|
||||
|
||||
Existing groups stay; only members get appended and new groups get added.
|
||||
|
||||
Usage (inside control-pipeline container):
|
||||
python3 /app/scripts/gpre1_object_groups_incremental.py --since 2026-05-18T02:53:00+00:00 --dry-run
|
||||
python3 /app/scripts/gpre1_object_groups_incremental.py --since 2026-05-18T02:53:00+00:00
|
||||
python3 /app/scripts/gpre1_object_groups_incremental.py --since 2026-05-18T02:53:00+00:00 --threshold 0.82
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
import httpx
|
||||
import numpy as np
|
||||
from sqlalchemy import create_engine, text
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger("gpre1_inc")
|
||||
|
||||
DB_URL = os.getenv("DATABASE_URL", "postgresql://breakpilot:breakpilot123@postgres:5432/breakpilot_db")
|
||||
EMBEDDING_URL = os.getenv("EMBEDDING_URL", "http://embedding-service:8087")
|
||||
BATCH_SIZE = 64
|
||||
|
||||
|
||||
def embed_batch(texts: list[str]) -> np.ndarray:
|
||||
"""Embed a list of strings via bge-m3 embedding-service."""
|
||||
with httpx.Client(timeout=120.0) as c:
|
||||
resp = c.post(f"{EMBEDDING_URL}/embed", json={"texts": texts, "normalize": True})
|
||||
resp.raise_for_status()
|
||||
return np.array(resp.json()["embeddings"], dtype=np.float32)
|
||||
|
||||
|
||||
def embed_many(texts: list[str], label: str = "") -> np.ndarray:
|
||||
"""Embed many strings in batches."""
|
||||
n = len(texts)
|
||||
out = np.zeros((n, 1024), dtype=np.float32)
|
||||
for i in range(0, n, BATCH_SIZE):
|
||||
batch = texts[i:i + BATCH_SIZE]
|
||||
out[i:i + len(batch)] = embed_batch(batch)
|
||||
if (i // BATCH_SIZE) % 20 == 0:
|
||||
logger.info(" %s: %d/%d embedded", label, i + len(batch), n)
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--since", required=True, help="ISO datetime — consider atomics from this date onwards")
|
||||
parser.add_argument("--threshold", type=float, default=0.85,
|
||||
help="Cosine threshold for appending to existing group (default 0.85)")
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
since_dt = datetime.fromisoformat(args.since.replace("Z", "+00:00"))
|
||||
logger.info("Incremental object_groups update since %s, threshold=%.2f, dry_run=%s",
|
||||
since_dt.isoformat(), args.threshold, args.dry_run)
|
||||
|
||||
engine = create_engine(DB_URL, connect_args={"options": "-c search_path=compliance,public"})
|
||||
|
||||
# 1. Load existing object_groups (id, canonical_name, members)
|
||||
with engine.connect() as c:
|
||||
rows = c.execute(text("""
|
||||
SELECT group_id, canonical_name, members FROM object_groups
|
||||
""")).fetchall()
|
||||
existing_groups = [(r[0], r[1], json.loads(r[2]) if isinstance(r[2], str) else r[2]) for r in rows]
|
||||
logger.info("Loaded %d existing object_groups", len(existing_groups))
|
||||
|
||||
existing_members: set[str] = set()
|
||||
for _, _, members in existing_groups:
|
||||
for m in members:
|
||||
existing_members.add(m)
|
||||
logger.info("Existing union of members: %d distinct strings", len(existing_members))
|
||||
|
||||
# 2. Find unmatched objects from atomics since `since`
|
||||
from services.control_dedup import normalize_object
|
||||
with engine.connect() as c:
|
||||
rows = c.execute(text("""
|
||||
SELECT DISTINCT split_part(generation_metadata->>'merge_group_hint', ':', 2) AS obj
|
||||
FROM canonical_controls
|
||||
WHERE decomposition_method = 'pass0b'
|
||||
AND created_at >= :since
|
||||
AND generation_metadata->>'merge_group_hint' IS NOT NULL
|
||||
AND generation_metadata->>'merge_group_hint' != ''
|
||||
AND release_state NOT IN ('deprecated', 'rejected', 'duplicate')
|
||||
"""), {"since": since_dt}).fetchall()
|
||||
new_objects_raw = [r[0] for r in rows if r[0]]
|
||||
logger.info("Distinct objects in new atomics: %d", len(new_objects_raw))
|
||||
|
||||
# Normalize each + dedupe; track originals → normalized
|
||||
normed_to_originals: dict[str, set[str]] = {}
|
||||
for obj in new_objects_raw:
|
||||
normed = normalize_object(obj)
|
||||
if not normed:
|
||||
continue
|
||||
if normed in existing_members or obj in existing_members:
|
||||
continue # already in some group
|
||||
normed_to_originals.setdefault(normed, set()).update([normed, obj])
|
||||
|
||||
unmatched_normed = list(normed_to_originals.keys())
|
||||
logger.info("Unmatched normalized objects: %d", len(unmatched_normed))
|
||||
|
||||
if not unmatched_normed:
|
||||
logger.info("Nothing to do — all objects already mapped.")
|
||||
return
|
||||
|
||||
# 3. Embed existing canonical_names + unmatched objects
|
||||
logger.info("Embedding %d existing canonical_names...", len(existing_groups))
|
||||
existing_emb = embed_many([g[1] for g in existing_groups], label="existing")
|
||||
logger.info("Embedding %d unmatched objects...", len(unmatched_normed))
|
||||
unmatched_emb = embed_many(unmatched_normed, label="unmatched")
|
||||
|
||||
# 4. Nearest-neighbor: for each unmatched, find best existing match
|
||||
# cosine = dot product (both already L2-normalized)
|
||||
logger.info("Computing nearest-neighbor matches...")
|
||||
sims = unmatched_emb @ existing_emb.T # (N_unmatched, N_existing)
|
||||
best_idx = sims.argmax(axis=1)
|
||||
best_score = sims.max(axis=1)
|
||||
|
||||
appends: dict[int, list[str]] = {} # group_id → list of new members
|
||||
new_groups: list[tuple[str, list[str]]] = [] # (canonical_name, members)
|
||||
|
||||
for i, normed in enumerate(unmatched_normed):
|
||||
originals = sorted(normed_to_originals[normed])
|
||||
if best_score[i] >= args.threshold:
|
||||
gid = existing_groups[int(best_idx[i])][0]
|
||||
appends.setdefault(gid, []).extend(originals)
|
||||
else:
|
||||
# Create a new group with this object as canonical
|
||||
new_groups.append((normed, originals))
|
||||
|
||||
# Stats
|
||||
distinct_groups_to_extend = len(appends)
|
||||
total_appends = sum(len(v) for v in appends.values())
|
||||
logger.info("Plan: extend %d existing groups (+%d members), create %d new groups",
|
||||
distinct_groups_to_extend, total_appends, len(new_groups))
|
||||
|
||||
if args.dry_run:
|
||||
logger.info("DRY RUN — no writes")
|
||||
# Sample
|
||||
if appends:
|
||||
sample = list(appends.items())[:5]
|
||||
for gid, members in sample:
|
||||
gname = next((g[1] for g in existing_groups if g[0] == gid), "?")
|
||||
logger.info(" Extend group_id=%d (%s) with: %s", gid, gname, members[:3])
|
||||
if new_groups:
|
||||
for name, members in new_groups[:5]:
|
||||
logger.info(" NEW group: %s — members=%s", name, members[:3])
|
||||
return
|
||||
|
||||
# 5. Write — pure INSERT/UPDATE
|
||||
with engine.begin() as c:
|
||||
c.execute(text("SET search_path TO compliance, public"))
|
||||
|
||||
# UPDATE existing groups (append to members JSONB)
|
||||
for gid, new_members in appends.items():
|
||||
c.execute(text("""
|
||||
UPDATE object_groups
|
||||
SET members = (
|
||||
SELECT jsonb_agg(DISTINCT m)
|
||||
FROM jsonb_array_elements_text(members || CAST(:new_members AS jsonb)) AS x(m)
|
||||
),
|
||||
member_count = (
|
||||
SELECT count(DISTINCT m)
|
||||
FROM jsonb_array_elements_text(members || CAST(:new_members AS jsonb)) AS x(m)
|
||||
)
|
||||
WHERE group_id = :gid
|
||||
"""), {"gid": gid, "new_members": json.dumps(new_members)})
|
||||
|
||||
# INSERT new groups with next free group_id
|
||||
next_gid_row = c.execute(text("SELECT COALESCE(MAX(group_id), 0) + 1 FROM object_groups")).fetchone()
|
||||
next_gid = next_gid_row[0] if next_gid_row else 1
|
||||
for name, members in new_groups:
|
||||
c.execute(text("""
|
||||
INSERT INTO object_groups (group_id, canonical_name, member_count, members, top_controls_count)
|
||||
VALUES (:gid, :name, :count, CAST(:members AS jsonb), 0)
|
||||
"""), {
|
||||
"gid": next_gid,
|
||||
"name": name[:200],
|
||||
"count": len(members),
|
||||
"members": json.dumps(members),
|
||||
})
|
||||
next_gid += 1
|
||||
|
||||
logger.info("DONE — extended %d existing groups (+%d members), created %d new groups",
|
||||
distinct_groups_to_extend, total_appends, len(new_groups))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,267 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
G-pre2 INCREMENTAL: Add new atomic controls to Master Controls without rebuild.
|
||||
|
||||
Unlike gpre2_master_controls.py which DELETEs and rebuilds the entire
|
||||
master_controls table, this script is non-destructive:
|
||||
- Existing master_controls stay untouched (same UUIDs, same MC-IDs)
|
||||
- For each object_group that gained new atomic controls:
|
||||
* If MC exists: append new members + update total_controls/phase_counts
|
||||
* If MC missing AND group now has >= min_phases: create new MC + all members
|
||||
|
||||
Usage:
|
||||
python3 /app/scripts/gpre2_master_controls_incremental.py --since 2026-05-18T02:53:00+00:00
|
||||
python3 /app/scripts/gpre2_master_controls_incremental.py --since 2026-05-18T02:53:00+00:00 --dry-run
|
||||
python3 /app/scripts/gpre2_master_controls_incremental.py --since 2026-05-18T02:53:00+00:00 --min-phases 2
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import create_engine, text
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger("gpre2_incremental")
|
||||
|
||||
DB_URL = os.getenv("DATABASE_URL", "postgresql://breakpilot:breakpilot123@postgres:5432/breakpilot_db")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--since", required=True, help="ISO datetime — only consider atomics created at/after this")
|
||||
parser.add_argument("--min-phases", type=int, default=2, help="Min distinct phases to form a new MC (default 2)")
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
since_dt = datetime.fromisoformat(args.since.replace("Z", "+00:00"))
|
||||
logger.info("Incremental run since %s, min_phases=%d, dry_run=%s",
|
||||
since_dt.isoformat(), args.min_phases, args.dry_run)
|
||||
|
||||
engine = create_engine(DB_URL, connect_args={"options": "-c search_path=compliance,public"})
|
||||
|
||||
# Step 1: object → group_id reverse index
|
||||
object_to_group = {}
|
||||
with engine.connect() as c:
|
||||
groups = c.execute(text("SELECT group_id, canonical_name, members FROM object_groups")).fetchall()
|
||||
for gid, canonical, members_json in groups:
|
||||
members = json.loads(members_json) if isinstance(members_json, str) else members_json
|
||||
for member in members:
|
||||
object_to_group[member] = (gid, canonical)
|
||||
logger.info("Reverse index: %d objects → %d groups", len(object_to_group), len(groups))
|
||||
|
||||
# Step 2: Load ALL atomics with merge_group_hint (we need full picture)
|
||||
with engine.connect() as c:
|
||||
all_rows = c.execute(text("""
|
||||
SELECT id, control_id,
|
||||
generation_metadata->>'merge_group_hint' AS hint,
|
||||
title,
|
||||
created_at
|
||||
FROM canonical_controls
|
||||
WHERE generation_metadata->>'merge_group_hint' IS NOT NULL
|
||||
AND generation_metadata->>'merge_group_hint' != ''
|
||||
AND release_state NOT IN ('deprecated', 'rejected', 'duplicate')
|
||||
""")).fetchall()
|
||||
logger.info("Loaded %d atomic controls total", len(all_rows))
|
||||
|
||||
# Step 3: Build group_phases (gid → phase → [(uuid, control_id, action, title, is_new)])
|
||||
from services.control_dedup import normalize_object
|
||||
group_phases: dict[int, dict[str, list]] = defaultdict(lambda: defaultdict(list))
|
||||
group_names: dict[int, str] = {}
|
||||
new_atomic_count = 0
|
||||
new_groups_touched: set[int] = set()
|
||||
unmatched = 0
|
||||
|
||||
for uuid, control_id, hint, title, created_at in all_rows:
|
||||
parts = hint.split(":", 2)
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
action = parts[0]
|
||||
obj = parts[1]
|
||||
phase = parts[2] if len(parts) > 2 else "implementation"
|
||||
normed = normalize_object(obj)
|
||||
if normed in object_to_group:
|
||||
gid, canonical = object_to_group[normed]
|
||||
elif obj in object_to_group:
|
||||
gid, canonical = object_to_group[obj]
|
||||
else:
|
||||
unmatched += 1
|
||||
continue
|
||||
is_new = created_at >= since_dt
|
||||
group_phases[gid][phase].append((str(uuid), control_id, action, title, is_new))
|
||||
group_names[gid] = canonical
|
||||
if is_new:
|
||||
new_atomic_count += 1
|
||||
new_groups_touched.add(gid)
|
||||
|
||||
logger.info("Total: %d new atomics across %d object_groups (%d unmatched)",
|
||||
new_atomic_count, len(new_groups_touched), unmatched)
|
||||
|
||||
if not new_groups_touched:
|
||||
logger.info("Nothing to do — no new atomics matched to any object_group.")
|
||||
return
|
||||
|
||||
# Step 4: For each touched object_group, decide action
|
||||
stats = {
|
||||
"groups_examined": len(new_groups_touched),
|
||||
"mcs_existing_updated": 0,
|
||||
"mcs_new_created": 0,
|
||||
"members_inserted": 0,
|
||||
"members_skipped_existing": 0,
|
||||
"groups_skipped_below_min_phases": 0,
|
||||
"groups_skipped_no_member_change": 0,
|
||||
}
|
||||
|
||||
# Load existing master_controls index: master_control_id → uuid
|
||||
with engine.connect() as c:
|
||||
mc_index = {row[1]: (str(row[0]), row[2]) for row in c.execute(text(
|
||||
"SELECT id, master_control_id, total_controls FROM master_controls"
|
||||
)).fetchall()}
|
||||
logger.info("Existing master_controls: %d", len(mc_index))
|
||||
|
||||
# Load existing members for touched MCs (avoid duplicate inserts)
|
||||
touched_mc_ids = ["MC-%d" % gid for gid in new_groups_touched]
|
||||
existing_members: dict[str, set[str]] = defaultdict(set)
|
||||
with engine.connect() as c:
|
||||
for mc_id_str in touched_mc_ids:
|
||||
mc_uuid_info = mc_index.get(mc_id_str)
|
||||
if not mc_uuid_info:
|
||||
continue
|
||||
mc_uuid = mc_uuid_info[0]
|
||||
for row in c.execute(text(
|
||||
"SELECT control_uuid FROM master_control_members WHERE master_control_uuid = CAST(:u AS uuid)"
|
||||
), {"u": mc_uuid}).fetchall():
|
||||
existing_members[mc_id_str].add(str(row[0]))
|
||||
|
||||
# Build INSERT/UPDATE plans
|
||||
inserts_new_mcs = []
|
||||
inserts_members = []
|
||||
updates_mcs = []
|
||||
|
||||
PHASE_ORDER = {
|
||||
"scope": 0, "definition": 1, "governance": 1, "design": 2,
|
||||
"implementation": 3, "configuration": 3, "operation": 4, "training": 4,
|
||||
"monitoring": 5, "testing": 6, "review": 7, "assessment": 8,
|
||||
"remediation": 8, "validation": 9, "reporting": 10, "evidence": 11,
|
||||
}
|
||||
|
||||
for gid in new_groups_touched:
|
||||
mc_id_str = "MC-%d" % gid
|
||||
phases = group_phases[gid]
|
||||
canonical = group_names[gid]
|
||||
all_phases = sorted(phases.keys(), key=lambda p: PHASE_ORDER.get(p, 99))
|
||||
phase_counts = {p: len(ctrls) for p, ctrls in phases.items()}
|
||||
total = sum(phase_counts.values())
|
||||
|
||||
existing_mc = mc_index.get(mc_id_str)
|
||||
|
||||
if existing_mc:
|
||||
# MC exists — append only NEW atomics that aren't already members
|
||||
mc_uuid = existing_mc[0]
|
||||
existing_set = existing_members[mc_id_str]
|
||||
added_for_this_mc = 0
|
||||
for phase, controls in phases.items():
|
||||
for ctrl_uuid, ctrl_id, action, title, is_new in controls:
|
||||
if ctrl_uuid in existing_set:
|
||||
stats["members_skipped_existing"] += 1
|
||||
continue
|
||||
inserts_members.append({
|
||||
"mc_uuid": mc_uuid, "control_uuid": ctrl_uuid,
|
||||
"phase": phase, "action": action,
|
||||
})
|
||||
stats["members_inserted"] += 1
|
||||
added_for_this_mc += 1
|
||||
if added_for_this_mc > 0:
|
||||
updates_mcs.append({
|
||||
"mc_uuid": mc_uuid,
|
||||
"phases_covered": json.dumps(all_phases),
|
||||
"phase_control_count": json.dumps(phase_counts),
|
||||
"total_controls": total,
|
||||
})
|
||||
stats["mcs_existing_updated"] += 1
|
||||
else:
|
||||
stats["groups_skipped_no_member_change"] += 1
|
||||
else:
|
||||
# MC missing — create only if group now meets min_phases threshold
|
||||
if len(phases) < args.min_phases:
|
||||
stats["groups_skipped_below_min_phases"] += 1
|
||||
continue
|
||||
inserts_new_mcs.append({
|
||||
"master_control_id": mc_id_str,
|
||||
"object_group_id": gid,
|
||||
"canonical_name": canonical,
|
||||
"phases_covered": json.dumps(all_phases),
|
||||
"phase_control_count": json.dumps(phase_counts),
|
||||
"total_controls": total,
|
||||
"_members": [
|
||||
{"control_uuid": c[0], "phase": p, "action": c[2]}
|
||||
for p, ctrls in phases.items() for c in ctrls
|
||||
],
|
||||
})
|
||||
stats["mcs_new_created"] += 1
|
||||
|
||||
logger.info("Plan summary: %s", stats)
|
||||
|
||||
if args.dry_run:
|
||||
logger.info("DRY RUN — no writes")
|
||||
# Show first few examples
|
||||
if inserts_new_mcs:
|
||||
logger.info("Sample NEW MCs (up to 5):")
|
||||
for mc in inserts_new_mcs[:5]:
|
||||
logger.info(" %s: %s — total=%d, phases=%s",
|
||||
mc["master_control_id"], mc["canonical_name"],
|
||||
mc["total_controls"], mc["phases_covered"])
|
||||
if updates_mcs:
|
||||
logger.info("Updates to existing MCs: %d", len(updates_mcs))
|
||||
return
|
||||
|
||||
# Step 5: WRITE — strictly INSERT/UPDATE, no DELETE
|
||||
with engine.begin() as c:
|
||||
c.execute(text("SET search_path TO compliance, public"))
|
||||
|
||||
# 5a: Insert new MCs + their members
|
||||
for mc in inserts_new_mcs:
|
||||
new_uuid_row = c.execute(text("""
|
||||
INSERT INTO master_controls
|
||||
(master_control_id, object_group_id, canonical_name,
|
||||
phases_covered, phase_control_count, total_controls)
|
||||
VALUES (:master_control_id, :object_group_id, :canonical_name,
|
||||
CAST(:phases_covered AS jsonb), CAST(:phase_control_count AS jsonb),
|
||||
:total_controls)
|
||||
RETURNING id
|
||||
"""), {k: v for k, v in mc.items() if k != "_members"}).fetchone()
|
||||
new_mc_uuid = str(new_uuid_row[0])
|
||||
for mem in mc["_members"]:
|
||||
c.execute(text("""
|
||||
INSERT INTO master_control_members
|
||||
(master_control_uuid, control_uuid, phase, action)
|
||||
VALUES (CAST(:mc_uuid AS uuid), CAST(:control_uuid AS uuid), :phase, :action)
|
||||
"""), {"mc_uuid": new_mc_uuid, **mem})
|
||||
|
||||
# 5b: Append new members to existing MCs
|
||||
for mem in inserts_members:
|
||||
c.execute(text("""
|
||||
INSERT INTO master_control_members
|
||||
(master_control_uuid, control_uuid, phase, action)
|
||||
VALUES (CAST(:mc_uuid AS uuid), CAST(:control_uuid AS uuid), :phase, :action)
|
||||
"""), mem)
|
||||
|
||||
# 5c: Update phase counts / totals on touched existing MCs
|
||||
for upd in updates_mcs:
|
||||
c.execute(text("""
|
||||
UPDATE master_controls
|
||||
SET phases_covered = CAST(:phases_covered AS jsonb),
|
||||
phase_control_count = CAST(:phase_control_count AS jsonb),
|
||||
total_controls = :total_controls
|
||||
WHERE id = CAST(:mc_uuid AS uuid)
|
||||
"""), upd)
|
||||
|
||||
logger.info("DONE — wrote %d new MCs, updated %d existing MCs, %d members inserted",
|
||||
stats["mcs_new_created"], stats["mcs_existing_updated"], stats["members_inserted"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Parse BSI QUAIDAL Markdown catalog into a structural index.
|
||||
|
||||
Clean-Room principle: this script does NOT persist any QUAIDAL prose to disk.
|
||||
It only extracts non-protectable structural facts (IDs, type, file paths,
|
||||
cross-references to other QUAIDAL entries, references to external norms).
|
||||
|
||||
The derivation step (derive_quaidal_mcs.py) reads the index plus the original
|
||||
.md files from the gitignored clone and asks the LLM to produce our own
|
||||
wordings, never copying the BSI prose into our own controls/database.
|
||||
|
||||
Input: legal-sources/bsi-quaidal/0000_Markdown/**/*.md (gitignored clone)
|
||||
Output: control-pipeline/data/quaidal/quaidal_index.json (structural only)
|
||||
|
||||
Usage:
|
||||
python3 control-pipeline/scripts/ingest_bsi_quaidal.py
|
||||
python3 control-pipeline/scripts/ingest_bsi_quaidal.py --check # validate only
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
print("ERROR: PyYAML missing. Install with: pip install pyyaml", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
SOURCE_ROOT = REPO_ROOT / "legal-sources" / "bsi-quaidal"
|
||||
MARKDOWN_ROOT = SOURCE_ROOT / "0000_Markdown"
|
||||
OUTPUT_DIR = REPO_ROOT / "control-pipeline" / "data" / "quaidal"
|
||||
OUTPUT_FILE = OUTPUT_DIR / "quaidal_index.json"
|
||||
|
||||
# Map folder name -> our internal kind. Sub-folders inside the Methoden tree
|
||||
# (e.g. "QM-10_Dimension Reduction") are treated as method variants of their
|
||||
# parent QM.
|
||||
KIND_BY_PARENT_DIR = {
|
||||
"0000_Qualitätskriterien": "criterion", # QKB → Master Control candidates
|
||||
"0001_Qualitätsbausteine": "building_block", # QB → atomic controls
|
||||
"0002_Maßnahmen": "measure", # M → mitigations
|
||||
"0003_Qualitätsmetriken_methoden": "metric", # QM → runtime check / metric
|
||||
"0002_Referenz-Matrizen": "matrix", # cross-walk matrix
|
||||
"9998_CustomTemplates": "template",
|
||||
}
|
||||
|
||||
FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
|
||||
ID_RE = re.compile(r"\b((?:QKB|QB|MA|QM)-\d+[a-zA-Z]?)", re.IGNORECASE)
|
||||
|
||||
|
||||
@dataclass
|
||||
class IndexEntry:
|
||||
id: str # Canonical ID: QKB-01, QB-03, M-12, QM-07
|
||||
kind: str # criterion / building_block / measure / metric / matrix / template
|
||||
title_de: str
|
||||
title_en: str
|
||||
source_path: str # relative to SOURCE_ROOT
|
||||
referenced_ids: list[str] = field(default_factory=list) # other QUAIDAL IDs linked in this file
|
||||
external_refs: list[dict] = field(default_factory=list) # {framework, citation, ref_id}
|
||||
tags: list[str] = field(default_factory=list)
|
||||
share: bool | None = None
|
||||
|
||||
|
||||
def parse_frontmatter(text: str) -> dict:
|
||||
m = FRONTMATTER_RE.match(text)
|
||||
if not m:
|
||||
return {}
|
||||
try:
|
||||
return yaml.safe_load(m.group(1)) or {}
|
||||
except yaml.YAMLError:
|
||||
return {}
|
||||
|
||||
|
||||
def canonical_id(raw_id: str | list | None, filename: str) -> str | None:
|
||||
"""QUAIDAL files sometimes list multiple IDs or odd casing — normalise."""
|
||||
candidates: list[str] = []
|
||||
if isinstance(raw_id, list):
|
||||
candidates.extend(str(x) for x in raw_id)
|
||||
elif isinstance(raw_id, str):
|
||||
candidates.append(raw_id)
|
||||
# Fallback: derive from filename
|
||||
candidates.append(filename)
|
||||
for c in candidates:
|
||||
m = ID_RE.search(c)
|
||||
if m:
|
||||
return m.group(1).upper().replace(" ", "-")
|
||||
return None
|
||||
|
||||
|
||||
def determine_kind(path: Path) -> str:
|
||||
for parent in path.parents:
|
||||
if parent.name in KIND_BY_PARENT_DIR:
|
||||
return KIND_BY_PARENT_DIR[parent.name]
|
||||
return "unknown"
|
||||
|
||||
|
||||
def collect_referenced_ids(body: str, own_id: str) -> list[str]:
|
||||
found = {m.group(1).upper() for m in ID_RE.finditer(body)}
|
||||
found.discard(own_id)
|
||||
return sorted(found)
|
||||
|
||||
|
||||
REF_FRAMEWORKS = [
|
||||
("AI Act", ["AI-Act", "AI Act", "Verordnung (EU) 2024/1689", "KI-VO"]),
|
||||
("EU GDPR", ["DSGVO", "Verordnung (EU) 2016/679", "GDPR"]),
|
||||
("ISO/IEC 25012", ["ISO/IEC 25012", "ISO 25012"]),
|
||||
("ISO/IEC 25024", ["ISO/IEC 25024", "ISO 25024"]),
|
||||
("ISO/IEC 23894", ["ISO/IEC 23894", "ISO 23894"]),
|
||||
("ISO/IEC 42001", ["ISO/IEC 42001", "ISO 42001"]),
|
||||
("NIST AI RMF", ["NIST AI RMF", "AI Risk Management Framework"]),
|
||||
("BSI Grundschutz", ["IT-Grundschutz", "Grundschutz"]),
|
||||
("BSI AIC4", ["AIC4", "AI Cloud Service Compliance Criteria"]),
|
||||
]
|
||||
|
||||
|
||||
def detect_external_refs(body: str) -> list[dict]:
|
||||
refs: list[dict] = []
|
||||
seen: set[tuple[str, str]] = set()
|
||||
# Section "Referenzen" tables — pick up first column ref-id and first
|
||||
# textual hit of the framework. We do NOT store the BSI "Kurzbeschr."
|
||||
# column to avoid copying their prose.
|
||||
for line in body.splitlines():
|
||||
for framework, patterns in REF_FRAMEWORKS:
|
||||
for pat in patterns:
|
||||
if pat.lower() in line.lower():
|
||||
# Try to grab an article/section nearby (e.g. "Artikel 10")
|
||||
art = re.search(r"(Artikel|Art\.?|Section|§)\s*([0-9]+[a-z]?)", line, re.IGNORECASE)
|
||||
citation = f"{art.group(1)} {art.group(2)}" if art else None
|
||||
key = (framework, citation or "")
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
refs.append({"framework": framework, "citation": citation})
|
||||
break
|
||||
return refs
|
||||
|
||||
|
||||
def parse_file(path: Path) -> IndexEntry | None:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
fm = parse_frontmatter(text)
|
||||
body = text[text.find("---", 3) + 3 :] if text.startswith("---") else text
|
||||
|
||||
own_id = canonical_id(fm.get("ID"), path.stem)
|
||||
if not own_id:
|
||||
return None
|
||||
|
||||
title_de = str(fm.get("TitleGer") or fm.get("Title") or path.stem).strip()
|
||||
title_en = str(fm.get("Title") or "").strip()
|
||||
tags_raw = fm.get("tags") or []
|
||||
if isinstance(tags_raw, str):
|
||||
tags_raw = [tags_raw]
|
||||
tags = [str(t).strip() for t in tags_raw if t]
|
||||
|
||||
share_val = fm.get("share")
|
||||
share = bool(share_val) if share_val is not None else None
|
||||
|
||||
return IndexEntry(
|
||||
id=own_id,
|
||||
kind=determine_kind(path),
|
||||
title_de=title_de,
|
||||
title_en=title_en,
|
||||
source_path=str(path.relative_to(SOURCE_ROOT)),
|
||||
referenced_ids=collect_referenced_ids(body, own_id),
|
||||
external_refs=detect_external_refs(body),
|
||||
tags=tags,
|
||||
share=share,
|
||||
)
|
||||
|
||||
|
||||
def get_commit_sha() -> str | None:
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["git", "-C", str(SOURCE_ROOT), "rev-parse", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return out.stdout.strip()
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--check", action="store_true", help="Parse + validate, do not write output")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not MARKDOWN_ROOT.exists():
|
||||
print(f"ERROR: clone not found at {SOURCE_ROOT}", file=sys.stderr)
|
||||
print("Run: git clone --depth=1 https://github.com/BSI-Bund/QUAIDAL.git legal-sources/bsi-quaidal", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
entries: list[IndexEntry] = []
|
||||
skipped: list[Path] = []
|
||||
for path in sorted(MARKDOWN_ROOT.rglob("*.md")):
|
||||
entry = parse_file(path)
|
||||
if entry is None:
|
||||
skipped.append(path)
|
||||
continue
|
||||
entries.append(entry)
|
||||
|
||||
by_kind: dict[str, int] = {}
|
||||
for e in entries:
|
||||
by_kind[e.kind] = by_kind.get(e.kind, 0) + 1
|
||||
|
||||
print(f"Parsed {len(entries)} entries (skipped {len(skipped)} without ID):")
|
||||
for kind, count in sorted(by_kind.items()):
|
||||
print(f" {kind:18s} {count}")
|
||||
|
||||
if args.check:
|
||||
return 0
|
||||
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
payload = {
|
||||
"source": "BSI QUAIDAL",
|
||||
"source_url": "https://github.com/BSI-Bund/QUAIDAL",
|
||||
"commit_sha": get_commit_sha(),
|
||||
"license_note": (
|
||||
"BSI-Veroeffentlichung. Repo enthaelt keine SPDX-Lizenzdatei. "
|
||||
"Frontmatter share:true. Veroeffentlichung durch Bundesbehoerde, "
|
||||
"§ 5 UrhG (amtliche Werke) anwendbar. BSI hat 05/2026 die Annahme "
|
||||
"CC-BY-SA-4.0 in unserer Anfrage nicht widersprochen, aber auch "
|
||||
"nicht aktiv bestaetigt. Wir derivieren Clean-Room (eigene "
|
||||
"Formulierungen, nur Referenz auf BSI QUAIDAL Sektion)."
|
||||
),
|
||||
"entries": [asdict(e) for e in entries],
|
||||
}
|
||||
OUTPUT_FILE.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"\nWrote index: {OUTPUT_FILE.relative_to(REPO_ROOT)}")
|
||||
print(f"Commit SHA: {payload['commit_sha']}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,414 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Ingest CRA-relevant ENISA documents into the RAG (collection `bp_compliance_ce`).
|
||||
|
||||
Source files live under `legal-sources/enisa/` in this repo. The script extracts
|
||||
PDF text with pdfplumber (HTML for the SRP FAQ), normalizes it, and uploads via
|
||||
the RAG service with `chunk_strategy='legal'` so that section metadata is
|
||||
attached to every chunk.
|
||||
|
||||
Each document carries a `requirement_strength` field so downstream consumers
|
||||
can distinguish normative material from guidance and consultation drafts:
|
||||
- mandatory — binding (none in this batch; CRA itself is the law)
|
||||
- guidance — official ENISA / EUCC guidance, citable
|
||||
- consultation_draft — public-consultation drafts (use with caveat)
|
||||
|
||||
Usage (run on Mac Mini after copying the legal-sources/enisa/ folder, or via SSH
|
||||
with the repo mounted):
|
||||
python3 control-pipeline/scripts/ingest_enisa_cra.py --dry-run
|
||||
python3 control-pipeline/scripts/ingest_enisa_cra.py
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import unicodedata
|
||||
from html.parser import HTMLParser
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
import pdfplumber
|
||||
|
||||
RAG_URL = "https://localhost:8097"
|
||||
QDRANT_URL = "http://localhost:6333"
|
||||
UPLOAD_TIMEOUT = 1800.0
|
||||
COLLECTION = "bp_compliance_ce"
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
SOURCE_DIR = REPO_ROOT / "legal-sources" / "enisa"
|
||||
|
||||
DOCS = [
|
||||
{
|
||||
"regulation_id": "enisa_cra_requirements_standards_mapping",
|
||||
"filename": "enisa_cra_requirements_standards_mapping.pdf",
|
||||
"upload_filename": "enisa_cra_requirements_standards_mapping.txt",
|
||||
"extra_metadata": {
|
||||
"regulation_id": "enisa_cra_requirements_standards_mapping",
|
||||
"regulation_short": "ENISA CRA Standards Mapping",
|
||||
"guideline_name": "Cyber Resilience Act Requirements Standards Mapping",
|
||||
"doc_type": "standards_mapping",
|
||||
"requirement_strength": "guidance",
|
||||
"publication_year": "2024",
|
||||
"license": "reuse_with_attribution",
|
||||
"source": "enisa.europa.eu",
|
||||
"attribution": "ENISA, CC BY 4.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
"regulation_id": "enisa_cra_implementation_via_eucc",
|
||||
"filename": "enisa_cra_implementation_via_eucc.pdf",
|
||||
"upload_filename": "enisa_cra_implementation_via_eucc.txt",
|
||||
"extra_metadata": {
|
||||
"regulation_id": "enisa_cra_implementation_via_eucc",
|
||||
"regulation_short": "ENISA CRA via EUCC",
|
||||
"guideline_name": "CRA Implementation via EUCC and its Applicable Technical Elements",
|
||||
"doc_type": "certification_guidance",
|
||||
"requirement_strength": "guidance",
|
||||
"license": "reuse_with_attribution",
|
||||
"source": "enisa.europa.eu",
|
||||
"attribution": "ENISA, CC BY 4.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
"regulation_id": "enisa_cra_implementation_via_eucc_annex",
|
||||
"filename": "enisa_cra_implementation_via_eucc_annex.pdf",
|
||||
"upload_filename": "enisa_cra_implementation_via_eucc_annex.txt",
|
||||
"extra_metadata": {
|
||||
"regulation_id": "enisa_cra_implementation_via_eucc_annex",
|
||||
"regulation_short": "ENISA CRA via EUCC (Annex)",
|
||||
"guideline_name": "Annex — CRA Implementation via EUCC",
|
||||
"doc_type": "certification_guidance_annex",
|
||||
"requirement_strength": "guidance",
|
||||
"license": "reuse_with_attribution",
|
||||
"source": "enisa.europa.eu",
|
||||
"attribution": "ENISA, CC BY 4.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
"regulation_id": "enisa_eucc_vulnerability_management_disclosure",
|
||||
"filename": "enisa_eucc_vulnerability_management_disclosure.pdf",
|
||||
"upload_filename": "enisa_eucc_vulnerability_management_disclosure.txt",
|
||||
"extra_metadata": {
|
||||
"regulation_id": "enisa_eucc_vulnerability_management_disclosure",
|
||||
"regulation_short": "EUCC Vuln Management & Disclosure",
|
||||
"guideline_name": "EUCC Guidelines — Vulnerability Management and Disclosure v1.1",
|
||||
"doc_type": "vulnerability_guidance",
|
||||
"requirement_strength": "guidance",
|
||||
"license": "reuse_with_attribution",
|
||||
"source": "enisa.europa.eu",
|
||||
"attribution": "ENISA, CC BY 4.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
"regulation_id": "enisa_eccg_opinion_vulnerability_management",
|
||||
"filename": "enisa_eccg_opinion_vulnerability_management.pdf",
|
||||
"upload_filename": "enisa_eccg_opinion_vulnerability_management.txt",
|
||||
"extra_metadata": {
|
||||
"regulation_id": "enisa_eccg_opinion_vulnerability_management",
|
||||
"regulation_short": "ECCG Opinion Vuln Management",
|
||||
"guideline_name": "Final ECCG Opinion — Guidance on Vulnerability Management",
|
||||
"doc_type": "eccg_opinion",
|
||||
"requirement_strength": "guidance",
|
||||
"license": "reuse_with_attribution",
|
||||
"source": "enisa.europa.eu",
|
||||
"attribution": "ENISA, CC BY 4.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
"regulation_id": "enisa_nis2_technical_implementation_guidance",
|
||||
"filename": "enisa_nis2_technical_implementation_guidance.pdf",
|
||||
"upload_filename": "enisa_nis2_technical_implementation_guidance.txt",
|
||||
"extra_metadata": {
|
||||
"regulation_id": "enisa_nis2_technical_implementation_guidance",
|
||||
"regulation_short": "ENISA NIS2 TIG v1.0",
|
||||
"guideline_name": "ENISA Technical Implementation Guidance on Cybersecurity Risk Management Measures v1.0",
|
||||
"doc_type": "technical_guidance",
|
||||
"requirement_strength": "guidance",
|
||||
"publication_year": "2025",
|
||||
"license": "reuse_with_attribution",
|
||||
"source": "enisa.europa.eu",
|
||||
"attribution": "ENISA, CC BY 4.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
"regulation_id": "enisa_nis2_security_measures_consultation",
|
||||
"filename": "enisa_nis2_security_measures_implementation_guidance_consultation.pdf",
|
||||
"upload_filename": "enisa_nis2_security_measures_consultation.txt",
|
||||
"extra_metadata": {
|
||||
"regulation_id": "enisa_nis2_security_measures_consultation",
|
||||
"regulation_short": "ENISA NIS2 Security Measures (Draft)",
|
||||
"guideline_name": "Implementation Guidance on Security Measures — Public Consultation Draft",
|
||||
"doc_type": "consultation_draft",
|
||||
"requirement_strength": "consultation_draft",
|
||||
"license": "reuse_with_attribution",
|
||||
"source": "enisa.europa.eu",
|
||||
"attribution": "ENISA, CC BY 4.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
"regulation_id": "enisa_cra_single_reporting_platform_faq",
|
||||
"filename": "enisa_cra_single_reporting_platform_faq.html",
|
||||
"upload_filename": "enisa_cra_single_reporting_platform_faq.txt",
|
||||
"extra_metadata": {
|
||||
"regulation_id": "enisa_cra_single_reporting_platform_faq",
|
||||
"regulation_short": "ENISA SRP FAQ",
|
||||
"guideline_name": "CRA Single Reporting Platform (SRP) FAQ",
|
||||
"doc_type": "faq",
|
||||
"requirement_strength": "guidance",
|
||||
"license": "reuse_with_attribution",
|
||||
"source": "enisa.europa.eu",
|
||||
"attribution": "ENISA, CC BY 4.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
"regulation_id": "enisa_eucc_evaluation_methodology_product_series",
|
||||
"filename": "enisa_eucc_evaluation_methodology_product_series.pdf",
|
||||
"upload_filename": "enisa_eucc_evaluation_methodology_product_series.txt",
|
||||
"extra_metadata": {
|
||||
"regulation_id": "enisa_eucc_evaluation_methodology_product_series",
|
||||
"regulation_short": "EUCC Eval Methodology Product Series",
|
||||
"guideline_name": "EUCC Guidelines — Evaluation Methodology for Product Series v1.0",
|
||||
"doc_type": "evaluation_methodology",
|
||||
"requirement_strength": "guidance",
|
||||
"publication_year": "2025",
|
||||
"license": "reuse_with_attribution",
|
||||
"source": "enisa.europa.eu",
|
||||
"attribution": "ENISA, CC BY 4.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
"regulation_id": "enisa_threat_landscape_2025",
|
||||
"filename": "enisa_threat_landscape_2025.pdf",
|
||||
"upload_filename": "enisa_threat_landscape_2025.txt",
|
||||
"extra_metadata": {
|
||||
"regulation_id": "enisa_threat_landscape_2025",
|
||||
"regulation_short": "ENISA Threat Landscape 2025",
|
||||
"guideline_name": "ENISA Threat Landscape 2025 v1.2",
|
||||
"doc_type": "threat_landscape",
|
||||
"requirement_strength": "evidentiary",
|
||||
"publication_year": "2025",
|
||||
"license": "reuse_with_attribution",
|
||||
"source": "enisa.europa.eu",
|
||||
"attribution": "ENISA, CC BY 4.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
"regulation_id": "enisa_cvd_policies_eu_2022",
|
||||
"filename": "enisa_cvd_policies_eu_2022.pdf",
|
||||
"upload_filename": "enisa_cvd_policies_eu_2022.txt",
|
||||
"extra_metadata": {
|
||||
"regulation_id": "enisa_cvd_policies_eu_2022",
|
||||
"regulation_short": "ENISA CVD Policies EU 2022",
|
||||
"guideline_name": "Coordinated Vulnerability Disclosure Policies in the EU (2022)",
|
||||
"doc_type": "policy_study",
|
||||
"requirement_strength": "guidance",
|
||||
"publication_year": "2022",
|
||||
"license": "reuse_with_attribution",
|
||||
"source": "enisa.europa.eu",
|
||||
"attribution": "ENISA, CC BY 4.0",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def normalize_text(text: str) -> str:
|
||||
text = unicodedata.normalize("NFKC", text)
|
||||
text = text.replace("", "").replace("", "")
|
||||
prev = None
|
||||
while prev != text:
|
||||
prev = text
|
||||
text = re.sub(r"(\d+)\s+\.\s+(\d+)", r"\1.\2", text)
|
||||
text = re.sub(r"\b([A-Z]{2,4})\s+-\s+(\d+)\b", r"\1-\2", text)
|
||||
text = re.sub(r"\(\s+(\d+)\s+\)", r"(\1)", text)
|
||||
text = re.sub(r"[^\S\n]{2,}", " ", text)
|
||||
return text
|
||||
|
||||
|
||||
class _HTMLToText(HTMLParser):
|
||||
SKIP = {"script", "style", "nav", "header", "footer", "noscript"}
|
||||
BLOCK = {"p", "div", "li", "br", "h1", "h2", "h3", "h4", "h5", "h6", "tr", "section"}
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._buf: list[str] = []
|
||||
self._skip_depth = 0
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag in self.SKIP:
|
||||
self._skip_depth += 1
|
||||
if tag in self.BLOCK:
|
||||
self._buf.append("\n")
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag in self.SKIP and self._skip_depth > 0:
|
||||
self._skip_depth -= 1
|
||||
if tag in self.BLOCK:
|
||||
self._buf.append("\n")
|
||||
|
||||
def handle_data(self, data):
|
||||
if self._skip_depth == 0:
|
||||
self._buf.append(data)
|
||||
|
||||
def text(self) -> str:
|
||||
raw = "".join(self._buf)
|
||||
raw = re.sub(r"\n{3,}", "\n\n", raw)
|
||||
return raw.strip()
|
||||
|
||||
|
||||
def extract_pdf(path: Path) -> str:
|
||||
print(f" Extracting PDF: {path.name}")
|
||||
parts: list[str] = []
|
||||
with pdfplumber.open(path) as pdf:
|
||||
for i, page in enumerate(pdf.pages):
|
||||
t = page.extract_text(x_tolerance=3, y_tolerance=4)
|
||||
if t:
|
||||
parts.append(t)
|
||||
if (i + 1) % 50 == 0:
|
||||
print(f" {i + 1}/{len(pdf.pages)} pages...")
|
||||
return normalize_text("\n\n".join(parts))
|
||||
|
||||
|
||||
def extract_html(path: Path) -> str:
|
||||
print(f" Extracting HTML: {path.name}")
|
||||
html = path.read_text(encoding="utf-8", errors="replace")
|
||||
parser = _HTMLToText()
|
||||
parser.feed(html)
|
||||
return normalize_text(parser.text())
|
||||
|
||||
|
||||
def get_text(doc) -> str:
|
||||
path = SOURCE_DIR / doc["filename"]
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(path)
|
||||
if path.suffix.lower() == ".pdf":
|
||||
text = extract_pdf(path)
|
||||
elif path.suffix.lower() in {".html", ".htm"}:
|
||||
text = extract_html(path)
|
||||
else:
|
||||
raise ValueError(f"Unsupported file type: {path.suffix}")
|
||||
print(f" Extracted {len(text):,} chars")
|
||||
return text
|
||||
|
||||
|
||||
def upload_text_legal(text: str, filename: str, extra_metadata: dict) -> dict:
|
||||
form_data = {
|
||||
"collection": COLLECTION,
|
||||
"data_type": "compliance",
|
||||
"bundesland": "bund",
|
||||
"use_case": "compliance",
|
||||
"year": "2026",
|
||||
"chunk_strategy": "legal",
|
||||
"chunk_size": "1500",
|
||||
"chunk_overlap": "100",
|
||||
"metadata_json": json.dumps(extra_metadata, ensure_ascii=False),
|
||||
}
|
||||
with httpx.Client(timeout=UPLOAD_TIMEOUT, verify=False) as c:
|
||||
resp = c.post(
|
||||
f"{RAG_URL}/api/v1/documents/upload",
|
||||
files={"file": (filename, text.encode("utf-8"), "text/plain")},
|
||||
data=form_data,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def count_chunks(regulation_id: str) -> int:
|
||||
with httpx.Client(timeout=30) as c:
|
||||
resp = c.post(
|
||||
f"{QDRANT_URL}/collections/{COLLECTION}/points/count",
|
||||
json={
|
||||
"filter": {
|
||||
"must": [
|
||||
{"key": "regulation_id", "match": {"value": regulation_id}}
|
||||
]
|
||||
},
|
||||
"exact": True,
|
||||
},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()["result"]["count"]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--dry-run", action="store_true",
|
||||
help="Extract text and report sizes, but do not upload.")
|
||||
parser.add_argument("--only", action="append", default=[],
|
||||
help="Limit run to one or more regulation_ids.")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not SOURCE_DIR.exists():
|
||||
print(f"ERROR: source dir not found: {SOURCE_DIR}")
|
||||
return 2
|
||||
|
||||
docs = DOCS
|
||||
if args.only:
|
||||
wanted = set(args.only)
|
||||
docs = [d for d in DOCS if d["regulation_id"] in wanted]
|
||||
missing = wanted - {d["regulation_id"] for d in docs}
|
||||
if missing:
|
||||
print(f"ERROR: unknown regulation_id(s): {sorted(missing)}")
|
||||
return 2
|
||||
|
||||
print("=" * 70)
|
||||
print(f"ENISA CRA ingestion → collection={COLLECTION}")
|
||||
print(f"Source dir: {SOURCE_DIR}")
|
||||
print(f"Documents: {len(docs)} Dry run: {args.dry_run}")
|
||||
print("=" * 70)
|
||||
|
||||
results = []
|
||||
for i, doc in enumerate(docs, 1):
|
||||
reg_id = doc["regulation_id"]
|
||||
print(f"\n[{i}/{len(docs)}] {reg_id}")
|
||||
|
||||
existing = count_chunks(reg_id) if not args.dry_run else "?"
|
||||
print(f" Existing chunks in Qdrant: {existing}")
|
||||
|
||||
try:
|
||||
text = get_text(doc)
|
||||
except Exception as e:
|
||||
print(f" ERROR extracting text: {e}")
|
||||
results.append({"id": reg_id, "chars": 0, "new": 0,
|
||||
"strength": doc["extra_metadata"]["requirement_strength"]})
|
||||
continue
|
||||
|
||||
if args.dry_run:
|
||||
results.append({"id": reg_id, "chars": len(text), "new": "?",
|
||||
"strength": doc["extra_metadata"]["requirement_strength"]})
|
||||
continue
|
||||
|
||||
if existing and existing > 0:
|
||||
print(f" SKIP — {existing} chunks already present. "
|
||||
f"Use Qdrant delete-by-filter before re-ingesting.")
|
||||
results.append({"id": reg_id, "chars": len(text), "new": 0,
|
||||
"strength": doc["extra_metadata"]["requirement_strength"]})
|
||||
continue
|
||||
|
||||
print(" Uploading with chunk_strategy='legal'...")
|
||||
result = upload_text_legal(
|
||||
text, doc["upload_filename"], doc["extra_metadata"]
|
||||
)
|
||||
new_chunks = result.get("chunks_count", 0)
|
||||
new_doc_id = result.get("document_id", "")
|
||||
print(f" -> {new_chunks} chunks (doc_id={new_doc_id})")
|
||||
|
||||
results.append({"id": reg_id, "chars": len(text), "new": new_chunks,
|
||||
"strength": doc["extra_metadata"]["requirement_strength"]})
|
||||
|
||||
if i < len(docs):
|
||||
time.sleep(2)
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("SUMMARY")
|
||||
print("=" * 70)
|
||||
for r in results:
|
||||
print(f" {r['id']:<55} chars={r['chars']:<9} new={r['new']:<5} "
|
||||
f"strength={r['strength']}")
|
||||
total_new = sum(r["new"] for r in results if isinstance(r["new"], int))
|
||||
print(f"\nTotal new chunks: {total_new}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -22,6 +22,7 @@ import json
|
||||
import logging
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
@@ -108,24 +109,37 @@ class BatchDedupRunner:
|
||||
self._progress_phase = ""
|
||||
self._progress_count = 0
|
||||
self._progress_total = 0
|
||||
self._since = None # set by run() when scoped run requested
|
||||
|
||||
async def run(
|
||||
self,
|
||||
dry_run: bool = False,
|
||||
hint_filter: str = None,
|
||||
since: datetime = None,
|
||||
) -> dict:
|
||||
"""Run the full batch dedup pipeline.
|
||||
|
||||
Args:
|
||||
dry_run: If True, compute stats but don't modify DB/Qdrant.
|
||||
hint_filter: If set, only process groups matching this hint prefix.
|
||||
since: If set, only process controls with created_at >= since.
|
||||
Useful for incremental dedup after single-document ingestion.
|
||||
|
||||
Returns:
|
||||
Stats dict with counts.
|
||||
"""
|
||||
start = time.monotonic()
|
||||
logger.info("BatchDedup starting (dry_run=%s, hint_filter=%s)",
|
||||
dry_run, hint_filter)
|
||||
logger.info("BatchDedup starting (dry_run=%s, hint_filter=%s, since=%s)",
|
||||
dry_run, hint_filter, since)
|
||||
|
||||
# Scoped runs reset checkpoint to avoid skipping new controls whose
|
||||
# control_id sorts before the stale last_id of a previous full run.
|
||||
self._since = since
|
||||
if since and not dry_run:
|
||||
self.db.execute(text(
|
||||
"DELETE FROM canonical_generation_jobs WHERE status = 'dedup_phase2_checkpoint'"
|
||||
))
|
||||
self.db.commit()
|
||||
|
||||
if not dry_run:
|
||||
await ensure_qdrant_collection(collection=self.collection)
|
||||
@@ -133,7 +147,7 @@ class BatchDedupRunner:
|
||||
# Phase 1: Intra-group dedup (same merge_group_hint)
|
||||
# Optimization: skip singleton groups (they're automatically masters)
|
||||
self._progress_phase = "phase1"
|
||||
groups = self._load_merge_groups(hint_filter)
|
||||
groups = self._load_merge_groups(hint_filter, since)
|
||||
self._progress_total = self.stats["total_controls"]
|
||||
|
||||
multi_groups = [(h, c) for h, c in groups if len(c) > 1]
|
||||
@@ -171,7 +185,7 @@ class BatchDedupRunner:
|
||||
logger.info("BatchDedup completed in %.1fs: %s", elapsed, self.stats)
|
||||
return self.stats
|
||||
|
||||
def _load_merge_groups(self, hint_filter: str = None) -> list:
|
||||
def _load_merge_groups(self, hint_filter: str = None, since: datetime = None) -> list:
|
||||
"""Load all Pass 0b controls grouped by merge_group_hint, largest first."""
|
||||
conditions = [
|
||||
"decomposition_method = 'pass0b'",
|
||||
@@ -184,6 +198,10 @@ class BatchDedupRunner:
|
||||
conditions.append("generation_metadata->>'merge_group_hint' LIKE :hf")
|
||||
params["hf"] = f"{hint_filter}%"
|
||||
|
||||
if since:
|
||||
conditions.append("created_at >= :since")
|
||||
params["since"] = since
|
||||
|
||||
where = " AND ".join(conditions)
|
||||
rows = self.db.execute(text(f"""
|
||||
SELECT id::text, control_id, title, objective,
|
||||
@@ -335,13 +353,15 @@ class BatchDedupRunner:
|
||||
"""
|
||||
logger.info("BatchDedup Phase 2: Cross-group pass starting...")
|
||||
|
||||
# Count total
|
||||
total_row = self.db.execute(text("""
|
||||
# Count total — respect scoped run if since is set
|
||||
since_clause = " AND created_at >= :since" if self._since else ""
|
||||
params = {"since": self._since} if self._since else {}
|
||||
total_row = self.db.execute(text(f"""
|
||||
SELECT COUNT(*) FROM canonical_controls
|
||||
WHERE decomposition_method = 'pass0b'
|
||||
AND release_state != 'duplicate'
|
||||
AND release_state != 'deprecated'
|
||||
""")).fetchone()
|
||||
AND release_state != 'deprecated'{since_clause}
|
||||
"""), params).fetchone()
|
||||
total = total_row[0] if total_row else 0
|
||||
|
||||
self._progress_total = total
|
||||
@@ -360,13 +380,16 @@ class BatchDedupRunner:
|
||||
last_control_id = checkpoint_row[0] if checkpoint_row else ""
|
||||
|
||||
if last_control_id:
|
||||
skip_row = self.db.execute(text("""
|
||||
skip_params = {"last_id": last_control_id}
|
||||
if self._since:
|
||||
skip_params["since"] = self._since
|
||||
skip_row = self.db.execute(text(f"""
|
||||
SELECT COUNT(*) FROM canonical_controls
|
||||
WHERE decomposition_method = 'pass0b'
|
||||
AND release_state != 'duplicate'
|
||||
AND release_state != 'deprecated'
|
||||
AND control_id <= :last_id
|
||||
"""), {"last_id": last_control_id}).fetchone()
|
||||
AND control_id <= :last_id{since_clause}
|
||||
"""), skip_params).fetchone()
|
||||
skipped = skip_row[0] if skip_row else 0
|
||||
self._progress_count = skipped
|
||||
logger.info("BatchDedup Cross-group: RESUMING from %s (skipping %d already processed)",
|
||||
@@ -382,17 +405,20 @@ class BatchDedupRunner:
|
||||
total, last_control_id or "beginning")
|
||||
|
||||
while True:
|
||||
rows = self.db.execute(text("""
|
||||
page_params = {"last_id": last_control_id, "page_size": DB_PAGE}
|
||||
if self._since:
|
||||
page_params["since"] = self._since
|
||||
rows = self.db.execute(text(f"""
|
||||
SELECT id::text, control_id, title,
|
||||
generation_metadata->>'merge_group_hint' as merge_group_hint
|
||||
FROM canonical_controls
|
||||
WHERE decomposition_method = 'pass0b'
|
||||
AND release_state != 'duplicate'
|
||||
AND release_state != 'deprecated'
|
||||
AND control_id > :last_id
|
||||
AND control_id > :last_id{since_clause}
|
||||
ORDER BY control_id
|
||||
LIMIT :page_size
|
||||
"""), {"last_id": last_control_id, "page_size": DB_PAGE}).fetchall()
|
||||
"""), page_params).fetchall()
|
||||
|
||||
if not rows:
|
||||
break
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# Golden Dataset for MC Assignment Quality
|
||||
# Manually verified controls with their expected MC topics.
|
||||
# Used for regression testing after pipeline changes.
|
||||
# Created: 2026-05-10, verified by manual review (19/20 correct)
|
||||
|
||||
golden_controls:
|
||||
# ── Data Protection ──
|
||||
- control_id: "DATA-3291-A06"
|
||||
expected_topic_prefix: "data_retention"
|
||||
reason: "Speicherfristen für personenbezogene Daten definieren"
|
||||
|
||||
- control_id: "SEC-7449-A01"
|
||||
expected_topic_prefix: "personal_data"
|
||||
reason: "Fahrzeugnutzungsdaten in Telematikbox (Datenminimierung)"
|
||||
|
||||
- control_id: "DATA-3518-A06"
|
||||
expected_topic_prefix: "data_subject_rights"
|
||||
reason: "Betroffene über Lösch-Ausnahmen informieren"
|
||||
|
||||
- control_id: "GOV-963-A02"
|
||||
expected_topic_prefix: "consent"
|
||||
reason: "Zustimmung des Urhebers vor Veröffentlichung einholen"
|
||||
|
||||
# ── Security ──
|
||||
- control_id: "CRYP-1454-A07"
|
||||
expected_topic_prefix: "encryption"
|
||||
reason: "RSASSA-PSS in TLS 1.3 verifizieren"
|
||||
|
||||
- control_id: "NET-1141-A08"
|
||||
expected_topic_prefix: "monitoring"
|
||||
reason: "Sampling-Strategien konfigurieren"
|
||||
|
||||
- control_id: "SEC-2244-A05"
|
||||
expected_topic_prefix: "asset_management"
|
||||
reason: "Systeminventar kontinuierlich aktualisieren"
|
||||
|
||||
- control_id: "AUTH-3468-A06"
|
||||
expected_topic_prefix: "access_control"
|
||||
reason: "Rollenkonzept mit abgestuften Zugriffsrechten"
|
||||
|
||||
# ── Governance ──
|
||||
- control_id: "AUTH-2364-A09"
|
||||
expected_topic_prefix: "supervisory_authority"
|
||||
reason: "Zusammenarbeit mit Wirtschaftsakteuren dokumentieren"
|
||||
|
||||
- control_id: "SEC-5972-A14"
|
||||
expected_topic_prefix: "third_party_management"
|
||||
reason: "Cybersicherheitsrichtlinien kritischer Lieferanten prüfen"
|
||||
|
||||
- control_id: "SEC-3441-A02"
|
||||
expected_topic_prefix: "human_resources_security"
|
||||
reason: "Mitarbeiter vor Nachteil bei Verweigerung schützen"
|
||||
|
||||
- control_id: "SEC-3502-A06"
|
||||
expected_topic_prefix: "awareness"
|
||||
reason: "Organisationskultur für Sicherheitsverbesserung"
|
||||
|
||||
- control_id: "GOV-1748-A04"
|
||||
expected_topic_prefix: "policy"
|
||||
reason: "Annahme von Geschenken untersagen"
|
||||
|
||||
# ── Regulatory ──
|
||||
- control_id: "AI-1287-A01"
|
||||
expected_topic_prefix: "ai_system"
|
||||
reason: "Akteure des KI-Systems identifizieren"
|
||||
|
||||
- control_id: "AI-1732-A11"
|
||||
expected_topic_prefix: "ai_system"
|
||||
reason: "Menschliche Kontrolle für KI-Entscheidungen"
|
||||
|
||||
- control_id: "COMP-1352-A04"
|
||||
expected_topic_prefix: "certification"
|
||||
reason: "Amateurfunkprüfungszeugnis vorlegen"
|
||||
|
||||
- control_id: "FIN-1212-A02"
|
||||
expected_topic_prefix: "financial_reporting"
|
||||
reason: "Jahresabschluss gemäß EU-Richtlinie aufstellen"
|
||||
|
||||
- control_id: "AUTH-1165-A01"
|
||||
expected_topic_prefix: "data_classification"
|
||||
reason: "Öffentliche IP-Adressen als Stammdaten klassifizieren"
|
||||
|
||||
- control_id: "SEC-7367-A10"
|
||||
expected_topic_prefix: "audit_logging"
|
||||
reason: "Banner-Version Rückverfolgung testen"
|
||||
|
||||
- control_id: "LAB-034-A03"
|
||||
expected_topic_prefix: "third_party_management"
|
||||
reason: "Verträge auf unzulässige Klauseln prüfen"
|
||||
|
||||
quality_thresholds:
|
||||
min_accuracy: 0.90
|
||||
max_controls_per_mc: 300
|
||||
min_master_controls: 10000
|
||||
@@ -0,0 +1,166 @@
|
||||
"""
|
||||
Master Control Quality Tests.
|
||||
|
||||
Regression tests to ensure MC assignment quality stays above 90%.
|
||||
Uses golden dataset of manually verified controls.
|
||||
"""
|
||||
|
||||
import os
|
||||
import yaml
|
||||
import pytest
|
||||
from sqlalchemy import create_engine, text
|
||||
|
||||
DB_URL = os.getenv(
|
||||
"DATABASE_URL",
|
||||
"postgresql://breakpilot:breakpilot123@postgres:5432/breakpilot_db",
|
||||
)
|
||||
|
||||
_engine = None
|
||||
|
||||
|
||||
def get_engine():
|
||||
global _engine
|
||||
if _engine is None:
|
||||
_engine = create_engine(
|
||||
DB_URL,
|
||||
connect_args={"options": "-c search_path=compliance,public"},
|
||||
)
|
||||
return _engine
|
||||
|
||||
|
||||
def load_golden():
|
||||
path = os.path.join(os.path.dirname(__file__), "golden_mc_assignments.yaml")
|
||||
with open(path) as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
|
||||
# ── Golden Dataset Tests ──
|
||||
|
||||
|
||||
class TestGoldenMCAssignments:
|
||||
"""Each golden control must be in the correct MC."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self):
|
||||
self.golden = load_golden()
|
||||
self.engine = get_engine()
|
||||
|
||||
def test_golden_controls_correctly_assigned(self):
|
||||
"""All golden controls must be in an MC matching their expected topic prefix."""
|
||||
errors = []
|
||||
with self.engine.connect() as c:
|
||||
for gc in self.golden["golden_controls"]:
|
||||
row = c.execute(text("""
|
||||
SELECT mc.canonical_name
|
||||
FROM master_controls mc
|
||||
JOIN master_control_members mcm ON mcm.master_control_uuid = mc.id
|
||||
JOIN canonical_controls cc ON cc.id = mcm.control_uuid
|
||||
WHERE cc.control_id = :cid
|
||||
LIMIT 1
|
||||
"""), {"cid": gc["control_id"]}).fetchone()
|
||||
|
||||
if row is None:
|
||||
errors.append(f"{gc['control_id']}: not found in any MC")
|
||||
elif not row[0].startswith(gc["expected_topic_prefix"]):
|
||||
errors.append(
|
||||
f"{gc['control_id']}: expected {gc['expected_topic_prefix']}*, "
|
||||
f"got {row[0]}"
|
||||
)
|
||||
|
||||
if errors:
|
||||
pytest.fail(
|
||||
f"{len(errors)} golden controls misassigned:\n"
|
||||
+ "\n".join(f" - {e}" for e in errors)
|
||||
)
|
||||
|
||||
|
||||
# ── Structural Quality Tests ──
|
||||
|
||||
|
||||
class TestMCStructuralQuality:
|
||||
"""Structural invariants for Master Controls."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self):
|
||||
self.golden = load_golden()
|
||||
self.thresholds = self.golden["quality_thresholds"]
|
||||
self.engine = get_engine()
|
||||
|
||||
def test_minimum_master_controls(self):
|
||||
"""Must have at least 10K Master Controls."""
|
||||
with self.engine.connect() as c:
|
||||
count = c.execute(
|
||||
text("SELECT count(*) FROM master_controls")
|
||||
).scalar()
|
||||
assert count >= self.thresholds["min_master_controls"], (
|
||||
f"Only {count} MCs, expected >= {self.thresholds['min_master_controls']}"
|
||||
)
|
||||
|
||||
def test_max_controls_per_mc(self):
|
||||
"""No MC should have more than 300 controls."""
|
||||
with self.engine.connect() as c:
|
||||
max_mc = c.execute(
|
||||
text("SELECT max(total_controls) FROM master_controls")
|
||||
).scalar()
|
||||
assert max_mc <= self.thresholds["max_controls_per_mc"], (
|
||||
f"Max MC has {max_mc} controls, limit is {self.thresholds['max_controls_per_mc']}"
|
||||
)
|
||||
|
||||
def test_no_empty_master_controls(self):
|
||||
"""Every MC must have at least 1 member."""
|
||||
with self.engine.connect() as c:
|
||||
empty = c.execute(text("""
|
||||
SELECT count(*) FROM master_controls
|
||||
WHERE total_controls = 0
|
||||
""")).scalar()
|
||||
assert empty == 0, f"{empty} empty MCs found"
|
||||
|
||||
def test_all_members_reference_valid_controls(self):
|
||||
"""Every MC member must reference an existing control."""
|
||||
with self.engine.connect() as c:
|
||||
orphans = c.execute(text("""
|
||||
SELECT count(*) FROM master_control_members mcm
|
||||
LEFT JOIN canonical_controls cc ON cc.id = mcm.control_uuid
|
||||
WHERE cc.id IS NULL
|
||||
""")).scalar()
|
||||
assert orphans == 0, f"{orphans} orphan members found"
|
||||
|
||||
|
||||
# ── Doc Check Controls Tests ──
|
||||
|
||||
|
||||
class TestDocCheckControls:
|
||||
"""Validate doc_check_controls table."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self):
|
||||
self.engine = get_engine()
|
||||
|
||||
def test_doc_check_controls_exist(self):
|
||||
"""Must have doc_check_controls."""
|
||||
with self.engine.connect() as c:
|
||||
count = c.execute(
|
||||
text("SELECT count(*) FROM doc_check_controls")
|
||||
).scalar()
|
||||
assert count > 100, f"Only {count} doc_check_controls"
|
||||
|
||||
def test_all_doc_types_covered(self):
|
||||
"""All 8 document types must have controls."""
|
||||
expected = {"dse", "cookie", "impressum", "widerruf",
|
||||
"agb", "dsfa", "avv", "loeschkonzept"}
|
||||
with self.engine.connect() as c:
|
||||
rows = c.execute(text(
|
||||
"SELECT DISTINCT doc_type FROM doc_check_controls"
|
||||
)).fetchall()
|
||||
actual = {r[0] for r in rows}
|
||||
missing = expected - actual
|
||||
assert not missing, f"Missing doc types: {missing}"
|
||||
|
||||
def test_check_questions_not_empty(self):
|
||||
"""Every doc_check_control must have a check_question."""
|
||||
with self.engine.connect() as c:
|
||||
empty = c.execute(text("""
|
||||
SELECT count(*) FROM doc_check_controls
|
||||
WHERE check_question IS NULL OR check_question = ''
|
||||
""")).scalar()
|
||||
assert empty == 0, f"{empty} controls without check_question"
|
||||
@@ -909,3 +909,20 @@ services:
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- breakpilot-network
|
||||
|
||||
# =========================================================
|
||||
# MARKETING WEBSITE - BreakPilot Produktwebsite
|
||||
# =========================================================
|
||||
marketing-website:
|
||||
build:
|
||||
context: ./marketing-website
|
||||
dockerfile: Dockerfile
|
||||
container_name: bp-core-marketing-website
|
||||
platform: linux/arm64
|
||||
ports:
|
||||
- "3014:3000"
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- breakpilot-network
|
||||
|
||||
@@ -0,0 +1,860 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr" class="h-100">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="MobileOptimized" content="width" />
|
||||
<meta name="HandheldFriendly" content="true" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="/themes/custom/enisaweb/favicon.ico" type="image/png" />
|
||||
<link rel="alternate" type="application/rss+xml" title="Single Reporting Platform (SRP)" href="https://www.enisa.europa.eu/taxonomy/term/1317/feed" />
|
||||
<script>window.a2a_config=window.a2a_config||{};a2a_config.callbacks=[];a2a_config.overlays=[];a2a_config.templates={};</script>
|
||||
|
||||
<meta content="ENISA: Every day we experience the Information Society. Interconnected networks touch our everyday lives, at home and at work. It is therefore vital that computers, mobile phones, banking, and the Internet function, to support Europe’s digital economy. That is why ENISA is working with Cybersecurity for the EU and the Member States." name="DC.description">
|
||||
<meta name="description" content="ENISA is the EU agency dedicated to enhancing cybersecurity in Europe. They offer guidance, tools, and resources to safeguard citizens and businesses from cyber threats.">
|
||||
<meta name="keywords" content="Cybersecurity, EU, ENISA, computer security, Cyber Threats, EU Cyber Crisis, Incident Management, Market and Standards, Product Security, Security certification, Risk Management, Skills and competences, State of cybersecurity in the EU, Vulnerability Disclosure, Artificial Intelligence, Next Gen Technologies, Awareness, Cyber Hygiene, Digital Identity, Data Protection, Education and career path">
|
||||
<title>Single Reporting Platform (SRP) | ENISA</title>
|
||||
|
||||
<link rel="stylesheet" media="all" href="/sites/default/files/css/css_H8-PkuOemoNPxq-HW0ue4hGKWqBFO5KaLA29hyssQWk.css?delta=0&language=en&theme=enisaweb&include=eJxtj2sOwyAMgy9E4UhVCl6HRpKJ0FXd6Uf3lrY_kfPJkR1KqSnJFugp_KGqNBdPSLlpHSlGrSmrhLd6WCDJdYAgWplKvsJBstGKKUxkX9tcdKIyWNtKlvnDGWY0w5xpzFRG7pE0ds_JwqEndJMpw0flswp6qz_GX-TbEQxnmzXwo8olY7Vwn541LQVuRb-ZiSFLD6vsVww7GHYyvD68AUVHcCg" />
|
||||
<link rel="stylesheet" media="all" href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css" />
|
||||
<link rel="stylesheet" media="all" href="/sites/default/files/css/css_XfzkZLkUSSs_yRcqoRmh-VWG0krtdRIrQV-ENlV19ao.css?delta=2&language=en&theme=enisaweb&include=eJxtj2sOwyAMgy9E4UhVCl6HRpKJ0FXd6Uf3lrY_kfPJkR1KqSnJFugp_KGqNBdPSLlpHSlGrSmrhLd6WCDJdYAgWplKvsJBstGKKUxkX9tcdKIyWNtKlvnDGWY0w5xpzFRG7pE0ds_JwqEndJMpw0flswp6qz_GX-TbEQxnmzXwo8olY7Vwn541LQVuRb-ZiSFLD6vsVww7GHYyvD68AUVHcCg" />
|
||||
<link rel="stylesheet" media="all" href="/sites/default/files/css/css_gcXUcvuow4apg85qsW-WFQB8ls5BPBU3WeuPLmwnlqQ.css?delta=3&language=en&theme=enisaweb&include=eJxtj2sOwyAMgy9E4UhVCl6HRpKJ0FXd6Uf3lrY_kfPJkR1KqSnJFugp_KGqNBdPSLlpHSlGrSmrhLd6WCDJdYAgWplKvsJBstGKKUxkX9tcdKIyWNtKlvnDGWY0w5xpzFRG7pE0ds_JwqEndJMpw0flswp6qz_GX-TbEQxnmzXwo8olY7Vwn541LQVuRb-ZiSFLD6vsVww7GHYyvD68AUVHcCg" />
|
||||
|
||||
|
||||
|
||||
</head>
|
||||
<body class="path-taxonomy d-flex flex-column h-100">
|
||||
|
||||
|
||||
<div class="dialog-off-canvas-main-canvas d-flex flex-column h-100" data-off-canvas-main-canvas>
|
||||
|
||||
|
||||
|
||||
<header>
|
||||
<p class="sr-only"><a href="#main-content" accesskey="M">Go to the main content</a></p>
|
||||
|
||||
<div class="navbar navbar-expand-lg navbar-dark text-light bg-primary header">
|
||||
<div class="container logo-menu-wrapper d-flex">
|
||||
|
||||
<div class="region region-nav-branding">
|
||||
<div id="block-enisaweb-branding" class="block block-system block-system-branding-block">
|
||||
|
||||
|
||||
<div class="navbar-brand d-flex align-items-center">
|
||||
|
||||
<a href="/" title="Home" rel="home" class="site-logo d-block">
|
||||
<img src="/sites/default/files/enisa-logo.svg" alt="Home" fetchpriority="high" />
|
||||
</a>
|
||||
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<nav class="collapse navbar-collapse justify-content-end main-menu" id="navbarSupportedContent" role="navigation" aria-label="Main menu and Search box">
|
||||
<div class="region region-nav-main">
|
||||
<div id="block-enisaweb-mainnavigation-2" class="block block-we-megamenu block-we-megamenu-blockmain">
|
||||
|
||||
|
||||
<div class="region-we-mega-menu">
|
||||
<a href="javascript:" class="navbar-toggle collapsed" aria-label="Open/close menu" aria-controls="mainMenuResponsive" name="menu-button" role="button">
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</a>
|
||||
<nav class="main navbar navbar-default navbar-we-mega-menu mobile-collapse hover-action" data-menu-name="main" data-block-theme="enisaweb" data-style="Default" data-animation="None" data-delay="" data-duration="" data-autoarrow="" data-alwayshowsubmenu="" data-action="hover" data-mobile-collapse="0" aria-label="ENISA main menu" id="mainMenuResponsive">
|
||||
<div class="container-fluid">
|
||||
<ul class="we-mega-menu-ul nav nav-tabs">
|
||||
<li class="we-mega-menu-li justify dropdown-menu" title="" data-level="0" data-element-type="" data-id="320e2c86-310b-4f7b-a4a9-188df34c3e43" data-submenu="1" data-hide-sub-when-collapse="" data-group="0" data-caption="" data-alignsub="justify" data-target="" data-icon="" >
|
||||
<span class="we-megamenu-nolink">
|
||||
Topics</span>
|
||||
<div class="we-mega-menu-submenu" data-element-type="we-mega-menu-submenu" data-submenu-width="" data-class="" style="width: px">
|
||||
<div class="we-mega-menu-submenu-inner">
|
||||
<div class="we-mega-menu-row" data-element-type="we-mega-menu-row" data-custom-row="1">
|
||||
<div class="we-mega-menu-col span3" data-element-type="we-mega-menu-col" data-width="3" data-block="enisaweb_topics" data-blocktitle="0" data-hidewhencollapse="" data-class="">
|
||||
<div class="type-of-block"><div class="block-inner"><div id="block-enisaweb-topics" class="block block-block-content block-block-contentec3f1f7d-35d4-4776-a03c-7f97a2fcfc8f">
|
||||
|
||||
<p class="title"><a href="/topics" target="_self" title="Access to All Topics page">Topics</a></p>
|
||||
|
||||
|
||||
<div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item"><p>Learn more about the topics</p>
|
||||
<p><a class="button" href="/topics" data-entity-type="node" data-entity-uuid="5f4db810-e19d-49a4-baaa-86ec782eb91e" data-entity-substitution="canonical" title="Topics">Access</a></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
</div>
|
||||
<div class="we-mega-menu-col span3" data-element-type="we-mega-menu-col" data-width="3" data-block="enisaweb_views_block__topics_tax_block_1" data-blocktitle="0" data-hidewhencollapse="" data-class="">
|
||||
<div class="type-of-block"><div class="block-inner"><div class="views-element-container block block-views block-views-blocktopics-tax-block-1" id="block-enisaweb-views-block-topics-tax-block-1">
|
||||
|
||||
<p class="title"><a href="/audience/national-eu-authorities" target="_self" title="Access to For National / EU authorities page">For National / EU authorities</a></p>
|
||||
|
||||
<div data-block="nav_main"><div class="view view-topics-tax view-id-topics_tax view-display-id-block_1 js-view-dom-id-a9128a4de087e3f9960a31f862089b7e6946eeb6d529b846901e74f4ba95f84d">
|
||||
|
||||
|
||||
|
||||
<div class="view-content">
|
||||
<div class="item-list">
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="/topics/cyber-threats" hreflang="en">Cyber Threats</a></li>
|
||||
<li><a href="/topics/eu-incident-response-and-cyber-crisis-management" hreflang="en">EU incident response and cyber crisis management</a></li>
|
||||
<li><a href="/topics/market" hreflang="en">Market</a></li>
|
||||
<li><a href="/topics/product-security-and-certification" hreflang="en">Product Security and Certification</a></li>
|
||||
<li><a href="/topics/risk-management" hreflang="en">Risk Management</a></li>
|
||||
<li><a href="/topics/skills-and-competences" hreflang="en">Skills and competences</a></li>
|
||||
<li><a href="/topics/state-of-cybersecurity-in-the-eu" hreflang="en">State of cybersecurity in the EU</a></li>
|
||||
<li><a href="/topics/vulnerability-disclosure" hreflang="en">Vulnerability Disclosure</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div></div></div>
|
||||
|
||||
</div>
|
||||
<div class="we-mega-menu-col span3" data-element-type="we-mega-menu-col" data-width="3" data-block="enisaweb_views_block__topics_tax_block_2" data-blocktitle="0" data-hidewhencollapse="" data-class="">
|
||||
<div class="type-of-block"><div class="block-inner"><div class="views-element-container title block block-views block-views-blocktopics-tax-block-2" id="block-enisaweb-views-block-topics-tax-block-2">
|
||||
|
||||
<p class="title"><a href="/audience/private-sector" target="_self" title="Access to Private Sector page">Private Sector</a></p>
|
||||
|
||||
<div data-block="nav_main"><div class="view view-topics-tax view-id-topics_tax view-display-id-block_2 js-view-dom-id-ca2d4c425826cfbc7b36885c48a7a8c805021f1f25dec545fefac51557010f38">
|
||||
|
||||
|
||||
|
||||
<div class="view-content">
|
||||
<div class="item-list">
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="/topics/artificial-intelligence-and-next-gen-technologies" hreflang="en">Artificial Intelligence and Next Gen Technologies</a></li>
|
||||
<li><a href="/topics/awareness-and-cyber-hygiene" hreflang="en">Awareness and Cyber Hygiene</a></li>
|
||||
<li><a href="/topics/certification-and-standards" hreflang="en">Certification and Standards</a></li>
|
||||
<li><a href="/topics/cyber-threats" hreflang="en">Cyber Threats</a></li>
|
||||
<li><a href="/topics/cybersecurity-of-critical-sectors" hreflang="en">Cybersecurity of Critical Sectors</a></li>
|
||||
<li><a href="/topics/digital-identity-and-data-protection" hreflang="en">Digital Identity and Data Protection</a></li>
|
||||
<li><a href="/topics/incident-management" hreflang="en">Incident management</a></li>
|
||||
<li><a href="/topics/risk-management" hreflang="en">Risk Management</a></li>
|
||||
<li><a href="/topics/skills-and-competences-for-companies" hreflang="en">Skills and competences (for companies)</a></li>
|
||||
<li><a href="/topics/vulnerability-disclosure" hreflang="en">Vulnerability Disclosure</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div></div></div>
|
||||
|
||||
</div>
|
||||
<div class="we-mega-menu-col span3" data-element-type="we-mega-menu-col" data-width="3" data-block="enisaweb_views_block__topics_tax_block_3" data-blocktitle="0" data-hidewhencollapse="" data-class="">
|
||||
<div class="type-of-block"><div class="block-inner"><div class="views-element-container title block block-views block-views-blocktopics-tax-block-3" id="block-enisaweb-views-block-topics-tax-block-3">
|
||||
|
||||
<p class="title"><a href="/audience/citizens" target="_self" title="Access to Citizens page">Citizens</a></p>
|
||||
|
||||
<div data-block="nav_main"><div class="view view-topics-tax view-id-topics_tax view-display-id-block_3 js-view-dom-id-b69cbf795aad827e795977b39f2678f43cdc75c6765696171efc1a9faca05ccd">
|
||||
|
||||
|
||||
|
||||
<div class="view-content">
|
||||
<div class="item-list">
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="/topics/cyber-hygiene" hreflang="en">Cyber Hygiene</a></li>
|
||||
<li><a href="/topics/cyber-incident-awareness" hreflang="en">Cyber Incident Awareness</a></li>
|
||||
<li><a href="/topics/education-and-career-path" hreflang="en">Education and career path</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div></div></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</li><li class="we-mega-menu-li" title="Access to all publications" data-level="0" data-element-type="" data-id="2d24a690-803d-44ad-8d7e-8cf0eb7d0e40" data-submenu="0" data-hide-sub-when-collapse="" data-group="0" data-caption="" data-alignsub="" data-target="_self" data-icon="" >
|
||||
<a class="we-mega-menu-li" title="" href="/publications" target="_self">
|
||||
Publications </a>
|
||||
|
||||
</li><li class="we-mega-menu-li dropdown-menu" title="" data-level="0" data-element-type="" data-id="54b069d8-f99f-45e7-9d50-9345e82ad14f" data-submenu="1" data-hide-sub-when-collapse="" data-group="0" data-caption="" data-alignsub="" data-target="" data-icon="" >
|
||||
<span class="we-megamenu-nolink">
|
||||
Newsroom & Events</span>
|
||||
<div class="we-mega-menu-submenu" data-element-type="we-mega-menu-submenu" data-submenu-width="" data-class="" style="width: px">
|
||||
<div class="we-mega-menu-submenu-inner">
|
||||
<div class="we-mega-menu-row" data-element-type="we-mega-menu-row" data-custom-row="1">
|
||||
<div class="we-mega-menu-col span4" data-element-type="we-mega-menu-col" data-width="4" data-block="enisaweb_news" data-blocktitle="0" data-hidewhencollapse="" data-class="">
|
||||
<div class="type-of-block"><div class="block-inner"><div id="block-enisaweb-news" class="block block-block-content block-block-content00f65cb0-e1b0-41a1-8a0d-00ce6ced0f0e">
|
||||
|
||||
<p class="title"><a href="/news" target="_self" title="Access to All News">News</a></p>
|
||||
|
||||
|
||||
<div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item"><p>Cybersecurity in focus: News & updates from ENISA</p>
|
||||
<p><a class="button" href="/news" data-entity-type="node" data-entity-uuid="088dd847-33ff-4c8e-b094-f0d38a66bbdd" data-entity-substitution="canonical" title="News">Access</a></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
</div>
|
||||
<div class="we-mega-menu-col span4" data-element-type="we-mega-menu-col" data-width="4" data-block="enisaweb_events" data-blocktitle="0" data-hidewhencollapse="" data-class="">
|
||||
<div class="type-of-block"><div class="block-inner"><div id="block-enisaweb-events" class="block block-block-content block-block-contentb3c06780-f342-437d-9828-54fe86eb9786">
|
||||
|
||||
<p class="title"><a href="/events" target="_self" title="Access to All Events">Events</a></p>
|
||||
|
||||
|
||||
<div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item"><p>Cybersecurity in practice: Events & Workshops by ENISA</p>
|
||||
<p><a class="button" href="/events" data-entity-type="node" data-entity-uuid="34f66c6d-0f07-4977-85e3-e8578a59d59c" data-entity-substitution="canonical" title="Events">Access</a></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
</div>
|
||||
<div class="we-mega-menu-col span4" data-element-type="we-mega-menu-col" data-width="4" data-block="enisaweb_pressoffice" data-blocktitle="0" data-hidewhencollapse="" data-class="">
|
||||
<div class="type-of-block"><div class="block-inner"><div id="block-enisaweb-pressoffice" class="block block-system block-system-menu-blockpress-office">
|
||||
|
||||
<p class="title"><a href="/press-office" target="_self" title="Access to Press office page">Press office</a></p>
|
||||
|
||||
|
||||
<ul data-block="nav_main" class="nav navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a href="/press-office/corporate-identity" class="nav-link" data-drupal-link-system-path="node/11030">Corporate identity</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/press-office/cybersecurity-material" class="nav-link" data-drupal-link-system-path="node/11031">Cybersecurity material</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/form/seek-an-expert" class="nav-link" data-drupal-link-system-path="webform/seek_an_expert">Seek an expert or Request a speaker</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
</div></div></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</li><li class="we-mega-menu-li dropdown-menu" title="" data-level="0" data-element-type="" data-id="eaba29ce-6446-4da3-930a-c3d97495f00b" data-submenu="1" data-hide-sub-when-collapse="" data-group="0" data-caption="" data-alignsub="" data-target="" data-icon="" >
|
||||
<span class="we-megamenu-nolink">
|
||||
About</span>
|
||||
<div class="we-mega-menu-submenu" data-element-type="we-mega-menu-submenu" data-submenu-width="" data-class="" style="width: px">
|
||||
<div class="we-mega-menu-submenu-inner">
|
||||
<div class="we-mega-menu-row" data-element-type="we-mega-menu-row" data-custom-row="1">
|
||||
<div class="we-mega-menu-col span4" data-element-type="we-mega-menu-col" data-width="4" data-block="enisaweb_whatwedosubmenubutton_2" data-blocktitle="0" data-hidewhencollapse="" data-class="">
|
||||
<div class="type-of-block"><div class="block-inner"><div id="block-enisaweb-whatwedosubmenubutton-2" class="block block-block-content block-block-contente0a96203-1eca-4991-9533-33064cace452">
|
||||
|
||||
<p class="title"><a href="/about-enisa/what-we-do" target="_self" title="Access to What we do page">What we do</a></p>
|
||||
|
||||
|
||||
<div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item"><p>Achieving a high common level of cybersecurity across Europe</p>
|
||||
<p><a class="button" href="/about-enisa/what-we-do" data-entity-type="node" data-entity-uuid="0619d110-d4dc-4f6b-aa0d-9a56252da07d" data-entity-substitution="canonical" title="What we do">Access</a></p>
|
||||
</div>
|
||||
|
||||
</div></div></div>
|
||||
|
||||
</div>
|
||||
<div class="we-mega-menu-col span4" data-element-type="we-mega-menu-col" data-width="4" data-block="enisaweb_whatwedosubmenu_2" data-blocktitle="0" data-hidewhencollapse="" data-class="">
|
||||
<div class="type-of-block"><div class="block-inner"><div id="block-enisaweb-whatwedosubmenu-2" class="block block-block-content block-block-content99599d89-56f7-43f1-907b-72614045018d">
|
||||
|
||||
<p class="title"><a href="/about-enisa/who-we-are" target="_self" title="Access to Who we are page">Who we are</a></p>
|
||||
|
||||
|
||||
<div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item"><p>Towards a Trusted and Cyber Secure Europe </p>
|
||||
<p><a class="button" href="/about-enisa/who-we-are" data-entity-type="node" data-entity-uuid="3c46c210-298c-472c-b871-b167dfe2642c" data-entity-substitution="canonical" title="Who we are">Access</a></p>
|
||||
</div>
|
||||
|
||||
</div></div></div>
|
||||
|
||||
</div>
|
||||
<div class="we-mega-menu-col span4" data-element-type="we-mega-menu-col" data-width="4" data-block="enisaweb_transparency" data-blocktitle="0" data-hidewhencollapse="" data-class="">
|
||||
<div class="type-of-block"><div class="block-inner"><div id="block-enisaweb-transparency" class="block block-system block-system-menu-blocktransparency">
|
||||
|
||||
<p class="title"><a href="/about-enisa/How-we-work" target="_self" title="Access to How we work page">How we work</a></p>
|
||||
|
||||
|
||||
<ul data-block="nav_main" class="nav navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a href="/about-enisa/accounting-finance/accounting-finance" class="nav-link" data-drupal-link-system-path="node/17339">Accounting and Finance</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/about-enisa/procedures-and-policies" class="nav-link dropdown-toggle" data-drupal-link-system-path="node/17360">Policies and Procedures</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/about-enisa/data-protection/data-protection" class="nav-link dropdown-toggle" data-drupal-link-system-path="node/17346">Data Protection</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/about-enisa/transparency" class="nav-link dropdown-toggle" data-drupal-link-system-path="node/17404">Transparency</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/enisa-a-climate-neutral-agency" class="nav-link" data-drupal-link-system-path="node/18893">ENISA, a climate neutral agency </a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="we-mega-menu-row" data-element-type="we-mega-menu-row" data-custom-row="1">
|
||||
<div class="we-mega-menu-col span12 transversal-menu" data-element-type="we-mega-menu-col" data-width="12" data-block="enisaweb_transversaloptionsofaboutmenu" data-blocktitle="0" data-hidewhencollapse="" data-class="transversal-menu">
|
||||
<div class="type-of-block"><div class="block-inner"><div id="block-enisaweb-transversaloptionsofaboutmenu" class="block block-system block-system-menu-blocktransversal-options-about-menu">
|
||||
|
||||
|
||||
|
||||
<ul data-block="nav_main" class="nav navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a href="/about-enisa/international-cooperation" class="nav-link" data-drupal-link-system-path="node/19377">International Cooperation</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</li><li class="we-mega-menu-li dropdown-menu" title="" data-level="0" data-element-type="" data-id="44fbec5d-1aaf-4cb3-b06b-a79b571ad50c" data-submenu="1" data-hide-sub-when-collapse="" data-group="0" data-caption="" data-alignsub="" data-target="" data-icon="" >
|
||||
<span class="we-megamenu-nolink">
|
||||
Working with us</span>
|
||||
<div class="we-mega-menu-submenu" data-element-type="we-mega-menu-submenu" data-submenu-width="" data-class="" style="width: px">
|
||||
<div class="we-mega-menu-submenu-inner">
|
||||
<div class="we-mega-menu-row" data-element-type="we-mega-menu-row" data-custom-row="1">
|
||||
<div class="we-mega-menu-col span4" data-element-type="we-mega-menu-col" data-width="4" data-block="enisaweb_workwithenisa" data-blocktitle="0" data-hidewhencollapse="" data-class="">
|
||||
<div class="type-of-block"><div class="block-inner"><div id="block-enisaweb-workwithenisa" class="block block-block-content block-block-content86d5ce2f-abc2-4160-afda-442fe2c68258">
|
||||
|
||||
<p class="title"><a href="/working-with-us/working-for-enisa" target="_self" title="Access to Working for ENISA page">Working for ENISA</a></p>
|
||||
|
||||
|
||||
<div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item"><p>Explore the benefits of working for ENISA</p>
|
||||
<p><a class="button" href="/working-with-us/working-for-enisa" data-entity-type="node" data-entity-uuid="f10da531-46ad-49f6-84cb-0770c69d839e" data-entity-substitution="canonical" title="Working for ENISA">Access</a></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div></div>
|
||||
|
||||
</div>
|
||||
<div class="we-mega-menu-col span8" data-element-type="we-mega-menu-col" data-width="8" data-block="enisaweb_workingwithus" data-blocktitle="0" data-hidewhencollapse="" data-class="">
|
||||
<div class="type-of-block"><div class="block-inner"><div id="block-enisaweb-workingwithus" class="title block block-system block-system-menu-blockworking-with-us">
|
||||
|
||||
<p class="title"><a href="/work-with-us" target="_self" title="Access to Working with us page">Working with us</a></p>
|
||||
|
||||
|
||||
<ul data-block="nav_main" class="nav navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a href="/careers" class="nav-link" data-drupal-link-system-path="node/12487">Careers</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/working-with-us/procurement" class="nav-link" data-drupal-link-system-path="node/17421">Procurement</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/working-with-us/ad-hoc-working-groups-calls" class="nav-link" data-drupal-link-system-path="node/17422">Ad hoc working groups</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
</div></div></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<p class="btn-search">
|
||||
<a href="javascript:" title="Show the search field">Search</a>
|
||||
</p>
|
||||
<div class="search-form-wrapper" aria-label="Search field">
|
||||
<div class="region region-nav-additional">
|
||||
<div id="block-enisaweb-search" class="search-block-form block block-search container-inline" data-drupal-selector="search-block-form" id="block-enisaweb-search-form" role="search">
|
||||
|
||||
|
||||
<form data-block="nav_additional" action="/search" method="get" id="search-block-form" accept-charset="UTF-8">
|
||||
<div class="js-form-item form-item js-form-type-search form-type-search js-form-item-keys form-item-keys form-no-label">
|
||||
<label class="visually-hidden" for="edit-keys">Search</label>
|
||||
<input class="form-search form-control" title="Enter the terms you wish to search for." data-drupal-selector="edit-keys" type="search" id="edit-keys" name="keys" value="" size="15" maxlength="128">
|
||||
</div>
|
||||
<div class="form-actions js-form-wrapper form-wrapper" data-drupal-selector="edit-actions" id="edit-actions-search">
|
||||
<button class="button js-form-submit form-submit btn-enisa btn-primary" data-drupal-selector="edit-submit" type="submit" id="edit-submit" value="Search">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<p class="btn-close-search"><a href="javascript:" title="Hide the search field">Close</a></p>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main role="main" class="container-full image-banner">
|
||||
<div class="title-breadcrumbs container">
|
||||
<div id="block-enisaweb-page-title" class="block block-core block-page-title-block">
|
||||
|
||||
|
||||
|
||||
<h1>Single Reporting Platform (SRP)</h1>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="region region-breadcrumb">
|
||||
<div id="block-enisaweb-breadcrumbs" class="block block-system block-system-breadcrumb-block">
|
||||
|
||||
|
||||
|
||||
<nav aria-label="breadcrumb">
|
||||
<p id="system-breadcrumb" class="visually-hidden">Breadcrumb</p>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="/" aria-label="Access to ">Home</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="/topics" aria-label="Access to ">Topics</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="/topics/product-security-and-certification" aria-label="Access to ">Product Security and Certification</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
Single Reporting Platform (SRP)
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="main container container-taxonomy">
|
||||
<a id="main-content" tabindex="-1"></a>
|
||||
<div class="container">
|
||||
<div data-drupal-messages-fallback class="hidden"></div>
|
||||
|
||||
|
||||
|
||||
<div class="row g-0 publications-list-section">
|
||||
|
||||
<div class="sidebar-first order-lg-1 col-12 col-lg-3">
|
||||
<div id="block-enisaweb-topictaxonomymenublock" class="block block-enisa-path-block block-topic-taxonomy-menu-block">
|
||||
|
||||
|
||||
<h2>Subtopics</h2>
|
||||
|
||||
<div class="item-list">
|
||||
<ul class="submenu">
|
||||
<li>
|
||||
<a href="https://www.enisa.europa.eu//topics/product-security-and-certification/single-reporting-platform-srp" hreflang="en">Single Reporting Platform (SRP)</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div class="content-wrapper order-lg-2 col-12 col-lg-9">
|
||||
<div id="block-enisaweb-content" class="block block-system block-system-main-block">
|
||||
|
||||
|
||||
<div class="views-element-container"><div class="view view-taxonomy-term view-id-taxonomy_term view-display-id-page_1 js-view-dom-id-fe094e9a97c4c6ce1116eacf5ed98f737017da96312d42131b1bb835e76fa9cb">
|
||||
|
||||
|
||||
<div class="view-header">
|
||||
|
||||
<div id="taxonomy-term-1317" class="taxonomy-term vocabulary-topics">
|
||||
<div class="content-description row">
|
||||
<div class="col-md-12 col-lg-12 tax-description">
|
||||
<div class="quote-wrapper">
|
||||
<p>The Cyber Resilience Act (CRA) introduces the Single Reporting Platform (SRP) for cybersecurity incident reporting in the EU Digital Single Market.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 tax-content">
|
||||
|
||||
<div class="clearfix text-formatted field field--name-field-body field--type-text-with-summary field--label-hidden field__item"><p>The Single Reporting Platform (SRP) provided for in the Cyber Resilience Act (CRA) shall become a technical tool to use for the reporting of actively exploited vulnerabilities and incidents impacting products with digital elements operating in the EU Digital Single Market. </p>
|
||||
<p>The SRP will be used by CSIRTs and manufacturers for mandatory reporting and could be used by any natural/legal persons for voluntary reporting.</p>
|
||||
<p>The CRA mandates manufacturers of products with digital elements to report actively exploited vulnerabilities and severe incidents having an impact on the security of the product as of 11 September 2026 onwards using the Single Reporting Platform. Throughout 2025 and 2026, ENISA is undertaking a number of necessary steps to support the successful implementation of the platform.</p>
|
||||
<p>The CRA brings transparency to the vulnerability disclosure processes and strengthens how EU CSIRTs can mitigate risks stemming from vulnerabilities. </p>
|
||||
<p>Further information: <a href="https://eur-lex.europa.eu/eli/reg/2024/2847/oj/eng">Regulation - 2024/2847 - EN - EUR-Lex</a></p>
|
||||
<h3>Frequently Asked Questions</h3>
|
||||
<p>This is a collection of frequently asked questions on Cyber Resilience Act Single Reporting Platform (CRA SRP). Document is intended for publication on ENISA website and to be updated during implementation of CRA SRP</p>
|
||||
<p>Please see also information about CRA reporting <a href="https://digital-strategy.ec.europa.eu/en/policies/cra-reporting">https://digital-strategy.ec.europa.eu/en/policies/cra-reporting</a> in particular FAQ file there <a href="https://ec.europa.eu/newsroom/dae/redirection/document/122331">https://ec.europa.eu/newsroom/dae/redirection/document/122331</a></p>
|
||||
<dl class="ckeditor-accordion">
|
||||
<dt>What is the Cyber Resilience Act’s Single Reporting Platform (CRA SRP)?</dt>
|
||||
<dd>
|
||||
<p class="text-align-justify">The CRA SRP is an electronic system designed to simplify the reporting obligations for manufacturers under the Cyber Resilience Act. It allows for manufacturers to report actively exploited vulnerabilities and severe incidents having an impact on the security of products with digital elements only once, rather than having to notify multiple national authorities individually.</p>
|
||||
</dd>
|
||||
<dt>Who is responsible for establishing and managing the platform?</dt>
|
||||
<dd>
|
||||
<p>ENISA is tasked with establishing, managing, and maintaining the day-to-day operations of the CRA SRP. ENISA must also ensure the platform's security and implement appropriate technical and organisational measures to protect the information submitted.</p>
|
||||
</dd>
|
||||
<dt>When will the Single Reporting Platform be operational?</dt>
|
||||
<dd>
|
||||
<p>The platform is scheduled to be operational by 11 September 2026. This coincides with the date when the <strong>mandatory reporting</strong> obligations for manufacturers officially enter into application (art.14 of Cyber Resilience Act). A testing period is expected to take place before this date.</p>
|
||||
</dd>
|
||||
<dt>What must be reported via the platform?</dt>
|
||||
<dd>
|
||||
<p>Manufacturers must use the platform to notify two specific types of events:</p>
|
||||
<ul>
|
||||
<li><strong>Actively Exploited Vulnerabilities:</strong> Vulnerabilities in products with digital elements that are known to be currently exploited by a malicious actor.</li>
|
||||
<li><strong>Severe Incidents:</strong> Incidents that have a severe impact on the security of the product with digital elements (e.g., compromising availability, authenticity, integrity, or confidentiality); the criteria for severity are defined in Article 14(5).</li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt>What else can be reported in the platform? </dt>
|
||||
<dd>
|
||||
<p>The platform will also offer functionality to allow voluntary reporting. Any natural or legal person may notify on a voluntary basis: </p>
|
||||
<ul>
|
||||
<li><strong>Vulnerabilities </strong>contained in a product with digital elements;</li>
|
||||
<li><strong>Cyber threats</strong> that could affect the risk profile of a product with digital elements;</li>
|
||||
<li><strong>Incidents</strong> having an impact on the security of a product;</li>
|
||||
<li><strong>Near misses</strong> that could have resulted in an incident.</li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt>What are the deadlines for reporting?</dt>
|
||||
<dd>
|
||||
<p>Manufacturers must adhere to a multi-stage reporting timeline via the platform:</p>
|
||||
<ul>
|
||||
<li><strong>Early Warning: </strong>Without undue delay and in any case within <strong>24 hours </strong>of becoming aware of the vulnerability or incident.\</li>
|
||||
<li><strong>Vulnerability/Incident Notification: </strong>Without undue delay and in any case within <strong>72 hours</strong> of becoming aware, providing general information and an initial assessment.</li>
|
||||
<li><strong>Final Report:</strong>
|
||||
<ul>
|
||||
<li>For <strong>vulnerabilities</strong>: No later than <strong>14 days</strong> after a corrective measure (e.g., patch) is available.</li>
|
||||
<li>For <strong>severe incidents</strong>: Within <strong>1 month</strong> after the initial notification.<br> </li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt>How does the Single Reporting Platform operate?</dt>
|
||||
<dd>
|
||||
<p>Manufacturers submit notifications electronically through the platform, which automatically routes them to the designated CSIRT coordinator (based on the manufacturer's main establishment) and ENISA simultaneously. The CSIRT then disseminates the information without delay to other relevant CSIRTs in Member States where the product is available, and to market surveillance authorities as needed. For sensitive reports, dissemination may be delayed on security grounds, with ENISA informed and able to recommend broader sharing if risks are systemic. The platform incorporates security measures to protect confidentiality. </p>
|
||||
</dd>
|
||||
<dt>How do I know what is my designated CSIRT?</dt>
|
||||
<dd>
|
||||
<p>Your designated CSIRT is determined by your location of establishment:</p>
|
||||
<p>If you are established in the EU: Your designated CSIRT is the national CSIRT designated as the coordinator in the Member State where you have your main establishment. (please see CRA Art 14(7) for more details)</p>
|
||||
<p>If you are NOT established in the EU: Your designated CSIRT is the one designated as coordinator in the Member State where your authorised representative is established. (please see CRA Art 14(7) for more details)</p>
|
||||
</dd>
|
||||
<dt>What are the responsibilities of key entities involved with the CRA SRP?</dt>
|
||||
<dd>
|
||||
<ul>
|
||||
<li>Manufacturers: Submit timely notifications and comply with the other obligations established by the CRA. </li>
|
||||
<li>ENISA: Manages the platform, processes reports, prepares biennial trend reports (first due within 24 months of the reporting obligations starting), operates a helpdesk (especially for SMEs), and discloses fixed vulnerabilities to the European Vulnerability Database.</li>
|
||||
<li>CSIRTs Designated as Coordinators: Receive and assess reports, decide on dissemination delays, inform market surveillance authorities and the public if necessary, and provide helpdesk support alongside ENISA.</li>
|
||||
<li>European Commission: Adopts delegated and implementing acts (e.g., for delay criteria and report formats), evaluates the platform's effectiveness, and supports coordination of enforcement activities.</li>
|
||||
<li>Market Surveillance Authorities: Receive disseminated information and enforce compliance, such as through investigations or corrective actions.</li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt>Who receives the reports submitted to the platform?</dt>
|
||||
<dd>
|
||||
<p>As a general rule, when a manufacturer submits a report to the CRA SRP, it is simultaneously notified to:</p>
|
||||
<ul>
|
||||
<li>The <strong>CSIRT</strong> (Computer Security Incident Response Team) designated as the coordinator in the Member State where the manufacturer is established.</li>
|
||||
<li><strong>ENISA </strong>(unless particularly exceptional circumstances apply).</li>
|
||||
</ul>
|
||||
<p>The CSIRT designated as coordinator that initially receives the notification is then responsible for disseminating it without delay to other relevant CSIRTs across the EU via the platform.</p>
|
||||
</dd>
|
||||
<dt>Can the dissemination of a report be delayed or withheld?</dt>
|
||||
<dd>
|
||||
<p>Yes. In exceptional circumstances, the receiving CSIRT may decide to delay or withhold the dissemination of a notification to other Member States. This is strictly limited to cases where immediate dissemination is justified on security related grounds (e.g., if spreading the information would pose an even greater security risk).</p>
|
||||
<p>The European Commission adopted a delegated act on <strong>11 December 2025</strong> to further specify the terms and conditions for applying these grounds. [<a href="https://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=PI_COM:C(2025)8407">https://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=PI_COM:C(2025)8407</a>] </p>
|
||||
<p>In particularly exceptional circumstances, ENISA will not receive the full content of the 72-hour notification. This is only the case where, in the 72-hour notification, the manufacturer actively marks that at least one of the conditions listed in points (a) to (c) of Article 16(2) applies. In such case, ENISA only receives partial information, until the receiving CSIRT discloses the full notification.</p>
|
||||
</dd>
|
||||
<dt>How does the platform ensure security?</dt>
|
||||
<dd>
|
||||
<p>ENISA is legally required to take appropriate measures to manage risks to the platform's security and must notify the CSIRTs Network and the Commission of any security incidents affecting the platform itself.</p>
|
||||
</dd>
|
||||
<dt>How is the CSIRTs network involved?</dt>
|
||||
<dd>
|
||||
<p>As provided in CRA Article 16 ENISA is engaging the CSIRTs Network in development and future testing of the CRA SRP.</p>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<p class="btn-back back-to-tax"><a href="https://www.enisa.europa.eu/taxonomy/term/519">Back to main topic</a></p>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</main>
|
||||
|
||||
<section class="subscribe-section" role="complementary">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-lg-3 subscribe-image"></div>
|
||||
<div class="col-md-9 col-lg-9 subscribe-wrapper">
|
||||
<h2>Subscribe</h2>
|
||||
<p><strong>Stay updated with ENISA!</strong> Sign up for email alerts on publications, events, vacancies,
|
||||
and more.</p>
|
||||
<p><a href="/alertservice" class="btn-all left">Sign up now</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="mt-auto enisa-footer">
|
||||
<div class="container">
|
||||
<div class="footer container">
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-lg-3">
|
||||
<div id="block-enisaweb-enisalogos">
|
||||
|
||||
|
||||
|
||||
<div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item">
|
||||
<div class="enisa-logo">
|
||||
<img src="/themes/custom/enisaweb/images/enisa-logo-white.svg"
|
||||
alt="ENISA, European Union Agency for Cybersecurity" width="220" height="150"
|
||||
class="align-left" loading="lazy">
|
||||
<p><em>A Trusted and Cyber Secure Europe</em></p>
|
||||
</div>
|
||||
<div class="agencies-network-logo">
|
||||
<img src="/themes/custom/enisaweb/images/agencies-network.png"
|
||||
alt="Agencies network logo" width="39" height="36" class="align-left"
|
||||
loading="lazy">
|
||||
<p><a href="https://agencies-network.europa.eu/index_en">EU Agencies Network</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="block-enisaweb-socialmedialinks" class="block-social-media-links block block-social-media-links-block">
|
||||
|
||||
<p>Follow us on</p>
|
||||
|
||||
|
||||
|
||||
<ul class="social-media-links--platforms platforms vertical">
|
||||
<li>
|
||||
<a class="social-media-link-icon--youtube" href="https://www.youtube.com/user/ENISAvideos" target="_blank" >
|
||||
<span class='fab fa-youtube fa-2x'></span>
|
||||
<span class="platform-name">Youtube</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="social-media-link-icon--twitter" href="https://x.com/enisa_eu" target="_blank" >
|
||||
<span class='fab fa-x-twitter fa-2x'></span>
|
||||
<span class="platform-name">X</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="social-media-link-icon--linkedin" href="https://www.linkedin.com/company/european-union-agency-for-cybersecurity-enisa/" target="_blank" >
|
||||
<span class='fab fa-linkedin fa-2x'></span>
|
||||
<span class="platform-name">LinkedIn</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="social-media-link-icon--facebook" href="https://www.facebook.com/ENISAEUAGENCY" target="_blank" >
|
||||
<span class='fab fa-facebook fa-2x'></span>
|
||||
<span class="platform-name">Facebook</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6 col-lg-3 border-left">
|
||||
<nav aria-labelledby="block-enisaweb-contactus-menu" id="block-enisaweb-contactus" class="block block-menu navigation menu--contact-us">
|
||||
|
||||
<p id="block-enisaweb-contactus-menu">Contact us</p>
|
||||
|
||||
|
||||
|
||||
<ul data-block="footer" class="nav navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a href="/about-enisa/contact/contact" class="nav-link" data-drupal-link-system-path="node/17344">Contacts</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/form/contact-form" class="nav-link" data-drupal-link-system-path="webform/contact_form">General queries</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/working-with-us/procurement" class="nav-link" data-drupal-link-system-path="node/17421">Public Procurement</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/form/media-inquiries" class="nav-link" data-drupal-link-system-path="webform/media_inquiries">Media inquiries</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
</nav>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6 col-lg-3 border-left">
|
||||
<div id="block-enisaweb-findoutaboutus" class="block block-system block-system-menu-blockfind-out-about-us">
|
||||
|
||||
<p>Find out about us</p>
|
||||
|
||||
|
||||
<ul data-block="footer" class="nav navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a href="/accessibility-statement" class="nav-link" data-drupal-link-system-path="node/18887">Accessibility</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/about-enisa/legal-notice" class="nav-link" data-drupal-link-system-path="node/17355">Legal Notice</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/about-enisa/data-protection/data-protection" class="nav-link" data-drupal-link-system-path="node/17346">Data Protection</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/about-enisa/cookies" class="nav-link" data-drupal-link-system-path="node/17345">Cookies</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/sitemap" target="_blank" class="nav-link" data-drupal-link-system-path="sitemap">Sitemap</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6 col-lg-3 border-left">
|
||||
<div id="block-enisaweb-pageofinterest" class="block block-system block-system-menu-blockpage-of-interest">
|
||||
|
||||
<p>Page of interest</p>
|
||||
|
||||
|
||||
<ul data-block="footer" class="nav navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a href="/publications" class="nav-link" data-drupal-link-system-path="publications">Publications </a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/press-office" class="nav-link" data-drupal-link-system-path="node/11033">Press Office</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/digital-tools" class="nav-link" data-drupal-link-system-path="digital-tools">Digital Tools</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/work-with-us" class="nav-link" data-drupal-link-system-path="node/11443">Working with us</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/about-enisa/public-access-to-documents" class="nav-link" data-drupal-link-system-path="node/17363">Public access to documents</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="copy container">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-12 col-lg-6 alignleft">
|
||||
<div id="block-enisaweb-copyrightfooter" class="block block-block-content block-block-content0f0b270e-cc5d-448c-a771-6cc7c0621340">
|
||||
|
||||
|
||||
|
||||
<div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item"><p>© 2026 by the European Union Agency for Cybersecurity</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-12 col-lg-6 alignright">
|
||||
<div id="block-enisaweb-enisadescriptionfooter" class="block block-block-content block-block-content2b4d8729-4032-4438-ba87-1a58fbc364db">
|
||||
|
||||
|
||||
|
||||
<div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item"><p>ENISA is an agency of the European Union</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p><a href="#" id="totop" class="totop"><span class="sr-only">Go to top</span></a></p>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<script type="application/json">{"utility":"piwik","siteID":"5847bf6f-3ce3-4800-8749-1c565b34b7b6","sitePath":["www.enisa.europa.eu"],"instance":"ec"}</script>
|
||||
<script type="application/json">{"utility":"cck","url":"\/about-enisa\/cookies"}</script>
|
||||
|
||||
<script type="application/json" data-drupal-selector="drupal-settings-json">{"path":{"baseUrl":"\/","pathPrefix":"","currentPath":"taxonomy\/term\/1317","currentPathIsAdmin":false,"isFront":false,"currentLanguage":"en"},"pluralDelimiter":"\u0003","suppressDeprecationErrors":true,"ckeditorAccordion":{"accordionStyle":{"collapseAll":1,"keepRowsOpen":0,"animateAccordionOpenAndClose":1,"openTabsWithHash":0,"allowHtmlInTitles":1}},"csp":{"nonce":"wb2Cd5rPB6d4U2ahlwtoVw"},"user":{"uid":0,"permissionsHash":"c1c359b7541ecd1c4f0e321882d2e1eba1197d85c8ad3b45b15aff5871a9e6d0"}}</script>
|
||||
<script src="/sites/default/files/js/js_RQ57ED_QkadU0X-0Q8nEhDKVEkkdta8wY8_icwESnuY.js?scope=footer&delta=0&language=en&theme=enisaweb&include=eJxlj0EOgzAMBD-EG6kfQiYxEOrYNHGE8vuCWkql3mZn97IYgilKc_iB25hVrPMPCtE09-i95hBV3JfeE5LQkcSCGw3uBFgKMBoVu7qJdUCGYo2jTJdfnpVyA98805-tEUyrn2Gt4uefuqjcO6V-D6bKxYVcV-TbmaEkzMaKgXK3UZ9owkRS3ag57Ss4BBwGzhsvrCdlDA"></script>
|
||||
<script src="https://static.addtoany.com/menu/page.js" defer></script>
|
||||
<script src="/sites/default/files/js/js_kppnwVGSNMO58MOFQJXEYZNwpiIbQ8uG_I-yvuC5qBs.js?scope=footer&delta=2&language=en&theme=enisaweb&include=eJxlj0EOgzAMBD-EG6kfQiYxEOrYNHGE8vuCWkql3mZn97IYgilKc_iB25hVrPMPCtE09-i95hBV3JfeE5LQkcSCGw3uBFgKMBoVu7qJdUCGYo2jTJdfnpVyA98805-tEUyrn2Gt4uefuqjcO6V-D6bKxYVcV-TbmaEkzMaKgXK3UZ9owkRS3ag57Ss4BBwGzhsvrCdlDA"></script>
|
||||
<script src="/modules/contrib/ckeditor_accordion/js/accordion.frontend.min.js?telxj6"></script>
|
||||
<script src="/sites/default/files/js/js_fPzrD9aZOLJS9JI2GLgD7Zs-CzoWHT18p8hYIEuW9h4.js?scope=footer&delta=4&language=en&theme=enisaweb&include=eJxlj0EOgzAMBD-EG6kfQiYxEOrYNHGE8vuCWkql3mZn97IYgilKc_iB25hVrPMPCtE09-i95hBV3JfeE5LQkcSCGw3uBFgKMBoVu7qJdUCGYo2jTJdfnpVyA98805-tEUyrn2Gt4uefuqjcO6V-D6bKxYVcV-TbmaEkzMaKgXK3UZ9owkRS3ag57Ss4BBwGzhsvrCdlDA"></script>
|
||||
<script src="https://webtools.europa.eu/load.js" defer></script>
|
||||
<script src="/sites/default/files/js/js_Pj1gX-gXRHcdCBDI1-WO0jTP3o3GK7ZZPT2TZokpFjY.js?scope=footer&delta=6&language=en&theme=enisaweb&include=eJxlj0EOgzAMBD-EG6kfQiYxEOrYNHGE8vuCWkql3mZn97IYgilKc_iB25hVrPMPCtE09-i95hBV3JfeE5LQkcSCGw3uBFgKMBoVu7qJdUCGYo2jTJdfnpVyA98805-tEUyrn2Gt4uefuqjcO6V-D6bKxYVcV-TbmaEkzMaKgXK3UZ9owkRS3ag57Ss4BBwGzhsvrCdlDA"></script>
|
||||
|
||||
<script async="" src="/themes/custom/enisaweb/js/application.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+40
-40
@@ -10,7 +10,7 @@
|
||||
"dependencies": {
|
||||
"framer-motion": "^11.15.0",
|
||||
"lucide-react": "^0.468.0",
|
||||
"next": "^15.1.0",
|
||||
"next": "^15.5.16",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
@@ -552,15 +552,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "15.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.12.tgz",
|
||||
"integrity": "sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg==",
|
||||
"version": "15.5.16",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.16.tgz",
|
||||
"integrity": "sha512-9QMKolCl+JnJtaRAQSXy4RQrhgfe8W7/G1+Hl3QSB/HZY7zQMzTwPDdTRwwio8BS96ps1MHpHhbS8qxoNV3JIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "15.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.12.tgz",
|
||||
"integrity": "sha512-RnRjBtH8S8eXCpUNkQ+543DUc7ys8y15VxmFU9HRqlo9BG3CcBUiwNtF8SNoi2xvGCVJq1vl2yYq+3oISBS0Zg==",
|
||||
"version": "15.5.16",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.16.tgz",
|
||||
"integrity": "sha512-wzdER4JZj+31vNkhaZ1Ght3IsNI8DMwj7VqadfIOqJB5sh8FiOqNSopYADQn6mgEPomzDd/DHqBcfo2fmVMYtg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -574,9 +574,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "15.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.12.tgz",
|
||||
"integrity": "sha512-nqa9/7iQlboF1EFtNhWxQA0rQstmYRSBGxSM6g3GxvxHxcoeqVXfGNr9stJOme674m2V7r4E3+jEhhGvSQhJRA==",
|
||||
"version": "15.5.16",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.16.tgz",
|
||||
"integrity": "sha512-PPTo+cvcanxkuDEuDyZGk28ntmu0WjfkxqlG7hw9Mhsiribs4x1C6h2Culn0cJKqsne1gFjjZRK3ax7WYlSxgg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -590,9 +590,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "15.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.12.tgz",
|
||||
"integrity": "sha512-dCzAjqhDHwmoB2M4eYfVKqXs99QdQxNQVpftvP1eGVppamXh/OkDAwV737Zr0KPXEqRUMN4uCjh6mjO+XtF3Mw==",
|
||||
"version": "15.5.16",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.16.tgz",
|
||||
"integrity": "sha512-Jl0IL9P7S8uNl5oI1TqrQmfmLp7OqjWM58000pVnUVIsHrvPP6m9QDW/uNWYUbmd+8IYvc6MTeZKICstBMBpew==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -606,9 +606,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "15.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.12.tgz",
|
||||
"integrity": "sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw==",
|
||||
"version": "15.5.16",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.16.tgz",
|
||||
"integrity": "sha512-Zf0BIqv/o5uOWfyRkzgGhyV2Tky7HLt0bG+w7XWdaU1JpyX0tltM3TrSfa/Y9c597SJG4CzN47+u2InhgZZ4vg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -622,9 +622,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "15.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.12.tgz",
|
||||
"integrity": "sha512-jSLvgdRRL/hrFAPqEjJf1fFguC719kmcptjNVDJl26BnJIpjL3KH5h6mzR4mAweociLQaqvt4UyzfbFjgAdDcw==",
|
||||
"version": "15.5.16",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.16.tgz",
|
||||
"integrity": "sha512-HCDDU1TRLeUDV180QQTWrs5Oa4lIcI7XH9nF0UVUVmYLN/boZ6LqyFtm3814gc1fv+lOVyKaw5B6bVC9BpXTSQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -638,9 +638,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "15.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.12.tgz",
|
||||
"integrity": "sha512-/uaF0WfmYqQgLfPmN6BvULwxY0dufI2mlN2JbOKqqceZh1G4hjREyi7pg03zjfyS6eqNemHAZPSoP84x17vo6w==",
|
||||
"version": "15.5.16",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.16.tgz",
|
||||
"integrity": "sha512-kvXUY1dn5wxKuMkXxQRUbPjEnKxW1PR9uKOm0zpIpj3574+cFfaePhYFmBVtrOuwt+w34OdDzNaJr5Iixf+HBQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -654,9 +654,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "15.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.12.tgz",
|
||||
"integrity": "sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg==",
|
||||
"version": "15.5.16",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.16.tgz",
|
||||
"integrity": "sha512-zpOQuF+eyENMXRjglp2hZCIrUjTdO37suEBnDn1mX4PXSuetXZDMLpjKOh4dYSw3SiDTnOoOUwBl5i5Elr6nnQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -670,9 +670,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "15.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.12.tgz",
|
||||
"integrity": "sha512-Z1Dh6lhFkxvBDH1FoW6OU/L6prYwPSlwjLiZkExIAh8fbP6iI/M7iGTQAJPYJ9YFlWobCZ1PHbchFhFYb2ADkw==",
|
||||
"version": "15.5.16",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.16.tgz",
|
||||
"integrity": "sha512-LnwKYpiSmIzXlTq76hMeeIzZoDcFwu848p6H+QBkGFJIbZphgzNUPdHruJcHM/bFnaFeco0l1Frie5I27VKglA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1272,12 +1272,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "15.5.12",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.5.12.tgz",
|
||||
"integrity": "sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==",
|
||||
"version": "15.5.16",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.5.16.tgz",
|
||||
"integrity": "sha512-aZExBk/V6JCu3NCFc90twdj9L/M3y0+ukeQwUAZbOiqRhAX+h2oMEa0NZFhcpj6HYRYjVS3V2/3xvyOpNnmw7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "15.5.12",
|
||||
"@next/env": "15.5.16",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
"postcss": "8.4.31",
|
||||
@@ -1290,14 +1290,14 @@
|
||||
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "15.5.12",
|
||||
"@next/swc-darwin-x64": "15.5.12",
|
||||
"@next/swc-linux-arm64-gnu": "15.5.12",
|
||||
"@next/swc-linux-arm64-musl": "15.5.12",
|
||||
"@next/swc-linux-x64-gnu": "15.5.12",
|
||||
"@next/swc-linux-x64-musl": "15.5.12",
|
||||
"@next/swc-win32-arm64-msvc": "15.5.12",
|
||||
"@next/swc-win32-x64-msvc": "15.5.12",
|
||||
"@next/swc-darwin-arm64": "15.5.16",
|
||||
"@next/swc-darwin-x64": "15.5.16",
|
||||
"@next/swc-linux-arm64-gnu": "15.5.16",
|
||||
"@next/swc-linux-arm64-musl": "15.5.16",
|
||||
"@next/swc-linux-x64-gnu": "15.5.16",
|
||||
"@next/swc-linux-x64-musl": "15.5.16",
|
||||
"@next/swc-win32-arm64-msvc": "15.5.16",
|
||||
"@next/swc-win32-x64-msvc": "15.5.16",
|
||||
"sharp": "^0.34.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"dependencies": {
|
||||
"framer-motion": "^11.15.0",
|
||||
"lucide-react": "^0.468.0",
|
||||
"next": "^15.1.0",
|
||||
"next": "^15.5.16",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
.next/
|
||||
.env.local
|
||||
*.tsbuildinfo
|
||||
@@ -0,0 +1,27 @@
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
FROM base AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm ci
|
||||
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN mkdir -p public
|
||||
RUN npm run build
|
||||
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
CMD ["node", "server.js"]
|
||||
@@ -0,0 +1,61 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
|
||||
const SYSTEM_PROMPT = `Du bist der BreakPilot Compliance Agent — ein technischer Berater fuer die BreakPilot Plattform.
|
||||
|
||||
Kernbotschaften:
|
||||
- BreakPilot ist eine deterministische Regulatory Engineering Plattform
|
||||
- Keine Halluzinationen: Jedes Ergebnis verweist auf eine konkrete Rechtsquelle
|
||||
- EU-souveraen: Kein US-Cloud-Anbieter, on-premise deploybar
|
||||
- 294.000+ atomare Controls aus 380+ Rechtsquellen
|
||||
- Unterstuetzte Regulierungen: DSGVO, NIS2, EU AI Act, Maschinenverordnung, TDDDG, DORA, BSI IT-Grundschutz
|
||||
|
||||
Sage NIEMALS "ChatGPT fuer CE" oder "KI-Assistent". Sage stattdessen "Deterministic Analysis" oder "Compliance Engine".
|
||||
Antworte auf Deutsch, professionell und praezise. Halte Antworten kurz (max 200 Woerter).`
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { message, history } = await req.json()
|
||||
|
||||
// Placeholder: In production, connect to the actual Compliance Agent API
|
||||
// For now, return a static response as a stream
|
||||
const responses: Record<string, string> = {
|
||||
'default': `Vielen Dank fuer Ihre Frage.
|
||||
|
||||
BreakPilot ist eine deterministische Regulatory Engineering Plattform. Im Unterschied zu LLM-basierten Tools analysieren wir regulatorische Anforderungen regelbasiert — jedes Ergebnis verweist auf eine konkrete Rechtsquelle (Artikel, Absatz, Erwaegungs\u00ADgrund).
|
||||
|
||||
Unsere Plattform umfasst:
|
||||
- 294.000+ atomare Compliance Controls
|
||||
- 380+ Rechtsquellen (DSGVO, NIS2, AI Act, Maschinenverordnung u.a.)
|
||||
- Vollstaendiger Decision Trail: Rechtsquelle → Obligation → Control → Massnahme
|
||||
- EU-souveraene Infrastruktur ohne US-Cloud-Abhaengigkeit
|
||||
|
||||
Fuer eine persoenliche Demo kontaktieren Sie uns unter info@breakpilot.ai.`,
|
||||
}
|
||||
|
||||
void history
|
||||
void SYSTEM_PROMPT
|
||||
|
||||
const responseText = responses['default']
|
||||
|
||||
// Simulate streaming by sending chunks
|
||||
const encoder = new TextEncoder()
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const words = responseText.split(' ')
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
const chunk = (i === 0 ? '' : ' ') + words[i]
|
||||
controller.enqueue(encoder.encode(chunk))
|
||||
await new Promise(resolve => setTimeout(resolve, 30))
|
||||
}
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
|
||||
void message
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const res = await fetch(`${BACKEND_URL}/consent/${id}`, {
|
||||
method: 'DELETE',
|
||||
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({ error: 'Consent service not reachable' }, { status: 503 })
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
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'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
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',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': TENANT_ID,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
|
||||
const resBody = await res.text()
|
||||
return new NextResponse(resBody, {
|
||||
status: res.status,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Consent proxy error:', err)
|
||||
return NextResponse.json({ error: 'Consent service not reachable' }, { status: 503 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* POST /api/scan/start
|
||||
* Proxy to compliance backend /api/compliance/agent/saving-scan/start.
|
||||
*
|
||||
* Body: { url: string; email: string; consent?: boolean }
|
||||
*
|
||||
* Server-side proxy avoids cross-origin POST from breakpilot.ai to
|
||||
* api-dev.breakpilot.ai — same-origin from the browser, secure egress
|
||||
* from the Next.js server. Backend handles rate-limit + TDM + lead-DB.
|
||||
*/
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL =
|
||||
process.env.COMPLIANCE_BACKEND_URL || 'https://api-dev.breakpilot.ai'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
let body: unknown
|
||||
try {
|
||||
body = await request.json()
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: 'Body muss JSON sein' }, { status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${BACKEND_URL}/api/compliance/agent/saving-scan/start`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(20000),
|
||||
},
|
||||
)
|
||||
const data = await res.json().catch(() => ({}))
|
||||
return NextResponse.json(data, { status: res.status })
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: 'Backend nicht erreichbar' }, { status: 503 },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* GET /api/scan/status/<checkId>
|
||||
* Proxy to compliance backend /api/compliance/agent/compliance-check/<id>.
|
||||
*
|
||||
* Polled every ~5s by the savings-scan page until status==completed/failed.
|
||||
*/
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL =
|
||||
process.env.COMPLIANCE_BACKEND_URL || 'https://api-dev.breakpilot.ai'
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ checkId: string }> },
|
||||
) {
|
||||
const { checkId } = await params
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${BACKEND_URL}/api/compliance/agent/compliance-check/${checkId}`,
|
||||
{ signal: AbortSignal.timeout(15000) },
|
||||
)
|
||||
const data = await res.json().catch(() => ({}))
|
||||
return NextResponse.json(data, { status: res.status })
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: 'Backend nicht erreichbar' }, { status: 503 },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import Navbar from '@/components/layout/Navbar'
|
||||
import Footer from '@/components/layout/Footer'
|
||||
import ChatFAB from '@/components/layout/ChatFAB'
|
||||
import ArchitectureSection from '@/components/sections/ArchitectureSection'
|
||||
import SovereignSection from '@/components/sections/SovereignSection'
|
||||
|
||||
export default function ArchitekturPage() {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="pt-16" />
|
||||
<ArchitectureSection />
|
||||
<SovereignSection />
|
||||
<Footer />
|
||||
<ChatFAB />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import Navbar from '@/components/layout/Navbar'
|
||||
import Footer from '@/components/layout/Footer'
|
||||
import ChatFAB from '@/components/layout/ChatFAB'
|
||||
import PageHeader from '@/components/ui/PageHeader'
|
||||
import CEFlowSection from '@/components/sections/CEFlowSection'
|
||||
|
||||
export default function CEProzessPage() {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<PageHeader
|
||||
tag="CE-PROZESS"
|
||||
title="Von der Maschinenbeschreibung"
|
||||
titleHighlight="zur CE-Akte."
|
||||
subtitle="6 deterministische Schritte — vom Textfeld zur vollständigen Technischen Dokumentation nach MVO 2023/1230."
|
||||
/>
|
||||
</div>
|
||||
<CEFlowSection />
|
||||
</main>
|
||||
<Footer />
|
||||
<ChatFAB />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function DatenschutzPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-enterprise-dark text-white">
|
||||
<div className="max-w-3xl mx-auto px-4 py-24">
|
||||
<Link href="/" className="text-sm text-white/40 hover:text-white/60 transition-colors mb-8 inline-block">
|
||||
← Zurueck zur Startseite
|
||||
</Link>
|
||||
|
||||
<h1 className="text-4xl font-bold mb-8">Datenschutzerklaerung</h1>
|
||||
|
||||
<div className="space-y-6 text-white/60 text-sm leading-relaxed">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white mb-2">1. Verantwortlicher</h2>
|
||||
<p>BreakPilot GmbH (i.Gr.)</p>
|
||||
<p>[Adresse wird nach Gruendung ergaenzt]</p>
|
||||
<p>E-Mail: datenschutz@breakpilot.ai</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white mb-2">2. Datenschutzbeauftragter</h2>
|
||||
<p>
|
||||
[Wird nach Gruendung benannt]<br />
|
||||
E-Mail: datenschutz@breakpilot.ai
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white mb-2">3. Hosting</h2>
|
||||
<p>
|
||||
Diese Website wird auf Servern der Hetzner Online GmbH in Deutschland gehostet.
|
||||
Es findet kein Drittlandtransfer fuer das Hosting statt.
|
||||
</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>
|
||||
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white mb-2">4. Cookies und Einwilligungsverwaltung</h2>
|
||||
<p>
|
||||
Diese Website verwendet ein Consent-Management-System (BreakPilot CMP), um Ihre Cookie-Einstellungen
|
||||
zu verwalten. Beim ersten Besuch wird Ihnen ein Cookie-Banner angezeigt, ueber das Sie Ihre
|
||||
Einwilligung fuer verschiedene Cookie-Kategorien erteilen oder verweigern koennen.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
<span className="text-white font-medium">Rechtsgrundlage:</span> Art. 6 Abs. 1 lit. a DSGVO (Einwilligung)
|
||||
i.V.m. Paragraph 25 Abs. 1 TDDDG fuer nicht-essenzielle Cookies.
|
||||
Fuer technisch notwendige Cookies: Paragraph 25 Abs. 2 TDDDG (unbedingt erforderlich).
|
||||
</p>
|
||||
<h3 className="text-sm font-semibold text-white mt-4 mb-2">Eingesetzte Cookies</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs border border-white/10">
|
||||
<thead>
|
||||
<tr className="border-b border-white/10 text-white/80">
|
||||
<th className="text-left px-3 py-2">Name</th>
|
||||
<th className="text-left px-3 py-2">Anbieter</th>
|
||||
<th className="text-left px-3 py-2">Zweck</th>
|
||||
<th className="text-left px-3 py-2">Speicherdauer</th>
|
||||
<th className="text-left px-3 py-2">Typ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/5">
|
||||
<tr>
|
||||
<td className="px-3 py-2 font-mono">bp_consent</td>
|
||||
<td className="px-3 py-2">BreakPilot (First-Party)</td>
|
||||
<td className="px-3 py-2">Speichert Ihre Cookie-Einwilligung</td>
|
||||
<td className="px-3 py-2">12 Monate</td>
|
||||
<td className="px-3 py-2">Notwendig</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p className="mt-2">
|
||||
Sie koennen Ihre Einwilligung jederzeit widerrufen, indem Sie den Link
|
||||
"Cookie-Einstellungen" im Seitenfuss klicken. Der Widerruf ist gemaess Art. 7 Abs. 3 DSGVO
|
||||
genauso einfach wie die Erteilung der Einwilligung.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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,
|
||||
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. Schriften</h2>
|
||||
<p>
|
||||
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>
|
||||
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white mb-2">7. Zwecke der Verarbeitung</h2>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
<li>Bereitstellung und Betrieb der Website (Art. 6 Abs. 1 lit. f DSGVO)</li>
|
||||
<li>Verwaltung Ihrer Cookie-Einwilligungen (Art. 6 Abs. 1 lit. c DSGVO — Nachweispflicht Art. 7 Abs. 1 DSGVO)</li>
|
||||
<li>Kontaktaufnahme per E-Mail (Art. 6 Abs. 1 lit. b DSGVO — vorvertragliche Massnahmen)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<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>
|
||||
</ul>
|
||||
<p className="mt-1">Schriftarten werden lokal gehostet — kein Drittanbieter-Transfer.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white mb-2">9. Speicherdauer</h2>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
<li>Server-Logfiles: 7 Tage</li>
|
||||
<li>Cookie-Einwilligung (bp_consent): 12 Monate</li>
|
||||
<li>Consent-Nachweis (Backend): 13 Monate (CNIL-Empfehlung)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white mb-2">10. Ihre Rechte</h2>
|
||||
<p>Sie haben gegenueber uns folgende Rechte bezueglich Ihrer personenbezogenen Daten:</p>
|
||||
<ul className="list-disc list-inside space-y-1 mt-2">
|
||||
<li>Recht auf Auskunft (Art. 15 DSGVO)</li>
|
||||
<li>Recht auf Berichtigung (Art. 16 DSGVO)</li>
|
||||
<li>Recht auf Loeschung (Art. 17 DSGVO)</li>
|
||||
<li>Recht auf Einschraenkung der Verarbeitung (Art. 18 DSGVO)</li>
|
||||
<li>Recht auf Datenuebertragbarkeit (Art. 20 DSGVO)</li>
|
||||
<li>Recht auf Widerspruch (Art. 21 DSGVO)</li>
|
||||
</ul>
|
||||
<p className="mt-2">
|
||||
Zur Ausuebung Ihrer Rechte wenden Sie sich an: datenschutz@breakpilot.ai
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white mb-2">11. Beschwerderecht</h2>
|
||||
<p>
|
||||
Sie haben das Recht, sich bei einer Aufsichtsbehoerde zu beschweren.
|
||||
Zustaendig fuer uns ist:
|
||||
</p>
|
||||
<p className="mt-1">
|
||||
Die Landesbeauftragte fuer den Datenschutz Niedersachsen<br />
|
||||
Prinzenstrasse 5, 30159 Hannover<br />
|
||||
<a href="https://www.lfd.niedersachsen.de" className="text-accent-electric hover:underline" target="_blank" rel="noopener noreferrer">
|
||||
www.lfd.niedersachsen.de
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
/* 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;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--bg-primary: #0a0a1a;
|
||||
--bg-secondary: #06060f;
|
||||
--bg-card: rgba(255, 255, 255, 0.06);
|
||||
--bg-card-hover: rgba(255, 255, 255, 0.10);
|
||||
--border-subtle: rgba(255, 255, 255, 0.08);
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: rgba(255, 255, 255, 0.6);
|
||||
--text-muted: rgba(255, 255, 255, 0.4);
|
||||
--accent-electric: #3b82f6;
|
||||
--accent-signal: #22c55e;
|
||||
--accent-indigo: #6366f1;
|
||||
--accent-purple: #a78bfa;
|
||||
--glass-bg: rgba(255, 255, 255, 0.06);
|
||||
--glass-border: rgba(255, 255, 255, 0.08);
|
||||
--glass-hover: rgba(255, 255, 255, 0.10);
|
||||
--scrollbar-thumb: rgba(255, 255, 255, 0.15);
|
||||
--scrollbar-hover: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-family: 'Inter', 'Plus Jakarta Sans', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: rgba(59, 130, 246, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thumb);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--scrollbar-hover);
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.glass {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.glass-hover:hover {
|
||||
background: var(--glass-hover);
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, var(--accent-electric), var(--accent-indigo), var(--accent-purple));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.gradient-text-signal {
|
||||
background: linear-gradient(135deg, var(--accent-signal), #34d399, var(--accent-electric));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.text-shadow-glow {
|
||||
text-shadow: 0 0 40px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.glow-blue {
|
||||
box-shadow: 0 0 30px rgba(59, 130, 246, 0.15), 0 0 60px rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
.glow-signal {
|
||||
box-shadow: 0 0 20px rgba(34, 197, 94, 0.15);
|
||||
}
|
||||
|
||||
.mono-label {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.enterprise-grid {
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
|
||||
background-size: 60px 60px;
|
||||
}
|
||||
|
||||
.section-alt {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
/* === Light Mode === */
|
||||
.theme-light {
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f8fafc;
|
||||
--bg-card: #f8fafc;
|
||||
--bg-card-hover: #f1f5f9;
|
||||
--border-subtle: #e2e8f0;
|
||||
--text-primary: #0f172a;
|
||||
--text-secondary: #334155;
|
||||
--text-muted: #64748b;
|
||||
--accent-electric: #2563eb;
|
||||
--accent-signal: #059669;
|
||||
--accent-indigo: #4f46e5;
|
||||
--accent-purple: #7c3aed;
|
||||
--glass-bg: #f8fafc;
|
||||
--glass-border: #e2e8f0;
|
||||
--glass-hover: #f1f5f9;
|
||||
--scrollbar-thumb: #cbd5e1;
|
||||
--scrollbar-hover: #94a3b8;
|
||||
}
|
||||
|
||||
.theme-light body {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.theme-light ::selection {
|
||||
background: rgba(37, 99, 235, 0.15);
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
/* Text overrides */
|
||||
.theme-light .text-white { color: #0f172a; }
|
||||
.theme-light .text-white\/80 { color: #1e293b; }
|
||||
.theme-light .text-white\/70 { color: #334155; }
|
||||
.theme-light .text-white\/60 { color: #475569; }
|
||||
.theme-light .text-white\/50 { color: #64748b; }
|
||||
.theme-light .text-white\/40 { color: #64748b; }
|
||||
.theme-light .text-white\/30 { color: #94a3b8; }
|
||||
.theme-light .text-white\/20 { color: #cbd5e1; }
|
||||
|
||||
/* Card backgrounds */
|
||||
.theme-light .bg-white\/\[0\.06\],
|
||||
.theme-light .bg-white\/\[0\.04\],
|
||||
.theme-light .bg-white\/\[0\.03\] {
|
||||
background-color: #f8fafc !important;
|
||||
}
|
||||
|
||||
.theme-light .border-white\/\[0\.08\],
|
||||
.theme-light .border-white\/\[0\.06\],
|
||||
.theme-light .border-white\/10 {
|
||||
border-color: #e2e8f0 !important;
|
||||
}
|
||||
|
||||
/* No blur in light mode */
|
||||
.theme-light .backdrop-blur-xl,
|
||||
.theme-light .backdrop-blur {
|
||||
backdrop-filter: none !important;
|
||||
-webkit-backdrop-filter: none !important;
|
||||
}
|
||||
|
||||
/* Navbar */
|
||||
.theme-light .bg-enterprise-dark\/80 {
|
||||
background-color: rgba(255, 255, 255, 0.9) !important;
|
||||
}
|
||||
|
||||
/* Enterprise grid */
|
||||
.theme-light .enterprise-grid {
|
||||
background-image:
|
||||
linear-gradient(rgba(0, 0, 0, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0, 0, 0, 0.03) 1px, transparent 1px);
|
||||
}
|
||||
|
||||
/* Gradient text — stronger in light */
|
||||
.theme-light .gradient-text {
|
||||
background: linear-gradient(135deg, #2563eb, #4f46e5, #7c3aed) !important;
|
||||
-webkit-background-clip: text !important;
|
||||
background-clip: text !important;
|
||||
}
|
||||
|
||||
.theme-light .gradient-text-signal {
|
||||
background: linear-gradient(135deg, #059669, #10b981, #2563eb) !important;
|
||||
-webkit-background-clip: text !important;
|
||||
background-clip: text !important;
|
||||
}
|
||||
|
||||
/* Mono label */
|
||||
.theme-light .mono-label { color: #64748b; }
|
||||
|
||||
/* Status dots */
|
||||
.theme-light .text-shadow-glow { text-shadow: none; }
|
||||
.theme-light .glow-blue { box-shadow: 0 4px 14px -3px rgba(37, 99, 235, 0.15); }
|
||||
|
||||
/* Accent backgrounds */
|
||||
.theme-light .bg-accent-electric\/10 { background-color: #eff6ff !important; }
|
||||
.theme-light .bg-accent-electric\/5 { background-color: #f0f9ff !important; }
|
||||
.theme-light .bg-accent-indigo\/10 { background-color: #eef2ff !important; }
|
||||
.theme-light .bg-accent-indigo\/5 { background-color: #eef2ff !important; }
|
||||
.theme-light .bg-accent-purple\/10 { background-color: #faf5ff !important; }
|
||||
.theme-light .bg-accent-purple\/\[0\.04\] { background-color: #faf5ff !important; }
|
||||
.theme-light .bg-amber-500\/10 { background-color: #fefce8 !important; }
|
||||
|
||||
/* Colored borders */
|
||||
.theme-light .border-red-500\/20 { border-color: #fecaca !important; }
|
||||
.theme-light .border-red-500\/15 { border-color: #fecaca !important; }
|
||||
.theme-light .border-green-500\/20 { border-color: #bbf7d0 !important; }
|
||||
.theme-light .border-green-500\/15 { border-color: #bbf7d0 !important; }
|
||||
.theme-light .border-accent-electric\/30 { border-color: #bfdbfe !important; }
|
||||
.theme-light .border-accent-indigo\/30 { border-color: #c7d2fe !important; }
|
||||
.theme-light .border-accent-purple\/30 { border-color: #ddd6fe !important; }
|
||||
.theme-light .border-accent-purple\/20 { border-color: #e9d5ff !important; }
|
||||
.theme-light .border-accent-electric\/20 { border-color: #bfdbfe !important; }
|
||||
|
||||
/* Colored text */
|
||||
.theme-light .text-red-400 { color: #dc2626 !important; }
|
||||
.theme-light .text-green-400 { color: #059669 !important; }
|
||||
.theme-light .text-amber-400 { color: #d97706 !important; }
|
||||
.theme-light .text-accent-electric { color: #2563eb !important; }
|
||||
.theme-light .text-accent-indigo { color: #4f46e5 !important; }
|
||||
.theme-light .text-accent-purple { color: #7c3aed !important; }
|
||||
.theme-light .text-accent-signal\/80 { color: #059669 !important; }
|
||||
|
||||
/* Colored backgrounds for tinted cards */
|
||||
.theme-light .bg-red-500\/\[0\.04\] { background-color: #fef2f2 !important; }
|
||||
.theme-light .bg-red-500\/\[0\.03\] { background-color: #fef2f2 !important; }
|
||||
.theme-light .bg-green-500\/\[0\.04\] { background-color: #f0fdf4 !important; }
|
||||
.theme-light .bg-green-500\/\[0\.03\] { background-color: #f0fdf4 !important; }
|
||||
.theme-light .bg-red-500\/10 { background-color: #fef2f2 !important; }
|
||||
.theme-light .bg-blue-500\/10 { background-color: #eff6ff !important; }
|
||||
.theme-light .bg-green-500\/10 { background-color: #f0fdf4 !important; }
|
||||
|
||||
/* Terminal / code blocks */
|
||||
.theme-light .bg-enterprise-darker {
|
||||
background-color: #f1f5f9 !important;
|
||||
}
|
||||
|
||||
/* Chat panel */
|
||||
.theme-light .bg-black\/90 {
|
||||
background-color: #ffffff !important;
|
||||
border: 1px solid #e2e8f0 !important;
|
||||
}
|
||||
.theme-light .bg-black\/60 {
|
||||
background-color: rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
/* Hover states */
|
||||
.theme-light .hover\:bg-white\/\[0\.06\]:hover,
|
||||
.theme-light .hover\:bg-white\/\[0\.04\]:hover { background-color: #f1f5f9 !important; }
|
||||
.theme-light .hover\:bg-white\/20:hover { background-color: #e2e8f0 !important; }
|
||||
|
||||
/* Shadows */
|
||||
.theme-light .shadow-lg { box-shadow: 0 4px 6px -1px rgba(0,0,0,0.06) !important; }
|
||||
.theme-light .shadow-2xl { box-shadow: 0 10px 25px -5px rgba(0,0,0,0.08) !important; }
|
||||
|
||||
/* Table */
|
||||
.theme-light .hover\:bg-white\/\[0\.02\]:hover { background-color: #f8fafc !important; }
|
||||
.theme-light .bg-white\/\[0\.02\] { background-color: #f8fafc !important; }
|
||||
@@ -0,0 +1,10 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="32" height="32" rx="8" fill="url(#g)"/>
|
||||
<text x="16" y="22" text-anchor="middle" font-family="Inter, sans-serif" font-weight="700" font-size="18" fill="white">B</text>
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0" y1="0" x2="32" y2="32">
|
||||
<stop stop-color="#3b82f6"/>
|
||||
<stop offset="1" stop-color="#6366f1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 468 B |
@@ -0,0 +1,62 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function ImpressumPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-enterprise-dark text-white">
|
||||
<div className="max-w-3xl mx-auto px-4 py-24">
|
||||
<Link href="/" className="text-sm text-white/40 hover:text-white/60 transition-colors mb-8 inline-block">
|
||||
← Zurueck zur Startseite
|
||||
</Link>
|
||||
|
||||
<h1 className="text-4xl font-bold mb-8">Impressum</h1>
|
||||
|
||||
<div className="space-y-6 text-white/60 text-sm">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white mb-2">Angaben gemaess Paragraph 5 TMG</h2>
|
||||
<p>BreakPilot GmbH (i.Gr.)</p>
|
||||
<p>[Adresse wird nach Gruendung ergaenzt]</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white mb-2">Kontakt</h2>
|
||||
<p>E-Mail: info@breakpilot.ai</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white mb-2">Verantwortlich fuer den Inhalt nach Paragraph 18 Abs. 2 MStV</h2>
|
||||
<p>[Wird nach Gruendung ergaenzt]</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white mb-2">EU-Streitschlichtung</h2>
|
||||
<p>
|
||||
Die Europaeische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit.
|
||||
Unsere E-Mail-Adresse finden Sie oben im Impressum.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white mb-2">Quellen und Lizenzen der Compliance-Inhalte</h2>
|
||||
<p>
|
||||
Die BreakPilot Compliance-Plattform stuetzt sich auf rund 315.000 klassifizierte
|
||||
Controls aus oeffentlichen Quellen: EU-Recht (EUR-Lex), deutsches und oesterreichisches
|
||||
Bundesrecht, US Federal Code (OSHA, NIST), Behoerden-Leitfaeden (ENISA, EDPB, BAuA),
|
||||
freie Sicherheits-Frameworks unter CC-BY-SA (OWASP-Familie, OECD AI Principles) und
|
||||
eigene Texte. Jeder Control traegt eine deterministische Lizenzregel (R1 woertlich, R2
|
||||
mit Attribution, R3 nur Identifier-Verweis), die das Render-Verhalten in Berichten,
|
||||
PDF-Exports und Frontend steuert. Die vollstaendige Quellenliste mit Aufschluesselung
|
||||
pro Lizenzklasse ist im SDK unter <code className="text-white/80">/sdk/licenses</code>
|
||||
eingesehen. Pflicht-Attributionen fuer R2-Quellen erscheinen automatisch im
|
||||
Quellen-Footer jedes generierten Berichts.
|
||||
</p>
|
||||
<p className="mt-2 text-xs">
|
||||
Hinweis: Dieser Pauschalvermerk ersetzt nicht die werknahe Attribution. Jede
|
||||
Berichts- oder Frontend-Ausgabe nennt die konkret verwendeten Quellen direkt am
|
||||
Werk (Auto-Footer in PDFs, Inline-Citation im Frontend).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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 = {
|
||||
title: 'BreakPilot | Deterministic Regulatory Engineering Platform',
|
||||
description: 'Deterministische regulatorische Analyse für Maschinenbau, Fertigung und kritische Infrastruktur. Keine Halluzinationen. Keine US-Cloud. Volle Nachvollziehbarkeit.',
|
||||
keywords: ['Compliance', 'Regulatory Engineering', 'CE-Kennzeichnung', 'Maschinenverordnung', 'DSGVO', 'NIS2', 'AI Act', 'Sovereign AI', 'CRA', 'OTA'],
|
||||
robots: { index: true, follow: true },
|
||||
openGraph: {
|
||||
title: 'BreakPilot | Deterministic Regulatory Engineering',
|
||||
description: 'Deterministische regulatorische Analyse. Keine Halluzinationen. Keine Compliance-Lücken.',
|
||||
type: 'website',
|
||||
locale: 'de_DE',
|
||||
},
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="de">
|
||||
<body className="antialiased">
|
||||
<AppProvider>
|
||||
{children}
|
||||
<ConsentBanner />
|
||||
<ScriptManager />
|
||||
</AppProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import Navbar from '@/components/layout/Navbar'
|
||||
import Footer from '@/components/layout/Footer'
|
||||
import ChatFAB from '@/components/layout/ChatFAB'
|
||||
import HeroSection from '@/components/sections/HeroSection'
|
||||
import ProblemFlowSection from '@/components/sections/ProblemFlowSection'
|
||||
import SavingsSection from '@/components/sections/SavingsSection'
|
||||
import UseCaseCards from '@/components/sections/UseCaseCards'
|
||||
import TrustBar from '@/components/sections/TrustBar'
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main>
|
||||
<HeroSection />
|
||||
<ProblemFlowSection />
|
||||
<SavingsSection />
|
||||
<UseCaseCards />
|
||||
<TrustBar />
|
||||
</main>
|
||||
<Footer />
|
||||
<ChatFAB />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import Navbar from '@/components/layout/Navbar'
|
||||
import Footer from '@/components/layout/Footer'
|
||||
import ChatFAB from '@/components/layout/ChatFAB'
|
||||
import PlatformBridgeSection from '@/components/sections/PlatformBridgeSection'
|
||||
import ComparisonSection from '@/components/sections/ComparisonSection'
|
||||
import ContinuousSection from '@/components/sections/ContinuousSection'
|
||||
|
||||
export default function PlattformPage() {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="pt-20" />
|
||||
<PlatformBridgeSection />
|
||||
<ComparisonSection />
|
||||
<ContinuousSection />
|
||||
<Footer />
|
||||
<ChatFAB />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import Navbar from '@/components/layout/Navbar'
|
||||
import Footer from '@/components/layout/Footer'
|
||||
import ChatFAB from '@/components/layout/ChatFAB'
|
||||
import PricingSection from '@/components/sections/PricingSection'
|
||||
|
||||
export default function PreisePage() {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<PricingSection />
|
||||
<Footer />
|
||||
<ChatFAB />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import Navbar from '@/components/layout/Navbar'
|
||||
import Footer from '@/components/layout/Footer'
|
||||
import ChatFAB from '@/components/layout/ChatFAB'
|
||||
import PageHeader from '@/components/ui/PageHeader'
|
||||
import DeltaImpactSection from '@/components/sections/DeltaImpactSection'
|
||||
import SecurityToolchainSection from '@/components/sections/SecurityToolchainSection'
|
||||
import CRAFahrplanSection from '@/components/sections/CRAFahrplanSection'
|
||||
import SafetySection from '@/components/sections/SafetySection'
|
||||
import TargetSection from '@/components/sections/TargetSection'
|
||||
|
||||
export default function ProductCompliancePage() {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<PageHeader
|
||||
tag="PRODUCT COMPLIANCE"
|
||||
title="Muss ich mein Produkt"
|
||||
titleHighlight="redesignen?"
|
||||
subtitle="Delta-Impact-Analyse für bestehende Produkte. CRA, RED, Maschinenverordnung — priorisiert statt aufgelistet."
|
||||
/>
|
||||
</div>
|
||||
<DeltaImpactSection />
|
||||
<SecurityToolchainSection />
|
||||
<CRAFahrplanSection />
|
||||
<SafetySection />
|
||||
<TargetSection />
|
||||
</main>
|
||||
<Footer />
|
||||
<ChatFAB />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import Navbar from '@/components/layout/Navbar'
|
||||
import Footer from '@/components/layout/Footer'
|
||||
import ChatFAB from '@/components/layout/ChatFAB'
|
||||
import PageHeader from '@/components/ui/PageHeader'
|
||||
import GlassCard from '@/components/ui/GlassCard'
|
||||
import FadeInView from '@/components/ui/FadeInView'
|
||||
import { Database, Layers, Calculator, AlertTriangle, Globe, Cookie } from 'lucide-react'
|
||||
|
||||
export default function SavingsMethodikPage() {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<PageHeader
|
||||
tag="METHODIK"
|
||||
title="Wie Cookies"
|
||||
titleHighlight="Marketing-Budgets entlarven"
|
||||
subtitle="4-Stufen-Analyse: vom rohen Cookie-Footprint zur fundierten Saving-Schaetzung. Jede Stufe nachvollziehbar, jede Zahl mit Quelle, jede Annahme transparent."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 4 Stufen */}
|
||||
<section className="py-12 sm:py-16">
|
||||
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 space-y-8">
|
||||
<FadeInView>
|
||||
<GlassCard>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-emerald-500/10 flex items-center justify-center flex-shrink-0">
|
||||
<Cookie className="w-6 h-6 text-emerald-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="mono-label text-emerald-400 mb-1">STUFE 1</div>
|
||||
<h3 className="text-xl font-bold mb-2">Cookie-Footprint extrahieren</h3>
|
||||
<p className="text-sm text-white/60 mb-3">
|
||||
Playwright laedt die Webseite vollstaendig (inkl. JavaScript-Rendering)
|
||||
und erfasst jeden gesetzten Cookie + jeden CMP-Payload
|
||||
(ePaaS, OneTrust, Usercentrics, Cookiebot, Didomi, TrustArc).
|
||||
</p>
|
||||
<ul className="text-sm text-white/50 space-y-1">
|
||||
<li>• Cookie-Namen, Werte, Domains, Lifetimes</li>
|
||||
<li>• IAB TCF v2.2 Vendor-Liste auswerten (Vendor-IDs zur eindeutigen Zuordnung)</li>
|
||||
<li>• Drittanbieter-Quote pro Cookie</li>
|
||||
<li>• Premium-Feature-Cookies erkennen (z.B. <code className="text-emerald-300">s_target_qa</code> = Adobe Target Enterprise)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</FadeInView>
|
||||
|
||||
<FadeInView delay={0.1}>
|
||||
<GlassCard>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-emerald-500/10 flex items-center justify-center flex-shrink-0">
|
||||
<Database className="w-6 h-6 text-emerald-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="mono-label text-emerald-400 mb-1">STUFE 2</div>
|
||||
<h3 className="text-xl font-bold mb-2">Wissens-Datenbank-Abgleich</h3>
|
||||
<p className="text-sm text-white/60 mb-3">
|
||||
Jeder Cookie wird gegen unsere kuratierte Wissens-DB mit derzeit
|
||||
~50 Top-Vendors abgeglichen. Pro Cookie wissen wir:
|
||||
</p>
|
||||
<ul className="text-sm text-white/50 space-y-1">
|
||||
<li>• Setzender Anbieter + Sitzland</li>
|
||||
<li>• Exakter funktionaler Zweck (nicht nur Kategorie)</li>
|
||||
<li>• Welche Datenfelder gesammelt werden (Client-ID, IP, etc.)</li>
|
||||
<li>• Re-Identifikations-Risiko (low/medium/high)</li>
|
||||
<li>• §25(2) TDDDG technische Notwendigkeit</li>
|
||||
<li>• Schrems-II-Status + relevante EuGH-/CNIL-Urteile</li>
|
||||
<li>• Konkreter EU-Alternativ-Cookie + EU-Alternativ-Vendor</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</FadeInView>
|
||||
|
||||
<FadeInView delay={0.2}>
|
||||
<GlassCard>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-emerald-500/10 flex items-center justify-center flex-shrink-0">
|
||||
<Layers className="w-6 h-6 text-emerald-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="mono-label text-emerald-400 mb-1">STUFE 3</div>
|
||||
<h3 className="text-xl font-bold mb-2">Tier-Inferenz + Funktionale Kategorisierung</h3>
|
||||
<p className="text-sm text-white/60 mb-3">
|
||||
Pro Vendor leiten wir das Pricing-Tier aus dem Cookie-Footprint ab:
|
||||
</p>
|
||||
<ul className="text-sm text-white/50 space-y-1">
|
||||
<li>• <strong className="text-white/80"><10 Cookies</strong> = Starter-Plan</li>
|
||||
<li>• <strong className="text-white/80">10-30 Cookies</strong> = Professional / Mid-Market</li>
|
||||
<li>• <strong className="text-white/80">30-60 Cookies</strong> = Enterprise</li>
|
||||
<li>• <strong className="text-white/80">>60 Cookies + Premium-Features</strong> = Premier-Tier</li>
|
||||
</ul>
|
||||
<p className="text-sm text-white/60 mt-3 mb-2">
|
||||
Parallel werden alle Vendors funktional klassifiziert (Web-Analytics,
|
||||
Werbung, CDN, Marketing-Automation, …). Mehrere Vendors in
|
||||
derselben Kategorie = Konsolidierungs-Kandidat.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</FadeInView>
|
||||
|
||||
<FadeInView delay={0.3}>
|
||||
<GlassCard>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-emerald-500/10 flex items-center justify-center flex-shrink-0">
|
||||
<Calculator className="w-6 h-6 text-emerald-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="mono-label text-emerald-400 mb-1">STUFE 4</div>
|
||||
<h3 className="text-xl font-bold mb-2">Kosten-Schaetzung + EU-Konsolidierung</h3>
|
||||
<p className="text-sm text-white/60 mb-3">
|
||||
Pro Tier multiplizieren wir mit unseren Pricing-Lookups
|
||||
(Gartner/Forrester 2025 + oeffentliche Listpreise).
|
||||
Ergebnis: jaehrlicher Kostenbereich pro Vendor.
|
||||
</p>
|
||||
<ul className="text-sm text-white/50 space-y-1">
|
||||
<li>• Master-Vertrag-Dedupe (1 Adobe-Lizenz, viele Features)</li>
|
||||
<li>• EU-Alternative mit gleicher Funktion + Listpreis</li>
|
||||
<li>• Multi-Funktions-Tools die mehrere Kategorien gleichzeitig ersetzen</li>
|
||||
<li>• Sparpotenzial = Aktuelle Listpreise − EU-Tool-Listpreis</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</FadeInView>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Caveats — ehrlich */}
|
||||
<section className="py-12 sm:py-16 bg-amber-500/[0.03]">
|
||||
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h2 className="text-2xl font-bold mb-6 flex items-center gap-3">
|
||||
<AlertTriangle className="w-6 h-6 text-amber-400" />
|
||||
Was wir NICHT versprechen
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<GlassCard>
|
||||
<h3 className="font-bold mb-2">Listpreise ≠ Vertragspreise</h3>
|
||||
<p className="text-sm text-white/60">
|
||||
Konzern-Konditionen liegen ueblicherweise 30–50% unter Listpreis.
|
||||
Wir geben Bereiche an, nicht exakte Zahlen. Verifikation mit dem
|
||||
eigenen Einkauf ist Pflicht.
|
||||
</p>
|
||||
</GlassCard>
|
||||
<GlassCard>
|
||||
<h3 className="font-bold mb-2">Funktionale Redundanz ≠ Strategische Redundanz</h3>
|
||||
<p className="text-sm text-white/60">
|
||||
Mehrere Analytics-Tools koennen legitim sein (A/B-Test, regional split,
|
||||
Marketing vs Produkt). Wir nennen die bekannten Gruende explizit.
|
||||
</p>
|
||||
</GlassCard>
|
||||
<GlassCard>
|
||||
<h3 className="font-bold mb-2">Media-Spend nicht enthalten</h3>
|
||||
<p className="text-sm text-white/60">
|
||||
Google-Ads-/Meta-Ads-/Programmatic-Budget ist NICHT in der Saving-
|
||||
Schaetzung. Nur Tool-Lizenzen. Media-Optimierung ist ein separates
|
||||
Thema.
|
||||
</p>
|
||||
</GlassCard>
|
||||
<GlassCard>
|
||||
<h3 className="font-bold mb-2">Migrations-Kosten nicht abgezogen</h3>
|
||||
<p className="text-sm text-white/60">
|
||||
Tool-Wechsel kostet Zeit + interne Implementation. Faustregel:
|
||||
3-6 Monate Amortisation einrechnen. Saving-Schaetzung ist Brutto.
|
||||
</p>
|
||||
</GlassCard>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Datenquellen */}
|
||||
<section className="py-12 sm:py-16">
|
||||
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h2 className="text-2xl font-bold mb-6 flex items-center gap-3">
|
||||
<Globe className="w-6 h-6 text-emerald-400" />
|
||||
Datenquellen + Updates
|
||||
</h2>
|
||||
<GlassCard>
|
||||
<ul className="text-sm text-white/60 space-y-3">
|
||||
<li>
|
||||
<strong className="text-white/90">Cookie-Wissen:</strong>{' '}
|
||||
Cookiepedia, IAB Europe TCF v2.2 Vendor-Liste, Cookiebot Public DB,
|
||||
Vendor-eigene Dokumentation
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-white/90">Pricing:</strong>{' '}
|
||||
Gartner Hype Cycle 2025, Forrester Wave MarTech 2025, oeffentliche
|
||||
Pricing-Pages, anonymisierte Kundengespraeche
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-white/90">Regulatorik:</strong>{' '}
|
||||
EDPB Cookie Guidelines 2/2023, DSK-Orientierungshilfe Telemedien 2024,
|
||||
CNIL Cookies-Recommendations
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-white/90">Updates:</strong>{' '}
|
||||
DB wird kontinuierlich gepflegt. Neue Kunden geben uns Ground-Truth
|
||||
fuer Kalibrierung.
|
||||
</li>
|
||||
</ul>
|
||||
</GlassCard>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
<ChatFAB />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import Navbar from '@/components/layout/Navbar'
|
||||
import Footer from '@/components/layout/Footer'
|
||||
import ChatFAB from '@/components/layout/ChatFAB'
|
||||
import PageHeader from '@/components/ui/PageHeader'
|
||||
import GlassCard from '@/components/ui/GlassCard'
|
||||
import FadeInView from '@/components/ui/FadeInView'
|
||||
import { Cookie, ShieldCheck, Mail, ArrowRight, CheckCircle2, AlertTriangle } from 'lucide-react'
|
||||
|
||||
export default function SavingsScanPage() {
|
||||
const [url, setUrl] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
const [consent, setConsent] = useState(true)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [done, setDone] = useState(false)
|
||||
const [checkId, setCheckId] = useState<string | null>(null)
|
||||
const [progress, setProgress] = useState<string>('')
|
||||
const [progressPct, setProgressPct] = useState<number>(0)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const pollingRef = useRef<boolean>(false)
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
if (!url || !email) return
|
||||
setError(null)
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const res = await fetch('/api/scan/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url, email, consent }),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!res.ok) {
|
||||
setError(data.detail || data.error || 'Scan konnte nicht gestartet werden')
|
||||
return
|
||||
}
|
||||
setCheckId(data.check_id)
|
||||
setDone(true)
|
||||
} catch {
|
||||
setError('Netzwerkfehler — bitte erneut versuchen.')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!checkId || pollingRef.current) return
|
||||
pollingRef.current = true
|
||||
let cancelled = false
|
||||
const poll = async () => {
|
||||
for (let i = 0; i < 60 && !cancelled; i++) {
|
||||
await new Promise(r => setTimeout(r, 5000))
|
||||
try {
|
||||
const res = await fetch(`/api/scan/status/${checkId}`)
|
||||
const data = await res.json()
|
||||
if (data.progress) setProgress(data.progress)
|
||||
if (typeof data.progress_pct === 'number') setProgressPct(data.progress_pct)
|
||||
if (['completed', 'failed', 'skipped_tdm'].includes(data.status)) {
|
||||
if (data.status !== 'completed') {
|
||||
setError(data.error || 'Scan abgebrochen')
|
||||
}
|
||||
return
|
||||
}
|
||||
} catch { /* retry */ }
|
||||
}
|
||||
}
|
||||
poll()
|
||||
return () => { cancelled = true; pollingRef.current = false }
|
||||
}, [checkId])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<PageHeader
|
||||
tag="KOSTENLOSER SAVING-SCAN"
|
||||
title="In 5 Minuten zur"
|
||||
titleHighlight="sechsstelligen Saving-Schaetzung"
|
||||
subtitle="URL eingeben — wir analysieren alle Cookies, identifizieren redundante Anbieter und schaetzen jaehrliche Einsparung. Kostenlos, ohne Login, ohne Vertrieb-Termin."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section className="py-12 sm:py-16">
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{!done ? (
|
||||
<GlassCard>
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div>
|
||||
<label htmlFor="url" className="block text-sm font-medium text-white/70 mb-2">
|
||||
Website-URL <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="url"
|
||||
type="url"
|
||||
required
|
||||
placeholder="https://www.ihre-firma.de"
|
||||
value={url}
|
||||
onChange={e => setUrl(e.target.value)}
|
||||
className="w-full px-4 py-3 rounded-lg bg-white/[0.04] border border-white/10
|
||||
text-white placeholder-white/30 focus:border-emerald-400 focus:outline-none"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-white/40">
|
||||
Wir crawlen die Startseite + automatisch erkennbare Unterseiten
|
||||
(DSI, Impressum, Cookie-Richtlinie).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-white/70 mb-2">
|
||||
E-Mail fuer den Bericht <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="ihr.name@firma.de"
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
className="w-full px-4 py-3 rounded-lg bg-white/[0.04] border border-white/10
|
||||
text-white placeholder-white/30 focus:border-emerald-400 focus:outline-none"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-white/40">
|
||||
Bericht kommt als PDF + JSON. Die Mailadresse wird ausschliesslich
|
||||
fuer diesen Scan verwendet.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<label className="flex items-start gap-2 text-xs text-white/60 cursor-pointer">
|
||||
<input type="checkbox" checked={consent} onChange={e => setConsent(e.target.checked)}
|
||||
className="mt-0.5 accent-emerald-500" />
|
||||
<span>
|
||||
Ich stimme zu, dass meine E-Mail fuer den Saving-Report + ein
|
||||
einmaliges Sales-Follow-Up genutzt wird. Widerruf jederzeit per E-Mail.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{error && (
|
||||
<p className="text-sm text-amber-300/80 bg-amber-500/10 border border-amber-400/30 rounded px-3 py-2">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting || !consent}
|
||||
className="inline-flex items-center gap-2 px-6 py-3 rounded-full
|
||||
bg-emerald-500 hover:bg-emerald-400 transition-colors
|
||||
text-enterprise-dark font-semibold disabled:opacity-50"
|
||||
>
|
||||
{submitting ? 'Wird gestartet …' : 'Saving-Scan starten'}
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<p className="text-xs text-white/40 pt-2">
|
||||
Wir analysieren ausschliesslich oeffentlich abrufbare Daten Ihrer Website
|
||||
unter Beachtung maschinenlesbarer Nutzungsvorbehalte (§ 44b UrhG).
|
||||
Pro Domain max. 1 Scan / 24h. Ergebnis innerhalb von ~3-5 Minuten per E-Mail.
|
||||
</p>
|
||||
</form>
|
||||
</GlassCard>
|
||||
) : (
|
||||
<GlassCard>
|
||||
<div className="text-center py-6">
|
||||
{error ? (
|
||||
<AlertTriangle className="w-12 h-12 text-amber-400 mx-auto mb-4" />
|
||||
) : (
|
||||
<CheckCircle2 className="w-12 h-12 text-emerald-400 mx-auto mb-4" />
|
||||
)}
|
||||
<h3 className="text-xl font-bold mb-2">
|
||||
{error ? 'Scan-Hinweis' : (progressPct >= 100 ? 'Scan abgeschlossen' : 'Scan laeuft')}
|
||||
</h3>
|
||||
{error ? (
|
||||
<p className="text-amber-300/80 mb-4 text-sm">{error}</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-white/60 mb-4">
|
||||
{progressPct >= 100
|
||||
? <>Der Bericht ist unterwegs an <strong className="text-white/90">{email}</strong>.</>
|
||||
: <>Wir analysieren <strong className="text-white/90">{url}</strong>. Bericht kommt in ~3-5 Min an <strong className="text-white/90">{email}</strong>.</>
|
||||
}
|
||||
</p>
|
||||
{progress && progressPct < 100 && (
|
||||
<div className="max-w-md mx-auto mt-4">
|
||||
<div className="text-xs text-white/50 mb-2">{progress}</div>
|
||||
<div className="w-full bg-white/10 rounded-full h-2 overflow-hidden">
|
||||
<div className="bg-emerald-400 h-full transition-all" style={{ width: `${progressPct}%` }} />
|
||||
</div>
|
||||
<div className="text-xs text-white/40 mt-1">{progressPct}%</div>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-white/40 mt-4">
|
||||
Pruefen Sie auch Ihren Spam-Ordner.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</GlassCard>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-16">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<FadeInView>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<GlassCard>
|
||||
<Cookie className="w-8 h-8 text-emerald-400 mb-4" />
|
||||
<h3 className="text-lg font-bold mb-2">Was wir analysieren</h3>
|
||||
<ul className="text-sm text-white/60 space-y-2">
|
||||
<li>• Alle Cookies + Vendor-Identifikation</li>
|
||||
<li>• Funktionale Kategorisierung (Analytics, Werbung, CDN, …)</li>
|
||||
<li>• Redundanz-Detection ueber Kategorien</li>
|
||||
<li>• Cookie-Tiefenanalyse mit Tier-Inferenz</li>
|
||||
</ul>
|
||||
</GlassCard>
|
||||
<GlassCard delay={0.1}>
|
||||
<ShieldCheck className="w-8 h-8 text-emerald-400 mb-4" />
|
||||
<h3 className="text-lg font-bold mb-2">Was Sie bekommen</h3>
|
||||
<ul className="text-sm text-white/60 space-y-2">
|
||||
<li>• Geschaetzte jaehrliche Tooling-Kosten (Listpreis-Range)</li>
|
||||
<li>• Sparpotenzial pro Konsolidierungs-Kandidat</li>
|
||||
<li>• EU-Alternative pro US-Vendor</li>
|
||||
<li>• Schrems-II-Risiko-Bewertung</li>
|
||||
</ul>
|
||||
</GlassCard>
|
||||
<GlassCard delay={0.2}>
|
||||
<Mail className="w-8 h-8 text-emerald-400 mb-4" />
|
||||
<h3 className="text-lg font-bold mb-2">Was es kostet</h3>
|
||||
<ul className="text-sm text-white/60 space-y-2">
|
||||
<li>• <strong className="text-white">Erster Scan: kostenlos</strong></li>
|
||||
<li>• Kein Login, kein Vertriebs-Termin</li>
|
||||
<li>• Daten werden nicht gespeichert</li>
|
||||
<li>• PDF + JSON zum Download</li>
|
||||
</ul>
|
||||
</GlassCard>
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
<ChatFAB />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import Navbar from '@/components/layout/Navbar'
|
||||
import Footer from '@/components/layout/Footer'
|
||||
import ChatFAB from '@/components/layout/ChatFAB'
|
||||
|
||||
// Stärken / USP-Seite — sieben Verkaufsargumente aus der IACE-Strategie
|
||||
// (Memory: project_marketing_website_3014_themes.md). Aufgebaut als
|
||||
// Long-Form-Page mit Anker-Sprungmarken — eine Nummerierte Differenzierung
|
||||
// pro Sektion, damit Sales-Calls über tiefe Links arbeiten können.
|
||||
|
||||
const usps = [
|
||||
{
|
||||
id: 'engine',
|
||||
no: '1',
|
||||
title: 'Engine, nicht Checkliste',
|
||||
sub: 'Wir leiten Gefährdungen ab. Wettbewerb fragt aus einer Liste.',
|
||||
body:
|
||||
'Marktstandard (DesignSafe, Pilz, Sick) ist Excel-aufgewertete Checkliste: der Engineer wählt aus einer Hazard-Bibliothek aus. ' +
|
||||
'BreakPilot betreibt eine deterministische Pattern-Engine mit über 1.200 Hazard-Patterns. Aus der Maschinenbeschreibung leitet sie ' +
|
||||
'die Gefährdungen ab — keine Auswahllisten, keine vergessenen Punkte.',
|
||||
proof: 'Audit-Suite cmd/iace-audit erkennt eigene Lücken (Methode A–E)',
|
||||
},
|
||||
{
|
||||
id: 'multi-markt',
|
||||
no: '2',
|
||||
title: 'Eine Risikobeurteilung — alle Märkte',
|
||||
sub: 'CE + OSHA + ANSI + GB + JIS aus einem Datenmodell.',
|
||||
body:
|
||||
'Die gleiche Pattern-Engine generiert pro Maschinenbeschreibung mehrere Compliance-Anhänge. Hersteller wählt seine Zielmärkte. ' +
|
||||
'EU-Recht zitieren wir wörtlich (Rule 1). OWASP unter CC-BY-SA mit Pflicht-Attribution (Rule 2). DIN/EN nur per Identifier (Rule 3). ' +
|
||||
'Norm-Cross-Reference-Bibliothek mappt ISO 12100 ↔ DIN EN ISO 12100 ↔ ANSI B11.0 ↔ GB/T 15706 ↔ JIS B 9700.',
|
||||
proof: '252 Regulationen klassifiziert · 314.811 Controls audited',
|
||||
},
|
||||
{
|
||||
id: 'folgegefahren',
|
||||
no: '3',
|
||||
title: 'Vom Bediener bis zum Endkunden',
|
||||
sub: 'Folgegefahren-Modell mit Sekundärschadens-Kette.',
|
||||
body:
|
||||
'Klassische Risikobeurteilung schaut nur den Bediener an. Wir modellieren die Schadenskette weiter: Glasbruch in der Abfüllanlage ' +
|
||||
'verletzt nicht nur den Bediener, sondern erreicht über Restsplitter den Endkunden. BreakPilot verbindet CE-Sicherheit mit ' +
|
||||
'Produkthaftung nach ProdHaftG, Lebensmittelrecht nach VO 178/2002 und ISO 31000 Unternehmensrisiko in einem Datenmodell.',
|
||||
proof: 'SecondaryHarm-Modell live für consumer_safety, product_liability, food_safety, environmental, reputation, financial',
|
||||
},
|
||||
{
|
||||
id: 'public-domain',
|
||||
no: '4',
|
||||
title: 'Public Domain als Rechtsanker',
|
||||
sub: 'Werte aus OSHA, NIST, EUR-Lex, BAuA — auditfähig zitiert.',
|
||||
body:
|
||||
'Mindestabstände der Maschinensicherheit kommen bei uns aus OSHA 29 CFR 1910 Subpart O — US Federal Public Domain, lizenzrechtlich ' +
|
||||
'unbedenklich. Engineering-Rundung auf safe-side mm-Raster wird transparent dokumentiert. EU-Normen erscheinen nur als Identifier-Verweis ' +
|
||||
'mit einer menschlich kuratierten "Strenger/Gleich/Weicher"-Annotation — kein Copyright-Risiko.',
|
||||
proof: 'OSHA Table O-10 + §1910.217 PSDI-Formel verbatim · DIN nur Identifier · 6 DGUV-Publikationen referenziert',
|
||||
},
|
||||
{
|
||||
id: 'audit-suite',
|
||||
no: '5',
|
||||
title: 'Audit findet Lücken, die der Fachmann übersieht',
|
||||
sub: 'Fünf deterministische Audits ohne Ground Truth.',
|
||||
body:
|
||||
'Unsere Engine kennt ihre eigenen Lücken. Methode A bis E (Reachability, Consistency, Vocabulary, Echo, Hierarchy) finden Gaps ' +
|
||||
'ohne Fachmann-Vergleich. Bei einem Test fanden wir 100 strukturell unerreichbare Patterns und 46 unvollständige Component-Tags — ' +
|
||||
'Probleme, die ein menschlicher Auditor in einem Einzelfall nie gesehen hätte.',
|
||||
proof: 'cmd/iace-audit · 1.213 Patterns transparent · 99,94% Recall verifiziert',
|
||||
},
|
||||
{
|
||||
id: 'made-in-germany',
|
||||
no: '6',
|
||||
title: 'Made in Germany meets US Federal Public Domain',
|
||||
sub: 'Deutscher Maschinenbau, der gleichzeitig US-Compliance liefert.',
|
||||
body:
|
||||
'Deutscher Exportweltmeister-Maschinenbau braucht UL/NRTL-Zulassung für die USA. Die gleichen Daten, die wir für CE generieren, ' +
|
||||
'liefern dem US-Auditor 80 % der Vorarbeit. Risikobeurteilung in einer Sprache, Compliance in zwei Märkten — ohne Mehraufwand für den Hersteller.',
|
||||
proof: 'OSHA-Anker im RAG · NRTL-fähige Compliance-Spur · DesignSafe-Marktstandard wird hier erweitert, nicht imitiert',
|
||||
},
|
||||
{
|
||||
id: 'tooling',
|
||||
no: '7',
|
||||
title: 'LLM-Gap-Review als Co-Pilot, nicht als Roboter-Anwalt',
|
||||
sub: 'Pattern-Engine als Audit-Spur, LLM als Lücken-Suchhund.',
|
||||
body:
|
||||
'Die deterministische Engine bleibt die auditfähige Quelle der Wahrheit. Ein nachgelagerter LLM-Gap-Review (Qwen / Claude) prüft, ' +
|
||||
'was die Engine übersehen hat — mit klarer Quellen-Provenance (R3 LLM-Review) und Adopt/Reject-UX. Halluzinationen können nicht in ' +
|
||||
'die finale Risikobeurteilung schlüpfen.',
|
||||
proof: 'POST /projects/:id/llm-gap-review · Konfidenz-Stufen · Fallback auf statische Checkliste',
|
||||
},
|
||||
] as const
|
||||
|
||||
const competitors = [
|
||||
{ feature: 'Pattern-Engine statt Checkliste', bp: '✓', ds: '—', pilz: '—', sick: '—', sphera: '—' },
|
||||
{ feature: 'Multi-Markt CE / US / CN / JP', bp: '✓', ds: 'nur US', pilz: 'nur EU', sick: 'nur EU', sphera: 'enterprise' },
|
||||
{ feature: 'Folgegefahren-Modell', bp: '✓', ds: '—', pilz: '—', sick: '—', sphera: 'Process' },
|
||||
{ feature: 'Audit-Suite (Engine-Lücken-Erkennung)', bp: '✓', ds: '—', pilz: '—', sick: '—', sphera: '—' },
|
||||
{ feature: 'OSHA-Anker (Public Domain Werte)', bp: '✓', ds: '✓', pilz: '—', sick: '—', sphera: '—' },
|
||||
{ feature: 'LLM-Gap-Review (Co-Pilot)', bp: '✓', ds: '—', pilz: '—', sick: '—', sphera: '—' },
|
||||
]
|
||||
|
||||
export default function StaerkenPage() {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main className="bg-enterprise-dark text-white pt-32 pb-24">
|
||||
<div className="max-w-5xl mx-auto px-4">
|
||||
<header className="mb-16">
|
||||
<h1 className="text-5xl font-bold mb-4">Was uns differenziert</h1>
|
||||
<p className="text-white/60 text-lg max-w-3xl">
|
||||
Sieben konkrete Punkte, die BreakPilot von DesignSafe, Pilz, Sick, TÜV-Tools und Sphera trennen.
|
||||
Jede Differenzierung ist im Produkt umgesetzt — kein Marketing-Versprechen.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<ol className="space-y-12">
|
||||
{usps.map((u) => (
|
||||
<li id={u.id} key={u.id} className="border-l-2 border-accent-electric pl-6">
|
||||
<div className="flex items-baseline gap-3 mb-2">
|
||||
<span className="text-accent-electric font-mono text-3xl font-bold">#{u.no}</span>
|
||||
<h2 className="text-2xl font-semibold">{u.title}</h2>
|
||||
</div>
|
||||
<p className="text-accent-electric/80 text-sm mb-3">{u.sub}</p>
|
||||
<p className="text-white/70 leading-relaxed mb-3">{u.body}</p>
|
||||
<p className="text-white/40 text-xs">
|
||||
<span className="text-white/60">Belegt durch:</span> {u.proof}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
<section className="mt-20">
|
||||
<h2 className="text-3xl font-bold mb-4">Direktvergleich</h2>
|
||||
<p className="text-white/60 mb-6 max-w-3xl">
|
||||
Stand 2026. Marktangaben basieren auf öffentlicher Produktinformation der genannten Anbieter.
|
||||
</p>
|
||||
<div className="overflow-x-auto border border-white/10 rounded-lg">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-white/[0.04] border-b border-white/10">
|
||||
<tr>
|
||||
<th className="text-left p-3 font-medium">Feature</th>
|
||||
<th className="text-left p-3 font-medium text-accent-electric">BreakPilot</th>
|
||||
<th className="text-left p-3 font-medium text-white/60">DesignSafe</th>
|
||||
<th className="text-left p-3 font-medium text-white/60">Pilz PASS</th>
|
||||
<th className="text-left p-3 font-medium text-white/60">Sick SD</th>
|
||||
<th className="text-left p-3 font-medium text-white/60">Sphera</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{competitors.map((c) => (
|
||||
<tr key={c.feature} className="border-t border-white/[0.06]">
|
||||
<td className="p-3 text-white/80">{c.feature}</td>
|
||||
<td className="p-3 text-accent-electric font-medium">{c.bp}</td>
|
||||
<td className="p-3 text-white/50">{c.ds}</td>
|
||||
<td className="p-3 text-white/50">{c.pilz}</td>
|
||||
<td className="p-3 text-white/50">{c.sick}</td>
|
||||
<td className="p-3 text-white/50">{c.sphera}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-20 border-t border-white/10 pt-12">
|
||||
<h2 className="text-2xl font-bold mb-3">Quellen & Lizenz-Architektur</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Die Plattform stützt sich auf öffentliche Quellen: EU-Recht (EUR-Lex), Bundesrecht (BetrSichV, ArbSchG),
|
||||
US Federal Code (OSHA, NIST), Behörden-Leitfäden (ENISA, EDPB, BAuA), freie Sicherheits-Frameworks unter
|
||||
CC-BY-SA (OWASP). Jeder Inhalt trägt eine deterministische Lizenzregel R1/R2/R3 und löst die
|
||||
entsprechende Attribution im Ausgabe-PDF und im Frontend automatisch aus. Vollständige Quellenliste
|
||||
im SDK unter <code className="bg-white/[0.06] px-1.5 py-0.5 rounded">/sdk/licenses</code>.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
<ChatFAB />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import Navbar from '@/components/layout/Navbar'
|
||||
import Footer from '@/components/layout/Footer'
|
||||
import ChatFAB from '@/components/layout/ChatFAB'
|
||||
import TeamSection from '@/components/sections/TeamSection'
|
||||
|
||||
export default function TeamPage() {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<TeamSection />
|
||||
<Footer />
|
||||
<ChatFAB />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { X, Send, Bot, User, Sparkles, Maximize2, Minimize2 } from 'lucide-react'
|
||||
import { t } from '@/lib/content'
|
||||
import { useApp } from '@/lib/context'
|
||||
|
||||
interface ChatMessage {
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
}
|
||||
|
||||
export default function ChatFAB() {
|
||||
const { lang } = useApp()
|
||||
const i = t(lang)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([])
|
||||
const [input, setInput] = useState('')
|
||||
const [isStreaming, setIsStreaming] = useState(false)
|
||||
const [isWaiting, setIsWaiting] = useState(false)
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const abortRef = useRef<AbortController | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, [messages])
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
setTimeout(() => inputRef.current?.focus(), 200)
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
async function sendMessage(text?: string) {
|
||||
const message = text || input.trim()
|
||||
if (!message || isStreaming) return
|
||||
|
||||
setInput('')
|
||||
setMessages(prev => [...prev, { role: 'user', content: message }])
|
||||
setIsStreaming(true)
|
||||
setIsWaiting(true)
|
||||
|
||||
abortRef.current = new AbortController()
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
message,
|
||||
history: messages.slice(-10),
|
||||
}),
|
||||
signal: abortRef.current.signal,
|
||||
})
|
||||
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
|
||||
const reader = res.body!.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let content = ''
|
||||
let firstChunk = true
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
content += decoder.decode(value, { stream: true })
|
||||
|
||||
if (firstChunk) {
|
||||
firstChunk = false
|
||||
setIsWaiting(false)
|
||||
setMessages(prev => [...prev, { role: 'assistant', content }])
|
||||
} else {
|
||||
const currentText = content
|
||||
setMessages(prev => {
|
||||
const updated = [...prev]
|
||||
updated[updated.length - 1] = { role: 'assistant', content: currentText }
|
||||
return updated
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && err.name === 'AbortError') return
|
||||
setIsWaiting(false)
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{ role: 'assistant', content: i.chat.error },
|
||||
])
|
||||
} finally {
|
||||
setIsStreaming(false)
|
||||
setIsWaiting(false)
|
||||
abortRef.current = null
|
||||
}
|
||||
}
|
||||
|
||||
function stopGeneration() {
|
||||
if (abortRef.current) {
|
||||
abortRef.current.abort()
|
||||
setIsStreaming(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* FAB Button */}
|
||||
<AnimatePresence>
|
||||
{!isOpen && (
|
||||
<motion.button
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
exit={{ scale: 0 }}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="fixed bottom-6 right-[5.5rem] z-50 w-14 h-14 rounded-full
|
||||
bg-accent-electric hover:bg-blue-500 text-white
|
||||
flex items-center justify-center shadow-lg shadow-blue-600/30
|
||||
transition-colors"
|
||||
aria-label="Compliance Agent oeffnen"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
|
||||
<circle cx="9" cy="10" r="1" fill="currentColor" />
|
||||
<circle cx="12" cy="10" r="1" fill="currentColor" />
|
||||
<circle cx="15" cy="10" r="1" fill="currentColor" />
|
||||
</svg>
|
||||
</motion.button>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Chat Panel */}
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className={`fixed bottom-6 right-6 z-50
|
||||
${isExpanded ? 'w-[700px] h-[80vh]' : 'w-[400px] h-[520px]'}
|
||||
rounded-2xl overflow-hidden
|
||||
bg-black/90 backdrop-blur-xl border border-white/10
|
||||
shadow-2xl shadow-black/50 flex flex-col
|
||||
transition-all duration-200`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-white/10 shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-7 h-7 rounded-full bg-accent-electric/20 flex items-center justify-center">
|
||||
<Bot className="w-4 h-4 text-accent-electric" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-semibold text-white">{i.chat.title}</span>
|
||||
<span className="text-xs text-white/30 ml-2">
|
||||
{isStreaming ? i.chat.responding : i.chat.online}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => setIsExpanded(prev => !prev)}
|
||||
className="w-7 h-7 rounded-full bg-white/10 flex items-center justify-center hover:bg-white/20 transition-colors"
|
||||
>
|
||||
{isExpanded ? <Minimize2 className="w-3.5 h-3.5 text-white/60" /> : <Maximize2 className="w-3.5 h-3.5 text-white/60" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="w-7 h-7 rounded-full bg-white/10 flex items-center justify-center hover:bg-white/20 transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4 text-white/60" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto px-4 py-3 space-y-3">
|
||||
{messages.length === 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-white/40 text-xs mb-3">
|
||||
<Sparkles className="w-3.5 h-3.5" />
|
||||
<span>{i.chat.ask}</span>
|
||||
</div>
|
||||
{i.chat.suggestions.map((q, idx) => (
|
||||
<motion.button
|
||||
key={idx}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 + idx * 0.08 }}
|
||||
onClick={() => sendMessage(q)}
|
||||
className="block w-full text-left px-3 py-2.5 rounded-xl
|
||||
bg-white/[0.05] border border-white/10
|
||||
hover:bg-white/[0.1] transition-colors
|
||||
text-xs text-white/70 hover:text-white"
|
||||
>
|
||||
{q}
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Waiting indicator */}
|
||||
<AnimatePresence>
|
||||
{isWaiting && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 6 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="flex gap-2.5"
|
||||
>
|
||||
<div className="w-7 h-7 rounded-full bg-accent-electric/20 flex items-center justify-center shrink-0 mt-0.5">
|
||||
<Bot className="w-3.5 h-3.5 text-accent-electric" />
|
||||
</div>
|
||||
<div className="bg-white/[0.06] rounded-2xl px-3.5 py-3 flex items-center gap-1">
|
||||
{[0, 1, 2].map(dotIdx => (
|
||||
<motion.span
|
||||
key={dotIdx}
|
||||
className="block w-1.5 h-1.5 rounded-full bg-accent-electric/70"
|
||||
animate={{ opacity: [0.3, 1, 0.3], y: [0, -3, 0] }}
|
||||
transition={{ duration: 0.7, repeat: Infinity, delay: dotIdx * 0.15 }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{messages.map((msg, idx) => (
|
||||
<div key={idx} className={`flex gap-2.5 ${msg.role === 'user' ? 'justify-end' : ''}`}>
|
||||
{msg.role === 'assistant' && (
|
||||
<div className="w-7 h-7 rounded-full bg-accent-electric/20 flex items-center justify-center shrink-0 mt-0.5">
|
||||
<Bot className="w-3.5 h-3.5 text-accent-electric" />
|
||||
</div>
|
||||
)}
|
||||
<div className={`max-w-[85%] rounded-2xl px-3.5 py-2.5 text-xs leading-relaxed ${
|
||||
msg.role === 'user' ? 'bg-accent-electric/20 text-white' : 'bg-white/[0.06] text-white/80'
|
||||
}`}>
|
||||
<div className="whitespace-pre-wrap">{msg.content}</div>
|
||||
{isStreaming && idx === messages.length - 1 && msg.role === 'assistant' && (
|
||||
<span className="inline-block w-1.5 h-3.5 bg-accent-electric animate-pulse ml-0.5" />
|
||||
)}
|
||||
</div>
|
||||
{msg.role === 'user' && (
|
||||
<div className="w-7 h-7 rounded-full bg-white/10 flex items-center justify-center shrink-0 mt-0.5">
|
||||
<User className="w-3.5 h-3.5 text-white/60" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="border-t border-white/10 px-4 py-3 shrink-0">
|
||||
{isStreaming && (
|
||||
<button
|
||||
onClick={stopGeneration}
|
||||
className="w-full mb-2 px-3 py-1.5 rounded-lg bg-white/[0.06] hover:bg-white/[0.1]
|
||||
text-xs text-white/50 transition-colors"
|
||||
>
|
||||
{i.chat.stop}
|
||||
</button>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && sendMessage()}
|
||||
placeholder={i.chat.placeholder}
|
||||
disabled={isStreaming}
|
||||
className="flex-1 bg-white/[0.06] border border-white/10 rounded-xl px-3.5 py-2.5
|
||||
text-xs text-white placeholder-white/30 outline-none
|
||||
focus:border-accent-electric/50 focus:ring-1 focus:ring-accent-electric/20
|
||||
disabled:opacity-50 transition-all"
|
||||
/>
|
||||
<button
|
||||
onClick={() => sendMessage()}
|
||||
disabled={isStreaming || !input.trim()}
|
||||
className="px-3.5 py-2.5 bg-accent-electric hover:bg-blue-600 disabled:opacity-30
|
||||
rounded-xl transition-all text-white"
|
||||
>
|
||||
<Send className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { Shield, ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import { useApp } from '@/lib/context'
|
||||
|
||||
const COOKIE_NAME = 'bp_consent'
|
||||
const SITE_ID = process.env.NEXT_PUBLIC_CONSENT_SITE_ID || 'breakpilot-marketing'
|
||||
|
||||
interface ConsentState {
|
||||
essential: boolean
|
||||
functional: boolean
|
||||
analytics: boolean
|
||||
}
|
||||
|
||||
const defaultConsent: ConsentState = {
|
||||
essential: true,
|
||||
functional: false,
|
||||
analytics: false,
|
||||
}
|
||||
|
||||
const texts = {
|
||||
de: {
|
||||
title: 'Cookie-Einwilligung',
|
||||
description: 'Wir verwenden Cookies, um unsere Website zu verbessern. Essenzielle Cookies sind für die Grundfunktionen erforderlich. Weitere Informationen finden Sie in unserer',
|
||||
privacyLink: 'Datenschutzerklärung',
|
||||
acceptAll: 'Alle akzeptieren',
|
||||
rejectAll: 'Nur notwendige',
|
||||
settings: 'Einstellungen',
|
||||
save: 'Auswahl speichern',
|
||||
categories: {
|
||||
essential: { name: 'Notwendig', description: 'Erforderlich für die Grundfunktionen der Website.', required: true },
|
||||
functional: { name: 'Funktional', description: 'Ermöglicht erweiterte Funktionen wie Spracheinstellungen und Theme-Präferenzen.' },
|
||||
analytics: { name: 'Analyse', description: 'Hilft uns zu verstehen, wie Besucher die Website nutzen.' },
|
||||
},
|
||||
},
|
||||
en: {
|
||||
title: 'Cookie Consent',
|
||||
description: 'We use cookies to improve our website. Essential cookies are required for basic functionality. For more information, please see our',
|
||||
privacyLink: 'Privacy Policy',
|
||||
acceptAll: 'Accept All',
|
||||
rejectAll: 'Essential Only',
|
||||
settings: 'Settings',
|
||||
save: 'Save Preferences',
|
||||
categories: {
|
||||
essential: { name: 'Essential', description: 'Required for basic website functionality.', required: true },
|
||||
functional: { name: 'Functional', description: 'Enables enhanced features like language settings and theme preferences.' },
|
||||
analytics: { name: 'Analytics', description: 'Helps us understand how visitors use the website.' },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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('|')
|
||||
let hash = 0
|
||||
for (let i = 0; i < raw.length; i++) {
|
||||
hash = ((hash << 5) - hash + raw.charCodeAt(i)) | 0
|
||||
}
|
||||
return 'fp_' + Math.abs(hash).toString(36)
|
||||
}
|
||||
|
||||
function getSavedConsent(): ConsentState | null {
|
||||
if (typeof window === 'undefined') return null
|
||||
try {
|
||||
const stored = localStorage.getItem(COOKIE_NAME)
|
||||
if (stored) return JSON.parse(stored)
|
||||
} catch { /* ignore */ }
|
||||
return null
|
||||
}
|
||||
|
||||
function detectDevice(): { device_type: string; browser: string; os: string } {
|
||||
const ua = navigator.userAgent
|
||||
const device_type = /Mobi|Android/i.test(ua)
|
||||
? (/Tablet|iPad/i.test(ua) ? 'tablet' : 'mobile')
|
||||
: 'desktop'
|
||||
const browserMatch = ua.match(/(Chrome|Safari|Firefox|Edge|Opera|OPR)\/[\d.]+/)
|
||||
const browser = browserMatch
|
||||
? browserMatch[0].replace('OPR', 'Opera')
|
||||
: 'Unknown'
|
||||
const osMatch = ua.match(/(Windows NT [\d.]+|Mac OS X [\d_.]+|Linux|Android [\d.]+|iOS [\d._]+)/)
|
||||
const os = osMatch ? osMatch[0].replace(/_/g, '.') : 'Unknown'
|
||||
return { device_type, browser, os }
|
||||
}
|
||||
|
||||
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, vendorConsents?: Record<string, boolean>): Promise<string | null> {
|
||||
try {
|
||||
const { device_type, browser, os } = detectDevice()
|
||||
const { blocked, released } = detectScripts()
|
||||
const cookies_set = detectCookies()
|
||||
const res = await fetch('/api/consent', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
site_id: SITE_ID,
|
||||
device_fingerprint: getFingerprint(),
|
||||
categories: [
|
||||
'essential',
|
||||
...(consent.functional ? ['functional'] : []),
|
||||
...(consent.analytics ? ['analytics'] : []),
|
||||
],
|
||||
vendors: Object.keys(vendorConsents || {}).filter(k => vendorConsents?.[k]),
|
||||
vendor_consents: vendorConsents || {},
|
||||
user_agent: navigator.userAgent,
|
||||
consent_method: method,
|
||||
page_url: window.location.href,
|
||||
referrer: document.referrer || null,
|
||||
device_type,
|
||||
browser,
|
||||
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,
|
||||
}),
|
||||
})
|
||||
const result = await res.json().catch(() => null)
|
||||
if (result?.id) {
|
||||
localStorage.setItem('bp_consent_id', result.id)
|
||||
return result.id
|
||||
}
|
||||
return null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
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, 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')
|
||||
const saveSelection = () => save(consent, 'custom_selection')
|
||||
|
||||
if (!visible) return null
|
||||
|
||||
const categories = Object.entries(t.categories) as [string, { name: string; description: string; required?: boolean }][]
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
initial={{ y: 100, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 100, opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: [0.22, 1, 0.36, 1] }}
|
||||
className="fixed bottom-0 left-0 right-0 z-[9999] p-4 md:p-6"
|
||||
>
|
||||
<div className="max-w-3xl mx-auto rounded-2xl bg-enterprise-dark/95 backdrop-blur-xl border border-white/[0.08] shadow-2xl shadow-black/40 p-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<div className="w-8 h-8 rounded-lg bg-accent-electric/10 flex items-center justify-center shrink-0 mt-0.5">
|
||||
<Shield className="w-4 h-4 text-accent-electric" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-white mb-1">{t.title}</h3>
|
||||
<p className="text-xs text-white/50 leading-relaxed">
|
||||
{t.description}{' '}
|
||||
<a href="/datenschutz" className="text-accent-electric hover:underline">{t.privacyLink}</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category details */}
|
||||
<AnimatePresence>
|
||||
{showDetails && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="overflow-hidden mb-4"
|
||||
>
|
||||
<div className="space-y-2 pt-2 border-t border-white/[0.06]">
|
||||
{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>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
|
||||
<button
|
||||
onClick={acceptAll}
|
||||
className="flex-1 px-4 py-2.5 rounded-xl bg-accent-electric text-white text-xs font-semibold hover:bg-blue-500 transition-colors"
|
||||
>
|
||||
{t.acceptAll}
|
||||
</button>
|
||||
<button
|
||||
onClick={rejectAll}
|
||||
className="flex-1 px-4 py-2.5 rounded-xl bg-white/[0.06] border border-white/[0.08] text-white/70 text-xs font-semibold hover:bg-white/[0.1] transition-colors"
|
||||
>
|
||||
{t.rejectAll}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => showDetails ? saveSelection() : setShowDetails(true)}
|
||||
className="flex-1 px-4 py-2.5 rounded-xl bg-white/[0.06] border border-white/[0.08] text-white/70 text-xs font-semibold hover:bg-white/[0.1] transition-colors flex items-center justify-center gap-1.5"
|
||||
>
|
||||
{showDetails ? t.save : t.settings}
|
||||
{!showDetails && <ChevronDown className="w-3 h-3" />}
|
||||
{showDetails && <ChevronUp className="w-3 h-3" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Powered by */}
|
||||
<p className="text-center mt-3 text-[10px] text-white/20 font-mono">
|
||||
Consent managed by BreakPilot CMP
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
'use client'
|
||||
|
||||
import { Cookie } from 'lucide-react'
|
||||
import { t } from '@/lib/content'
|
||||
import { useApp } from '@/lib/context'
|
||||
|
||||
async function reopenConsentBanner() {
|
||||
const consentId = localStorage.getItem('bp_consent_id')
|
||||
if (consentId) {
|
||||
fetch(`/api/consent/${consentId}`, { method: 'DELETE' }).catch(() => {})
|
||||
localStorage.removeItem('bp_consent_id')
|
||||
}
|
||||
localStorage.removeItem('bp_consent')
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
export default function Footer() {
|
||||
const { lang } = useApp()
|
||||
const i = t(lang)
|
||||
const year = new Date().getFullYear()
|
||||
|
||||
return (
|
||||
<footer className="border-t border-white/[0.06] bg-enterprise-darker">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-12">
|
||||
<div className="md:col-span-2">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-accent-electric to-accent-indigo flex items-center justify-center">
|
||||
<span className="text-white font-bold text-sm">B</span>
|
||||
</div>
|
||||
<span className="font-bold text-white text-lg">BreakPilot</span>
|
||||
</div>
|
||||
<p className="mono-label mb-2">{i.footer.tagline}</p>
|
||||
<p className="text-white/30 text-sm max-w-sm">
|
||||
{i.footer.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-semibold text-white/80 text-sm mb-4">Produkt</h4>
|
||||
<ul className="space-y-2">
|
||||
{i.footer.links.product.map(link => (
|
||||
<li key={link}>
|
||||
<a href={`#${link.toLowerCase()}`} className="text-sm text-white/40 hover:text-white/70 transition-colors">
|
||||
{link}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-semibold text-white/80 text-sm mb-4">Rechtliches</h4>
|
||||
<ul className="space-y-2">
|
||||
{i.footer.links.legal.map(link => (
|
||||
<li key={link}>
|
||||
<a
|
||||
href={link === 'Impressum' ? '/impressum' : link === 'Datenschutz' ? '/datenschutz' : '#'}
|
||||
className="text-sm text-white/40 hover:text-white/70 transition-colors"
|
||||
>
|
||||
{link}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 pt-8 border-t border-white/[0.04] flex flex-col sm:flex-row justify-between items-center gap-4">
|
||||
<p className="text-xs text-white/20">
|
||||
© {year} {i.footer.copyright}. Alle Rechte vorbehalten.
|
||||
</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={reopenConsentBanner}
|
||||
className="text-xs text-white/20 hover:text-white/50 transition-colors flex items-center gap-1.5"
|
||||
>
|
||||
<Cookie className="w-3 h-3" />
|
||||
Cookie-Einstellungen
|
||||
</button>
|
||||
<p className="text-xs text-white/20 font-mono">
|
||||
{i.footer.madeIn}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
'use client'
|
||||
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { X } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { navLinks } from '@/lib/sections'
|
||||
import { useApp } from '@/lib/context'
|
||||
|
||||
interface MobileMenuProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export default function MobileMenu({ open, onClose }: MobileMenuProps) {
|
||||
const { lang } = useApp()
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={onClose}
|
||||
className="fixed inset-0 z-50 bg-black/60"
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ x: '100%' }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: '100%' }}
|
||||
transition={{ type: 'spring', damping: 30, stiffness: 300 }}
|
||||
className="fixed right-0 top-0 bottom-0 z-50 w-72 bg-enterprise-dark border-l border-white/[0.08] p-6"
|
||||
>
|
||||
<button onClick={onClose} className="absolute top-4 right-4 text-white/60 hover:text-white">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<nav className="mt-12 flex flex-col gap-1">
|
||||
<Link
|
||||
href="/"
|
||||
onClick={onClose}
|
||||
className="px-4 py-3 rounded-xl text-sm text-white/60 hover:text-white hover:bg-white/[0.06] transition-colors"
|
||||
>
|
||||
Start
|
||||
</Link>
|
||||
{navLinks.map(link => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
onClick={onClose}
|
||||
className="px-4 py-3 rounded-xl text-sm text-white/60 hover:text-white hover:bg-white/[0.06] transition-colors"
|
||||
>
|
||||
{lang === 'de' ? link.labelDe : link.labelEn}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { Menu, Sun, Moon } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { navLinks } from '@/lib/sections'
|
||||
import { t } from '@/lib/content'
|
||||
import { useApp } from '@/lib/context'
|
||||
import CTAButton from '@/components/ui/CTAButton'
|
||||
import MobileMenu from './MobileMenu'
|
||||
|
||||
export default function Navbar() {
|
||||
const { lang, theme, toggleLang, toggleTheme } = useApp()
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
const [mobileOpen, setMobileOpen] = useState(false)
|
||||
const pathname = usePathname()
|
||||
const i = t(lang)
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => setScrolled(window.scrollY > 50)
|
||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
return () => window.removeEventListener('scroll', handleScroll)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<motion.nav
|
||||
initial={{ y: -100 }}
|
||||
animate={{ y: 0 }}
|
||||
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
|
||||
className={`
|
||||
fixed top-0 left-0 right-0 z-50 transition-all duration-300
|
||||
${scrolled
|
||||
? 'bg-enterprise-dark/80 backdrop-blur-xl border-b border-white/[0.06]'
|
||||
: 'bg-transparent'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-accent-electric to-accent-indigo flex items-center justify-center">
|
||||
<span className="text-white font-bold text-sm">B</span>
|
||||
</div>
|
||||
<span className="font-bold text-white text-lg">BreakPilot</span>
|
||||
</Link>
|
||||
|
||||
<div className="hidden md:flex items-center gap-1">
|
||||
{navLinks.map(link => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={`
|
||||
px-4 py-2 rounded-lg text-sm font-medium transition-colors duration-200
|
||||
${pathname === link.href
|
||||
? 'text-white bg-white/[0.08]'
|
||||
: 'text-white/50 hover:text-white hover:bg-white/[0.04]'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{lang === 'de' ? link.labelDe : link.labelEn}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={toggleLang}
|
||||
className="flex items-center gap-0.5 rounded-lg bg-white/[0.06] border border-white/[0.08] overflow-hidden"
|
||||
>
|
||||
<span className={`px-2 py-1 text-xs font-medium transition-colors ${lang === 'de' ? 'bg-accent-electric text-white' : 'text-white/40'}`}>
|
||||
DE
|
||||
</span>
|
||||
<span className={`px-2 py-1 text-xs font-medium transition-colors ${lang === 'en' ? 'bg-accent-electric text-white' : 'text-white/40'}`}>
|
||||
EN
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="w-8 h-8 rounded-lg bg-white/[0.06] border border-white/[0.08] flex items-center justify-center
|
||||
hover:bg-white/[0.1] transition-colors"
|
||||
aria-label={theme === 'dark' ? 'Light mode' : 'Dark mode'}
|
||||
>
|
||||
{theme === 'dark'
|
||||
? <Sun className="w-4 h-4 text-white/50" />
|
||||
: <Moon className="w-4 h-4 text-white/50" />
|
||||
}
|
||||
</button>
|
||||
|
||||
<CTAButton href="/preise" className="hidden sm:inline-flex text-xs px-4 py-2">
|
||||
{i.nav.cta}
|
||||
</CTAButton>
|
||||
|
||||
<button
|
||||
onClick={() => setMobileOpen(true)}
|
||||
className="md:hidden p-2 text-white/60 hover:text-white"
|
||||
>
|
||||
<Menu className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.nav>
|
||||
|
||||
<MobileMenu open={mobileOpen} onClose={() => setMobileOpen(false)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
|
||||
import { Check } from 'lucide-react'
|
||||
import { t } from '@/lib/content'
|
||||
import { useApp } from '@/lib/context'
|
||||
import SectionHeading from '@/components/ui/SectionHeading'
|
||||
import FadeInView from '@/components/ui/FadeInView'
|
||||
|
||||
const riskColors = {
|
||||
red: { bg: 'bg-red-500/10', border: 'border-red-500/20', text: 'text-red-400', bar: 'bg-red-500' },
|
||||
amber: { bg: 'bg-amber-500/10', border: 'border-amber-500/20', text: 'text-amber-400', bar: 'bg-amber-500' },
|
||||
blue: { bg: 'bg-blue-500/10', border: 'border-blue-500/20', text: 'text-blue-400', bar: 'bg-blue-500' },
|
||||
green: { bg: 'bg-green-500/10', border: 'border-green-500/20', text: 'text-green-400', bar: 'bg-green-500' },
|
||||
}
|
||||
|
||||
export default function AIGovernanceSection() {
|
||||
const { lang } = useApp()
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<section id="ai-governance" className="py-24 sm:py-32 section-alt">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading
|
||||
tag={i.aiGovernance.tag}
|
||||
title={i.aiGovernance.title}
|
||||
titleHighlight={i.aiGovernance.titleHighlight}
|
||||
subtitle={i.aiGovernance.subtitle}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<FadeInView direction="left">
|
||||
<div className="space-y-3">
|
||||
{i.aiGovernance.riskLevels.map((level, idx) => {
|
||||
const colors = riskColors[level.color]
|
||||
return (
|
||||
<div key={idx} className={`rounded-xl border ${colors.border} ${colors.bg} p-4 flex items-center gap-4`}>
|
||||
<div className={`w-1 h-10 rounded-full ${colors.bar} shrink-0`} />
|
||||
<div>
|
||||
<h4 className={`text-sm font-bold ${colors.text}`}>{level.level}</h4>
|
||||
<p className="text-xs text-white/40">{level.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
<FadeInView direction="right">
|
||||
<div className="rounded-2xl border border-white/[0.06] bg-white/[0.03] p-6 h-full">
|
||||
<h3 className="text-lg font-bold mb-4">Deterministische AI-Act-Compliance</h3>
|
||||
<ul className="space-y-3">
|
||||
{i.aiGovernance.features.map((feature, idx) => (
|
||||
<li key={idx} className="flex items-start gap-3 text-sm text-white/60">
|
||||
<Check className="w-4 h-4 text-accent-signal mt-0.5 shrink-0" />
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
'use client'
|
||||
|
||||
import { Layers, Server, Database } from 'lucide-react'
|
||||
import { t } from '@/lib/content'
|
||||
import { useApp } from '@/lib/context'
|
||||
import SectionHeading from '@/components/ui/SectionHeading'
|
||||
import FadeInView from '@/components/ui/FadeInView'
|
||||
import TechBadge from '@/components/ui/TechBadge'
|
||||
|
||||
const layerIcons = [Layers, Server, Database]
|
||||
const layerColors = ['border-accent-electric/30', 'border-accent-indigo/30', 'border-accent-purple/30']
|
||||
const layerBg = ['bg-accent-electric/5', 'bg-accent-indigo/5', 'bg-accent-purple/5']
|
||||
|
||||
export default function ArchitectureSection() {
|
||||
const { lang } = useApp()
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<section id="architecture" className="py-24 sm:py-32 section-alt">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading
|
||||
tag={i.architecture.tag}
|
||||
title={i.architecture.title}
|
||||
titleHighlight={i.architecture.titleHighlight}
|
||||
subtitle={i.architecture.subtitle}
|
||||
/>
|
||||
|
||||
<div className="space-y-4 mb-12">
|
||||
{i.architecture.layers.map((layer, idx) => {
|
||||
const Icon = layerIcons[idx]
|
||||
return (
|
||||
<FadeInView key={idx} delay={idx * 0.15}>
|
||||
<div className={`rounded-2xl border ${layerColors[idx]} ${layerBg[idx]} p-6`}>
|
||||
<div className="flex flex-col md:flex-row md:items-center gap-4">
|
||||
<div className="flex items-center gap-3 md:w-64 shrink-0">
|
||||
<Icon className="w-5 h-5 text-white/60" />
|
||||
<h3 className="font-bold text-sm">{layer.name}</h3>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-wrap gap-2">
|
||||
{layer.components.map((comp, ci) => (
|
||||
<span
|
||||
key={ci}
|
||||
className="px-3 py-1.5 rounded-lg text-xs bg-white/[0.06] border border-white/[0.06] text-white/70"
|
||||
>
|
||||
{comp}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<TechBadge className="shrink-0">{layer.tech}</TechBadge>
|
||||
</div>
|
||||
</div>
|
||||
</FadeInView>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<FadeInView delay={0.5}>
|
||||
<div className="flex flex-wrap justify-center gap-3">
|
||||
{i.architecture.badges.map((badge, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-full border border-white/[0.08] bg-white/[0.03] text-xs text-white/60 font-medium"
|
||||
>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accent-signal" />
|
||||
{badge}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { FileText, Cpu, Shield, Search, ClipboardCheck, Download } from 'lucide-react'
|
||||
import { useApp } from '@/lib/context'
|
||||
import FadeInView from '@/components/ui/FadeInView'
|
||||
import TechBadge from '@/components/ui/TechBadge'
|
||||
import { ANIMATION } from '@/lib/constants'
|
||||
|
||||
const stepIcons = [FileText, Cpu, Shield, Search, ClipboardCheck, Download]
|
||||
|
||||
const flow = {
|
||||
de: {
|
||||
steps: [
|
||||
{
|
||||
num: '01',
|
||||
title: 'Grenzen & Verwendung',
|
||||
description: 'Der Nutzer füllt 14 Textfelder aus — Maschinenbeschreibung, bestimmungsgemäße Verwendung, Energiequellen, Betriebsarten, Personengruppen.',
|
||||
example: '"Kollaborativer 6-Achs-Roboter UR10e mit Kraft-/Momentsensorik, elektrischer Antrieb 48V DC, Ethernet/PROFINET..."',
|
||||
tags: [],
|
||||
},
|
||||
{
|
||||
num: '02',
|
||||
title: 'Automatische Analyse',
|
||||
description: '6 deterministische Schritte in Sekunden: Narrative Parser (200 Keywords) → Komponenten → Pattern Engine (1.058 Patterns) → Gefährdungen → Maßnahmen (225 Bibliothek) → Normen (751 A/B/C).',
|
||||
example: 'Pattern HP059 "Kollisionsgefahr Cobot": RequiredTags [cobot, rotating_joint] — PATTERN FEUERT ✓',
|
||||
tags: ['Deterministisch', 'Kein LLM', 'AND/NOT-Logik'],
|
||||
},
|
||||
{
|
||||
num: '03',
|
||||
title: 'Risikobewertung',
|
||||
description: 'Im Hazard Log erscheinen alle Gefährdungen mit Erstbewertung (S/E/P), RPZ-Berechnung und automatischer SIL/PL-Ableitung nach ISO-Risikograph.',
|
||||
example: 'Cobot-Ergebnis: 12 Gefährdungen, RPZ 4-48, SIL 0-2, PL a-d',
|
||||
tags: ['ISO 12100', 'SIL/PL', 'RPZ'],
|
||||
},
|
||||
{
|
||||
num: '04',
|
||||
title: 'Regulatorische Hinweise',
|
||||
description: 'On-Demand RAG-Suche in 36.708 Chunks: BAuA (TRBS/TRGS/ASR), OSHA Technical Manual, EU-Verordnungen. Keine ISO-Texte — nur gemeinfreie Quellen.',
|
||||
example: 'Gefährdung "Kollision" → TRBS 2111 Kap. 4.3, OSHA 1910.212(a)(1)',
|
||||
tags: ['RAG', 'BAuA', 'OSHA'],
|
||||
},
|
||||
{
|
||||
num: '05',
|
||||
title: 'Verifikation & Nachweise',
|
||||
description: '25 Evidenztypen werden automatisch vorgeschlagen. Der Nutzer erstellt Verifikationspläne und ordnet Prüfberichte zu.',
|
||||
example: 'E01 Hazard Analysis Report, E04 Electrical Safety Test, E14 Software Validation',
|
||||
tags: ['25 Evidenztypen', 'Traceability'],
|
||||
},
|
||||
{
|
||||
num: '06',
|
||||
title: 'CE-Akte generieren',
|
||||
description: 'Strukturiertes Dokument nach MVO 2023/1230 Anhang IV: Beschreibung, Risikobeurteilung, Normen, Maßnahmen, Nachweise, Konformitätserklärung. Export als PDF.',
|
||||
example: 'Vollständige Technische Dokumentation per Klick',
|
||||
tags: ['MVO 2023/1230', 'Anhang IV', 'PDF'],
|
||||
},
|
||||
],
|
||||
},
|
||||
en: {
|
||||
steps: [
|
||||
{
|
||||
num: '01',
|
||||
title: 'Limits & Intended Use',
|
||||
description: 'The user fills in 14 text fields — machine description, intended use, energy sources, operating modes, user groups.',
|
||||
example: '"Collaborative 6-axis robot UR10e with force/torque sensing, electric drive 48V DC, Ethernet/PROFINET..."',
|
||||
tags: [],
|
||||
},
|
||||
{
|
||||
num: '02',
|
||||
title: 'Automatic Analysis',
|
||||
description: '6 deterministic steps in seconds: Narrative Parser (200 keywords) → Components → Pattern Engine (1,058 patterns) → Hazards → Mitigations (225 library) → Norms (751 A/B/C).',
|
||||
example: 'Pattern HP059 "Collision hazard cobot": RequiredTags [cobot, rotating_joint] — PATTERN FIRES ✓',
|
||||
tags: ['Deterministic', 'No LLM', 'AND/NOT logic'],
|
||||
},
|
||||
{
|
||||
num: '03',
|
||||
title: 'Risk Assessment',
|
||||
description: 'The Hazard Log shows all hazards with initial assessment (S/E/P), RPZ calculation and automatic SIL/PL derivation per ISO risk graph.',
|
||||
example: 'Cobot result: 12 hazards, RPZ 4-48, SIL 0-2, PL a-d',
|
||||
tags: ['ISO 12100', 'SIL/PL', 'RPZ'],
|
||||
},
|
||||
{
|
||||
num: '04',
|
||||
title: 'Regulatory Guidance',
|
||||
description: 'On-demand RAG search across 36,708 chunks: BAuA (TRBS/TRGS/ASR), OSHA Technical Manual, EU regulations. No ISO texts — only public domain sources.',
|
||||
example: 'Hazard "Collision" → TRBS 2111 Ch. 4.3, OSHA 1910.212(a)(1)',
|
||||
tags: ['RAG', 'BAuA', 'OSHA'],
|
||||
},
|
||||
{
|
||||
num: '05',
|
||||
title: 'Verification & Evidence',
|
||||
description: '25 evidence types are automatically suggested. Users create verification plans and assign test reports.',
|
||||
example: 'E01 Hazard Analysis Report, E04 Electrical Safety Test, E14 Software Validation',
|
||||
tags: ['25 evidence types', 'Traceability'],
|
||||
},
|
||||
{
|
||||
num: '06',
|
||||
title: 'Generate CE File',
|
||||
description: 'Structured document per MR 2023/1230 Annex IV: Description, risk assessment, norms, mitigations, evidence, declaration of conformity. Export as PDF.',
|
||||
example: 'Complete technical documentation with one click',
|
||||
tags: ['MR 2023/1230', 'Annex IV', 'PDF'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
function StepContent({ step, Icon }: { step: typeof flow.de.steps[0]; Icon: typeof FileText }) {
|
||||
return (
|
||||
<div className="relative pl-16">
|
||||
<div className="absolute left-0 w-12 h-12 rounded-xl bg-accent-electric/10 border border-accent-electric/20 flex items-center justify-center">
|
||||
<Icon className="w-5 h-5 text-accent-electric" />
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/[0.06] bg-white/[0.03] p-6">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<span className="font-mono text-xs text-accent-electric/60">{step.num}</span>
|
||||
<h3 className="text-lg font-bold">{step.title}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-white/50 mb-3">{step.description}</p>
|
||||
<div className="rounded-lg bg-enterprise-darker border border-white/[0.04] px-4 py-3 mb-3">
|
||||
<p className="font-mono text-xs text-white/40">{step.example}</p>
|
||||
</div>
|
||||
{step.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{step.tags.map(tag => (
|
||||
<TechBadge key={tag}>{tag}</TechBadge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function CEFlowSection() {
|
||||
const { lang } = useApp()
|
||||
const { steps } = flow[lang]
|
||||
|
||||
return (
|
||||
<section className="py-16 sm:py-24">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="relative">
|
||||
{/* Vertical line */}
|
||||
<div className="absolute left-6 top-0 bottom-0 w-px bg-white/[0.06]" />
|
||||
|
||||
<div className="space-y-8">
|
||||
{steps.map((step, idx) => {
|
||||
const Icon = stepIcons[idx]
|
||||
if (idx < 2) {
|
||||
return (
|
||||
<motion.div
|
||||
key={idx}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: ANIMATION.duration, delay: 0.3 + idx * 0.15, ease: ANIMATION.ease }}
|
||||
>
|
||||
<StepContent step={step} Icon={Icon} />
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<FadeInView key={idx} delay={0}>
|
||||
<StepContent step={step} Icon={Icon} />
|
||||
</FadeInView>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
'use client'
|
||||
|
||||
import { Check, AlertTriangle, X } from 'lucide-react'
|
||||
import { useApp } from '@/lib/context'
|
||||
import FadeInView from '@/components/ui/FadeInView'
|
||||
import SectionHeading from '@/components/ui/SectionHeading'
|
||||
|
||||
type ReqStatus = 'done' | 'partial' | 'missing'
|
||||
const statusIcons = { done: Check, partial: AlertTriangle, missing: X }
|
||||
const statusColors = { done: 'text-green-400 bg-green-500/10', partial: 'text-amber-400 bg-amber-500/10', missing: 'text-red-400 bg-red-500/10' }
|
||||
|
||||
const heading = {
|
||||
de: {
|
||||
tag: 'CRA COMPLIANCE',
|
||||
title: 'Was muss ich tun um',
|
||||
titleHighlight: 'CRA-konform zu werden?',
|
||||
subtitle: 'Der Cyber Resilience Act (EU 2024/2847) gilt ab September 2027. BreakPilot zeigt den Status pro Anforderung.',
|
||||
},
|
||||
en: {
|
||||
tag: 'CRA COMPLIANCE',
|
||||
title: 'What do I need to do to become',
|
||||
titleHighlight: 'CRA-compliant?',
|
||||
subtitle: 'The Cyber Resilience Act (EU 2024/2847) applies from September 2027. BreakPilot shows the status per requirement.',
|
||||
},
|
||||
}
|
||||
|
||||
const requirements = {
|
||||
de: [
|
||||
{ req: 'Schwachstellenmanagement einrichten', detail: 'Prozess für Identifikation, Bewertung und Behebung von Schwachstellen', status: 'missing' as ReqStatus },
|
||||
{ req: 'SBOM erstellen und pflegen', detail: 'Software Bill of Materials für jedes Produkt mit digitalen Elementen', status: 'partial' as ReqStatus },
|
||||
{ req: 'Security-Updates ermöglichen (OTA/SOTA)', detail: 'Mechanismus für sichere Software-Updates über die gesamte Lebensdauer', status: 'missing' as ReqStatus },
|
||||
{ req: 'Meldepflichten etablieren (24h/72h)', detail: 'Aktiv ausgenutzte Schwachstellen innerhalb 24h an ENISA melden', status: 'missing' as ReqStatus },
|
||||
{ req: 'Koordinierte Offenlegung (PSIRT)', detail: 'Product Security Incident Response Team und Disclosure Policy', status: 'missing' as ReqStatus },
|
||||
{ req: 'Technische Dokumentation aktualisieren', detail: 'Risikoanalyse, Design-Entscheidungen, Test-Ergebnisse dokumentieren', status: 'partial' as ReqStatus },
|
||||
{ req: 'Secure by Design', detail: 'Standardmäßig sichere Konfiguration, minimale Angriffsfläche', status: 'done' as ReqStatus },
|
||||
{ req: 'Keine bekannten Schwachstellen ausliefern', detail: 'Vor Inverkehrbringen alle bekannten CVEs beheben', status: 'partial' as ReqStatus },
|
||||
],
|
||||
en: [
|
||||
{ req: 'Establish vulnerability management', detail: 'Process for identification, assessment and remediation of vulnerabilities', status: 'missing' as ReqStatus },
|
||||
{ req: 'Create and maintain SBOM', detail: 'Software Bill of Materials for every product with digital elements', status: 'partial' as ReqStatus },
|
||||
{ req: 'Enable security updates (OTA/SOTA)', detail: 'Mechanism for secure software updates throughout the product lifetime', status: 'missing' as ReqStatus },
|
||||
{ req: 'Establish reporting obligations (24h/72h)', detail: 'Report actively exploited vulnerabilities to ENISA within 24h', status: 'missing' as ReqStatus },
|
||||
{ req: 'Coordinated disclosure (PSIRT)', detail: 'Product Security Incident Response Team and disclosure policy', status: 'missing' as ReqStatus },
|
||||
{ req: 'Update technical documentation', detail: 'Document risk analysis, design decisions, test results', status: 'partial' as ReqStatus },
|
||||
{ req: 'Secure by Design', detail: 'Secure configuration by default, minimal attack surface', status: 'done' as ReqStatus },
|
||||
{ req: 'Ship without known vulnerabilities', detail: 'Remediate all known CVEs before placing on market', status: 'partial' as ReqStatus },
|
||||
],
|
||||
}
|
||||
|
||||
export default function CRAFahrplanSection() {
|
||||
const { lang } = useApp()
|
||||
const h = heading[lang]
|
||||
const reqs = requirements[lang]
|
||||
|
||||
return (
|
||||
<section className="py-24 sm:py-32">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading tag={h.tag} title={h.title} titleHighlight={h.titleHighlight} subtitle={h.subtitle} />
|
||||
|
||||
<div className="space-y-3">
|
||||
{reqs.map((item, idx) => {
|
||||
const Icon = statusIcons[item.status]
|
||||
return (
|
||||
<FadeInView key={idx} delay={idx * 0.05}>
|
||||
<div className="rounded-xl border border-white/[0.06] bg-white/[0.03] p-4 flex items-start gap-4">
|
||||
<div className={`w-8 h-8 rounded-lg ${statusColors[item.status]} flex items-center justify-center shrink-0 mt-0.5`}>
|
||||
<Icon className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="text-sm font-bold mb-0.5">{item.req}</h4>
|
||||
<p className="text-xs text-white/40">{item.detail}</p>
|
||||
</div>
|
||||
<span className={`text-xs font-mono shrink-0 ${item.status === 'done' ? 'text-green-400' : item.status === 'partial' ? 'text-amber-400' : 'text-red-400'}`}>
|
||||
{item.status === 'done' ? '✓' : item.status === 'partial' ? '◐' : '✗'}
|
||||
</span>
|
||||
</div>
|
||||
</FadeInView>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
'use client'
|
||||
|
||||
import { Shield, GitBranch, Server, Scan, FileCheck, Layers } from 'lucide-react'
|
||||
import { useApp } from '@/lib/context'
|
||||
import SectionHeading from '@/components/ui/SectionHeading'
|
||||
import GlassCard from '@/components/ui/GlassCard'
|
||||
|
||||
const icons = [Shield, GitBranch, Scan, Server, FileCheck, Layers]
|
||||
|
||||
const content = {
|
||||
de: {
|
||||
tag: '05 / DIFFERENZIERUNG',
|
||||
title: 'Was BreakPilot',
|
||||
titleHighlight: 'einzigartig macht.',
|
||||
subtitle: 'Sechs Alleinstellungsmerkmale, die kein anderer Anbieter in einer Plattform vereint.',
|
||||
items: [
|
||||
{ title: 'Deterministisch, nicht generativ', description: 'Regelbasierte Analyse statt LLM-Interpretation. Jedes Ergebnis ist reproduzierbar und versioniert — unabhängig vom Modell.' },
|
||||
{ title: 'Lückenloser Decision Trail', description: 'Von der Rechtsquelle über die Obligation zum Control bis zur Maßnahme. Jeder Schritt ist auditierbar und dokumentiert.' },
|
||||
{ title: 'Code Security integriert', description: 'SAST, DAST, SBOM und Container Scanning als Teil der Compliance-Plattform — nicht als separates Tool.' },
|
||||
{ title: 'Vollständig on-premise deploybar', description: 'Kein US-Cloud-Anbieter in der gesamten Architektur. Betrieb auf eigener Hardware oder in BSI-zertifizierten Rechenzentren.' },
|
||||
{ title: 'Regulierungsübergreifend', description: 'DSGVO, NIS2, AI Act, Maschinenverordnung, TDDDG, DORA — eine Plattform statt sieben Einzellösungen.' },
|
||||
{ title: '294.000+ atomare Controls', description: 'Abgeleitet aus 380+ Rechtsquellen. Nicht manuell kuratiert, sondern systematisch aus Originaltext extrahiert und verifiziert.' },
|
||||
],
|
||||
},
|
||||
en: {
|
||||
tag: '05 / DIFFERENTIATION',
|
||||
title: 'What makes BreakPilot',
|
||||
titleHighlight: 'unique.',
|
||||
subtitle: 'Six unique selling points that no other provider combines in a single platform.',
|
||||
items: [
|
||||
{ title: 'Deterministic, not generative', description: 'Rule-based analysis instead of LLM interpretation. Every result is reproducible and versioned — independent of the model.' },
|
||||
{ title: 'Seamless decision trail', description: 'From legal source through obligation to control to action. Every step is auditable and documented.' },
|
||||
{ title: 'Code security integrated', description: 'SAST, DAST, SBOM and container scanning as part of the compliance platform — not as a separate tool.' },
|
||||
{ title: 'Fully on-premise deployable', description: 'No US cloud provider in the entire architecture. Operation on own hardware or in BSI-certified data centers.' },
|
||||
{ title: 'Cross-regulatory', description: 'GDPR, NIS2, AI Act, Machinery Regulation, TDDDG, DORA — one platform instead of seven individual solutions.' },
|
||||
{ title: '294,000+ atomic controls', description: 'Derived from 380+ legal sources. Not manually curated, but systematically extracted and verified from original text.' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export default function ComparisonSection() {
|
||||
const { lang } = useApp()
|
||||
const c = content[lang]
|
||||
|
||||
return (
|
||||
<section id="comparison" className="py-24 sm:py-32">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading
|
||||
tag={c.tag}
|
||||
title={c.title}
|
||||
titleHighlight={c.titleHighlight}
|
||||
subtitle={c.subtitle}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{c.items.map((item, idx) => {
|
||||
const Icon = icons[idx]
|
||||
return (
|
||||
<GlassCard key={idx} delay={idx * 0.08}>
|
||||
<div className="w-10 h-10 rounded-xl bg-accent-electric/10 flex items-center justify-center mb-4">
|
||||
<Icon className="w-5 h-5 text-accent-electric" />
|
||||
</div>
|
||||
<h3 className="text-sm font-bold mb-2">{item.title}</h3>
|
||||
<p className="text-xs text-white/40 leading-relaxed">{item.description}</p>
|
||||
</GlassCard>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
'use client'
|
||||
|
||||
import { X, Check } from 'lucide-react'
|
||||
import { t } from '@/lib/content'
|
||||
import { useApp } from '@/lib/context'
|
||||
import SectionHeading from '@/components/ui/SectionHeading'
|
||||
import FadeInView from '@/components/ui/FadeInView'
|
||||
import StatusIndicator from '@/components/ui/StatusIndicator'
|
||||
|
||||
export default function ContinuousSection() {
|
||||
const { lang } = useApp()
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<section id="continuous" className="py-24 sm:py-32 section-alt">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading
|
||||
tag={i.continuous.tag}
|
||||
title={i.continuous.title}
|
||||
titleHighlight={i.continuous.titleHighlight}
|
||||
subtitle={i.continuous.subtitle}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<FadeInView direction="left">
|
||||
<div className="rounded-2xl border border-red-500/15 bg-red-500/[0.03] p-6 h-full">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<StatusIndicator label="Offline" status="error" />
|
||||
<h3 className="text-sm font-bold text-red-400">{i.continuous.comparison.annual.title}</h3>
|
||||
</div>
|
||||
<ul className="space-y-3">
|
||||
{i.continuous.comparison.annual.points.map((point, idx) => (
|
||||
<li key={idx} className="flex items-start gap-3 text-sm text-white/40">
|
||||
<X className="w-4 h-4 text-red-400/50 mt-0.5 shrink-0" />
|
||||
{point}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
<FadeInView direction="right">
|
||||
<div className="rounded-2xl border border-green-500/15 bg-green-500/[0.03] p-6 h-full">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<StatusIndicator label="Live" status="active" />
|
||||
<h3 className="text-sm font-bold text-green-400">{i.continuous.comparison.continuous.title}</h3>
|
||||
</div>
|
||||
<ul className="space-y-3">
|
||||
{i.continuous.comparison.continuous.points.map((point, idx) => (
|
||||
<li key={idx} className="flex items-start gap-3 text-sm text-white/60">
|
||||
<Check className="w-4 h-4 text-green-400 mt-0.5 shrink-0" />
|
||||
{point}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { X } from 'lucide-react'
|
||||
import { useApp } from '@/lib/context'
|
||||
|
||||
const data = {
|
||||
de: {
|
||||
before: {
|
||||
title: 'Ausgangslage',
|
||||
items: [
|
||||
'15 Jahre altes Embedded Board',
|
||||
'Kein Secure Element / TPM',
|
||||
'Kein Secure Boot, kein OTA',
|
||||
'Hardcoded Credentials im Firmware',
|
||||
'Alter TCP/IP Stack ohne Patches',
|
||||
'Penetration-Test: 187 Findings',
|
||||
'"Was davon ist wirklich kritisch?"',
|
||||
],
|
||||
},
|
||||
after: {
|
||||
title: 'BreakPilot Delta-Analyse',
|
||||
items: [
|
||||
{ text: '3 Findings blockieren CE/CRA → sofort handeln', type: 'critical' as const },
|
||||
{ text: '12 Findings sind Software-only Fixes', type: 'fixable' as const },
|
||||
{ text: '172 Findings sind kosmetisch oder low-risk', type: 'ok' as const },
|
||||
{ text: 'Hardware-Redesign: wahrscheinlich NICHT nötig', type: 'ok' as const },
|
||||
{ text: 'RED-Re-Zertifizierung: nur bei Funkmodul-Änderung', type: 'fixable' as const },
|
||||
{ text: 'Geschätzter Aufwand: €15k statt €50k', type: 'ok' as const },
|
||||
{ text: 'Jira-Tickets mit Fix-Vorschlägen erstellt', type: 'ok' as const },
|
||||
],
|
||||
},
|
||||
},
|
||||
en: {
|
||||
before: {
|
||||
title: 'Starting Point',
|
||||
items: [
|
||||
'15-year-old embedded board',
|
||||
'No Secure Element / TPM',
|
||||
'No Secure Boot, no OTA',
|
||||
'Hardcoded credentials in firmware',
|
||||
'Legacy TCP/IP stack without patches',
|
||||
'Penetration test: 187 findings',
|
||||
'"Which ones actually matter?"',
|
||||
],
|
||||
},
|
||||
after: {
|
||||
title: 'BreakPilot Delta Analysis',
|
||||
items: [
|
||||
{ text: '3 findings block CE/CRA → act immediately', type: 'critical' as const },
|
||||
{ text: '12 findings are software-only fixes', type: 'fixable' as const },
|
||||
{ text: '172 findings are cosmetic or low-risk', type: 'ok' as const },
|
||||
{ text: 'Hardware redesign: probably NOT necessary', type: 'ok' as const },
|
||||
{ text: 'RED re-certification: only if RF module changes', type: 'fixable' as const },
|
||||
{ text: 'Estimated effort: €15k instead of €50k', type: 'ok' as const },
|
||||
{ text: 'Jira tickets with fix suggestions created', type: 'ok' as const },
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const typeColors = {
|
||||
critical: 'text-red-400',
|
||||
fixable: 'text-amber-400',
|
||||
ok: 'text-green-400',
|
||||
}
|
||||
|
||||
const typeIcons = {
|
||||
critical: '●',
|
||||
fixable: '◐',
|
||||
ok: '●',
|
||||
}
|
||||
|
||||
export default function DeltaImpactSection() {
|
||||
const { lang } = useApp()
|
||||
const d = data[lang]
|
||||
|
||||
return (
|
||||
<section className="py-16 sm:py-24">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 relative">
|
||||
{/* Before */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
|
||||
>
|
||||
<div className="rounded-2xl border border-red-500/15 bg-red-500/[0.03] p-6 h-full">
|
||||
<h3 className="text-sm font-bold text-red-400 mb-5 font-mono uppercase tracking-wider">
|
||||
{d.before.title}
|
||||
</h3>
|
||||
<ul className="space-y-3">
|
||||
{d.before.items.map((item, idx) => (
|
||||
<li key={idx} className="flex items-start gap-3 text-sm text-white/40">
|
||||
<X className="w-4 h-4 text-red-400/50 mt-0.5 shrink-0" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* After */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.1, ease: [0.22, 1, 0.36, 1] }}
|
||||
>
|
||||
<div className="rounded-2xl border border-green-500/15 bg-green-500/[0.03] p-6 h-full">
|
||||
<h3 className="text-sm font-bold text-green-400 mb-5 font-mono uppercase tracking-wider">
|
||||
{d.after.title}
|
||||
</h3>
|
||||
<ul className="space-y-3">
|
||||
{d.after.items.map((item, idx) => (
|
||||
<li key={idx} className="flex items-start gap-3 text-sm text-white/60">
|
||||
<span className={`mt-0.5 shrink-0 text-xs ${typeColors[item.type]}`}>
|
||||
{typeIcons[item.type]}
|
||||
</span>
|
||||
{item.text}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
'use client'
|
||||
|
||||
import { Shield, FileCheck, ClipboardCheck, Check, X } from 'lucide-react'
|
||||
import { t } from '@/lib/content'
|
||||
import { useApp } from '@/lib/context'
|
||||
import SectionHeading from '@/components/ui/SectionHeading'
|
||||
import GlassCard from '@/components/ui/GlassCard'
|
||||
import FadeInView from '@/components/ui/FadeInView'
|
||||
|
||||
const iconMap: Record<string, typeof Shield> = {
|
||||
Shield,
|
||||
FileCheck,
|
||||
ClipboardCheck,
|
||||
}
|
||||
|
||||
export default function DeterministicSection() {
|
||||
const { lang } = useApp()
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<section id="deterministic" className="py-24 sm:py-32">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading
|
||||
tag={i.deterministic.tag}
|
||||
title={i.deterministic.title}
|
||||
titleHighlight={i.deterministic.titleHighlight}
|
||||
subtitle={i.deterministic.subtitle}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-16">
|
||||
{i.deterministic.pillars.map((pillar, idx) => {
|
||||
const Icon = iconMap[pillar.icon]
|
||||
return (
|
||||
<GlassCard key={idx} delay={idx * 0.1}>
|
||||
<div className="w-12 h-12 rounded-xl bg-accent-indigo/10 flex items-center justify-center mb-4">
|
||||
<Icon className="w-6 h-6 text-accent-indigo" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold mb-2">{pillar.title}</h3>
|
||||
<p className="text-sm text-white/50">{pillar.description}</p>
|
||||
</GlassCard>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<FadeInView>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="rounded-2xl border border-red-500/20 bg-red-500/[0.04] p-6">
|
||||
<h4 className="text-sm font-bold text-red-400 mb-4 font-mono uppercase tracking-wider">
|
||||
{i.deterministic.comparison.llm.title}
|
||||
</h4>
|
||||
<ul className="space-y-3">
|
||||
{i.deterministic.comparison.llm.items.map((item, idx) => (
|
||||
<li key={idx} className="flex items-start gap-3 text-sm text-white/50">
|
||||
<X className="w-4 h-4 text-red-400/60 mt-0.5 shrink-0" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-green-500/20 bg-green-500/[0.04] p-6">
|
||||
<h4 className="text-sm font-bold text-green-400 mb-4 font-mono uppercase tracking-wider">
|
||||
{i.deterministic.comparison.breakpilot.title}
|
||||
</h4>
|
||||
<ul className="space-y-3">
|
||||
{i.deterministic.comparison.breakpilot.items.map((item, idx) => (
|
||||
<li key={idx} className="flex items-start gap-3 text-sm text-white/70">
|
||||
<Check className="w-4 h-4 text-green-400 mt-0.5 shrink-0" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { ArrowRight, ChevronDown } from 'lucide-react'
|
||||
import { t } from '@/lib/content'
|
||||
import { useApp } from '@/lib/context'
|
||||
import GradientText from '@/components/ui/GradientText'
|
||||
import CTAButton from '@/components/ui/CTAButton'
|
||||
import StatusIndicator from '@/components/ui/StatusIndicator'
|
||||
import { ANIMATION } from '@/lib/constants'
|
||||
|
||||
export default function HeroSection() {
|
||||
const { lang } = useApp()
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<section id="hero" className="relative min-h-screen flex items-center justify-center enterprise-grid overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-enterprise-dark" />
|
||||
|
||||
<div className="relative z-10 max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, ease: ANIMATION.ease }}
|
||||
>
|
||||
<div className="inline-flex items-center gap-3 mb-8 px-4 py-2 rounded-full border border-white/[0.08] bg-white/[0.04]">
|
||||
<StatusIndicator label={i.hero.status} />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.1, ease: ANIMATION.ease }}
|
||||
className="mb-3"
|
||||
>
|
||||
<span className="mono-label tracking-widest">{i.hero.badge}</span>
|
||||
</motion.div>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2, ease: ANIMATION.ease }}
|
||||
className="text-5xl sm:text-6xl lg:text-7xl font-bold mb-6 leading-tight text-shadow-glow"
|
||||
>
|
||||
{i.hero.title}
|
||||
<br />
|
||||
<GradientText>{i.hero.titleHighlight}</GradientText>
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3, ease: ANIMATION.ease }}
|
||||
className="text-lg sm:text-xl text-white/50 max-w-2xl mx-auto mb-10"
|
||||
>
|
||||
{i.hero.subtitle}
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.4, ease: ANIMATION.ease }}
|
||||
className="flex flex-col sm:flex-row items-center justify-center gap-4"
|
||||
>
|
||||
<CTAButton href="/plattform">
|
||||
{lang === 'de' ? 'Plattform entdecken' : 'Discover Platform'}
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</CTAButton>
|
||||
<CTAButton variant="ghost" href="/preise">
|
||||
{i.hero.cta}
|
||||
</CTAButton>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 1, delay: 1 }}
|
||||
className="absolute bottom-8 left-1/2 -translate-x-1/2"
|
||||
>
|
||||
<motion.div
|
||||
animate={{ y: [0, 8, 0] }}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: 'easeInOut' }}
|
||||
>
|
||||
<ChevronDown className="w-5 h-5 text-white/20" />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
'use client'
|
||||
|
||||
import { t } from '@/lib/content'
|
||||
import { useApp } from '@/lib/context'
|
||||
import SectionHeading from '@/components/ui/SectionHeading'
|
||||
import FadeInView from '@/components/ui/FadeInView'
|
||||
|
||||
const statusColors = {
|
||||
success: 'text-green-400',
|
||||
warning: 'text-amber-400',
|
||||
neutral: 'text-accent-electric',
|
||||
}
|
||||
|
||||
const statusDots = {
|
||||
success: 'bg-green-400',
|
||||
warning: 'bg-amber-400',
|
||||
neutral: 'bg-accent-electric',
|
||||
}
|
||||
|
||||
export default function ImpactSection() {
|
||||
const { lang } = useApp()
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<section id="impact" className="py-24 sm:py-32">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading
|
||||
tag={i.impact.tag}
|
||||
title={i.impact.title}
|
||||
titleHighlight={i.impact.titleHighlight}
|
||||
subtitle={i.impact.subtitle}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<FadeInView direction="left">
|
||||
<div className="rounded-2xl bg-enterprise-darker border border-white/[0.06] p-6 font-mono text-sm overflow-hidden">
|
||||
<div className="flex items-center gap-2 mb-4 pb-3 border-b border-white/[0.06]">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500/60" />
|
||||
<div className="w-3 h-3 rounded-full bg-amber-500/60" />
|
||||
<div className="w-3 h-3 rounded-full bg-green-500/60" />
|
||||
<span className="ml-2 text-xs text-white/30">regulatory-impact-analysis</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{i.impact.terminalLines.map((line, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`
|
||||
${line.type === 'input' ? 'text-white/70' : ''}
|
||||
${line.type === 'output' ? 'text-white/40' : ''}
|
||||
${line.type === 'signal' ? 'text-green-400' : ''}
|
||||
`}
|
||||
>
|
||||
{line.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
<FadeInView direction="right">
|
||||
<div className="grid grid-cols-2 gap-4 h-full">
|
||||
{i.impact.outputs.map((output, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="rounded-2xl bg-white/[0.04] border border-white/[0.06] p-5 flex flex-col justify-between"
|
||||
>
|
||||
<p className="text-xs text-white/40 mb-2">{output.label}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-2 h-2 rounded-full ${statusDots[output.status]}`} />
|
||||
<span className={`text-2xl font-bold ${statusColors[output.status]}`}>
|
||||
{output.value}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
'use client'
|
||||
|
||||
import { Check } from 'lucide-react'
|
||||
import { t } from '@/lib/content'
|
||||
import { useApp } from '@/lib/context'
|
||||
import SectionHeading from '@/components/ui/SectionHeading'
|
||||
import GlassCard from '@/components/ui/GlassCard'
|
||||
|
||||
const accentColors = ['border-t-accent-electric', 'border-t-accent-indigo', 'border-t-accent-purple']
|
||||
|
||||
export default function LegalSection() {
|
||||
const { lang } = useApp()
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<section id="legal" className="py-24 sm:py-32">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading
|
||||
tag={i.legal.tag}
|
||||
title={i.legal.title}
|
||||
titleHighlight={i.legal.titleHighlight}
|
||||
subtitle={i.legal.subtitle}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{i.legal.regulations.map((reg, idx) => (
|
||||
<GlassCard key={idx} delay={idx * 0.1} className={`border-t-2 ${accentColors[idx]}`}>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-2xl font-bold font-mono">{reg.name}</h3>
|
||||
<p className="text-xs text-white/40 mt-1">{reg.fullName}</p>
|
||||
</div>
|
||||
<ul className="space-y-2">
|
||||
{reg.features.map((feature, fi) => (
|
||||
<li key={fi} className="flex items-start gap-2 text-sm text-white/50">
|
||||
<Check className="w-3.5 h-3.5 text-accent-signal mt-0.5 shrink-0" />
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</GlassCard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,816 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef, useMemo } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import GradientText from '@/components/ui/GradientText'
|
||||
import FadeInView from '@/components/ui/FadeInView'
|
||||
import { useApp } from '@/lib/context'
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
const MONO: React.CSSProperties = {
|
||||
fontFamily: '"JetBrains Mono","SF Mono",ui-monospace,monospace',
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
}
|
||||
|
||||
const CSS_KF = `
|
||||
@keyframes uspFlowR { 0%{stroke-dashoffset:0} 100%{stroke-dashoffset:-14px} }
|
||||
@keyframes uspSpin { from{transform:rotate(0deg)} to{transform:rotate(360deg)} }
|
||||
@keyframes uspPulse {
|
||||
0%,100% { box-shadow: 0 0 38px rgba(167,139,250,.55), 0 0 80px rgba(167,139,250,.2), inset 0 3px 0 rgba(255,255,255,.35), inset 0 -6px 12px rgba(0,0,0,.35); }
|
||||
50% { box-shadow: 0 0 58px rgba(167,139,250,.85), 0 0 110px rgba(167,139,250,.35), inset 0 3px 0 rgba(255,255,255,.4), inset 0 -6px 12px rgba(0,0,0,.35); }
|
||||
}
|
||||
@keyframes uspPulseLight {
|
||||
0%,100% { box-shadow: 0 0 28px rgba(167,139,250,.4), 0 0 56px rgba(167,139,250,.15), inset 0 3px 0 rgba(255,255,255,.5), inset 0 -6px 12px rgba(0,0,0,.2); }
|
||||
50% { box-shadow: 0 0 44px rgba(167,139,250,.65), 0 0 80px rgba(167,139,250,.25), inset 0 3px 0 rgba(255,255,255,.55), inset 0 -6px 12px rgba(0,0,0,.2); }
|
||||
}
|
||||
@keyframes uspHeading {
|
||||
0%,100% { text-shadow: 0 0 22px rgba(167,139,250,.3); }
|
||||
50% { text-shadow: 0 0 36px rgba(167,139,250,.55); }
|
||||
}
|
||||
`
|
||||
|
||||
// ── Light mode hook ───────────────────────────────────────────────────────────
|
||||
function useIsLight() {
|
||||
const [isLight, setIsLight] = useState(false)
|
||||
useEffect(() => {
|
||||
const check = () => setIsLight(document.documentElement.classList.contains('theme-light'))
|
||||
check()
|
||||
const obs = new MutationObserver(check)
|
||||
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
|
||||
return () => obs.disconnect()
|
||||
}, [])
|
||||
return isLight
|
||||
}
|
||||
|
||||
// ── Ticker ────────────────────────────────────────────────────────────────────
|
||||
function useTicker(fn: () => void, min = 180, max = 420, skip = 0.1) {
|
||||
const ref = useRef(fn)
|
||||
ref.current = fn
|
||||
useEffect(() => {
|
||||
let t: ReturnType<typeof setTimeout>
|
||||
const loop = () => {
|
||||
if (Math.random() > skip) ref.current()
|
||||
t = setTimeout(loop, min + Math.random() * (max - min))
|
||||
}
|
||||
loop()
|
||||
return () => clearTimeout(t)
|
||||
}, [min, max, skip])
|
||||
}
|
||||
|
||||
function TickerShell({ tint, isLight, children }: { tint: string; isLight: boolean; children: React.ReactNode }) {
|
||||
return (
|
||||
<div style={{
|
||||
...MONO, marginTop: 10, padding: '5px 9px',
|
||||
background: isLight ? '#f1f5f9' : 'rgba(0,0,0,.38)',
|
||||
border: `1px solid ${tint}55`,
|
||||
borderRadius: 6, fontSize: 10.5,
|
||||
color: isLight ? '#475569' : 'rgba(236,233,247,.88)',
|
||||
display: 'flex', alignItems: 'center', gap: 7,
|
||||
whiteSpace: 'nowrap', overflow: 'hidden', height: 22,
|
||||
}}>{children}</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TickTrace({ tint, isLight }: { tint: string; isLight: boolean }) {
|
||||
const [n, setN] = useState(12748)
|
||||
useTicker(() => setN(v => v + 1 + Math.floor(Math.random() * 3)), 250, 500)
|
||||
return (
|
||||
<TickerShell tint={tint} isLight={isLight}>
|
||||
<span style={{ color: isLight ? '#16a34a' : '#4ade80' }}>●</span>
|
||||
<span style={{ color: tint, opacity: .85 }}>trace</span>
|
||||
<span style={{ color: isLight ? '#1a1a2e' : '#f5f3fc', fontWeight: 600 }}>{n.toLocaleString()}</span>
|
||||
<span style={{ color: isLight ? '#94a3b8' : 'rgba(236,233,247,.45)' }}>evidence-chain</span>
|
||||
</TickerShell>
|
||||
)
|
||||
}
|
||||
|
||||
function TickEngine({ tint, isLight }: { tint: string; isLight: boolean }) {
|
||||
const [v, setV] = useState(428)
|
||||
const [rate, setRate] = useState(99.4)
|
||||
useTicker(() => {
|
||||
setV(x => x + 1 + Math.floor(Math.random() * 4))
|
||||
setRate(r => Math.max(97, Math.min(99.9, r + (Math.random() - 0.5) * 0.3)))
|
||||
}, 220, 420)
|
||||
return (
|
||||
<TickerShell tint={tint} isLight={isLight}>
|
||||
<span style={{ color: isLight ? '#16a34a' : '#4ade80' }}>●</span>
|
||||
<span style={{ color: tint, opacity: .85 }}>validate</span>
|
||||
<span style={{ color: isLight ? '#1a1a2e' : '#f5f3fc', fontWeight: 600 }}>{v.toLocaleString()}</span>
|
||||
<span style={{ color: isLight ? '#16a34a' : '#4ade80' }}>{rate.toFixed(1)}%</span>
|
||||
</TickerShell>
|
||||
)
|
||||
}
|
||||
|
||||
function TickOptimizer({ tint, isLight }: { tint: string; isLight: boolean }) {
|
||||
const ops = ['ROI: 2.418 € / dev', 'gap → policy §4.2', 'dedup 128 tickets', 'sweet-spot: 22 KLOC', 'tradeoff: speed↔risk']
|
||||
const [i, setI] = useState(0)
|
||||
useTicker(() => setI(x => (x + 1) % ops.length), 900, 1600, 0.05)
|
||||
return (
|
||||
<TickerShell tint={tint} isLight={isLight}>
|
||||
<span style={{ color: '#fbbf24' }}>✦</span>
|
||||
<span style={{ color: tint, opacity: .85 }}>optimize</span>
|
||||
<span style={{ color: isLight ? '#1a1a2e' : '#f5f3fc', overflow: 'hidden', textOverflow: 'ellipsis', flex: 1 }}>{ops[i]}</span>
|
||||
</TickerShell>
|
||||
)
|
||||
}
|
||||
|
||||
function TickStack({ tint, isLight }: { tint: string; isLight: boolean }) {
|
||||
const regs = ['DSGVO', 'NIS-2', 'DORA', 'EU AI Act', 'ISO 27001', 'BSI C5']
|
||||
const [i, setI] = useState(0)
|
||||
const [c, setC] = useState(1208)
|
||||
useTicker(() => { setI(x => (x + 1) % regs.length); setC(v => v + Math.floor(Math.random() * 3)) }, 800, 1400, 0.05)
|
||||
return (
|
||||
<TickerShell tint={tint} isLight={isLight}>
|
||||
<span style={{ color: isLight ? '#16a34a' : '#4ade80' }}>●</span>
|
||||
<span style={{ color: tint, opacity: .85 }}>check</span>
|
||||
<span style={{ color: isLight ? '#1a1a2e' : '#f5f3fc', fontWeight: 600 }}>{regs[i]}</span>
|
||||
<span style={{ color: isLight ? '#94a3b8' : 'rgba(236,233,247,.4)' }}>·</span>
|
||||
<span style={{ color: isLight ? '#1a1a2e' : '#f5f3fc' }}>{c.toLocaleString()}</span>
|
||||
</TickerShell>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Data ──────────────────────────────────────────────────────────────────────
|
||||
interface DetailItem {
|
||||
tint: string
|
||||
icon: string
|
||||
kicker: string
|
||||
title: string
|
||||
body: string
|
||||
bullets?: string[]
|
||||
stat?: { k: string; v: string }
|
||||
}
|
||||
|
||||
function getDetails(de: boolean): Record<string, DetailItem> {
|
||||
return {
|
||||
rfq: {
|
||||
tint: '#a78bfa', icon: '⎈',
|
||||
kicker: de ? 'Säule · Regulatory' : 'Pillar · Regulatory',
|
||||
title: de ? 'DSGVO / NIS2 / AI Act' : 'GDPR / NIS2 / AI Act',
|
||||
body: de
|
||||
? '294.000+ atomare Controls aus 380+ Rechtsquellen. Jede Anforderung deterministisch abgeleitet und auf Artikel, Absatz und Erwägungsgrund rückführbar.'
|
||||
: '294,000+ atomic controls from 380+ legal sources. Every requirement deterministically derived and traceable to article, paragraph and recital.',
|
||||
bullets: de
|
||||
? ['DSGVO, NIS2, AI Act, DORA, TDDDG in einer Plattform', 'Automatische Verarbeitungsverzeichnisse und DSFA', 'Meldepflichten und Fristen automatisch überwacht']
|
||||
: ['GDPR, NIS2, AI Act, DORA, TDDDG in one platform', 'Automated records of processing and DPIA', 'Reporting obligations and deadlines automatically monitored'],
|
||||
stat: { k: de ? 'atomare Controls' : 'atomic controls', v: '294.000+' },
|
||||
},
|
||||
process: {
|
||||
tint: '#c084fc', icon: '⟲',
|
||||
kicker: de ? 'Säule · Regulatory' : 'Pillar · Regulatory',
|
||||
title: de ? 'CE & Maschinenverordnung' : 'CE & Machinery Regulation',
|
||||
body: de
|
||||
? 'Von der Maschinenbeschreibung zur CE-Akte. 1.058 Hazard Patterns, 225 Maßnahmen, 751 Normen — deterministisch zugeordnet, nicht generiert.'
|
||||
: 'From machine description to CE file. 1,058 hazard patterns, 225 mitigations, 751 standards — deterministically mapped, not generated.',
|
||||
bullets: de
|
||||
? ['Risikobeurteilung nach EN ISO 12100 automatisiert', 'SIL/PL-Berechnung aus RPZ-Werten', 'CE-Akte nach MVO 2023/1230 Anhang IV per Klick']
|
||||
: ['Risk assessment per EN ISO 12100 automated', 'SIL/PL calculation from RPZ values', 'CE file per MR 2023/1230 Annex IV with one click'],
|
||||
stat: { k: de ? 'Hazard Patterns' : 'hazard patterns', v: '1.058' },
|
||||
},
|
||||
bidir: {
|
||||
tint: '#fbbf24', icon: '⟨/⟩',
|
||||
kicker: de ? 'Säule · Code Security' : 'Pillar · Code Security',
|
||||
title: 'SAST / DAST / SBOM',
|
||||
body: de
|
||||
? 'Kontinuierliche Code-Analyse für Firmware, Embedded und Backend. Jedes Finding wird automatisch priorisiert: Blocker vs. Major vs. kosmetisch.'
|
||||
: 'Continuous code analysis for firmware, embedded and backend. Every finding is automatically prioritized: blocker vs. major vs. cosmetic.',
|
||||
bullets: de
|
||||
? ['Statische + dynamische Analyse bei jedem Commit', 'SBOM-Generierung (CRA-Pflicht ab 2027)', 'Secret Detection für hardcoded Credentials']
|
||||
: ['Static + dynamic analysis on every commit', 'SBOM generation (CRA mandatory from 2027)', 'Secret detection for hardcoded credentials'],
|
||||
stat: { k: de ? 'Validierungen / Tag' : 'validations / day', v: '~2.400' },
|
||||
},
|
||||
cont: {
|
||||
tint: '#f59e0b', icon: '◎',
|
||||
kicker: de ? 'Säule · Code Security' : 'Pillar · Code Security',
|
||||
title: de ? 'Continuous Pentesting' : 'Continuous Pentesting',
|
||||
body: de
|
||||
? 'Automatisierte Schwachstellensuche statt jährlicher Penetrationstests. Findings werden sofort als Jira-Tickets mit Fix-Vorschlägen erstellt.'
|
||||
: 'Automated vulnerability scanning instead of annual penetration tests. Findings immediately become Jira tickets with fix suggestions.',
|
||||
bullets: de
|
||||
? ['€30.000+ Einsparung vs. externe Pentests', 'Automatische Entscheidung: Hardware-Redesign oder Software-Fix?', 'CVE → Ticket → Fix → Nachweis in einer Pipeline']
|
||||
: ['€30,000+ savings vs. external pentests', 'Automatic decision: hardware redesign or software fix?', 'CVE → ticket → fix → evidence in one pipeline'],
|
||||
stat: { k: de ? 'Ø Kosten-Einsparung' : 'avg cost savings', v: '€30k+ / year' },
|
||||
},
|
||||
trace: {
|
||||
tint: '#a78bfa', icon: '⇄',
|
||||
kicker: de ? 'Integration' : 'Integration',
|
||||
title: de ? 'Jira / Linear Integration' : 'Jira / Linear Integration',
|
||||
body: de
|
||||
? 'Jedes Finding wird automatisch als Ticket im Issue-Tracker erstellt — mit Priorität, Kontext, Fix-Vorschlag und Verknüpfung zur Rechtsquelle.'
|
||||
: 'Every finding is automatically created as a ticket in your issue tracker — with priority, context, fix suggestion and link to legal source.',
|
||||
bullets: de
|
||||
? ['Jira, Linear, GitLab Issues, GitHub Issues', 'Blocker / Major / Minor automatisch klassifiziert', 'Fix-Vorschläge direkt im Ticket']
|
||||
: ['Jira, Linear, GitLab Issues, GitHub Issues', 'Blocker / Major / Minor automatically classified', 'Fix suggestions directly in the ticket'],
|
||||
},
|
||||
engine: {
|
||||
tint: '#c084fc', icon: '◉',
|
||||
kicker: de ? 'Integration' : 'Integration',
|
||||
title: de ? 'End-to-End Traceability' : 'End-to-End Traceability',
|
||||
body: de
|
||||
? 'Lückenloser Decision Trail: Rechtsquelle → Obligation → Control → Maßnahme → Code-Änderung → Nachweis. Ein Klick von der Klausel bis zur Codezeile.'
|
||||
: 'Seamless decision trail: Legal source → Obligation → Control → Action → Code change → Evidence. One click from clause to line of code.',
|
||||
bullets: de
|
||||
? ['Versionierte Evidence-Chain', 'Audit-Log pro Änderung signiert', 'Rechtsquelle bis Codezeile nachvollziehbar']
|
||||
: ['Versioned evidence chain', 'Audit log signed per change', 'Legal source to code line traceable'],
|
||||
},
|
||||
opt: {
|
||||
tint: '#fbbf24', icon: '✦',
|
||||
kicker: de ? 'Integration' : 'Integration',
|
||||
title: de ? 'Delta-Impact-Analyse' : 'Delta Impact Analysis',
|
||||
body: de
|
||||
? 'Neues Funkmodul eingebaut? BreakPilot zeigt sofort: neue Hazards, neue Normen, CRA-Relevanz, RED-Re-Zertifizierung nötig? Hardware-Redesign oder Software-Fix?'
|
||||
: 'New wireless module installed? BreakPilot immediately shows: new hazards, new standards, CRA relevance, RED re-certification needed? Hardware redesign or software fix?',
|
||||
bullets: de
|
||||
? ['Änderungsfolgenanalyse in Echtzeit', 'Hardware vs. Software Entscheidungsunterstützung', 'Spart €50k+ an externer Beratung']
|
||||
: ['Change impact analysis in real-time', 'Hardware vs. software decision support', 'Saves €50k+ in external consulting'],
|
||||
},
|
||||
stack: {
|
||||
tint: '#f59e0b', icon: '◎',
|
||||
kicker: de ? 'Integration' : 'Integration',
|
||||
title: de ? 'Sovereign AI · On-Premise' : 'Sovereign AI · On-Premise',
|
||||
body: de
|
||||
? 'Kein US-Cloud-Anbieter. Self-hosted LLM auf Apple Silicon. BSI-konforme Infrastruktur. Ihre Daten verlassen nie Ihr Netzwerk.'
|
||||
: 'No US cloud provider. Self-hosted LLM on Apple Silicon. BSI-compliant infrastructure. Your data never leaves your network.',
|
||||
bullets: de
|
||||
? ['Kein FISA 702, kein CLOUD Act', 'On-Premise Appliance ab €7.900', 'EU-souveräner Software-Stack']
|
||||
: ['No FISA 702, no CLOUD Act', 'On-premise appliance from €7,900', 'EU-sovereign software stack'],
|
||||
},
|
||||
hub: {
|
||||
tint: '#a78bfa', icon: '∞',
|
||||
kicker: de ? 'Die Schleife' : 'The Loop',
|
||||
title: de ? 'Compliance ↔ Code · Immer in Sync' : 'Compliance ↔ Code · Always in sync',
|
||||
body: de
|
||||
? 'Die Plattform ist eine einzige geschlossene Schleife. Jede Policy-Änderung fliesst in den Code; jede Code-Änderung fliesst in die Policy zurück.'
|
||||
: 'The platform is a single closed loop. Every policy change ripples into code; every code change ripples back into policy. That\'s the USP in one diagram.',
|
||||
bullets: de
|
||||
? ['Single Source of Truth, zwei Oberflächen', 'Echtzeit-Sync, kein Batch-Abgleich', 'Auditoren, Entwickler und Sales fragen denselben Graphen ab']
|
||||
: ['Single source of truth, two surfaces', 'Real-time sync, not batch reconciliation', 'Auditors, engineers and sales all query the same graph'],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pillar row ────────────────────────────────────────────────────────────────
|
||||
function PillarRow({ side, title, body, tint, onClick, active, isLight }: {
|
||||
side: 'left' | 'right'
|
||||
title: string; body: string; tint: string
|
||||
onClick: () => void; active: boolean; isLight: boolean
|
||||
}) {
|
||||
const [hover, setHover] = useState(false)
|
||||
const lit = hover || active
|
||||
const isLeft = side === 'left'
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'flex-start', gap: 12,
|
||||
flexDirection: isLeft ? 'row-reverse' : 'row',
|
||||
textAlign: isLeft ? 'right' : 'left',
|
||||
padding: '10px 14px', borderRadius: 10, cursor: 'pointer',
|
||||
transition: 'transform .25s, background .25s, box-shadow .25s',
|
||||
background: lit
|
||||
? `linear-gradient(${isLeft ? '270deg' : '90deg'}, ${tint}24 0%, ${tint}0a 70%, transparent 100%)`
|
||||
: 'transparent',
|
||||
boxShadow: lit
|
||||
? `0 10px 30px ${tint}26, inset 0 0 0 1px ${tint}44`
|
||||
: 'inset 0 0 0 1px transparent',
|
||||
transform: lit ? (isLeft ? 'translateX(-3px)' : 'translateX(3px)') : 'translateX(0)',
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
flex: '0 0 30px', width: 30, height: 30, borderRadius: 9,
|
||||
background: lit ? `${tint}3a` : `${tint}22`,
|
||||
border: `1px solid ${lit ? tint : tint + '66'}`,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: lit ? (isLight ? tint : '#fff') : tint, fontSize: 13, fontWeight: 700, marginTop: 2,
|
||||
boxShadow: lit ? `0 0 14px ${tint}88, inset 0 1px 0 ${tint}80` : `inset 0 1px 0 ${tint}50`,
|
||||
transition: 'all .25s',
|
||||
}}>◆</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{
|
||||
fontSize: 13, fontWeight: 700,
|
||||
color: isLight ? '#1a1a2e' : '#f7f5fc',
|
||||
letterSpacing: -0.15, marginBottom: 3,
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
justifyContent: isLeft ? 'flex-end' : 'flex-start',
|
||||
}}>
|
||||
<span>{title}</span>
|
||||
<span style={{
|
||||
fontSize: 10, color: tint, opacity: lit ? 1 : 0,
|
||||
transform: `translateX(${lit ? 0 : (isLeft ? 4 : -4)}px)`,
|
||||
transition: 'all .25s',
|
||||
}}>{isLeft ? '‹' : '›'}</span>
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: 11, lineHeight: 1.55,
|
||||
color: isLight
|
||||
? `rgba(71,85,105,${lit ? 1 : .78})`
|
||||
: `rgba(236,233,247,${lit ? .82 : .62})`,
|
||||
transition: 'color .25s',
|
||||
}}>{body}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Column header ─────────────────────────────────────────────────────────────
|
||||
function ColHeader({ side, label, color, icon, sub, isLight }: {
|
||||
side: 'left' | 'right'; label: string; color: string; icon: string; sub: string; isLight: boolean
|
||||
}) {
|
||||
const isLeft = side === 'left'
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
flexDirection: isLeft ? 'row-reverse' : 'row',
|
||||
paddingBottom: 10, borderBottom: `1px solid ${color}35`,
|
||||
}}>
|
||||
<div style={{
|
||||
width: 34, height: 34, borderRadius: 9,
|
||||
background: `linear-gradient(135deg, ${color}55, ${color}20)`,
|
||||
border: `1px solid ${color}88`,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: isLight ? color : '#fff', fontSize: 15, fontWeight: 700,
|
||||
boxShadow: `0 0 18px ${color}55, inset 0 1px 0 ${color}aa`,
|
||||
}}>{icon}</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 18, fontWeight: 700, color: isLight ? '#1a1a2e' : '#f7f5fc', letterSpacing: -0.3, lineHeight: 1 }}>{label}</div>
|
||||
<div style={{ ...MONO, fontSize: 9.5, letterSpacing: 2, color, opacity: .75, marginTop: 3, textTransform: 'uppercase' as const }}>{sub}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Central hub ───────────────────────────────────────────────────────────────
|
||||
function CentralHub({ caption, isLight }: { caption: string; isLight: boolean }) {
|
||||
return (
|
||||
<div style={{ position: 'relative', width: 260, height: 320, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{
|
||||
position: 'relative', width: 120, height: 120, borderRadius: '50%',
|
||||
background: 'radial-gradient(circle at 32% 28%, #f0e9ff 0%, #c4aaff 26%, #7b5cd6 58%, #2a1560 100%)',
|
||||
border: '1.5px solid rgba(216,202,255,.7)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
boxShadow: isLight
|
||||
? '0 0 30px rgba(167,139,250,.4), 0 0 60px rgba(167,139,250,.15), inset 0 3px 0 rgba(255,255,255,.5), inset 0 -8px 14px rgba(0,0,0,.2)'
|
||||
: '0 0 50px rgba(167,139,250,.65), 0 0 100px rgba(167,139,250,.25), inset 0 3px 0 rgba(255,255,255,.35), inset 0 -8px 14px rgba(0,0,0,.35)',
|
||||
animation: isLight ? 'uspPulseLight 2.6s ease-in-out infinite' : 'uspPulse 2.6s ease-in-out infinite',
|
||||
zIndex: 3,
|
||||
}}>
|
||||
<div style={{ position: 'absolute', inset: -14, borderRadius: '50%', border: `1px dashed ${isLight ? 'rgba(167,139,250,.5)' : 'rgba(216,202,255,.42)'}`, animation: 'uspSpin 14s linear infinite' }} />
|
||||
<div style={{ position: 'absolute', inset: -30, borderRadius: '50%', border: `1px dashed ${isLight ? 'rgba(167,139,250,.3)' : 'rgba(216,202,255,.2)'}`, animation: 'uspSpin 22s linear infinite reverse' }} />
|
||||
<svg width="54" height="26" viewBox="0 0 54 26" fill="none" stroke="#fff" strokeWidth="2.8" strokeLinecap="round" strokeLinejoin="round"
|
||||
style={{ filter: 'drop-shadow(0 1px 3px rgba(0,0,0,.5))' }}>
|
||||
<path d="M 10 13 C 10 5, 22 5, 27 13 C 32 21, 44 21, 44 13 C 44 5, 32 5, 27 13 C 22 21, 10 21, 10 13 Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div style={{
|
||||
position: 'absolute', left: 0, right: 0, bottom: 24, textAlign: 'center',
|
||||
...MONO, fontSize: 9.5, letterSpacing: 2.5,
|
||||
color: isLight ? 'rgba(109,77,194,.75)' : 'rgba(216,202,255,.75)',
|
||||
textTransform: 'uppercase' as const, fontWeight: 600,
|
||||
}}>{caption}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Bridge SVG connectors ─────────────────────────────────────────────────────
|
||||
function BridgeConnectors({ isLight }: { isLight: boolean }) {
|
||||
const rfpY = 130
|
||||
const sub2Y = 250
|
||||
const hubCx = 500
|
||||
const hubR = 72
|
||||
return (
|
||||
<svg viewBox="0 0 1000 400" preserveAspectRatio="none"
|
||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', pointerEvents: 'none', zIndex: 1 }}>
|
||||
<defs>
|
||||
<linearGradient id="uspFromL" x1="0" x2="1">
|
||||
<stop offset="0" stopColor="#a78bfa" stopOpacity="0" />
|
||||
<stop offset=".3" stopColor="#a78bfa" stopOpacity={isLight ? '.6' : '.85'} />
|
||||
<stop offset="1" stopColor="#c084fc" stopOpacity={isLight ? '.2' : '.3'} />
|
||||
</linearGradient>
|
||||
<linearGradient id="uspToR" x1="0" x2="1">
|
||||
<stop offset="0" stopColor="#c084fc" stopOpacity={isLight ? '.2' : '.3'} />
|
||||
<stop offset=".7" stopColor="#fbbf24" stopOpacity={isLight ? '.6' : '.85'} />
|
||||
<stop offset="1" stopColor="#fbbf24" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<line x1="40" y1={rfpY} x2={hubCx - hubR} y2={rfpY}
|
||||
stroke="url(#uspFromL)" strokeWidth="2" strokeDasharray="4 5"
|
||||
style={{ animation: 'uspFlowR 1.6s linear infinite' }} />
|
||||
<line x1={hubCx + hubR} y1={rfpY} x2="960" y2={rfpY}
|
||||
stroke="url(#uspToR)" strokeWidth="2" strokeDasharray="4 5"
|
||||
style={{ animation: 'uspFlowR 1.6s linear infinite' }} />
|
||||
<line x1="40" y1={sub2Y} x2={hubCx - hubR} y2={sub2Y}
|
||||
stroke="url(#uspFromL)" strokeWidth="2" strokeDasharray="4 5"
|
||||
style={{ animation: 'uspFlowR 1.6s linear infinite' }} />
|
||||
<line x1={hubCx + hubR} y1={sub2Y} x2="960" y2={sub2Y}
|
||||
stroke="url(#uspToR)" strokeWidth="2" strokeDasharray="4 5"
|
||||
style={{ animation: 'uspFlowR 1.6s linear infinite' }} />
|
||||
|
||||
{([rfpY, sub2Y] as number[]).map(y => (
|
||||
<g key={y}>
|
||||
<circle cx={hubCx - hubR} cy={y} r="4" fill={isLight ? '#eef2ff' : '#1a0f34'} stroke="#a78bfa" strokeWidth="1.2" />
|
||||
<circle cx={hubCx - hubR} cy={y} r="1.5" fill="#a78bfa" />
|
||||
<circle cx={hubCx + hubR} cy={y} r="4" fill={isLight ? '#eef2ff' : '#1a0f34'} stroke="#fbbf24" strokeWidth="1.2" />
|
||||
<circle cx={hubCx + hubR} cy={y} r="1.5" fill="#fbbf24" />
|
||||
</g>
|
||||
))}
|
||||
|
||||
<circle r="3" fill="#c4aaff" style={{ filter: 'drop-shadow(0 0 6px #a78bfa)' }}>
|
||||
<animate attributeName="cx" from="40" to="960" dur="3.5s" repeatCount="indefinite" />
|
||||
<animate attributeName="cy" values={`${rfpY};${rfpY}`} dur="3.5s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle r="3" fill="#fde68a" style={{ filter: 'drop-shadow(0 0 6px #fbbf24)' }}>
|
||||
<animate attributeName="cx" from="960" to="40" dur="3.5s" repeatCount="indefinite" />
|
||||
<animate attributeName="cy" values={`${sub2Y};${sub2Y}`} dur="3.5s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Under-the-hood feature card ───────────────────────────────────────────────
|
||||
function FeatureCard({ icon, title, body, tint, Ticker, onClick, active, isLight }: {
|
||||
icon: string; title: string; body: string; tint: string
|
||||
Ticker: React.ComponentType<{ tint: string; isLight: boolean }>
|
||||
onClick: () => void; active: boolean; isLight: boolean
|
||||
}) {
|
||||
const [hover, setHover] = useState(false)
|
||||
const lit = hover || active
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
position: 'relative', padding: '13px 15px',
|
||||
background: isLight
|
||||
? lit
|
||||
? `linear-gradient(180deg, ${tint}18 0%, ${tint}08 55%, rgba(248,250,252,.95) 100%)`
|
||||
: 'linear-gradient(180deg, #ffffff, #f8fafc)'
|
||||
: `linear-gradient(180deg, ${tint}${lit ? '2a' : '1a'} 0%, ${tint}07 55%, rgba(14,8,28,.85) 100%)`,
|
||||
border: `1px solid ${lit ? tint : isLight ? 'rgba(0,0,0,.1)' : tint + '4a'}`,
|
||||
borderRadius: 12,
|
||||
boxShadow: lit
|
||||
? `0 18px 40px ${tint}33, 0 0 0 1px ${tint}66, inset 0 1px 0 ${tint}60`
|
||||
: isLight
|
||||
? '0 2px 8px rgba(0,0,0,.08), inset 0 1px 0 rgba(255,255,255,.8)'
|
||||
: `0 10px 24px rgba(0,0,0,.4), inset 0 1px 0 ${tint}35`,
|
||||
minWidth: 0, cursor: 'pointer',
|
||||
transform: lit ? 'translateY(-3px)' : 'translateY(0)',
|
||||
transition: 'transform .25s, box-shadow .25s, background .25s, border-color .25s',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
|
||||
<span style={{
|
||||
width: 22, height: 22, borderRadius: 6,
|
||||
background: lit ? `${tint}44` : `${tint}22`,
|
||||
border: `1px solid ${lit ? tint : tint + '66'}`,
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: lit ? (isLight ? tint : '#fff') : tint, fontSize: 12,
|
||||
boxShadow: lit ? `0 0 12px ${tint}88` : 'none',
|
||||
transition: 'all .25s',
|
||||
}}>{icon}</span>
|
||||
<span style={{ fontSize: 12.5, fontWeight: 700, color: isLight ? '#1a1a2e' : '#f7f5fc', letterSpacing: -0.15, flex: 1 }}>{title}</span>
|
||||
<span style={{ fontSize: 10, color: tint, opacity: lit ? 1 : 0.5, transform: `translateX(${lit ? 0 : -3}px)`, transition: 'all .25s' }}>↗</span>
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: 11, lineHeight: 1.45,
|
||||
color: isLight
|
||||
? `rgba(71,85,105,${lit ? 1 : .78})`
|
||||
: `rgba(236,233,247,${lit ? .82 : .65})`,
|
||||
transition: 'color .25s',
|
||||
}}>{body}</div>
|
||||
<Ticker tint={tint} isLight={isLight} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Detail modal ──────────────────────────────────────────────────────────────
|
||||
function DetailModal({ item, onClose, isLight }: { item: DetailItem | null; onClose: () => void; isLight: boolean }) {
|
||||
useEffect(() => {
|
||||
if (!item) return
|
||||
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() }
|
||||
window.addEventListener('keydown', onKey)
|
||||
return () => window.removeEventListener('keydown', onKey)
|
||||
}, [item, onClose])
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{item && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: 'absolute', inset: 0, zIndex: 50,
|
||||
background: isLight ? 'rgba(240,244,255,.72)' : 'rgba(5,2,16,.72)',
|
||||
backdropFilter: 'blur(6px)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.94 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.94 }}
|
||||
transition={{ duration: 0.22 }}
|
||||
onClick={e => e.stopPropagation()}
|
||||
style={{
|
||||
width: 560, maxWidth: '88%',
|
||||
background: isLight
|
||||
? `linear-gradient(180deg, ${item.tint}10 0%, rgba(255,255,255,.98) 50%, rgba(248,250,252,.99) 100%)`
|
||||
: `linear-gradient(180deg, ${item.tint}18 0%, rgba(20,10,40,.96) 50%, rgba(14,8,28,.98) 100%)`,
|
||||
border: `1px solid ${item.tint}${isLight ? '44' : '66'}`,
|
||||
borderRadius: 16,
|
||||
boxShadow: isLight
|
||||
? `0 20px 60px rgba(0,0,0,.12), 0 0 40px ${item.tint}18, inset 0 1px 0 rgba(255,255,255,.9)`
|
||||
: `0 30px 80px rgba(0,0,0,.6), 0 0 60px ${item.tint}33, inset 0 1px 0 ${item.tint}55`,
|
||||
padding: '22px 26px',
|
||||
color: isLight ? '#1a1a2e' : '#ece9f7',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 14 }}>
|
||||
<div style={{
|
||||
width: 38, height: 38, borderRadius: 10,
|
||||
background: `linear-gradient(135deg, ${item.tint}66, ${item.tint}22)`,
|
||||
border: `1px solid ${item.tint}`,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: isLight ? item.tint : '#fff', fontSize: 16, fontWeight: 700,
|
||||
boxShadow: `0 0 18px ${item.tint}66`,
|
||||
}}>{item.icon}</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ ...MONO, fontSize: 9.5, letterSpacing: 2.5, color: item.tint, textTransform: 'uppercase' as const, fontWeight: 600, marginBottom: 2 }}>
|
||||
{item.kicker}
|
||||
</div>
|
||||
<div style={{ fontSize: 19, fontWeight: 700, color: isLight ? '#1a1a2e' : '#f7f5fc', letterSpacing: -0.3 }}>{item.title}</div>
|
||||
</div>
|
||||
<button onClick={onClose} style={{
|
||||
background: 'transparent', border: `1px solid ${item.tint}55`,
|
||||
borderRadius: 8, cursor: 'pointer', width: 30, height: 30,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: isLight ? '#64748b' : 'rgba(236,233,247,.6)',
|
||||
}}>
|
||||
<X style={{ width: 14, height: 14 }} />
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, lineHeight: 1.6, color: isLight ? '#475569' : 'rgba(236,233,247,.82)', marginBottom: 16 }}>
|
||||
{item.body}
|
||||
</div>
|
||||
{item.bullets && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 14 }}>
|
||||
{item.bullets.map((b, i) => (
|
||||
<div key={i} style={{
|
||||
display: 'flex', alignItems: 'flex-start', gap: 10,
|
||||
padding: '8px 12px', borderRadius: 8,
|
||||
background: isLight ? 'rgba(0,0,0,.04)' : 'rgba(0,0,0,.3)',
|
||||
border: `1px solid ${item.tint}${isLight ? '22' : '33'}`,
|
||||
}}>
|
||||
<span style={{ color: item.tint, fontSize: 12, marginTop: 1 }}>▸</span>
|
||||
<span style={{ fontSize: 12, lineHeight: 1.5, color: isLight ? '#475569' : 'rgba(236,233,247,.78)' }}>{b}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{item.stat && (
|
||||
<div style={{
|
||||
...MONO, padding: '10px 14px', borderRadius: 8,
|
||||
background: isLight ? 'rgba(0,0,0,.04)' : 'rgba(0,0,0,.45)',
|
||||
border: `1px solid ${item.tint}${isLight ? '33' : '55'}`,
|
||||
fontSize: 12, color: isLight ? '#475569' : 'rgba(236,233,247,.9)',
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
}}>
|
||||
<span style={{ color: isLight ? '#16a34a' : '#4ade80' }}>●</span>
|
||||
<span style={{ color: item.tint }}>{item.stat.k}</span>
|
||||
<span style={{ color: isLight ? '#1a1a2e' : '#f5f3fc', fontWeight: 600 }}>{item.stat.v}</span>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Star field ────────────────────────────────────────────────────────────────
|
||||
function StarField({ isLight }: { isLight: boolean }) {
|
||||
const stars = useMemo(() => {
|
||||
let s = 41
|
||||
const r = () => { s = (s * 9301 + 49297) % 233280; return s / 233280 }
|
||||
return Array.from({ length: 90 }, () => ({ x: r() * 100, y: r() * 100, size: r() * 1.4 + 0.3, op: r() * 0.5 + 0.15 }))
|
||||
}, [])
|
||||
if (isLight) return null
|
||||
return (
|
||||
<div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
|
||||
{stars.map((st, i) => (
|
||||
<div key={i} style={{
|
||||
position: 'absolute', left: `${st.x}%`, top: `${st.y}%`,
|
||||
width: st.size, height: st.size, borderRadius: '50%',
|
||||
background: '#fff', opacity: st.op,
|
||||
boxShadow: `0 0 ${st.size * 3}px rgba(180,160,255,.7)`,
|
||||
}} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Main slide ────────────────────────────────────────────────────────────────
|
||||
export default function PlatformBridgeSection() {
|
||||
const { lang } = useApp()
|
||||
const de = lang === 'de'
|
||||
const isLight = useIsLight()
|
||||
const details = getDetails(de)
|
||||
const [detail, setDetail] = useState<DetailItem | null>(null)
|
||||
const open = (k: string) => setDetail(details[k])
|
||||
const close = () => setDetail(null)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<style>{CSS_KF}</style>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
|
||||
className="text-center mb-1"
|
||||
>
|
||||
<h2 className="text-5xl md:text-6xl font-bold mb-1">
|
||||
<GradientText>{de ? 'Eine Plattform' : 'One Platform'}</GradientText>
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">
|
||||
{de ? 'Regulatorik, Code Security und CE — verbunden, nicht isoliert' : 'Regulatory, code security and CE — connected, not isolated'}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.15, ease: [0.22, 1, 0.36, 1] }}
|
||||
>
|
||||
{/* ── MAIN CANVAS ───────────────────────────────────────────────── */}
|
||||
<div style={{
|
||||
position: 'relative', overflow: 'hidden', borderRadius: 16,
|
||||
background: isLight
|
||||
? 'linear-gradient(160deg, #f0f4ff 0%, #eff6ff 50%, #f5f0ff 100%)'
|
||||
: 'radial-gradient(ellipse at 50% 30%, #1a0f34 0%, #0e0720 55%, #050210 100%)',
|
||||
color: isLight ? '#1a1a2e' : '#ece9f7',
|
||||
fontFamily: '"Inter", system-ui, sans-serif',
|
||||
WebkitFontSmoothing: 'antialiased',
|
||||
}}>
|
||||
{/* Ambient glow — dark only, subtle */}
|
||||
{!isLight && (
|
||||
<div style={{
|
||||
position: 'absolute', top: -80, left: '50%', transform: 'translateX(-50%)',
|
||||
width: 600, height: 400, borderRadius: '50%',
|
||||
background: 'radial-gradient(ellipse, rgba(167,139,250,.12), transparent 65%)',
|
||||
filter: 'blur(50px)', pointerEvents: 'none',
|
||||
}} />
|
||||
)}
|
||||
|
||||
{/* Bridge */}
|
||||
<div style={{ position: 'relative', margin: '16px 48px 0', height: 330 }}>
|
||||
<BridgeConnectors isLight={isLight} />
|
||||
<div style={{
|
||||
position: 'relative', zIndex: 2,
|
||||
display: 'grid', gridTemplateColumns: '1fr 260px 1fr', gap: 0,
|
||||
alignItems: 'start', height: '100%',
|
||||
}}>
|
||||
{/* LEFT — Compliance */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', paddingRight: 20 }}>
|
||||
<div style={{ height: 40, marginBottom: 36 }}>
|
||||
<ColHeader side="left" label={de ? 'Regulatorik' : 'Regulatory'} color="#a78bfa" icon="⎈" sub="dsgvo · nis2 · ce · cra" isLight={isLight} />
|
||||
</div>
|
||||
<div style={{ height: 110, display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<PillarRow side="left" tint="#a78bfa" isLight={isLight}
|
||||
title={de ? 'DSGVO / NIS2 / AI Act' : 'GDPR / NIS2 / AI Act'}
|
||||
body={de
|
||||
? '294.000+ atomare Controls aus 380+ Rechtsquellen. Deterministische Ableitung, keine Halluzinationen.'
|
||||
: '294,000+ atomic controls from 380+ legal sources. Deterministic derivation, no hallucinations.'}
|
||||
onClick={() => open('rfq')}
|
||||
active={detail?.title === details.rfq.title}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ height: 110, display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<PillarRow side="left" tint="#c084fc" isLight={isLight}
|
||||
title={de ? 'CE & Maschinenverordnung' : 'CE & Machinery Regulation'}
|
||||
body={de
|
||||
? '1.058 Hazard Patterns, 225 Maßnahmen, 751 Normen. CE-Akte nach MVO 2023/1230 per Klick.'
|
||||
: '1,058 hazard patterns, 225 mitigations, 751 standards. CE file per MR 2023/1230 with one click.'}
|
||||
onClick={() => open('process')}
|
||||
active={detail?.title === details.process.title}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CENTER hub */}
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
|
||||
<div
|
||||
onClick={() => open('hub')}
|
||||
style={{ cursor: 'pointer', transition: 'transform .25s, filter .25s' }}
|
||||
onMouseEnter={e => { (e.currentTarget as HTMLDivElement).style.transform = 'scale(1.05)'; (e.currentTarget as HTMLDivElement).style.filter = 'brightness(1.15)' }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLDivElement).style.transform = 'scale(1)'; (e.currentTarget as HTMLDivElement).style.filter = 'brightness(1)' }}
|
||||
>
|
||||
<CentralHub caption={de ? 'Immer in Sync' : 'Always in sync'} isLight={isLight} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RIGHT — Code */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', paddingLeft: 20 }}>
|
||||
<div style={{ height: 40, marginBottom: 36 }}>
|
||||
<ColHeader side="right" label="Code Security" color="#fbbf24" icon="⟨/⟩" sub="sast · dast · sbom · pentest" isLight={isLight} />
|
||||
</div>
|
||||
<div style={{ height: 110, display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<PillarRow side="right" tint="#fbbf24" isLight={isLight}
|
||||
title="SAST / DAST / SBOM"
|
||||
body={de
|
||||
? 'Kontinuierliche Code-Analyse für Firmware und Embedded. Jedes Finding automatisch priorisiert: Blocker vs. kosmetisch.'
|
||||
: 'Continuous code analysis for firmware and embedded. Every finding automatically prioritized: blocker vs. cosmetic.'}
|
||||
onClick={() => open('bidir')}
|
||||
active={detail?.title === details.bidir.title}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ height: 110, display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<PillarRow side="right" tint="#f59e0b" isLight={isLight}
|
||||
title={de ? 'Continuous Pentesting' : 'Continuous Pentesting'}
|
||||
body={de
|
||||
? 'Automatisierte Schwachstellensuche statt jährlicher Pentests. Findings werden sofort zu Jira-Tickets mit Fix-Vorschlägen.'
|
||||
: 'Automated vulnerability scanning instead of annual pentests. Findings become Jira tickets with fix suggestions.'}
|
||||
onClick={() => open('cont')}
|
||||
active={detail?.title === details.cont.title}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Integrations */}
|
||||
<div style={{ position: 'relative', zIndex: 2, padding: '0 48px 20px' }}>
|
||||
<div style={{
|
||||
...MONO, fontSize: 9.5, letterSpacing: 3.5,
|
||||
color: isLight ? 'rgba(109,77,194,.7)' : 'rgba(167,139,250,.7)',
|
||||
textTransform: 'uppercase', fontWeight: 600, textAlign: 'center', marginBottom: 12,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 14,
|
||||
}}>
|
||||
<span style={{ width: 80, height: 1, background: isLight ? 'linear-gradient(90deg, transparent, rgba(109,77,194,.4))' : 'linear-gradient(90deg, transparent, rgba(167,139,250,.5))' }} />
|
||||
{de ? 'Integrationen & Infrastruktur' : 'Integrations & Infrastructure'}
|
||||
<span style={{ width: 80, height: 1, background: isLight ? 'linear-gradient(270deg, transparent, rgba(109,77,194,.4))' : 'linear-gradient(270deg, transparent, rgba(167,139,250,.5))' }} />
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 10 }}>
|
||||
{([
|
||||
{ tint: '#a78bfa', icon: '⇄', key: 'trace',
|
||||
title: de ? 'Jira / Linear' : 'Jira / Linear',
|
||||
body: de ? 'Findings → Tickets mit Fix-Vorschlägen' : 'Findings → tickets with fix suggestions' },
|
||||
{ tint: '#c084fc', icon: '◉', key: 'engine',
|
||||
title: 'Traceability',
|
||||
body: de ? 'Rechtsquelle → Code → Nachweis' : 'Legal source → code → evidence' },
|
||||
{ tint: '#fbbf24', icon: '✦', key: 'opt',
|
||||
title: de ? 'Delta-Impact' : 'Delta Impact',
|
||||
body: de ? 'Änderungsfolgen sofort sehen' : 'See change impact instantly' },
|
||||
{ tint: '#f59e0b', icon: '◎', key: 'stack',
|
||||
title: 'Sovereign AI',
|
||||
body: de ? 'On-Premise, kein US-Cloud' : 'On-premise, no US cloud' },
|
||||
] as const).map((card) => (
|
||||
<div
|
||||
key={card.key}
|
||||
onClick={() => open(card.key)}
|
||||
style={{
|
||||
padding: '12px 14px', borderRadius: 10, cursor: 'pointer',
|
||||
background: isLight
|
||||
? `linear-gradient(180deg, ${card.tint}10 0%, rgba(248,250,252,.95) 100%)`
|
||||
: `linear-gradient(180deg, ${card.tint}18 0%, rgba(14,8,28,.85) 100%)`,
|
||||
border: `1px solid ${isLight ? 'rgba(0,0,0,.08)' : card.tint + '3a'}`,
|
||||
transition: 'transform .2s, box-shadow .2s',
|
||||
}}
|
||||
onMouseEnter={e => { (e.currentTarget as HTMLDivElement).style.transform = 'translateY(-2px)'; (e.currentTarget as HTMLDivElement).style.boxShadow = `0 8px 24px ${card.tint}33` }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLDivElement).style.transform = 'translateY(0)'; (e.currentTarget as HTMLDivElement).style.boxShadow = 'none' }}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 7, marginBottom: 4 }}>
|
||||
<span style={{
|
||||
width: 20, height: 20, borderRadius: 5,
|
||||
background: `${card.tint}22`, border: `1px solid ${card.tint}66`,
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: card.tint, fontSize: 11,
|
||||
}}>{card.icon}</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 700, color: isLight ? '#1a1a2e' : '#f7f5fc' }}>{card.title}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, lineHeight: 1.4, color: isLight ? 'rgba(71,85,105,.8)' : 'rgba(236,233,247,.55)' }}>{card.body}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DetailModal item={detail} onClose={close} isLight={isLight} />
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
'use client'
|
||||
|
||||
import { Check, ArrowRight, Cpu } from 'lucide-react'
|
||||
import { t } from '@/lib/content'
|
||||
import { useApp } from '@/lib/context'
|
||||
import SectionHeading from '@/components/ui/SectionHeading'
|
||||
import GlassCard from '@/components/ui/GlassCard'
|
||||
import CTAButton from '@/components/ui/CTAButton'
|
||||
import FadeInView from '@/components/ui/FadeInView'
|
||||
|
||||
export default function PricingSection() {
|
||||
const { lang } = useApp()
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<section id="pricing" className="py-24 sm:py-32">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading
|
||||
tag={i.pricing.tag}
|
||||
title={i.pricing.title}
|
||||
titleHighlight={i.pricing.titleHighlight}
|
||||
subtitle={i.pricing.subtitle}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
{i.pricing.tiers.map((tier, idx) => (
|
||||
<GlassCard
|
||||
key={idx}
|
||||
delay={idx * 0.1}
|
||||
className={`relative ${tier.highlighted ? 'border-accent-electric/30 ring-1 ring-accent-electric/20' : ''}`}
|
||||
>
|
||||
{tier.highlighted && (
|
||||
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
|
||||
<span className="px-3 py-1 rounded-full text-xs font-bold bg-accent-electric text-white">
|
||||
{tier.badge}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{!tier.highlighted && (
|
||||
<span className="inline-block mono-label text-xs mb-2">{tier.badge}</span>
|
||||
)}
|
||||
|
||||
<h3 className="text-xl font-bold mt-2 mb-1">{tier.name}</h3>
|
||||
<div className="flex items-baseline gap-1 mb-3">
|
||||
<span className="text-3xl font-bold">EUR {tier.price}</span>
|
||||
<span className="text-white/40 text-sm">{tier.period}</span>
|
||||
</div>
|
||||
<p className="text-sm text-white/40 mb-6">{tier.description}</p>
|
||||
|
||||
<ul className="space-y-3 mb-8">
|
||||
{tier.features.map((feature, fi) => (
|
||||
<li key={fi} className="flex items-start gap-2 text-sm text-white/60">
|
||||
<Check className="w-4 h-4 text-accent-signal mt-0.5 shrink-0" />
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<CTAButton
|
||||
variant={tier.highlighted ? 'primary' : 'ghost'}
|
||||
href="#"
|
||||
className="w-full justify-center"
|
||||
>
|
||||
{i.pricing.cta}
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</CTAButton>
|
||||
</GlassCard>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<FadeInView>
|
||||
<div className="rounded-2xl border border-white/[0.08] bg-white/[0.03] p-6 md:p-8">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center gap-6">
|
||||
<div className="w-14 h-14 rounded-2xl bg-accent-purple/10 flex items-center justify-center shrink-0">
|
||||
<Cpu className="w-7 h-7 text-accent-purple" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<h3 className="text-lg font-bold">{i.pricing.appliance.name}</h3>
|
||||
<span className="mono-label text-xs px-2 py-0.5 rounded bg-white/[0.06]">
|
||||
{i.pricing.appliance.badge}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-white/40 mb-3">{i.pricing.appliance.description}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{i.pricing.appliance.features.map((f, idx) => (
|
||||
<span key={idx} className="inline-flex items-center gap-1.5 text-xs text-white/50 bg-white/[0.04] px-2.5 py-1 rounded-md">
|
||||
<Check className="w-3 h-3 text-accent-signal" />
|
||||
{f}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right shrink-0">
|
||||
<div className="text-2xl font-bold">EUR {i.pricing.appliance.priceRange}</div>
|
||||
<div className="text-xs text-white/40">{i.pricing.appliance.priceLabel}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
'use client'
|
||||
|
||||
import { Mail, Table, UserSearch, Clock, AlertCircle, Euro, Timer, HelpCircle } from 'lucide-react'
|
||||
import { useApp } from '@/lib/context'
|
||||
import FadeInView from '@/components/ui/FadeInView'
|
||||
import GradientText from '@/components/ui/GradientText'
|
||||
|
||||
const steps = {
|
||||
de: [
|
||||
{ icon: Mail, label: 'Alles per Email', detail: 'Anforderungen, Rückfragen, Freigaben — verstreut in 200 Threads' },
|
||||
{ icon: Table, label: 'Alles per Excel', detail: 'Risikobeurteilungen, Maßnahmenlisten, Normverweise in 15 Dateien' },
|
||||
{ icon: UserSearch, label: 'Hinterherlaufen', detail: '"Wer war nochmal zuständig für die EMV-Messung?"' },
|
||||
{ icon: Clock, label: 'Wartezeiten & Urlaub', detail: 'CE-Review blockiert weil der Experte 3 Wochen weg ist' },
|
||||
{ icon: AlertCircle, label: 'CE-Berater zu spät', detail: 'Maschine ist fertig, CE-Bewertung fängt erst jetzt an' },
|
||||
{ icon: Euro, label: '€50.000+ pro Jahr', detail: 'Externe Audits, Penetrationstests, CE-Beratung, Normen kaufen' },
|
||||
{ icon: Timer, label: 'Time-to-Market verzögert', detail: 'Wochen bis Monate für eine vollständige CE-Akte' },
|
||||
{ icon: HelpCircle, label: 'Silo-Experten', detail: 'CRA, AI Act, MVO, NIS2 — jeder kennt nur sein Thema' },
|
||||
],
|
||||
en: [
|
||||
{ icon: Mail, label: 'Everything by email', detail: 'Requirements, follow-ups, approvals — scattered across 200 threads' },
|
||||
{ icon: Table, label: 'Everything in Excel', detail: 'Risk assessments, mitigation lists, norm references in 15 files' },
|
||||
{ icon: UserSearch, label: 'Chasing people', detail: '"Who was responsible for the EMC measurement again?"' },
|
||||
{ icon: Clock, label: 'Wait times & vacation', detail: 'CE review blocked because the expert is away for 3 weeks' },
|
||||
{ icon: AlertCircle, label: 'CE consultants too late', detail: 'Machine is finished, CE assessment only starts now' },
|
||||
{ icon: Euro, label: '€50,000+ per year', detail: 'External audits, penetration tests, CE consulting, buying norms' },
|
||||
{ icon: Timer, label: 'Time-to-market delayed', detail: 'Weeks to months for a complete CE file' },
|
||||
{ icon: HelpCircle, label: 'Silo experts', detail: 'CRA, AI Act, MVO, NIS2 — everyone only knows their topic' },
|
||||
],
|
||||
}
|
||||
|
||||
const heading = {
|
||||
de: { tag: 'DAS PROBLEM', title: 'So läuft es', titleHighlight: 'heute.', bridge: 'BreakPilot baut die Brücke zwischen Code, Produkt und Regulation — in Echtzeit, ohne Excel, Email und ohne Hinterherlaufen.' },
|
||||
en: { tag: 'THE PROBLEM', title: "How it works", titleHighlight: 'today.', bridge: 'BreakPilot bridges the gap between code, product and regulation — in real-time, without Excel, email or chasing people.' },
|
||||
}
|
||||
|
||||
export default function ProblemFlowSection() {
|
||||
const { lang } = useApp()
|
||||
const h = heading[lang]
|
||||
const items = steps[lang]
|
||||
|
||||
return (
|
||||
<section className="py-24 sm:py-32 section-alt">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<FadeInView className="text-center mb-16">
|
||||
<p className="mono-label mb-4">{h.tag}</p>
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-6">
|
||||
{h.title} <GradientText>{h.titleHighlight}</GradientText>
|
||||
</h2>
|
||||
</FadeInView>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-16">
|
||||
{items.map((step, idx) => {
|
||||
const Icon = step.icon
|
||||
return (
|
||||
<FadeInView key={idx} delay={idx * 0.06}>
|
||||
<div className="rounded-xl border border-red-500/10 bg-red-500/[0.03] p-5 h-full">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-red-500/10 flex items-center justify-center">
|
||||
<Icon className="w-4 h-4 text-red-400" />
|
||||
</div>
|
||||
<span className="font-mono text-xs text-red-400/60">{String(idx + 1).padStart(2, '0')}</span>
|
||||
</div>
|
||||
<h3 className="text-sm font-bold mb-1 text-red-300/80">{step.label}</h3>
|
||||
<p className="text-xs text-white/30 leading-relaxed">{step.detail}</p>
|
||||
</div>
|
||||
</FadeInView>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<FadeInView>
|
||||
<div className="rounded-2xl border border-accent-electric/20 bg-accent-electric/[0.04] p-8 text-center">
|
||||
<p className="text-lg text-white/70 max-w-3xl mx-auto font-medium leading-relaxed">
|
||||
{h.bridge}
|
||||
</p>
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp, ShieldAlert, Euro } from 'lucide-react'
|
||||
import { t } from '@/lib/content'
|
||||
import { useApp } from '@/lib/context'
|
||||
import SectionHeading from '@/components/ui/SectionHeading'
|
||||
import GlassCard from '@/components/ui/GlassCard'
|
||||
import AnimatedCounter from '@/components/ui/AnimatedCounter'
|
||||
import FadeInView from '@/components/ui/FadeInView'
|
||||
|
||||
const icons = [TrendingUp, ShieldAlert, Euro]
|
||||
|
||||
export default function ProblemSection() {
|
||||
const { lang } = useApp()
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<section id="problem" className="py-24 sm:py-32 section-alt">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading
|
||||
tag={i.problem.tag}
|
||||
title={i.problem.title}
|
||||
titleHighlight={i.problem.titleHighlight}
|
||||
subtitle={i.problem.subtitle}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{i.problem.cards.map((card, idx) => {
|
||||
const Icon = icons[idx]
|
||||
return (
|
||||
<GlassCard key={idx} delay={idx * 0.1}>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-accent-electric/10 flex items-center justify-center">
|
||||
<Icon className="w-5 h-5 text-accent-electric" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-3xl font-bold mb-2">
|
||||
<AnimatedCounter value={card.metric} />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-white/80 mb-2">{card.label}</p>
|
||||
<p className="text-sm text-white/40 mb-3">{card.description}</p>
|
||||
<p className="mono-label text-xs">{card.source}</p>
|
||||
</GlassCard>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<FadeInView delay={0.4} className="mt-12 text-center">
|
||||
<p className="text-white/40 text-sm max-w-2xl mx-auto border-t border-white/[0.06] pt-8">
|
||||
Die Konsequenz: Unternehmen riskieren Bussgelder, Betriebsstillstand und Wettbewerbsnachteile
|
||||
— oder sie investieren in eine Plattform, die regulatorische Komplexitaet deterministisch beherrscht.
|
||||
</p>
|
||||
</FadeInView>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
'use client'
|
||||
|
||||
import { AlertTriangle, ShieldCheck, RefreshCw, Cpu, FileText, Link } from 'lucide-react'
|
||||
import { t } from '@/lib/content'
|
||||
import { useApp } from '@/lib/context'
|
||||
import SectionHeading from '@/components/ui/SectionHeading'
|
||||
import GlassCard from '@/components/ui/GlassCard'
|
||||
|
||||
const iconMap: Record<string, typeof AlertTriangle> = { AlertTriangle, ShieldCheck, RefreshCw, Cpu, FileText, Link }
|
||||
|
||||
export default function SafetySection() {
|
||||
const { lang } = useApp()
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<section id="safety" className="py-24 sm:py-32 section-alt">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading
|
||||
tag={i.safety.tag}
|
||||
title={i.safety.title}
|
||||
titleHighlight={i.safety.titleHighlight}
|
||||
subtitle={i.safety.subtitle}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{i.safety.features.map((feature, idx) => {
|
||||
const Icon = iconMap[feature.icon]
|
||||
return (
|
||||
<GlassCard key={idx} delay={idx * 0.08}>
|
||||
<div className="w-10 h-10 rounded-xl bg-amber-500/10 flex items-center justify-center mb-4">
|
||||
<Icon className="w-5 h-5 text-amber-400" />
|
||||
</div>
|
||||
<h3 className="text-sm font-bold mb-2">{feature.title}</h3>
|
||||
<p className="text-xs text-white/40 leading-relaxed">{feature.description}</p>
|
||||
</GlassCard>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingDown, Cookie, Scale, AlertTriangle, ArrowRight } from 'lucide-react'
|
||||
import { t } from '@/lib/content'
|
||||
import { useApp } from '@/lib/context'
|
||||
import SectionHeading from '@/components/ui/SectionHeading'
|
||||
import GlassCard from '@/components/ui/GlassCard'
|
||||
import FadeInView from '@/components/ui/FadeInView'
|
||||
import GradientText from '@/components/ui/GradientText'
|
||||
|
||||
const pillarIcons = [Cookie, Scale, AlertTriangle]
|
||||
|
||||
export default function SavingsSection() {
|
||||
const { lang } = useApp()
|
||||
const i = t(lang)
|
||||
const s = (i as unknown as { savings: typeof i.problem & { promise: string; caseStudy: { label: string; headline: string; bullets: string[]; saving: string; side: string }; pillars: { title: string; description: string }[]; cta: string; ctaSecondary: string } }).savings
|
||||
|
||||
return (
|
||||
<section id="savings" className="py-24 sm:py-32 relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-emerald-500/[0.02] to-transparent" />
|
||||
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading
|
||||
tag={s.tag}
|
||||
title={s.title}
|
||||
titleHighlight={s.titleHighlight}
|
||||
subtitle={s.subtitle}
|
||||
/>
|
||||
|
||||
{/* PROMISE — high-impact one-liner */}
|
||||
<FadeInView>
|
||||
<div className="text-center mb-16">
|
||||
<div className="inline-flex items-center gap-3 px-6 py-3 rounded-full
|
||||
border border-emerald-400/30 bg-emerald-500/[0.08]">
|
||||
<TrendingDown className="w-5 h-5 text-emerald-400" />
|
||||
<span className="text-lg font-semibold text-emerald-300">
|
||||
{s.promise}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
{/* CASE STUDY card */}
|
||||
<FadeInView>
|
||||
<div className="rounded-2xl border border-emerald-400/20 bg-gradient-to-br
|
||||
from-emerald-500/[0.06] to-white/[0.02] p-8 mb-16">
|
||||
<div className="mono-label text-emerald-400 mb-3 tracking-widest">
|
||||
{s.caseStudy.label}
|
||||
</div>
|
||||
<h3 className="text-2xl sm:text-3xl font-bold mb-6">
|
||||
{s.caseStudy.headline}
|
||||
</h3>
|
||||
<ul className="space-y-3 mb-8">
|
||||
{s.caseStudy.bullets.map((b: string, idx: number) => (
|
||||
<li key={idx} className="flex items-start gap-3 text-white/70">
|
||||
<span className="mt-1 w-1.5 h-1.5 rounded-full bg-emerald-400 flex-shrink-0" />
|
||||
<span className="text-sm">{b}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 pt-6 border-t border-white/10">
|
||||
<div>
|
||||
<div className="mono-label text-white/40 mb-1">JAEHRLICHES SPARPOTENZIAL</div>
|
||||
<div className="text-3xl font-bold">
|
||||
<GradientText>{s.caseStudy.saving}</GradientText>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mono-label text-white/40 mb-1">PLUS</div>
|
||||
<div className="text-sm text-white/70">{s.caseStudy.side}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
{/* Pillars: HOW we do it */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-16">
|
||||
{s.pillars.map((p: { title: string; description: string }, idx: number) => {
|
||||
const Icon = pillarIcons[idx] || Cookie
|
||||
return (
|
||||
<GlassCard key={idx} delay={idx * 0.1}>
|
||||
<div className="w-12 h-12 rounded-xl bg-emerald-500/10 flex items-center justify-center mb-4">
|
||||
<Icon className="w-6 h-6 text-emerald-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold mb-2">{p.title}</h3>
|
||||
<p className="text-sm text-white/50">{p.description}</p>
|
||||
</GlassCard>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<FadeInView>
|
||||
<div className="text-center">
|
||||
<a
|
||||
href="/savings-scan"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 rounded-full
|
||||
bg-emerald-500 hover:bg-emerald-400 transition-colors
|
||||
text-enterprise-dark font-semibold"
|
||||
>
|
||||
{s.cta}
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</a>
|
||||
<a
|
||||
href="/savings-methodik"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 ml-3
|
||||
text-white/60 hover:text-white transition-colors text-sm"
|
||||
>
|
||||
{s.ctaSecondary}
|
||||
</a>
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
'use client'
|
||||
|
||||
import { ArrowRight } from 'lucide-react'
|
||||
import { t } from '@/lib/content'
|
||||
import { useApp } from '@/lib/context'
|
||||
import SectionHeading from '@/components/ui/SectionHeading'
|
||||
import GlassCard from '@/components/ui/GlassCard'
|
||||
import FadeInView from '@/components/ui/FadeInView'
|
||||
import TechBadge from '@/components/ui/TechBadge'
|
||||
|
||||
export default function SecuritySection() {
|
||||
const { lang } = useApp()
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<section id="security" className="py-24 sm:py-32">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading
|
||||
tag={i.security.tag}
|
||||
title={i.security.title}
|
||||
titleHighlight={i.security.titleHighlight}
|
||||
subtitle={i.security.subtitle}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<FadeInView direction="left">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||
{i.security.tools.map((tool, idx) => (
|
||||
<GlassCard key={idx} delay={idx * 0.05} className="p-4">
|
||||
<p className="font-mono text-sm font-bold text-accent-electric mb-1">{tool.name}</p>
|
||||
<p className="text-xs text-white/40">{tool.description}</p>
|
||||
</GlassCard>
|
||||
))}
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
<FadeInView direction="right">
|
||||
<div className="rounded-2xl border border-white/[0.06] bg-white/[0.03] p-6 h-full flex flex-col justify-center">
|
||||
<h3 className="text-lg font-bold mb-2">{i.security.integration.title}</h3>
|
||||
<p className="text-sm text-white/40 mb-6">{i.security.integration.description}</p>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="px-3 py-1.5 rounded-lg bg-red-500/10 border border-red-500/20 text-xs text-red-400 font-mono">
|
||||
CVE-2024-XXXX
|
||||
</span>
|
||||
<ArrowRight className="w-4 h-4 text-white/20" />
|
||||
<span className="px-3 py-1.5 rounded-lg bg-accent-electric/10 border border-accent-electric/20 text-xs text-accent-electric font-mono">
|
||||
JIRA-SEC-142
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
{i.security.integration.targets.map((target, idx) => (
|
||||
<TechBadge key={idx}>{target}</TechBadge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
'use client'
|
||||
|
||||
import { ArrowRight } from 'lucide-react'
|
||||
import { useApp } from '@/lib/context'
|
||||
import FadeInView from '@/components/ui/FadeInView'
|
||||
import GlassCard from '@/components/ui/GlassCard'
|
||||
import TechBadge from '@/components/ui/TechBadge'
|
||||
import SectionHeading from '@/components/ui/SectionHeading'
|
||||
|
||||
const tools = [
|
||||
{ name: 'SAST', de: 'Statische Code-Analyse für Firmware/Embedded', en: 'Static code analysis for firmware/embedded' },
|
||||
{ name: 'DAST', de: 'Dynamische Sicherheitstests gegen laufende Systeme', en: 'Dynamic security testing against running systems' },
|
||||
{ name: 'SBOM', de: 'Software Bill of Materials — CRA-Pflicht ab 2027', en: 'Software Bill of Materials — CRA mandatory from 2027' },
|
||||
{ name: 'Pentesting', de: 'Automatisierte Schwachstellensuche', en: 'Automated vulnerability scanning' },
|
||||
{ name: 'Secret Detection', de: 'Hardcoded Credentials im Code finden', en: 'Find hardcoded credentials in code' },
|
||||
{ name: 'Dependency Audit', de: 'Bekannte CVEs in Abhängigkeiten', en: 'Known CVEs in dependencies' },
|
||||
]
|
||||
|
||||
const heading = {
|
||||
de: {
|
||||
tag: 'SECURITY TOOLCHAIN',
|
||||
title: 'Scan → Priorisierung →',
|
||||
titleHighlight: 'Fix → Nachweis.',
|
||||
subtitle: 'Jedes Finding wird automatisch priorisiert, als Jira-Ticket erstellt und mit Code-Fix-Vorschlag versehen.',
|
||||
pipeline: ['Scan', 'Findings', 'Priorisierung', 'Jira-Ticket', 'Fix', 'Verifikation', 'Evidence'],
|
||||
jiraTitle: 'Automatische Entscheidungsunterstützung',
|
||||
jiraItems: [
|
||||
'Blocker: Finding blockiert CE/CRA-Konformität',
|
||||
'Major: Software-only Fix möglich',
|
||||
'Minor: Kosmetisch, kein Compliance-Impact',
|
||||
'Hardware vs. Software: Brauche ich ein Board-Redesign?',
|
||||
'Code-Fix-Vorschlag direkt im Ticket',
|
||||
],
|
||||
},
|
||||
en: {
|
||||
tag: 'SECURITY TOOLCHAIN',
|
||||
title: 'Scan → Prioritize →',
|
||||
titleHighlight: 'Fix → Evidence.',
|
||||
subtitle: 'Every finding is automatically prioritized, created as a Jira ticket and provided with a code fix suggestion.',
|
||||
pipeline: ['Scan', 'Findings', 'Prioritization', 'Jira Ticket', 'Fix', 'Verification', 'Evidence'],
|
||||
jiraTitle: 'Automatic decision support',
|
||||
jiraItems: [
|
||||
'Blocker: Finding blocks CE/CRA compliance',
|
||||
'Major: Software-only fix possible',
|
||||
'Minor: Cosmetic, no compliance impact',
|
||||
'Hardware vs. Software: Do I need a board redesign?',
|
||||
'Code fix suggestion directly in the ticket',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export default function SecurityToolchainSection() {
|
||||
const { lang } = useApp()
|
||||
const h = heading[lang]
|
||||
|
||||
return (
|
||||
<section className="py-24 sm:py-32 section-alt">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading tag={h.tag} title={h.title} titleHighlight={h.titleHighlight} subtitle={h.subtitle} />
|
||||
|
||||
{/* Pipeline visualization */}
|
||||
<FadeInView className="mb-16">
|
||||
<div className="flex flex-wrap items-center justify-center gap-2">
|
||||
{h.pipeline.map((step, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2">
|
||||
<span className="px-3 py-1.5 rounded-lg bg-accent-electric/10 border border-accent-electric/15 text-xs text-accent-electric font-mono">
|
||||
{step}
|
||||
</span>
|
||||
{idx < h.pipeline.length - 1 && (
|
||||
<ArrowRight className="w-3 h-3 text-white/20" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
{/* Tools grid + Jira integration */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<FadeInView direction="left">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||
{tools.map((tool, idx) => (
|
||||
<GlassCard key={idx} delay={idx * 0.05} className="p-4">
|
||||
<p className="font-mono text-sm font-bold text-accent-electric mb-1">{tool.name}</p>
|
||||
<p className="text-xs text-white/40">{lang === 'de' ? tool.de : tool.en}</p>
|
||||
</GlassCard>
|
||||
))}
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
<FadeInView direction="right">
|
||||
<div className="rounded-2xl border border-white/[0.06] bg-white/[0.03] p-6 h-full">
|
||||
<h3 className="text-lg font-bold mb-4">{h.jiraTitle}</h3>
|
||||
<ul className="space-y-3 mb-6">
|
||||
{h.jiraItems.map((item, idx) => (
|
||||
<li key={idx} className="flex items-start gap-3 text-sm text-white/50">
|
||||
<span className={`mt-1 w-2 h-2 rounded-full shrink-0 ${idx === 0 ? 'bg-red-400' : idx === 1 ? 'bg-amber-400' : idx === 2 ? 'bg-green-400' : 'bg-accent-electric'}`} />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{['Jira', 'Linear', 'GitLab', 'GitHub'].map(t => (
|
||||
<TechBadge key={t}>{t}</TechBadge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
'use client'
|
||||
|
||||
import { Server, Shield, Globe } from 'lucide-react'
|
||||
import { t } from '@/lib/content'
|
||||
import { useApp } from '@/lib/context'
|
||||
import SectionHeading from '@/components/ui/SectionHeading'
|
||||
import GlassCard from '@/components/ui/GlassCard'
|
||||
import FadeInView from '@/components/ui/FadeInView'
|
||||
import TechBadge from '@/components/ui/TechBadge'
|
||||
|
||||
export default function SovereignSection() {
|
||||
const { lang } = useApp()
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<section id="sovereign" className="py-24 sm:py-32 section-alt">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading
|
||||
tag={i.sovereign.tag}
|
||||
title={i.sovereign.title}
|
||||
titleHighlight={i.sovereign.titleHighlight}
|
||||
subtitle={i.sovereign.subtitle}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
{i.sovereign.features.map((feature, idx) => {
|
||||
const icons = [Server, Shield, Globe]
|
||||
const Icon = icons[idx]
|
||||
return (
|
||||
<GlassCard key={idx} delay={idx * 0.1}>
|
||||
<div className="w-10 h-10 rounded-xl bg-accent-purple/10 flex items-center justify-center mb-4">
|
||||
<Icon className="w-5 h-5 text-accent-purple" />
|
||||
</div>
|
||||
<h3 className="text-sm font-bold mb-2">{feature.title}</h3>
|
||||
<p className="text-xs text-white/40">{feature.description}</p>
|
||||
</GlassCard>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<FadeInView>
|
||||
<div className="rounded-2xl border border-accent-purple/20 bg-accent-purple/[0.04] p-6 md:p-8">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center gap-6">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-bold mb-2">{i.sovereign.appliance.title}</h3>
|
||||
<p className="text-sm text-white/40 mb-4">{i.sovereign.appliance.description}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{i.sovereign.appliance.specs.map((spec, idx) => (
|
||||
<TechBadge key={idx}>{spec}</TechBadge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-32 h-32 rounded-2xl bg-white/[0.04] border border-white/[0.06] flex items-center justify-center shrink-0">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-3 bg-white/10 rounded-sm mx-auto mb-2" />
|
||||
<div className="w-12 h-12 bg-white/[0.06] rounded-lg mx-auto border border-white/[0.08]" />
|
||||
<div className="w-4 h-0.5 bg-white/10 rounded-full mx-auto mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
'use client'
|
||||
|
||||
import { Factory, Truck, Cpu, Zap } from 'lucide-react'
|
||||
import { t } from '@/lib/content'
|
||||
import { useApp } from '@/lib/context'
|
||||
import SectionHeading from '@/components/ui/SectionHeading'
|
||||
import GlassCard from '@/components/ui/GlassCard'
|
||||
import TechBadge from '@/components/ui/TechBadge'
|
||||
|
||||
const iconMap: Record<string, typeof Factory> = { Factory, Truck, Cpu, Zap }
|
||||
|
||||
export default function TargetSection() {
|
||||
const { lang } = useApp()
|
||||
const i = t(lang)
|
||||
|
||||
return (
|
||||
<section id="targets" className="py-24 sm:py-32">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading
|
||||
tag={i.targets.tag}
|
||||
title={i.targets.title}
|
||||
titleHighlight={i.targets.titleHighlight}
|
||||
subtitle={i.targets.subtitle}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{i.targets.industries.map((industry, idx) => {
|
||||
const Icon = iconMap[industry.icon]
|
||||
return (
|
||||
<GlassCard key={idx} delay={idx * 0.1}>
|
||||
<div className="w-10 h-10 rounded-xl bg-accent-electric/10 flex items-center justify-center mb-4">
|
||||
<Icon className="w-5 h-5 text-accent-electric" />
|
||||
</div>
|
||||
<h3 className="text-sm font-bold mb-3">{industry.name}</h3>
|
||||
<div className="flex flex-wrap gap-1.5 mb-4">
|
||||
{industry.regulations.map((reg, ri) => (
|
||||
<TechBadge key={ri}>{reg}</TechBadge>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-accent-signal/80 font-mono">{industry.roi}</p>
|
||||
</GlassCard>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { Linkedin, Github, ArrowUpRight } from 'lucide-react'
|
||||
import { useApp } from '@/lib/context'
|
||||
import SectionHeading from '@/components/ui/SectionHeading'
|
||||
import FadeInView from '@/components/ui/FadeInView'
|
||||
|
||||
interface TeamMember {
|
||||
name: string
|
||||
roleDe: string
|
||||
roleEn: string
|
||||
bioDe: string
|
||||
bioEn: string
|
||||
equityPct: number
|
||||
expertise: string[]
|
||||
links: { type: 'linkedin' | 'github'; url: string }[]
|
||||
initials: string
|
||||
gradient: string
|
||||
}
|
||||
|
||||
const team: TeamMember[] = [
|
||||
{
|
||||
name: 'Benjamin Bönisch',
|
||||
roleDe: 'CEO & Co-Founder',
|
||||
roleEn: 'CEO & Co-Founder',
|
||||
bioDe: 'Ehemaliger Lehrer mit Leidenschaft für EdTech und Datenschutz. Hat BreakPilot als DSGVO-konforme Bildungsplattform gegründet und zum Self-Hosted Compliance-Anbieter weiterentwickelt.',
|
||||
bioEn: 'Former teacher with a passion for EdTech and data privacy. Founded BreakPilot as a GDPR-compliant education platform and evolved it into a self-hosted compliance provider.',
|
||||
equityPct: 50,
|
||||
expertise: ['EdTech', 'DSGVO', 'Produktstrategie', 'Go-to-Market'],
|
||||
links: [
|
||||
{ type: 'linkedin', url: 'https://linkedin.com/in/benjamin-boenisch' },
|
||||
],
|
||||
initials: 'BB',
|
||||
gradient: 'from-accent-electric to-accent-indigo',
|
||||
},
|
||||
{
|
||||
name: 'Sharang Parnerkar',
|
||||
roleDe: 'CTO & Co-Founder',
|
||||
roleEn: 'CTO & Co-Founder',
|
||||
bioDe: 'Full-Stack-Ingenieur mit Expertise in KI/ML, Apple Silicon Optimierung und verteilten Systemen. Verantwortlich für die technische Architektur der ComplAI-Plattform.',
|
||||
bioEn: 'Full-stack engineer with expertise in AI/ML, Apple Silicon optimization, and distributed systems. Responsible for the technical architecture of the ComplAI platform.',
|
||||
equityPct: 50,
|
||||
expertise: ['AI/ML', 'Apple Silicon', 'Full-Stack', 'DevOps'],
|
||||
links: [
|
||||
{ type: 'github', url: 'https://github.com/sharangp' },
|
||||
],
|
||||
initials: 'SP',
|
||||
gradient: 'from-accent-indigo to-accent-purple',
|
||||
},
|
||||
]
|
||||
|
||||
const headingContent = {
|
||||
de: {
|
||||
tag: 'TEAM',
|
||||
title: 'Die Menschen hinter',
|
||||
titleHighlight: 'BreakPilot.',
|
||||
subtitle: 'Gründer mit Domain-Expertise in Compliance, KI und Produktentwicklung.',
|
||||
equity: 'Equity',
|
||||
cta: 'Offene Positionen ansehen',
|
||||
hiring: 'Wir stellen ein',
|
||||
hiringText: 'BreakPilot wächst. Wir suchen Ingenieure, die regulatorische Komplexität als technisches Problem lösen wollen.',
|
||||
roles: ['Backend Engineer (Python/Go)', 'Regulatory Analyst', 'DevOps Engineer'],
|
||||
},
|
||||
en: {
|
||||
tag: 'TEAM',
|
||||
title: 'The people behind',
|
||||
titleHighlight: 'BreakPilot.',
|
||||
subtitle: 'Founders with domain expertise in compliance, AI and product development.',
|
||||
equity: 'Equity',
|
||||
cta: 'View open positions',
|
||||
hiring: "We're hiring",
|
||||
hiringText: 'BreakPilot is growing. We are looking for engineers who want to solve regulatory complexity as a technical problem.',
|
||||
roles: ['Backend Engineer (Python/Go)', 'Regulatory Analyst', 'DevOps Engineer'],
|
||||
},
|
||||
}
|
||||
|
||||
const LinkIcons = { linkedin: Linkedin, github: Github }
|
||||
|
||||
export default function TeamSection() {
|
||||
const { lang } = useApp()
|
||||
const h = headingContent[lang]
|
||||
|
||||
return (
|
||||
<section className="py-24 sm:py-32 pt-32">
|
||||
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionHeading
|
||||
tag={h.tag}
|
||||
title={h.title}
|
||||
titleHighlight={h.titleHighlight}
|
||||
subtitle={h.subtitle}
|
||||
/>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6 mb-16">
|
||||
{team.map((member, idx) => (
|
||||
<motion.div
|
||||
key={member.name}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: idx * 0.15, duration: 0.5 }}
|
||||
className="bg-white/[0.04] backdrop-blur-xl border border-white/[0.08] rounded-2xl p-6 flex flex-col hover:border-accent-electric/20 transition-colors"
|
||||
>
|
||||
{/* Header: avatar + name + role + equity */}
|
||||
<div className="flex items-center gap-4 mb-5">
|
||||
<div className={`w-16 h-16 rounded-2xl bg-gradient-to-br ${member.gradient} flex items-center justify-center shrink-0 shadow-lg shadow-accent-electric/20`}>
|
||||
<span className="text-xl font-bold text-white">{member.initials}</span>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 mb-0.5 flex-wrap">
|
||||
<h3 className="text-xl font-bold text-white truncate">{member.name}</h3>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{member.links.map((link) => {
|
||||
const Icon = LinkIcons[link.type]
|
||||
return (
|
||||
<a
|
||||
key={link.type}
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-white/25 hover:text-accent-electric transition-colors"
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-accent-electric text-sm font-medium">
|
||||
{lang === 'de' ? member.roleDe : member.roleEn}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Equity pill */}
|
||||
<div className="text-right shrink-0">
|
||||
<div className="text-[10px] uppercase tracking-wider text-white/30">{h.equity}</div>
|
||||
<div className="text-base font-bold text-white tabular-nums">{member.equityPct}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bio */}
|
||||
<p className="text-sm text-white/60 leading-relaxed mb-5 flex-1">
|
||||
{lang === 'de' ? member.bioDe : member.bioEn}
|
||||
</p>
|
||||
|
||||
{/* Expertise tags */}
|
||||
<div className="flex flex-wrap gap-1.5 pt-4 border-t border-white/[0.06]">
|
||||
{member.expertise.map((skill) => (
|
||||
<span
|
||||
key={skill}
|
||||
className="text-xs px-2.5 py-1 rounded-full bg-accent-electric/10 text-accent-electric/80 border border-accent-electric/15"
|
||||
>
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Hiring CTA */}
|
||||
<FadeInView>
|
||||
<div className="rounded-2xl border border-accent-electric/15 bg-accent-electric/[0.04] p-8 text-center">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-accent-signal/10 border border-accent-signal/20 text-accent-signal text-xs font-medium mb-4">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-accent-signal animate-pulse" />
|
||||
{h.hiring}
|
||||
</div>
|
||||
<p className="text-sm text-white/50 max-w-lg mx-auto mb-6">
|
||||
{h.hiringText}
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-2 mb-6">
|
||||
{h.roles.map((role) => (
|
||||
<span key={role} className="px-3 py-1.5 rounded-lg bg-white/[0.06] border border-white/[0.06] text-xs text-white/60 font-mono">
|
||||
{role}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<a
|
||||
href="mailto:jobs@breakpilot.ai"
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl bg-accent-electric text-white text-sm font-medium hover:bg-blue-500 transition-colors glow-blue"
|
||||
>
|
||||
{h.cta}
|
||||
<ArrowUpRight className="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
'use client'
|
||||
|
||||
import { Shield, FileCheck, ClipboardCheck } from 'lucide-react'
|
||||
import { useApp } from '@/lib/context'
|
||||
import FadeInView from '@/components/ui/FadeInView'
|
||||
import AnimatedCounter from '@/components/ui/AnimatedCounter'
|
||||
|
||||
const pillars = {
|
||||
de: [
|
||||
{ icon: Shield, label: 'Deterministisch', detail: 'Regelbasiert, nicht generativ' },
|
||||
{ icon: FileCheck, label: 'Nachvollziehbar', detail: 'Jedes Ergebnis mit Rechtsquelle' },
|
||||
{ icon: ClipboardCheck, label: 'Auditierbar', detail: 'Vollständiger Decision Trail' },
|
||||
],
|
||||
en: [
|
||||
{ icon: Shield, label: 'Deterministic', detail: 'Rule-based, not generative' },
|
||||
{ icon: FileCheck, label: 'Traceable', detail: 'Every result with legal source' },
|
||||
{ icon: ClipboardCheck, label: 'Auditable', detail: 'Complete decision trail' },
|
||||
],
|
||||
}
|
||||
|
||||
const stats = [
|
||||
{ value: '294.000+', labelDe: 'atomare Controls', labelEn: 'atomic controls' },
|
||||
{ value: '380+', labelDe: 'Rechtsquellen', labelEn: 'legal sources' },
|
||||
{ value: '1.058', labelDe: 'Hazard Patterns', labelEn: 'hazard patterns' },
|
||||
{ value: '751', labelDe: 'Normen', labelEn: 'standards' },
|
||||
]
|
||||
|
||||
export default function TrustBar() {
|
||||
const { lang } = useApp()
|
||||
const items = pillars[lang]
|
||||
|
||||
return (
|
||||
<section className="py-24 sm:py-32 section-alt">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* Trust pillars */}
|
||||
<FadeInView>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-16">
|
||||
{items.map((pillar, idx) => {
|
||||
const Icon = pillar.icon
|
||||
return (
|
||||
<div key={idx} className="flex items-center gap-4 p-4 rounded-xl border border-white/[0.06] bg-white/[0.03]">
|
||||
<div className="w-10 h-10 rounded-xl bg-accent-indigo/10 flex items-center justify-center shrink-0">
|
||||
<Icon className="w-5 h-5 text-accent-indigo" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-bold">{pillar.label}</h3>
|
||||
<p className="text-xs text-white/40">{pillar.detail}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
{/* Stats */}
|
||||
<FadeInView delay={0.2}>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
{stats.map((stat, idx) => (
|
||||
<div key={idx} className="text-center">
|
||||
<div className="text-3xl font-bold mb-1 gradient-text">
|
||||
<AnimatedCounter value={stat.value} />
|
||||
</div>
|
||||
<p className="text-xs text-white/40">{lang === 'de' ? stat.labelDe : stat.labelEn}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { ArrowRight, Scan, FileCheck, Users } from 'lucide-react'
|
||||
import { useApp } from '@/lib/context'
|
||||
import FadeInView from '@/components/ui/FadeInView'
|
||||
import GlassCard from '@/components/ui/GlassCard'
|
||||
|
||||
const icons = [Scan, FileCheck, Users]
|
||||
const gradients = ['from-red-500/10 to-amber-500/10', 'from-accent-electric/10 to-accent-indigo/10', 'from-accent-purple/10 to-accent-electric/10']
|
||||
|
||||
const cases = {
|
||||
de: [
|
||||
{
|
||||
title: '187 Pentest-Findings — was davon ist wirklich kritisch?',
|
||||
description: 'SAST/DAST + Pentesting liefern 187 Findings für ein Embedded Board. BreakPilot priorisiert automatisch: 3 blockieren CE/CRA, 12 sind Software-only Fixes, der Rest ist kosmetisch. Jira-Tickets mit Code-Fix-Vorschlägen werden automatisch erstellt.',
|
||||
highlight: 'Hardware-Redesign für €50k? Wahrscheinlich nicht nötig.',
|
||||
href: '/product-compliance',
|
||||
cta: 'Product Compliance ansehen',
|
||||
},
|
||||
{
|
||||
title: 'CE-Akte in Stunden statt Monaten',
|
||||
description: 'Sondermaschinenbauer beschreibt einen Cobot in 14 Textfeldern. 1.058 Hazard Patterns feuern deterministisch. 12 Gefährdungen, 30 Maßnahmen, 45 Normen — automatisch zugeordnet, nicht generiert.',
|
||||
highlight: 'CE-Akte nach MVO 2023/1230 Anhang IV per Klick.',
|
||||
href: '/plattform',
|
||||
cta: 'Plattform entdecken',
|
||||
},
|
||||
{
|
||||
title: 'Der CE-Experte geht in Rente',
|
||||
description: 'Ein Junior-Konstrukteur nutzt BreakPilot für seine erste Risikobeurteilung. Konsistente Erstbewertung mit Explainability. Der Senior reviewt nur noch, statt alles selbst zu machen.',
|
||||
highlight: 'Expertenwissen operationalisiert, nicht in Köpfen eingesperrt.',
|
||||
href: '/plattform',
|
||||
cta: 'Plattform entdecken',
|
||||
},
|
||||
],
|
||||
en: [
|
||||
{
|
||||
title: '187 pentest findings — which ones actually matter?',
|
||||
description: 'SAST/DAST + pentesting deliver 187 findings for an embedded board. BreakPilot prioritizes automatically: 3 block CE/CRA, 12 are software-only fixes, the rest is cosmetic. Jira tickets with code fix suggestions are created automatically.',
|
||||
highlight: 'Hardware redesign for €50k? Probably not necessary.',
|
||||
href: '/product-compliance',
|
||||
cta: 'View Product Compliance',
|
||||
},
|
||||
{
|
||||
title: 'CE file in hours instead of months',
|
||||
description: 'A special machine builder describes a cobot in 14 text fields. 1,058 hazard patterns fire deterministically. 12 hazards, 30 mitigations, 45 norms — automatically mapped, not generated.',
|
||||
highlight: 'CE file per Machinery Regulation 2023/1230 Annex IV with one click.',
|
||||
href: '/plattform',
|
||||
cta: 'Discover Platform',
|
||||
},
|
||||
{
|
||||
title: 'The CE expert is retiring',
|
||||
description: 'A junior engineer uses BreakPilot for their first risk assessment. Consistent initial assessment with explainability. The senior only reviews instead of doing everything.',
|
||||
highlight: 'Expert knowledge operationalized, not locked in heads.',
|
||||
href: '/plattform',
|
||||
cta: 'Discover Platform',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default function UseCaseCards() {
|
||||
const { lang } = useApp()
|
||||
const items = cases[lang]
|
||||
|
||||
return (
|
||||
<section className="py-24 sm:py-32">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{items.map((item, idx) => {
|
||||
const Icon = icons[idx]
|
||||
return (
|
||||
<GlassCard key={idx} delay={idx * 0.1} hover={false} className="flex flex-col">
|
||||
<div className={`w-12 h-12 rounded-2xl bg-gradient-to-br ${gradients[idx]} flex items-center justify-center mb-5`}>
|
||||
<Icon className="w-6 h-6 text-white/70" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold mb-3 leading-snug">{item.title}</h3>
|
||||
<p className="text-sm text-white/40 mb-4 flex-1">{item.description}</p>
|
||||
<p className="text-sm text-accent-electric font-medium mb-5">{item.highlight}</p>
|
||||
<Link
|
||||
href={item.href}
|
||||
className="inline-flex items-center gap-2 text-sm text-white/50 hover:text-white transition-colors group"
|
||||
>
|
||||
{item.cta}
|
||||
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</GlassCard>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
interface AnimatedCounterProps {
|
||||
value: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function AnimatedCounter({ value, className = '' }: AnimatedCounterProps) {
|
||||
const [display, setDisplay] = useState('0')
|
||||
const ref = useRef<HTMLSpanElement>(null)
|
||||
const hasAnimated = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (hasAnimated.current) return
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && !hasAnimated.current) {
|
||||
hasAnimated.current = true
|
||||
const numericMatch = value.match(/^([\d.]+)/)
|
||||
if (!numericMatch) {
|
||||
setDisplay(value)
|
||||
return
|
||||
}
|
||||
const target = parseFloat(numericMatch[1])
|
||||
const suffix = value.slice(numericMatch[1].length)
|
||||
const isFloat = value.includes('.')
|
||||
const duration = 1500
|
||||
const start = performance.now()
|
||||
|
||||
const animate = (now: number) => {
|
||||
const progress = Math.min((now - start) / duration, 1)
|
||||
const eased = 1 - Math.pow(1 - progress, 3)
|
||||
const current = target * eased
|
||||
setDisplay(
|
||||
(isFloat ? current.toFixed(1) : Math.floor(current).toLocaleString('de-DE')) + suffix
|
||||
)
|
||||
if (progress < 1) requestAnimationFrame(animate)
|
||||
}
|
||||
requestAnimationFrame(animate)
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 }
|
||||
)
|
||||
|
||||
if (ref.current) observer.observe(ref.current)
|
||||
return () => observer.disconnect()
|
||||
}, [value])
|
||||
|
||||
return <span ref={ref} className={className}>{display}</span>
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
interface CTAButtonProps {
|
||||
children: React.ReactNode
|
||||
variant?: 'primary' | 'ghost'
|
||||
href?: string
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
export default function CTAButton({ children, variant = 'primary', href, className = '', onClick }: CTAButtonProps) {
|
||||
const baseClass = 'inline-flex items-center gap-2 px-6 py-3 rounded-xl font-semibold text-sm transition-all duration-200'
|
||||
|
||||
const variantClass = variant === 'primary'
|
||||
? 'bg-accent-electric text-white glow-blue hover:bg-blue-500'
|
||||
: 'border border-white/[0.12] text-white/80 hover:bg-white/[0.06] hover:text-white'
|
||||
|
||||
const Component = href ? motion.a : motion.button
|
||||
|
||||
return (
|
||||
<Component
|
||||
href={href}
|
||||
onClick={onClick}
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
className={`${baseClass} ${variantClass} ${className}`}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Check, X, Minus } from 'lucide-react'
|
||||
|
||||
interface ComparisonCellProps {
|
||||
value: boolean | 'partial'
|
||||
}
|
||||
|
||||
export default function ComparisonCell({ value }: ComparisonCellProps) {
|
||||
if (value === true) {
|
||||
return <Check className="w-4 h-4 text-green-400 mx-auto" />
|
||||
}
|
||||
if (value === 'partial') {
|
||||
return <Minus className="w-4 h-4 text-amber-400 mx-auto" />
|
||||
}
|
||||
return <X className="w-4 h-4 text-white/20 mx-auto" />
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { ReactNode } from 'react'
|
||||
import { ANIMATION } from '@/lib/constants'
|
||||
|
||||
interface FadeInViewProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
delay?: number
|
||||
direction?: 'up' | 'down' | 'left' | 'right' | 'none'
|
||||
duration?: number
|
||||
}
|
||||
|
||||
const directionMap = {
|
||||
up: { y: 30 },
|
||||
down: { y: -30 },
|
||||
left: { x: 30 },
|
||||
right: { x: -30 },
|
||||
none: {},
|
||||
}
|
||||
|
||||
export default function FadeInView({
|
||||
children,
|
||||
className = '',
|
||||
delay = 0,
|
||||
direction = 'up',
|
||||
duration = ANIMATION.duration,
|
||||
}: FadeInViewProps) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, ...directionMap[direction] }}
|
||||
whileInView={{ opacity: 1, x: 0, y: 0 }}
|
||||
viewport={{ once: true, margin: '100px 0px -60px 0px' }}
|
||||
transition={{ duration, delay, ease: ANIMATION.ease }}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { ReactNode } from 'react'
|
||||
import { ANIMATION } from '@/lib/constants'
|
||||
|
||||
interface GlassCardProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
delay?: number
|
||||
hover?: boolean
|
||||
}
|
||||
|
||||
export default function GlassCard({ children, className = '', delay = 0, hover = true }: GlassCardProps) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '100px 0px -60px 0px' }}
|
||||
transition={{ duration: ANIMATION.duration, delay }}
|
||||
whileHover={hover ? { scale: 1.02, backgroundColor: 'rgba(255, 255, 255, 0.10)' } : undefined}
|
||||
className={`
|
||||
bg-white/[0.06] backdrop-blur-xl
|
||||
border border-white/[0.08] rounded-2xl
|
||||
p-6 transition-colors duration-200
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
interface GradientTextProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
variant?: 'default' | 'signal'
|
||||
}
|
||||
|
||||
export default function GradientText({ children, className = '', variant = 'default' }: GradientTextProps) {
|
||||
const gradientClass = variant === 'signal' ? 'gradient-text-signal' : 'gradient-text'
|
||||
return (
|
||||
<span className={`${gradientClass} ${className}`}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import GradientText from './GradientText'
|
||||
|
||||
const ease = [0.22, 1, 0.36, 1] as const
|
||||
|
||||
interface PageHeaderProps {
|
||||
tag: string
|
||||
title: string
|
||||
titleHighlight: string
|
||||
subtitle: string
|
||||
}
|
||||
|
||||
export default function PageHeader({ tag, title, titleHighlight, subtitle }: PageHeaderProps) {
|
||||
return (
|
||||
<div className="pt-32 pb-16 text-center">
|
||||
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, ease }}>
|
||||
<p className="mono-label mb-4">{tag}</p>
|
||||
</motion.div>
|
||||
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: 0.1, ease }}>
|
||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold mb-6">
|
||||
{title}{' '}
|
||||
<GradientText>{titleHighlight}</GradientText>
|
||||
</h1>
|
||||
</motion.div>
|
||||
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: 0.2, ease }}>
|
||||
<p className="text-white/50 text-lg max-w-3xl mx-auto">
|
||||
{subtitle}
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
'use client'
|
||||
|
||||
import FadeInView from './FadeInView'
|
||||
import GradientText from './GradientText'
|
||||
|
||||
interface SectionHeadingProps {
|
||||
tag: string
|
||||
title: string
|
||||
titleHighlight: string
|
||||
subtitle: string
|
||||
center?: boolean
|
||||
}
|
||||
|
||||
export default function SectionHeading({ tag, title, titleHighlight, subtitle, center = true }: SectionHeadingProps) {
|
||||
return (
|
||||
<div className={`mb-16 ${center ? 'text-center' : ''}`}>
|
||||
<FadeInView>
|
||||
<p className="mono-label mb-4">{tag}</p>
|
||||
</FadeInView>
|
||||
<FadeInView delay={0.1}>
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-6">
|
||||
{title}{' '}
|
||||
<GradientText>{titleHighlight}</GradientText>
|
||||
</h2>
|
||||
</FadeInView>
|
||||
<FadeInView delay={0.2}>
|
||||
<p className={`text-white/50 text-lg ${center ? 'max-w-3xl mx-auto' : 'max-w-3xl'}`}>
|
||||
{subtitle}
|
||||
</p>
|
||||
</FadeInView>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
interface StatusIndicatorProps {
|
||||
label: string
|
||||
status?: 'active' | 'warning' | 'error'
|
||||
className?: string
|
||||
}
|
||||
|
||||
const statusColors = {
|
||||
active: 'bg-green-500',
|
||||
warning: 'bg-amber-500',
|
||||
error: 'bg-red-500',
|
||||
}
|
||||
|
||||
export default function StatusIndicator({ label, status = 'active', className = '' }: StatusIndicatorProps) {
|
||||
return (
|
||||
<div className={`inline-flex items-center gap-2 ${className}`}>
|
||||
<span className="relative flex h-2.5 w-2.5">
|
||||
<span className={`animate-ping absolute inline-flex h-full w-full rounded-full ${statusColors[status]} opacity-75`} />
|
||||
<span className={`relative inline-flex rounded-full h-2.5 w-2.5 ${statusColors[status]}`} />
|
||||
</span>
|
||||
<span className="font-mono text-xs text-white/50">{label}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
interface TechBadgeProps {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function TechBadge({ children, className = '' }: TechBadgeProps) {
|
||||
return (
|
||||
<span className={`
|
||||
inline-block px-3 py-1 rounded-md
|
||||
font-mono text-xs
|
||||
bg-white/[0.06] border border-white/[0.08]
|
||||
text-white/60
|
||||
${className}
|
||||
`}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export const ANIMATION = {
|
||||
duration: 0.5,
|
||||
ease: [0.22, 1, 0.36, 1] as const,
|
||||
staggerDelay: 0.1,
|
||||
} as const
|
||||
|
||||
export const COLORS = {
|
||||
electric: '#3b82f6',
|
||||
signal: '#22c55e',
|
||||
indigo: '#6366f1',
|
||||
purple: '#a78bfa',
|
||||
amber: '#f59e0b',
|
||||
red: '#ef4444',
|
||||
} as const
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Savings-Section content (DE + EN) — ausgelagert aus content.ts,
|
||||
* damit content.ts unter dem 500-LOC-Hard-Cap bleibt.
|
||||
*/
|
||||
|
||||
export const savingsDE = {
|
||||
tag: '03 / NEBENEFFEKT: COST-OPTIMIZATION',
|
||||
title: 'Compliance entdeckt',
|
||||
titleHighlight: 'sechsstellige Einsparungen.',
|
||||
subtitle:
|
||||
'Beim Cookie-Audit identifizieren wir Anbieter, die das Gleiche tun. ' +
|
||||
'Pro Anbieter eine Kosten-Schaetzung auf Basis von Tier-Inferenz ' +
|
||||
'(Cookie-Anzahl, Premium-Feature-Detection, Drittanbieter-Quote).',
|
||||
promise: 'Software bezahlt sich beim ERSTEN Scan.',
|
||||
caseStudy: {
|
||||
label: 'CASE-STUDY: DAX-Automotive-Konzern',
|
||||
headline: '90 Cookie-Anbieter → 25 nach Konsolidierung',
|
||||
bullets: [
|
||||
'5 Web-Analytics-Tools (Adobe Analytics, Content Square, Dynatrace, …) → 1 Matomo Pro',
|
||||
'10 Retargeting-Pixel (Criteo, Adform, Outbrain, Taboola, Meta, …) → 3 Kern-Kanaele',
|
||||
'5 CDN/Speed-Tools (Akamai, AWS, Baqend, Speedkit, SpeedCurve) → IONOS Cloud + Bunny',
|
||||
],
|
||||
saving: '€500k–€3M / Jahr',
|
||||
side: 'Plus 100% DSGVO-Konformitaet ohne Schrems-II-Risiko',
|
||||
},
|
||||
pillars: [
|
||||
{
|
||||
title: 'Tier-Inferenz aus Cookie-Footprint',
|
||||
description:
|
||||
'>30 Cookies = Enterprise. Premium-Feature-Cookies (s_target_qa, _ab_test, ' +
|
||||
'aam_uuid) markieren Add-on-Module. Drittanbieter-Quote + Lebensdauer ' +
|
||||
'verraten Tracking-Intensitaet.',
|
||||
},
|
||||
{
|
||||
title: 'Konsolidierungs-Vorschlaege',
|
||||
description:
|
||||
'Pro Funktions-Kategorie (Analytics, Werbung, CDN, …) ein konkreter ' +
|
||||
'EU-Ersatz inkl. Listpreis. Multi-Funktions-Tools (SAP CX, Matomo Pro, ' +
|
||||
'IONOS Cloud) ersetzen mehrere Anbieter gleichzeitig.',
|
||||
},
|
||||
{
|
||||
title: 'Ehrliche Caveats',
|
||||
description:
|
||||
'Bekannte Gruende fuer Mehrfach-Einsatz (Saisonal, A/B-Test, Regional) ' +
|
||||
'werden explizit aufgefuehrt — kein Pseudo-Saving. Konzern-Konditionen ' +
|
||||
'liegen ueblicherweise 30-50% unter Listpreis (transparent ausgewiesen).',
|
||||
},
|
||||
],
|
||||
cta: 'Kostenlosen 5-Min-Saving-Scan starten',
|
||||
ctaSecondary: 'Methodik ansehen',
|
||||
}
|
||||
|
||||
export const savingsEN = {
|
||||
tag: '03 / SIDE EFFECT: COST OPTIMIZATION',
|
||||
title: 'Compliance reveals',
|
||||
titleHighlight: 'six-figure savings.',
|
||||
subtitle:
|
||||
'During the cookie audit we identify vendors that do the same thing. ' +
|
||||
'Per vendor we infer the pricing tier from cookie footprint (count, ' +
|
||||
'premium-feature detection, third-party ratio).',
|
||||
promise: 'The software pays for itself with the FIRST scan.',
|
||||
caseStudy: {
|
||||
label: 'CASE STUDY: DAX automotive group',
|
||||
headline: '90 cookie vendors → 25 after consolidation',
|
||||
bullets: [
|
||||
'5 Web-Analytics tools (Adobe Analytics, Content Square, Dynatrace, …) → 1 Matomo Pro',
|
||||
'10 retargeting pixels (Criteo, Adform, Outbrain, Taboola, Meta, …) → 3 core channels',
|
||||
'5 CDN/Speed tools (Akamai, AWS, Baqend, Speedkit, SpeedCurve) → IONOS Cloud + Bunny',
|
||||
],
|
||||
saving: '€500k–€3M / year',
|
||||
side: 'Plus 100% GDPR-compliant without Schrems II risk',
|
||||
},
|
||||
pillars: [
|
||||
{
|
||||
title: 'Tier inference from cookie footprint',
|
||||
description:
|
||||
'>30 cookies = enterprise. Premium-feature cookies (s_target_qa, _ab_test, ' +
|
||||
'aam_uuid) mark add-on modules. Third-party share + lifetime reveal ' +
|
||||
'tracking intensity.',
|
||||
},
|
||||
{
|
||||
title: 'Consolidation suggestions',
|
||||
description:
|
||||
'Per functional category (analytics, advertising, CDN, …) a concrete ' +
|
||||
'EU substitute incl. list price. Multi-function tools (SAP CX, Matomo Pro, ' +
|
||||
'IONOS Cloud) replace multiple vendors at once.',
|
||||
},
|
||||
{
|
||||
title: 'Honest caveats',
|
||||
description:
|
||||
'Known reasons for multi-vendor use (seasonal, A/B testing, regional) ' +
|
||||
'are stated explicitly — no pseudo-savings. Group discounts typically ' +
|
||||
'30–50% below list price (transparently disclosed).',
|
||||
},
|
||||
],
|
||||
cta: 'Run the free 5-min savings scan',
|
||||
ctaSecondary: 'See the methodology',
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
import { savingsDE, savingsEN } from './content.savings'
|
||||
|
||||
type Lang = 'de' | 'en'
|
||||
type TerminalType = 'input' | 'output' | 'signal'
|
||||
type Status = 'neutral' | 'success' | 'warning'
|
||||
type IconName = 'Shield' | 'FileCheck' | 'ClipboardCheck' | 'AlertTriangle' | 'ShieldCheck' | 'RefreshCw' | 'Cpu' | 'FileText' | 'Link' | 'BadgeCheck' | 'Code'
|
||||
type IndustryIcon = 'Factory' | 'Truck' | 'Cpu' | 'Zap'
|
||||
type RiskColor = 'red' | 'amber' | 'blue' | 'green'
|
||||
|
||||
const de = {
|
||||
nav: { cta: 'Demo anfordern' },
|
||||
hero: {
|
||||
badge: 'DETERMINISTIC REGULATORY ENGINEERING',
|
||||
title: 'Regulatorische Analyse.',
|
||||
titleHighlight: 'Deterministisch. Nachvollziehbar.',
|
||||
subtitle: 'Keine Halluzinationen. Keine Compliance-Lücken. Keine Abhängigkeit von US-Cloud-Anbietern.',
|
||||
cta: 'Demo anfordern',
|
||||
ctaSecondary: 'Architektur ansehen',
|
||||
status: '4 Engines aktiv',
|
||||
},
|
||||
problem: {
|
||||
tag: '01 / DAS PROBLEM',
|
||||
title: 'Regulatorische Komplexität',
|
||||
titleHighlight: 'wächst exponentiell',
|
||||
subtitle: 'Manuelle Compliance-Prozesse skalieren nicht mit der Geschwindigkeit regulatorischer Änderungen.',
|
||||
cards: [
|
||||
{
|
||||
metric: '37.000+',
|
||||
label: 'regulatorische Änderungen pro Jahr',
|
||||
description: 'EU-, Bundes- und Landesebene erzeugen eine nicht manuell beherrschbare Regulierungsdichte.',
|
||||
source: 'VDMA / Bitkom 2025',
|
||||
},
|
||||
{
|
||||
metric: '83%',
|
||||
label: 'der KMU sehen Compliance als Innovationsbremse',
|
||||
description: 'Datenschutzrisiken, CE-Anforderungen und NIS2-Pflichten binden Ressourcen, die für Produktentwicklung fehlen.',
|
||||
source: 'DIHK Digitalisierungsbericht',
|
||||
},
|
||||
{
|
||||
metric: '50.000+',
|
||||
label: 'EUR jährliche Compliance-Kosten',
|
||||
description: 'Externe Audits, Penetrationstests, CE-Bewertungen und Datenschutzberatung summieren sich.',
|
||||
source: 'Branchendurchschnitt KMU',
|
||||
},
|
||||
],
|
||||
},
|
||||
impact: {
|
||||
tag: '02 / REGULATORY IMPACT ANALYSIS',
|
||||
title: 'Von der Rechtsquelle',
|
||||
titleHighlight: 'zur Maßnahme',
|
||||
subtitle: 'Deterministische Analyse: Jede Anforderung wird auf ihre konkrete Rechtsquelle zurückgeführt.',
|
||||
terminalLines: [
|
||||
{ type: 'input' as TerminalType, text: '> analyzing EU 2023/1230 Anhang I ...' },
|
||||
{ type: 'output' as TerminalType, text: ' [OK] 127 Anforderungen extrahiert' },
|
||||
{ type: 'input' as TerminalType, text: '> mapping to control library ...' },
|
||||
{ type: 'output' as TerminalType, text: ' [OK] 42 betroffene Controls identifiziert' },
|
||||
{ type: 'input' as TerminalType, text: '> evaluating current state ...' },
|
||||
{ type: 'output' as TerminalType, text: ' [OK] 39 konform | 3 Handlungsbedarf' },
|
||||
{ type: 'input' as TerminalType, text: '> generating action items ...' },
|
||||
{ type: 'signal' as TerminalType, text: ' [DONE] 3 Maßnahmen mit Rechtsreferenz erstellt' },
|
||||
],
|
||||
outputs: [
|
||||
{ label: 'Controls identifiziert', value: '42', status: 'neutral' as Status },
|
||||
{ label: 'Konform', value: '39', status: 'success' as Status },
|
||||
{ label: 'Handlungsbedarf', value: '3', status: 'warning' as Status },
|
||||
{ label: 'Status', value: 'Aktionsplan erstellt', status: 'success' as Status },
|
||||
],
|
||||
},
|
||||
savings: savingsDE,
|
||||
deterministic: {
|
||||
tag: '04 / VERTRAUENSWÜRDIG DURCH DESIGN',
|
||||
title: 'Keine Halluzinationen.',
|
||||
titleHighlight: 'Konstruktionsbedingt.',
|
||||
subtitle: 'Jede Compliance-Entscheidung ist auf eine konkrete Rechtsquelle rückführbar.',
|
||||
pillars: [
|
||||
{
|
||||
title: 'Deterministische Analyse',
|
||||
description: '294.000+ atomare Controls, abgeleitet aus 380+ Rechtsquellen. Regelbasiert, nicht generativ.',
|
||||
icon: 'Shield' as IconName,
|
||||
},
|
||||
{
|
||||
title: 'Nachvollziehbare Ergebnisse',
|
||||
description: 'Jedes Ergebnis verweist auf Artikel, Absatz und Erwägungsgrund. Kein Black-Box-Modell.',
|
||||
icon: 'FileCheck' as IconName,
|
||||
},
|
||||
{
|
||||
title: 'Auditierbare Entscheidungen',
|
||||
description: 'Vollständiger Decision Trail: Rechtsquelle → Obligation → Control → Maßnahme → Nachweis.',
|
||||
icon: 'ClipboardCheck' as IconName,
|
||||
},
|
||||
],
|
||||
comparison: {
|
||||
llm: {
|
||||
title: 'LLM-basierte Tools',
|
||||
items: [
|
||||
'Generative Antworten ohne Quellengarantie',
|
||||
'Halluzinationsrisiko bei juristischen Aussagen',
|
||||
'Nicht auditierbar — „die KI hat gesagt"',
|
||||
'Modellabhängig — Ergebnis ändert sich mit Version',
|
||||
],
|
||||
},
|
||||
breakpilot: {
|
||||
title: 'BreakPilot Engine',
|
||||
items: [
|
||||
'Deterministische Analyse mit Rechtsquellenreferenz',
|
||||
'Keine Halluzinationen — regelbasierte Auswertung',
|
||||
'Vollständig auditierbar mit Decision Trail',
|
||||
'Versioniert — reproduzierbare Ergebnisse',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
architecture: {
|
||||
tag: '04 / ARCHITEKTUR',
|
||||
title: 'Enterprise-Architektur.',
|
||||
titleHighlight: 'EU-souverän.',
|
||||
subtitle: 'Drei Schichten, keine US-Abhängigkeit, vollständig on-premise deploybar.',
|
||||
layers: [
|
||||
{ name: 'Application Layer', components: ['Admin Dashboard', 'Compliance Engine', 'Audit Manager', 'Report Generator'], tech: 'Next.js, FastAPI, Go' },
|
||||
{ name: 'Gateway Layer', components: ['RAG Service', 'Embedding Service', 'Control Pipeline', 'Auth & RBAC'], tech: 'FastAPI, Qdrant, Vault' },
|
||||
{ name: 'Infrastructure Layer', components: ['PostgreSQL', 'Qdrant Vector DB', 'MinIO Storage', 'Self-hosted LLM'], tech: 'PostGIS, Ollama, Docker' },
|
||||
],
|
||||
badges: ['Kein US-Anbieter', 'BSI-konformes RZ', 'EU-souveräne Inferenz', 'On-Premise möglich'],
|
||||
},
|
||||
safety: {
|
||||
tag: '06 / PRODUCT COMPLIANCE',
|
||||
title: 'CE. CRA. OTA.',
|
||||
titleHighlight: 'Vom Sensor bis zum Update.',
|
||||
subtitle: 'Maschinenverordnung, Cyber Resilience Act und sichere Software-Updates — in einer Plattform.',
|
||||
features: [
|
||||
{ title: 'CE & Risikobeurteilung', description: 'Systematische Gefahrenanalyse nach EN ISO 12100 und Konformitätsbewertung nach Maschinenverordnung (EU) 2023/1230.', icon: 'AlertTriangle' as IconName },
|
||||
{ title: 'Cyber Resilience Act', description: 'Schwachstellenmanagement, SBOM-Pflicht und Meldepflichten für Produkte mit digitalen Elementen.', icon: 'ShieldCheck' as IconName },
|
||||
{ title: 'OTA / SOTA Updates', description: 'Compliance-Prüfung für Over-the-Air und Software-over-the-Air Updates nach UN R156 und CRA Anhang I.', icon: 'RefreshCw' as IconName },
|
||||
{ title: 'Firmware & Embedded Security', description: 'IEC 62443 für industrielle Steuerungen, ETSI EN 303 645 für IoT-Geräte, EN ISO 13849 für sicherheitsrelevante Software.', icon: 'Cpu' as IconName },
|
||||
{ title: 'Technische Dokumentation', description: 'Automatisierte Betriebsanleitung, EU-Konformitätserklärung und Technische Unterlagen nach Anhang IV.', icon: 'FileText' as IconName },
|
||||
{ title: 'Supply-Chain-Compliance', description: 'Zulieferer-Anforderungen nach CRA Art. 13, Maschinenverordnung Art. 10 und ISO/SAE 21434 für Automotive.', icon: 'Link' as IconName },
|
||||
],
|
||||
},
|
||||
targets: {
|
||||
tag: '07 / ZIELGRUPPEN',
|
||||
title: 'Gebaut für den',
|
||||
titleHighlight: 'deutschen Mittelstand.',
|
||||
subtitle: 'VDMA, VDA und die Unternehmen, die Maschinen, Fahrzeugteile und vernetzte Produkte entwickeln und exportieren.',
|
||||
industries: [
|
||||
{ name: 'Maschinenbau & Anlagenbau', icon: 'Factory' as IndustryIcon, regulations: ['Maschinenverordnung 2023/1230', 'EN ISO 12100', 'EN ISO 13849', 'IEC 62443'], roi: 'EUR 30.000+ / Jahr Einsparung bei CE-Bewertungen' },
|
||||
{ name: 'Automotive Zulieferer', icon: 'Truck' as IndustryIcon, regulations: ['UN R155/R156', 'ISO/SAE 21434', 'Cyber Resilience Act', 'TISAX'], roi: 'EUR 40.000+ / Jahr Einsparung bei CSMS-Audits' },
|
||||
{ name: 'IoT, Embedded & Firmware', icon: 'Cpu' as IconName, regulations: ['Cyber Resilience Act', 'RED 2014/53/EU', 'ETSI EN 303 645', 'OTA/SOTA-Pflichten'], roi: 'EUR 25.000+ / Jahr Einsparung bei Produktzertifizierung' },
|
||||
{ name: 'Elektrotechnik & Automatisierung', icon: 'Zap' as IndustryIcon, regulations: ['Niederspannungsrichtlinie', 'EMV-Richtlinie', 'ATEX', 'NIS2'], roi: 'EUR 35.000+ / Jahr Einsparung bei Konformität' },
|
||||
],
|
||||
},
|
||||
continuous: {
|
||||
tag: '08 / CONTINUOUS COMPLIANCE',
|
||||
title: 'Echtzeit statt Stichtag.',
|
||||
titleHighlight: 'Compliance ist kein Projekt.',
|
||||
subtitle: 'Kontinuierliches Monitoring statt jährlicher Audits — jede Änderung wird sofort bewertet.',
|
||||
comparison: {
|
||||
annual: { title: 'Jährliche Audits', points: ['Compliance-Status an 1 Tag im Jahr bekannt', '364 Tage Blindflug', 'Veraltete Dokumentation', 'Reaktiv statt präventiv', 'EUR 30.000+ externe Auditkosten'] },
|
||||
continuous: { title: 'Continuous Compliance', points: ['Compliance-Status in Echtzeit', '365 Tage vollständige Transparenz', 'Automatisch aktualisierte Dokumentation', 'Präventive Warnungen vor Ablauf', 'Integriert in bestehende Workflows'] },
|
||||
},
|
||||
},
|
||||
security: {
|
||||
tag: '09 / CODE SECURITY',
|
||||
title: 'Security Engineering.',
|
||||
titleHighlight: 'Nicht nur Compliance-Dokumente.',
|
||||
subtitle: 'Kontinuierliche Code-Analyse mit automatischer Ticket-Erstellung in Jira, Linear oder GitLab.',
|
||||
tools: [
|
||||
{ name: 'SAST', description: 'Statische Code-Analyse' },
|
||||
{ name: 'DAST', description: 'Dynamische Sicherheitstests' },
|
||||
{ name: 'SBOM', description: 'Software Bill of Materials' },
|
||||
{ name: 'Container Scanning', description: 'Image-Schwachstellen' },
|
||||
{ name: 'Secret Detection', description: 'Credentials im Code' },
|
||||
{ name: 'Dependency Audit', description: 'Abhängigkeiten prüfen' },
|
||||
],
|
||||
integration: {
|
||||
title: 'Automatische Ticket-Erstellung',
|
||||
description: 'Jedes Finding wird als Ticket mit Priorität, Kontext und Fix-Vorschlag erstellt.',
|
||||
targets: ['Jira', 'Linear', 'GitLab Issues', 'GitHub Issues'],
|
||||
},
|
||||
},
|
||||
aiGovernance: {
|
||||
tag: '10 / AI GOVERNANCE',
|
||||
title: 'EU AI Act.',
|
||||
titleHighlight: 'Verordnung (EU) 2024/1689.',
|
||||
subtitle: 'Risikokategorisierung, FRIA und Transparenzpflichten — deterministisch umgesetzt.',
|
||||
riskLevels: [
|
||||
{ level: 'Inakzeptabel', description: 'Social Scoring, biometrische Echtzeit-Überwachung', color: 'red' as RiskColor },
|
||||
{ level: 'Hochrisiko', description: 'Kritische Infrastruktur, Medizinprodukte, Personalentscheidungen', color: 'amber' as RiskColor },
|
||||
{ level: 'Begrenzt', description: 'Chatbots, Deepfakes — Transparenzpflichten', color: 'blue' as RiskColor },
|
||||
{ level: 'Minimal', description: 'Spamfilter, KI in Videospielen — keine Auflagen', color: 'green' as RiskColor },
|
||||
],
|
||||
features: [
|
||||
'Automatische Risikokategorisierung nach Art. 6',
|
||||
'FRIA (Fundamental Rights Impact Assessment)',
|
||||
'Technische Dokumentation nach Anhang IV',
|
||||
'Konformitätsbewertung nach Anhang VI/VII',
|
||||
'ISO 42001 Alignment',
|
||||
],
|
||||
},
|
||||
legal: {
|
||||
tag: '11 / LEGAL COMPLIANCE',
|
||||
title: 'DSGVO. NIS2. TDDDG.',
|
||||
titleHighlight: 'Automatisiert, nicht manuell.',
|
||||
subtitle: 'Drei Regulierungen, eine Plattform — deterministische Prüfung und Dokumentation.',
|
||||
regulations: [
|
||||
{ name: 'DSGVO', fullName: 'Datenschutz-Grundverordnung', features: ['Verarbeitungsverzeichnis (Art. 30)', 'Datenschutz-Folgenabschätzung (Art. 35)', 'Betroffenenrechte (Art. 15-22)', 'Technische Maßnahmen (Art. 32)', 'Auftragsverarbeitung (Art. 28)'] },
|
||||
{ name: 'NIS2', fullName: 'Netzwerk- und Informationssicherheit', features: ['Risikomanagement (Art. 21)', 'Meldepflichten (Art. 23)', 'Supply-Chain-Sicherheit', 'Incident Response', 'Business Continuity'] },
|
||||
{ name: 'TDDDG', fullName: 'Telekommunikation-Digitale-Dienste-Datenschutz', features: ['Cookie-Einwilligung (§ 25)', 'Informationspflichten', 'Technische Schutzmaßnahmen', 'Endgerätezugriff', 'Consent Management'] },
|
||||
],
|
||||
},
|
||||
sovereign: {
|
||||
tag: '12 / SOVEREIGN AI',
|
||||
title: 'Ihre Daten verlassen',
|
||||
titleHighlight: 'nie Ihr Netzwerk.',
|
||||
subtitle: 'On-Premise LLM-Inferenz auf eigener Hardware. Keine US-Cloud, kein Drittlandzugriff.',
|
||||
features: [
|
||||
{ title: 'Self-hosted LLM', description: 'Lokale KI-Modelle auf Apple Silicon oder GPU-Servern. Keine API-Aufrufe an OpenAI, Google oder Anthropic.' },
|
||||
{ title: 'BSI-konforme Infrastruktur', description: 'Deployment in BSI-zertifizierten Rechenzentren oder vollständig on-premise in Ihrem Netzwerk.' },
|
||||
{ title: 'Kein Patriot Act', description: 'Ausschließlich EU-Software-Stack. Kein FISA 702, kein CLOUD Act, kein Schrems-III-Risiko.' },
|
||||
],
|
||||
appliance: { title: 'BreakPilot Appliance', description: 'Vorkonfigurierte Hardware für sofortigen On-Premise-Betrieb.', specs: ['Apple M4 Pro / Max', '64-128 GB RAM', 'Vorkonfiguriert', 'Plug & Play'] },
|
||||
},
|
||||
pricing: {
|
||||
tag: '13 / PREISE',
|
||||
title: 'Transparente Preise.',
|
||||
titleHighlight: 'ROI ab Tag 1.',
|
||||
subtitle: 'Keine versteckten Kosten. Keine Feature-Gates. Jeder Plan enthält die volle Plattform.',
|
||||
tiers: [
|
||||
{ name: 'Starter', badge: 'Einstieg', price: '890', period: '/ Monat', description: 'Für kleine Teams und den Einstieg in deterministische Compliance.', features: ['Bis 10 Mitarbeiter', 'DSGVO + TDDDG Engine', 'Compliance-Dokumentation', 'Betroffenenrechte-Management', 'E-Mail Support'], highlighted: false },
|
||||
{ name: 'Professional', badge: 'Beliebt', price: '4.900', period: '/ Monat', description: 'Für wachsende Unternehmen mit komplexen regulatorischen Anforderungen.', features: ['Bis 250 Mitarbeiter', 'Alle Regulatory Engines', 'Code Security (SAST/DAST)', 'CE-Konformitätsbewertung', 'Jira / Linear Integration', 'Continuous Monitoring', 'Dedizierter Ansprechpartner'], highlighted: true },
|
||||
{ name: 'Enterprise', badge: 'Individuell', price: '150.000+', period: '/ Jahr', description: 'Für Konzerne und Unternehmen mit höchsten Sicherheitsanforderungen.', features: ['Unbegrenzte Mitarbeiter', 'On-Premise Deployment', 'Custom Regulatory Engines', 'SSO / LDAP Integration', 'SLA mit 99,9% Verfügbarkeit', 'Dedicated Customer Success', 'Individuelle Schulungen'], highlighted: false },
|
||||
],
|
||||
appliance: { name: 'Appliance', badge: 'On-Premise Hardware', priceRange: '7.900 - 14.900', priceLabel: 'EUR einmalig + Subscription', description: 'Vorkonfigurierte Hardware für vollständigen On-Premise-Betrieb ohne Cloud-Abhängigkeit.', features: ['Apple M4 Pro oder M4 Max', '64-128 GB Unified Memory', 'Alle LLM-Modelle vorinstalliert', 'Zero-Cloud-Architektur', 'Inkl. Starter oder Professional Plan'] },
|
||||
cta: 'Demo anfordern',
|
||||
},
|
||||
footer: {
|
||||
tagline: 'Deterministic Regulatory Engineering',
|
||||
copyright: 'BreakPilot GmbH',
|
||||
description: 'Deterministische regulatorische Analyse für europäische Unternehmen. Keine Halluzinationen. Volle Nachvollziehbarkeit.',
|
||||
links: { product: ['Plattform', 'Architektur', 'Preise', 'Security'], legal: ['Impressum', 'Datenschutz', 'AGB'] },
|
||||
madeIn: 'Made in Germany. EU-souverän.',
|
||||
},
|
||||
chat: {
|
||||
title: 'Compliance Agent',
|
||||
online: 'online',
|
||||
responding: 'antwortet...',
|
||||
ask: 'Fragen Sie den Compliance Agent:',
|
||||
placeholder: 'Frage stellen...',
|
||||
stop: 'Antwort stoppen',
|
||||
error: 'Verbindung fehlgeschlagen. Bitte versuchen Sie es erneut.',
|
||||
suggestions: [
|
||||
'Was unterscheidet BreakPilot von anderen Compliance-Tools?',
|
||||
'Wie funktioniert die deterministische Analyse?',
|
||||
'Kann ich BreakPilot on-premise betreiben?',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const en = {
|
||||
nav: { cta: 'Request Demo' },
|
||||
hero: {
|
||||
badge: 'DETERMINISTIC REGULATORY ENGINEERING',
|
||||
title: 'Regulatory Analysis.',
|
||||
titleHighlight: 'Deterministic. Traceable.',
|
||||
subtitle: 'No hallucinations. No compliance gaps. No dependency on US cloud providers.',
|
||||
cta: 'Request Demo',
|
||||
ctaSecondary: 'View Architecture',
|
||||
status: '4 Engines active',
|
||||
},
|
||||
problem: {
|
||||
tag: '01 / THE PROBLEM',
|
||||
title: 'Regulatory complexity',
|
||||
titleHighlight: 'is growing exponentially',
|
||||
subtitle: 'Manual compliance processes cannot scale with the pace of regulatory change.',
|
||||
cards: [
|
||||
{ metric: '37,000+', label: 'regulatory changes per year', description: 'EU, federal and state levels create a regulatory density that cannot be managed manually.', source: 'VDMA / Bitkom 2025' },
|
||||
{ metric: '83%', label: 'of SMEs see compliance as innovation blocker', description: 'Data protection risks, CE requirements and NIS2 obligations tie up resources needed for product development.', source: 'DIHK Digitization Report' },
|
||||
{ metric: '50,000+', label: 'EUR annual compliance costs', description: 'External audits, penetration tests, CE assessments and data protection consulting add up.', source: 'SME industry average' },
|
||||
],
|
||||
},
|
||||
impact: {
|
||||
tag: '02 / REGULATORY IMPACT ANALYSIS',
|
||||
title: 'From legal source',
|
||||
titleHighlight: 'to action item',
|
||||
subtitle: 'Deterministic analysis: Every requirement is traced back to its concrete legal source.',
|
||||
terminalLines: [
|
||||
{ type: 'input' as TerminalType, text: '> analyzing EU 2023/1230 Annex I ...' },
|
||||
{ type: 'output' as TerminalType, text: ' [OK] 127 requirements extracted' },
|
||||
{ type: 'input' as TerminalType, text: '> mapping to control library ...' },
|
||||
{ type: 'output' as TerminalType, text: ' [OK] 42 affected controls identified' },
|
||||
{ type: 'input' as TerminalType, text: '> evaluating current state ...' },
|
||||
{ type: 'output' as TerminalType, text: ' [OK] 39 compliant | 3 action required' },
|
||||
{ type: 'input' as TerminalType, text: '> generating action items ...' },
|
||||
{ type: 'signal' as TerminalType, text: ' [DONE] 3 actions with legal reference created' },
|
||||
],
|
||||
outputs: [
|
||||
{ label: 'Controls identified', value: '42', status: 'neutral' as Status },
|
||||
{ label: 'Compliant', value: '39', status: 'success' as Status },
|
||||
{ label: 'Action required', value: '3', status: 'warning' as Status },
|
||||
{ label: 'Status', value: 'Action plan created', status: 'success' as Status },
|
||||
],
|
||||
},
|
||||
savings: savingsEN,
|
||||
deterministic: {
|
||||
tag: '04 / TRUSTWORTHY BY DESIGN',
|
||||
title: 'No hallucinations.',
|
||||
titleHighlight: 'By design.',
|
||||
subtitle: 'Every compliance decision is traceable to a concrete legal source.',
|
||||
pillars: [
|
||||
{ title: 'Deterministic Analysis', description: '294,000+ atomic controls derived from 380+ legal sources. Rule-based, not generative.', icon: 'Shield' as IconName },
|
||||
{ title: 'Traceable Results', description: 'Every result references article, paragraph and recital. No black-box model.', icon: 'FileCheck' as IconName },
|
||||
{ title: 'Auditable Decisions', description: 'Complete decision trail: Legal source → Obligation → Control → Action → Evidence.', icon: 'ClipboardCheck' as IconName },
|
||||
],
|
||||
comparison: {
|
||||
llm: { title: 'LLM-based Tools', items: ['Generative answers without source guarantee', 'Hallucination risk for legal statements', 'Not auditable — "the AI said so"', 'Model-dependent — results change with version'] },
|
||||
breakpilot: { title: 'BreakPilot Engine', items: ['Deterministic analysis with legal source reference', 'No hallucinations — rule-based evaluation', 'Fully auditable with decision trail', 'Versioned — reproducible results'] },
|
||||
},
|
||||
},
|
||||
architecture: {
|
||||
tag: '04 / ARCHITECTURE',
|
||||
title: 'Enterprise Architecture.',
|
||||
titleHighlight: 'EU-sovereign.',
|
||||
subtitle: 'Three layers, no US dependency, fully deployable on-premise.',
|
||||
layers: [
|
||||
{ name: 'Application Layer', components: ['Admin Dashboard', 'Compliance Engine', 'Audit Manager', 'Report Generator'], tech: 'Next.js, FastAPI, Go' },
|
||||
{ name: 'Gateway Layer', components: ['RAG Service', 'Embedding Service', 'Control Pipeline', 'Auth & RBAC'], tech: 'FastAPI, Qdrant, Vault' },
|
||||
{ name: 'Infrastructure Layer', components: ['PostgreSQL', 'Qdrant Vector DB', 'MinIO Storage', 'Self-hosted LLM'], tech: 'PostGIS, Ollama, Docker' },
|
||||
],
|
||||
badges: ['No US provider', 'BSI-compliant DC', 'EU-sovereign inference', 'On-premise possible'],
|
||||
},
|
||||
safety: {
|
||||
tag: '06 / PRODUCT COMPLIANCE',
|
||||
title: 'CE. CRA. OTA.',
|
||||
titleHighlight: 'From sensor to update.',
|
||||
subtitle: 'Machinery Regulation, Cyber Resilience Act and secure software updates — in one platform.',
|
||||
features: [
|
||||
{ title: 'CE & Risk Assessment', description: 'Systematic hazard analysis per EN ISO 12100 and conformity assessment per Machinery Regulation (EU) 2023/1230.', icon: 'AlertTriangle' as IconName },
|
||||
{ title: 'Cyber Resilience Act', description: 'Vulnerability management, SBOM obligations and reporting duties for products with digital elements.', icon: 'ShieldCheck' as IconName },
|
||||
{ title: 'OTA / SOTA Updates', description: 'Compliance checks for Over-the-Air and Software-over-the-Air updates per UN R156 and CRA Annex I.', icon: 'RefreshCw' as IconName },
|
||||
{ title: 'Firmware & Embedded Security', description: 'IEC 62443 for industrial controls, ETSI EN 303 645 for IoT devices, EN ISO 13849 for safety-related software.', icon: 'Cpu' as IconName },
|
||||
{ title: 'Technical Documentation', description: 'Automated operating instructions, EU declaration of conformity and technical files per Annex IV.', icon: 'FileText' as IconName },
|
||||
{ title: 'Supply Chain Compliance', description: 'Supplier requirements per CRA Art. 13, Machinery Regulation Art. 10 and ISO/SAE 21434 for automotive.', icon: 'Link' as IconName },
|
||||
],
|
||||
},
|
||||
targets: {
|
||||
tag: '07 / TARGET INDUSTRIES',
|
||||
title: 'Built for the',
|
||||
titleHighlight: 'German Mittelstand.',
|
||||
subtitle: 'VDMA, VDA and the companies that develop and export machinery, vehicle components and connected products.',
|
||||
industries: [
|
||||
{ name: 'Machinery & Plant Engineering', icon: 'Factory' as IndustryIcon, regulations: ['Machinery Regulation 2023/1230', 'EN ISO 12100', 'EN ISO 13849', 'IEC 62443'], roi: 'EUR 30,000+ / year savings on CE assessments' },
|
||||
{ name: 'Automotive Suppliers', icon: 'Truck' as IndustryIcon, regulations: ['UN R155/R156', 'ISO/SAE 21434', 'Cyber Resilience Act', 'TISAX'], roi: 'EUR 40,000+ / year savings on CSMS audits' },
|
||||
{ name: 'IoT, Embedded & Firmware', icon: 'Cpu' as IconName, regulations: ['Cyber Resilience Act', 'RED 2014/53/EU', 'ETSI EN 303 645', 'OTA/SOTA duties'], roi: 'EUR 25,000+ / year savings on product certification' },
|
||||
{ name: 'Electrical & Automation', icon: 'Zap' as IndustryIcon, regulations: ['Low Voltage Directive', 'EMC Directive', 'ATEX', 'NIS2'], roi: 'EUR 35,000+ / year savings on conformity' },
|
||||
],
|
||||
},
|
||||
continuous: {
|
||||
tag: '08 / CONTINUOUS COMPLIANCE',
|
||||
title: 'Real-time, not deadlines.',
|
||||
titleHighlight: 'Compliance is not a project.',
|
||||
subtitle: 'Continuous monitoring instead of annual audits — every change is evaluated immediately.',
|
||||
comparison: {
|
||||
annual: { title: 'Annual Audits', points: ['Compliance status known 1 day per year', '364 days flying blind', 'Outdated documentation', 'Reactive instead of preventive', 'EUR 30,000+ external audit costs'] },
|
||||
continuous: { title: 'Continuous Compliance', points: ['Compliance status in real-time', '365 days full transparency', 'Automatically updated documentation', 'Preventive warnings before expiry', 'Integrated into existing workflows'] },
|
||||
},
|
||||
},
|
||||
security: {
|
||||
tag: '09 / CODE SECURITY',
|
||||
title: 'Security Engineering.',
|
||||
titleHighlight: 'Not just compliance documents.',
|
||||
subtitle: 'Continuous code analysis with automatic ticket creation in Jira, Linear or GitLab.',
|
||||
tools: [
|
||||
{ name: 'SAST', description: 'Static code analysis' },
|
||||
{ name: 'DAST', description: 'Dynamic security testing' },
|
||||
{ name: 'SBOM', description: 'Software Bill of Materials' },
|
||||
{ name: 'Container Scanning', description: 'Image vulnerabilities' },
|
||||
{ name: 'Secret Detection', description: 'Credentials in code' },
|
||||
{ name: 'Dependency Audit', description: 'Check dependencies' },
|
||||
],
|
||||
integration: { title: 'Automatic ticket creation', description: 'Every finding is created as a ticket with priority, context and fix suggestion.', targets: ['Jira', 'Linear', 'GitLab Issues', 'GitHub Issues'] },
|
||||
},
|
||||
aiGovernance: {
|
||||
tag: '10 / AI GOVERNANCE',
|
||||
title: 'EU AI Act.',
|
||||
titleHighlight: 'Regulation (EU) 2024/1689.',
|
||||
subtitle: 'Risk categorization, FRIA and transparency obligations — deterministically implemented.',
|
||||
riskLevels: [
|
||||
{ level: 'Unacceptable', description: 'Social scoring, real-time biometric surveillance', color: 'red' as RiskColor },
|
||||
{ level: 'High Risk', description: 'Critical infrastructure, medical devices, HR decisions', color: 'amber' as RiskColor },
|
||||
{ level: 'Limited', description: 'Chatbots, deepfakes — transparency obligations', color: 'blue' as RiskColor },
|
||||
{ level: 'Minimal', description: 'Spam filters, AI in video games — no obligations', color: 'green' as RiskColor },
|
||||
],
|
||||
features: ['Automatic risk categorization per Art. 6', 'FRIA (Fundamental Rights Impact Assessment)', 'Technical documentation per Annex IV', 'Conformity assessment per Annex VI/VII', 'ISO 42001 Alignment'],
|
||||
},
|
||||
legal: {
|
||||
tag: '11 / LEGAL COMPLIANCE',
|
||||
title: 'GDPR. NIS2. TDDDG.',
|
||||
titleHighlight: 'Automated, not manual.',
|
||||
subtitle: 'Three regulations, one platform — deterministic verification and documentation.',
|
||||
regulations: [
|
||||
{ name: 'GDPR', fullName: 'General Data Protection Regulation', features: ['Records of processing (Art. 30)', 'Data Protection Impact Assessment (Art. 35)', 'Data subject rights (Art. 15-22)', 'Technical measures (Art. 32)', 'Data processing agreements (Art. 28)'] },
|
||||
{ name: 'NIS2', fullName: 'Network and Information Security', features: ['Risk management (Art. 21)', 'Reporting obligations (Art. 23)', 'Supply chain security', 'Incident Response', 'Business Continuity'] },
|
||||
{ name: 'TDDDG', fullName: 'Telecommunications Digital Services Data Protection', features: ['Cookie consent (§ 25)', 'Information obligations', 'Technical safeguards', 'Terminal access', 'Consent Management'] },
|
||||
],
|
||||
},
|
||||
sovereign: {
|
||||
tag: '12 / SOVEREIGN AI',
|
||||
title: 'Your data never leaves',
|
||||
titleHighlight: 'your network.',
|
||||
subtitle: 'On-premise LLM inference on your own hardware. No US cloud, no third-country access.',
|
||||
features: [
|
||||
{ title: 'Self-hosted LLM', description: 'Local AI models on Apple Silicon or GPU servers. No API calls to OpenAI, Google or Anthropic.' },
|
||||
{ title: 'BSI-compliant Infrastructure', description: 'Deployment in BSI-certified data centers or fully on-premise in your network.' },
|
||||
{ title: 'No Patriot Act', description: 'Exclusively EU software stack. No FISA 702, no CLOUD Act, no Schrems III risk.' },
|
||||
],
|
||||
appliance: { title: 'BreakPilot Appliance', description: 'Pre-configured hardware for immediate on-premise operation.', specs: ['Apple M4 Pro / Max', '64-128 GB RAM', 'Pre-configured', 'Plug & Play'] },
|
||||
},
|
||||
pricing: {
|
||||
tag: '13 / PRICING',
|
||||
title: 'Transparent pricing.',
|
||||
titleHighlight: 'ROI from day 1.',
|
||||
subtitle: 'No hidden costs. No feature gates. Every plan includes the full platform.',
|
||||
tiers: [
|
||||
{ name: 'Starter', badge: 'Entry', price: '890', period: '/ month', description: 'For small teams getting started with deterministic compliance.', features: ['Up to 10 employees', 'GDPR + TDDDG Engine', 'Compliance documentation', 'Data subject rights management', 'Email support'], highlighted: false },
|
||||
{ name: 'Professional', badge: 'Popular', price: '4,900', period: '/ month', description: 'For growing companies with complex regulatory requirements.', features: ['Up to 250 employees', 'All Regulatory Engines', 'Code Security (SAST/DAST)', 'CE conformity assessment', 'Jira / Linear integration', 'Continuous Monitoring', 'Dedicated contact person'], highlighted: true },
|
||||
{ name: 'Enterprise', badge: 'Custom', price: '150,000+', period: '/ year', description: 'For corporations with the highest security requirements.', features: ['Unlimited employees', 'On-Premise Deployment', 'Custom Regulatory Engines', 'SSO / LDAP Integration', '99.9% uptime SLA', 'Dedicated Customer Success', 'Individual training'], highlighted: false },
|
||||
],
|
||||
appliance: { name: 'Appliance', badge: 'On-Premise Hardware', priceRange: '7,900 - 14,900', priceLabel: 'EUR one-time + subscription', description: 'Pre-configured hardware for full on-premise operation without cloud dependency.', features: ['Apple M4 Pro or M4 Max', '64-128 GB Unified Memory', 'All LLM models pre-installed', 'Zero-cloud architecture', 'Incl. Starter or Professional plan'] },
|
||||
cta: 'Request Demo',
|
||||
},
|
||||
footer: {
|
||||
tagline: 'Deterministic Regulatory Engineering',
|
||||
copyright: 'BreakPilot GmbH',
|
||||
description: 'Deterministic regulatory analysis for European enterprises. No hallucinations. Full traceability.',
|
||||
links: { product: ['Platform', 'Architecture', 'Pricing', 'Security'], legal: ['Legal Notice', 'Privacy Policy', 'Terms'] },
|
||||
madeIn: 'Made in Germany. EU-sovereign.',
|
||||
},
|
||||
chat: {
|
||||
title: 'Compliance Agent',
|
||||
online: 'online',
|
||||
responding: 'responding...',
|
||||
ask: 'Ask the Compliance Agent:',
|
||||
placeholder: 'Ask a question...',
|
||||
stop: 'Stop response',
|
||||
error: 'Connection failed. Please try again.',
|
||||
suggestions: [
|
||||
'What makes BreakPilot different from other compliance tools?',
|
||||
'How does the deterministic analysis work?',
|
||||
'Can I run BreakPilot on-premise?',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const translations: Record<Lang, typeof de> = { de, en }
|
||||
|
||||
export function t(lang: Lang): typeof de {
|
||||
return translations[lang]
|
||||
}
|
||||
|
||||
// Default export for components that don't use the language context yet
|
||||
export const content = de
|
||||
@@ -0,0 +1,51 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, useState, useCallback, useEffect, ReactNode } from 'react'
|
||||
|
||||
type Language = 'de' | 'en'
|
||||
type Theme = 'dark' | 'light'
|
||||
|
||||
interface AppContextType {
|
||||
lang: Language
|
||||
theme: Theme
|
||||
toggleLang: () => void
|
||||
toggleTheme: () => void
|
||||
}
|
||||
|
||||
const AppContext = createContext<AppContextType>({
|
||||
lang: 'de',
|
||||
theme: 'dark',
|
||||
toggleLang: () => {},
|
||||
toggleTheme: () => {},
|
||||
})
|
||||
|
||||
export function AppProvider({ children }: { children: ReactNode }) {
|
||||
const [lang, setLang] = useState<Language>('de')
|
||||
const [theme, setTheme] = useState<Theme>('dark')
|
||||
|
||||
const toggleLang = useCallback(() => {
|
||||
setLang(prev => prev === 'de' ? 'en' : 'de')
|
||||
}, [])
|
||||
|
||||
const toggleTheme = useCallback(() => {
|
||||
setTheme(prev => {
|
||||
const next = prev === 'dark' ? 'light' : 'dark'
|
||||
document.documentElement.classList.toggle('theme-light', next === 'light')
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.classList.toggle('theme-light', theme === 'light')
|
||||
}, [theme])
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={{ lang, theme, toggleLang, toggleTheme }}>
|
||||
{children}
|
||||
</AppContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useApp() {
|
||||
return useContext(AppContext)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user