106 Commits

Author SHA1 Message Date
Benjamin Admin
dd64e33e88 docs: SDK-Flow + Wiki — EU Registration Step + 4 Domain-Artikel
1. SDK-Flow: Neuer Step "EU AI Database Registrierung" (seq 350, CP-REG)
2. Wiki: 4 Domain-Compliance-Artikel (Recruiting, Bildung, Gesundheit, Finance)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 07:13:17 +02:00
Benjamin Admin
2f8269d115 test: Domain-Context Tests — 22 Tests (HR, Edu, HC, CritInfra, Marketing, Mfg, AGG)
BLOCK-Tests: AutomatedRejection, MinorsWithoutTeacher, MDRUnvalidated,
             SafetyCriticalNoRedundancy, DeepfakeUnlabeled, ManufacturingUnvalidated,
             ReviewManipulation
Positive Tests: HumanReview OK, TeacherReview OK, DeepfakeLabeled OK
Risk Tests: AGG visible, Triage high risk
Loader Tests: AGG + AI Act obligations count, applicability
Resolver Tests: HRContext, NilContext, HealthcareContext
Meta: TotalObligationsCount, DomainConstants

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 06:59:11 +02:00
Benjamin Admin
532febe35c fix: Build-Fehler — LegalContext Namenskollision + Registration Handler
- LegalContext → LegalDomainContext (Kollision mit legal_rag.go LegalContext)
- ExplainResponse.LegalContext bleibt unveraendert (RAG-Typ)
- Registration Handler: Intake ist struct, kein []byte
- Unbenutzten json Import entfernt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 23:57:00 +02:00
Benjamin Admin
0a0863f31c feat: Letzte 3 Domains abgedeckt — Finance/Banking + General (100%)
- Finance/Banking: Kredit-Scoring, AML/KYC, automatisierte Entscheidungen, Kunden-Profiling
- General: Universelle KI-Governance (Personenbezug, Automatisierung, sensible Daten)

Domains mit Fragen: 27 Gruppen fuer alle 54 Domains (100% Coverage)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 23:12:00 +02:00
Benjamin Admin
d892ad161f feat: Domain-Fragen fuer 10 weitere Domains (24 von 39 total, 62%)
10 neue Context-Structs + Field-Resolver + 22 YAML-Regeln + Frontend:
- Agriculture: Pestizid-KI, Tierwohl, Umweltdaten
- Social Services: Schutzbeduerftiger, Leistungszuteilung, Fallmanagement
- Hospitality: Gaeste-Profiling, dynamische Preise, Bewertungsmanipulation=BLOCK
- Insurance: Praemien, Schadensautomation, Betrugserkennung
- Investment: Algo-Trading, Robo Advisor (MiFID II)
- Defense: Dual-Use, Exportkontrolle, Verschlusssachen
- Supply Chain: Lieferantenueberwachung, Menschenrechte (LkSG)
- Facility: Zutrittskontrolle, Belegung, Energie
- Sports: Athleten-Tracking, Fan-Profiling

Domains mit Fragen: 24 von 39 (62%)
YAML-Regeln total: ~66
Neue BLOCKs: Bewertungsmanipulation (UWG/DSA)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 23:04:35 +02:00
Benjamin Admin
17153ccbe8 feat: Domain-Fragen fuer 10 weitere Domains (14 total)
10 neue Context-Structs + Field-Resolver + ~30 YAML-Regeln + Frontend:
- Legal/Justice: Rechtsberatung, Urteilsprognose, Mandantengeheimnis
- Public Sector: Verwaltungsentscheidungen, Leistungsverteilung, FRIA
- Critical Infra: Netzsteuerung, Sicherheitskritisch, Redundanz
- Automotive: Autonomes Fahren, ADAS, ISO 26262
- Retail/E-Commerce: Preise, Scoring, Dark Patterns
- IT/Cybersecurity: Surveillance, Threat Detection, Log-Retention
- Logistics: Fahrer-Tracking, Workload-Scoring
- Construction: Mieterauswahl, Arbeitsschutz
- Marketing/Media: Deepfakes=BLOCK, Minderjaehrige, Targeting
- Manufacturing: Maschinensicherheit=BLOCK, CE-Kennzeichnung

Domains mit Fragen: 14 von 39 (36%)
YAML-Regeln total: ~44 (14 vorher + 30 neu)
BLOCK-Regeln: Deepfakes ungekennzeichnet, Maschinensicherheit unvalidiert,
              Kritische Infra ohne Redundanz

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 22:50:26 +02:00
Benjamin Admin
352d7112c9 feat: Domain YAML-Regeln (14 Regeln) + Field-Resolver fuer HR/Edu/HC
1. 14 neue YAML-Regeln in Kategorie K (Domain-Hochrisiko):
   - HR: 5 Regeln (Screening, Absagen=BLOCK, AGG, Bias, Performance)
   - Education: 3 Regeln (Noten, Minderjaehrige=BLOCK, Zugangssteuerung)
   - Healthcare: 4 Regeln (Diagnose, Triage, MDR=BLOCK, Gesundheitsdaten)
2. Field-Resolver: getHRContextValue(), getEducationContextValue(), getHealthcareContextValue()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 22:35:48 +02:00
Benjamin Admin
0957254547 feat: Domain-spezifische UCCA-Fragen (HR, Education, Healthcare) + AGG-Modul
1. Domain-Context Structs: HRContext (7 Felder), EducationContext (6), HealthcareContext (6)
   — nach FinancialContext-Pattern, optionale Structs in UseCaseIntake
2. AGG Obligations Modul: 8 Obligations (§1-§22 AGG)
   — Bias-Audit, Beweislastumkehr, Proxy-Merkmale, Beschwerdemechanismus
   — Applicability: domain=hr/recruiting, country=DE
3. Frontend: Conditional Domain-Fragen in Step 4 des UCCA-Wizard
   — HR: 6 Fragen (Screening, Absagen, AGG, Bias-Audit, Human Review)
   — Education: 5 Fragen (Noten, Pruefungen, Minderjaehrige, Lehrkraft-Review)
   — Healthcare: 6 Fragen (Diagnose, Triage, MDR, klinische Validierung)
   — Farbcodierung: rot=Risiko, gruen=Schutzmassnahme
   — Domain-Contexts im Submit-Payload gemappt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 22:06:15 +02:00
Benjamin Admin
f17608a956 feat: EU AI Database Registration (Art. 49) — Backend + Frontend
Backend (Go):
- DB Migration 023: ai_system_registrations Tabelle
- RegistrationStore: CRUD + Status-Management + Export-JSON
- RegistrationHandlers: 7 Endpoints (Create, List, Get, Update, Status, Prefill, Export)
- Routes in main.go: /sdk/v1/ai-registration/*

Frontend (Next.js):
- 6-Step Wizard: Anbieter → System → Klassifikation → Konformitaet → Trainingsdaten → Pruefung
- System-Karten mit Status-Badges (Entwurf/Bereit/Eingereicht/Registriert)
- JSON-Export fuer EU-Datenbank-Submission
- Status-Workflow: draft → ready → submitted → registered
- API Proxy Routes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:13:39 +02:00
Benjamin Admin
ce3df9f080 feat: AI Act Obligations erweitert (60→81) + Decision Tree Q8 fix
1. 21 neue AI Act Obligations:
   - Art. 9 Risk Management (5 granulare Regeln)
   - Art. 10 Data Governance (3: Bias, Qualitaet, Versionierung)
   - Art. 12 Logging (3: I/O-Logging, Manipulationsschutz, Aufbewahrung)
   - Art. 14 Human Oversight (3: Override, Schulung, Automation Bias)
   - Art. 15 Accuracy/Cybersecurity (3: Genauigkeit, Robustheit, Security)
   - Art. 51/52/54/56 GPAI Governance (4: Klassifizierung, Kennzeichnung, EU-Rep, CoP)
2. Decision Tree Q8 praezisiert:
   "Stellst du ein KI-Modell fuer Dritte bereit?" statt generische GPAI-Frage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 16:41:29 +02:00
Benjamin Admin
2da39e035d docs: SDK-Flow + Wiki — BetrVG-Modul dokumentiert
1. SDK-Flow: Use-Case-Assessment Beschreibung aktualisiert
   - BetrVG-Toggles in Step 4 dokumentiert
   - Konflikt-Score und BAG-Urteile erwaehnt
2. Wiki: BetrVG-Artikel als SQL-Migration
   - Leitentscheidungen (M365, SAP, SaaS, Belastungsstatistik)
   - Konflikt-Score Erklaerung
   - Wird nach Compliance-Refactoring auf Production eingespielt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:04:54 +02:00
Benjamin Admin
1989c410a9 test: BetrVG-Modul Tests — Konflikt-Score, Escalation, Obligations, Applicability
10 Tests: Score-Berechnung (no data, monitoring, HR, consulted),
Escalation (E2/E3 Trigger), V2-Obligations-Loading, Applicability (DE/US/small).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 11:11:33 +02:00
Benjamin Admin
c55a6ab995 feat: BetrVG-Compliance-Modul — Obligations, Konflikt-Score, Frontend
1. BetrVG Obligations (JSON V2): 12 Pflichten basierend auf §87, §90, §94, §95, §99, §111
   - BAG-Rechtsprechung referenziert (M365, SAP, Standardsoftware)
   - Applicability: DE + >=5 Mitarbeiter
2. Betriebsrats-Konflikt-Score (0-100): Gewichtete Formel aus 8 Faktoren
   - Ueberwachungseignung, HR-Bezug, Individualisierbarkeit, Automation
   - Escalation-Trigger: Score>=50 ohne BR → E2, Score>=75 → E3
3. Frontend: 3 neue Intake-Felder (Monitoring, HR, BR-Konsultation)
   - BR-Konflikt-Badge in Use-Case-Liste + Detail-Seite
   - Farbcodierung: gruen/gelb/orange/rot

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 10:49:56 +02:00
Benjamin Admin
bc75b4455d feat: AI Act Decision Tree — Zwei-Achsen-Klassifikation (GPAI + High-Risk)
Interaktiver 12-Fragen-Entscheidungsbaum für die AI Act Klassifikation
auf zwei Achsen: High-Risk (Anhang III, Q1-Q7) und GPAI (Art. 51-56, Q8-Q12).
Deterministische Auswertung ohne LLM.

Backend (Go):
- Neue Structs: GPAIClassification, DecisionTreeAnswer, DecisionTreeResult
- Decision Tree Engine mit BuildDecisionTreeDefinition() und EvaluateDecisionTree()
- Store-Methoden für CRUD der Ergebnisse
- API-Endpoints: GET/POST /decision-tree, GET/DELETE /decision-tree/results
- 12 Unit Tests (alle bestanden)

Frontend (Next.js):
- DecisionTreeWizard: Wizard-UI mit Ja/Nein-Fragen, Dual-Progress-Bar, Ergebnis-Ansicht
- AI Act Page refactored: Tabs (Übersicht | Entscheidungsbaum | Ergebnisse)
- Proxy-Route für decision-tree Endpoints

Migration 083: ai_act_decision_tree_results Tabelle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 10:14:09 +02:00
Benjamin Admin
712fa8cb74 feat: Pass 0b quality — negative actions, container detection, session object classes
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 33s
CI/CD / test-python-backend-compliance (push) Successful in 30s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 16s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Successful in 2s
4 error class fixes from AUTH-1052 quality review:
1. Prohibitive action types (prevent/exclude/forbid) for "dürfen keine", "verboten" etc.
2. Container object detection (Sitzungsverwaltung, Token-Schutz → _requires_decomposition)
3. Session-specific object classes (session, cookie, jwt, federated_assertion)
4. Session lifecycle actions (invalidate, issue, rotate, enforce) with templates + severity caps

76 new tests (303 total), all passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 17:24:19 +01:00
Benjamin Admin
447ec08509 Add migration 082: widen source_article to TEXT, fix pass0b query filters
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 40s
CI/CD / test-python-backend-compliance (push) Successful in 31s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 18s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Successful in 5s
- source_article/source_regulation VARCHAR(100) → TEXT for long NIST refs
- Pass 0b NOT EXISTS queries now skip deprecated/duplicate controls
- Duplicate Guard excludes deprecated/duplicate from existence check

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 12:47:26 +01:00
Benjamin Admin
8cb1dc1108 Fix pass0b queries to skip deprecated/duplicate controls
The NOT EXISTS check and Duplicate Guard now exclude deprecated and
duplicate controls, enabling clean re-runs after invalidation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 09:09:16 +01:00
Benjamin Admin
f8d9919b97 Improve object normalization: shorter keys, synonym expansion, qualifier stripping
- Truncate object keys to 40 chars (was 80) at underscore boundary
- Strip German qualifying prepositional phrases (bei/für/gemäß/von/zur/...)
- Add 65 new synonym mappings for near-duplicate patterns found in analysis
- Strip trailing noise tokens (articles/prepositions)
- Add _truncate_at_boundary() helper and _QUALIFYING_PHRASE_RE regex
- 11 new tests for normalization improvements (227 total pass)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 08:55:48 +01:00
Benjamin Admin
fb2cf29b34 fix: Pass 0b — Duplicate Guard, Severity-Kalibrierung, Title-Truncation
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 55s
CI/CD / test-python-backend-compliance (push) Successful in 36s
CI/CD / test-python-document-crawler (push) Successful in 23s
CI/CD / test-python-dsms-gateway (push) Successful in 20s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Successful in 4s
1. Duplicate Guard: merge_hint-Lookup vor INSERT in _write_atomic_control()
   verhindert semantisch identische Controls unter demselben Parent.
2. Severity-Kalibrierung: action_type-basiert statt blind vom Parent.
   define/review/test → max medium, implement/monitor → max high.
3. Title-Truncation: Schnitt am Wortende statt mitten im Wort.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 08:38:33 +01:00
Benjamin Admin
f39e5a71af feat: Obligation-Deduplizierung — 34.617 Duplikate als 'duplicate' markiert
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 33s
CI/CD / test-python-backend-compliance (push) Successful in 35s
CI/CD / test-python-document-crawler (push) Successful in 30s
CI/CD / test-python-dsms-gateway (push) Successful in 20s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 3s
Neue Endpunkte POST /obligations/dedup und GET /obligations/dedup-stats.
Pro candidate_id wird der aelteste Eintrag behalten, alle weiteren erhalten
release_state='duplicate' mit merged_into_id + quality_flags fuer Traceability.
Detail-View filtert Duplikate aus. MKDocs aktualisiert.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 20:13:00 +01:00
Benjamin Admin
ac42a0aaa0 fix: Faceted Counts — NULL-Werte einbeziehen + AbortController fuer Race Conditions
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 34s
CI/CD / test-python-backend-compliance (push) Successful in 32s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 17s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Successful in 2s
Backend: Facets zaehlen jetzt Controls OHNE Wert (z.B. "Ohne Nachweis")
als __none__. Filter unterstuetzen __none__ fuer verification_method,
category, evidence_type. Counts addieren sich immer zum Total.

Frontend: "Ohne X" Optionen in Dropdowns. AbortController verhindert
dass aeltere API-Antworten neuere ueberschreiben.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 17:35:52 +01:00
Benjamin Admin
52e463a7c8 feat: Faceted Search — Dropdown-Counts passen sich aktiven Filtern an
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 36s
CI/CD / test-python-backend-compliance (push) Successful in 42s
CI/CD / test-python-document-crawler (push) Successful in 30s
CI/CD / test-python-dsms-gateway (push) Successful in 21s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 2s
Backend: controls-meta akzeptiert alle Filter-Parameter und berechnet
Faceted Counts (jede Dimension zaehlt mit allen ANDEREN Filtern).
Neue Facets: severity, verification_method, category, evidence_type,
release_state — zusaetzlich zu domains, sources, type_counts.

Frontend: loadMeta laedt bei jeder Filteraenderung neu, alle Dropdowns
zeigen kontextsensitive Zahlen. Proxy leitet Filter an controls-meta weiter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 15:00:40 +01:00
Benjamin Admin
2dee62fa6f feat: Eigenentwicklung-Filter im Typ-Dropdown mit Counts
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 36s
CI/CD / test-python-backend-compliance (push) Successful in 36s
CI/CD / test-python-document-crawler (push) Successful in 27s
CI/CD / test-python-dsms-gateway (push) Successful in 18s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Successful in 2s
Backend: control_type=eigenentwicklung in list_controls + count_controls,
type_counts (rich/atomic/eigenentwicklung) in controls-meta Endpoint.
Frontend: Typ-Dropdown zeigt Eigenentwicklung mit Anzahl.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 14:33:00 +01:00
Benjamin Admin
3fb07e201f fix: V1 Enrichment Threshold auf 0.70 gesenkt (typische Top-Scores 0.70-0.77)
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 46s
CI/CD / test-python-backend-compliance (push) Successful in 35s
CI/CD / test-python-document-crawler (push) Successful in 24s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Has been skipped
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 11:13:37 +01:00
Benjamin Admin
81c9ce5de3 fix: V1 Enrichment — Qdrant Collection + Parent-Resolution fuer regulatorische Matches
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 33s
CI/CD / test-python-backend-compliance (push) Successful in 30s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 16s
CI/CD / validate-canonical-controls (push) Successful in 9s
CI/CD / Deploy (push) Successful in 1s
Die atomic_controls_dedup Collection (51k Punkte) enthaelt nur atomare
Controls ohne source_citation. Jetzt wird der Parent-Control aufgeloest,
der die Rechtsgrundlage traegt. Deduplizierung nach Parent-UUID verhindert
mehrfache Eintraege fuer die gleiche Regulation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 10:52:41 +01:00
Benjamin Admin
db7c207464 feat: V1 Control Enrichment — Eigenentwicklung-Label, regulatorisches Matching & Vergleichsansicht
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 39s
CI/CD / test-python-backend-compliance (push) Successful in 32s
CI/CD / test-python-document-crawler (push) Successful in 20s
CI/CD / test-python-dsms-gateway (push) Successful in 16s
CI/CD / validate-canonical-controls (push) Successful in 9s
CI/CD / Deploy (push) Successful in 4s
863 v1-Controls (manuell geschrieben, ohne Rechtsgrundlage) werden als
"Eigenentwicklung" gekennzeichnet und automatisch mit regulatorischen
Controls (DSGVO, NIS2, OWASP etc.) per Embedding-Similarity abgeglichen.

Backend:
- Migration 080: v1_control_matches Tabelle (Cross-Reference)
- v1_enrichment.py: Batch-Matching via BGE-M3 + Qdrant (Threshold 0.75)
- 3 neue API-Endpoints: enrich-v1-matches, v1-matches, v1-enrichment-stats
- 6 Tests (dry-run, execution, matches, pagination, detection)

Frontend:
- Orange "Eigenentwicklung"-Badge statt grauem "v1" (wenn kein Source)
- "Regulatorische Abdeckung"-Sektion im ControlDetail mit Match-Karten
- Side-by-Side V1CompareView (Eigenentwicklung vs. regulatorisch gedeckt)
- Prev/Next Navigation durch alle Matches

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 10:32:08 +01:00
Benjamin Admin
cb034b8009 fix: DB-Rollback nach LLM-Fehler im Rationale-Backfill
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 42s
CI/CD / test-python-backend-compliance (push) Successful in 32s
CI/CD / test-python-document-crawler (push) Successful in 22s
CI/CD / test-python-dsms-gateway (push) Successful in 18s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Has been skipped
Verhindert 'invalid transaction' Fehler wenn ein LLM-Call fehlschlaegt
und nachfolgende DB-Operationen blockiert.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 23:51:27 +01:00
Benjamin Admin
564f93259b fix: Ollama think:false fuer qwen3.5 Thinking-Mode
qwen3.5 gibt Antworten im 'thinking'-Feld statt 'response' zurueck.
Mit think:false wird der Thinking-Mode deaktiviert und die Antwort
korrekt im response-Feld geliefert.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 23:25:14 +01:00
Benjamin Admin
89ac223c41 fix: LLM Provider erkennt COMPLIANCE_LLM_PROVIDER=ollama
Ollama als eigener Enum-Wert neben self_hosted, damit die
docker-compose-Konfiguration (ollama) korrekt aufgeloest wird.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 23:12:05 +01:00
Benjamin Admin
23dd5116b3 feat: LLM-basierter Rationale-Backfill fuer atomare Controls
POST /controls/backfill-rationale — ersetzt Placeholder "Aus Obligation
abgeleitet." durch LLM-generierte Begruendungen (Ollama/qwen3.5).
Optimierung: gruppiert ~86k Controls nach ~7k Parents, ein LLM-Call pro Parent.
Paginierung via batch_size/offset fuer kontrollierte Ausfuehrung.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 23:01:49 +01:00
Benjamin Admin
81ce9dde07 docs: Anti-Fake-Evidence MkDocs umfassend erweitert
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 32s
CI/CD / test-python-backend-compliance (push) Successful in 29s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 16s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Successful in 2s
- Delve-Vorfall als Motivation mit konkreten Haftungsrisiken
- 6 Guardrails als Mermaid-Diagramm mit Zusammenspiel
- Verbindung zu evidence_type (code/process/hybrid)
- Sicherheitsarchitektur: Warum E0-E4, warum Four-Eyes nur GOV/PRIV
- Same-Person-Schutz Erklaerung (Backend-Level, kein Admin-Bypass)
- Hard Blocks: SQL-Beispiele fuer Audit-Sperren
- Vollstaendiges DB-Schema (Enums, alle Tabellen, alle Spalten)
- Vollstaendige API-Referenz (Evidence, Assertions, Audit-Trail, LLM-Audit)
- FAQ-Sektion (E0 loeschen, Four-Eyes Timeout, Assertion-Extraktion)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 22:22:01 +01:00
Benjamin Admin
5e9cab6ab5 feat: evidence_type Feld (code/process/hybrid) fuer Controls
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 38s
CI/CD / test-python-backend-compliance (push) Successful in 31s
CI/CD / test-python-document-crawler (push) Successful in 19s
CI/CD / test-python-dsms-gateway (push) Successful in 17s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Successful in 4s
Neues Feld auf canonical_controls klassifiziert, ob ein Control
technisch im Source Code (code), organisatorisch via Dokumente (process)
oder beides (hybrid) nachgewiesen wird. Inklusive Backfill-Endpoint,
Frontend-Badge/Filter und MkDocs-Dokumentation.

- Migration 079: evidence_type VARCHAR(20) + Index
- Backend: Filter, Backfill-Endpoint mit Domain-Heuristik, CRUD
- Frontend: EvidenceTypeBadge (sky/amber/violet), Nachweisart-Dropdown
- Proxy: evidence_type Passthrough fuer controls + controls-count
- Tests: 22 Tests fuer Klassifikations-Heuristik
- Docs: Eigenes MkDocs-Kapitel mit Mermaid-Diagramm

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 21:53:40 +01:00
Benjamin Admin
a29bfdd588 fix: normative_strength 'may' statt 'can' (DB-Constraint)
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 34s
CI/CD / test-python-backend-compliance (push) Successful in 30s
CI/CD / test-python-document-crawler (push) Successful in 19s
CI/CD / test-python-dsms-gateway (push) Successful in 17s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Has been skipped
DB-Constraint erlaubt nur must/should/may. 'can' gibt es nicht.
Alle Referenzen auf 'can' durch 'may' ersetzt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 08:35:16 +01:00
Benjamin Admin
9dbb4cc5d2 fix: Backfill nutzt source_citation statt control_parent_links
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 34s
CI/CD / test-python-backend-compliance (push) Successful in 29s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 17s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Successful in 2s
Die Obligation kennt ihren Parent-Rich-Control direkt. Dessen
source_citation->>'source' gibt die Quell-Regulierung zuverlaessiger
als der Umweg ueber control_parent_links (M:N-Inflation).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 08:25:32 +01:00
Benjamin Admin
c56bccaedf fix: deploy.sh bash 3 kompatibel (keine assoziativen Arrays)
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 32s
CI/CD / test-python-backend-compliance (push) Successful in 30s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 16s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Successful in 2s
macOS ships mit bash 3, declare -A wird nicht unterstuetzt.
Ersetzt durch case-Funktion dir_to_service().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 08:19:38 +01:00
Benjamin Admin
230fbeb490 feat: Dreistufenmodell normative Verbindlichkeit + Duplikat-Filter + Auto-Deploy
- Source-Type-Klassifikation (58 Regulierungen: law/guideline/framework)
- Backfill-Endpoint POST /controls/backfill-normative-strength
- exclude_duplicates Filter fuer Control-Library (Backend + Proxy + UI-Toggle)
- MkDocs-Kapitel: Normative Verbindlichkeit mit Mermaid-Diagrammen
- scripts/deploy.sh: Auto-Push + Mac Mini rebuild + Coolify health monitoring
- 26 Unit Tests fuer Klassifikations-Logik

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 08:18:00 +01:00
Benjamin Admin
6d3bdf8e74 feat: Control-Detail Provenance + Atomare Controls Seite
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 41s
CI/CD / test-python-backend-compliance (push) Successful in 40s
CI/CD / test-python-document-crawler (push) Successful in 23s
CI/CD / test-python-dsms-gateway (push) Successful in 18s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Successful in 4s
Backend: provenance endpoint (obligations, doc refs, merged duplicates,
regulations summary) + atomic-stats aggregation endpoint.
Frontend: ControlDetail mit Provenance-Sektionen, klickbare Navigation,
neue /sdk/atomic-controls Seite mit Stats-Bar und gefilterer Liste.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 10:38:34 +01:00
Benjamin Admin
200facda6a fix: use CAST(:dd AS jsonb) instead of :dd::jsonb in _write_review
SQLAlchemy's text() parser doesn't properly handle :param::type
syntax — it fails to recognize :dd as a bind parameter when followed
by ::jsonb. Using CAST(:dd AS jsonb) instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 08:48:58 +01:00
Benjamin Admin
9282850138 fix: add db.rollback() to batch dedup error handlers
SQLAlchemy sessions enter a failed state after SQL errors.
Without rollback(), all subsequent queries on the same session
fail with InFailedSqlTransaction. Added try/except with rollback
in _mark_duplicate, _mark_duplicate_to, _write_review, cross-group
pass, and the main phase1 loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 08:41:36 +01:00
Benjamin Admin
770f0b5ab0 fix: adapt batch dedup to NULL pattern_id — group by merge_group_hint
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 31s
CI/CD / test-python-backend-compliance (push) Successful in 31s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Successful in 2s
All Pass 0b controls have pattern_id=NULL. Rewritten to:
- Phase 1: Group by merge_group_hint (action:object:trigger), 52k groups
- Phase 2: Cross-group embedding search for semantically similar masters
- Qdrant search uses unfiltered cross-regulation endpoint
- API param changed: pattern_id → hint_filter

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 07:24:02 +01:00
Benjamin Admin
35784c35eb feat: Batch Dedup Runner — 85k→~18-25k Master Controls
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 32s
CI/CD / test-python-backend-compliance (push) Successful in 30s
CI/CD / test-python-document-crawler (push) Successful in 20s
CI/CD / test-python-dsms-gateway (push) Successful in 16s
CI/CD / validate-canonical-controls (push) Successful in 9s
CI/CD / Deploy (push) Successful in 1s
Adds batch orchestration for deduplicating ~85k Pass 0b atomic controls
into ~18-25k unique masters with M:N parent linking.

New files:
- migrations/078_batch_dedup.sql: merged_into_uuid column, perf indexes,
  link_type CHECK extended for cross_regulation
- batch_dedup_runner.py: BatchDedupRunner with quality scoring, merge-hint
  grouping, title-identical short-circuit, parent-link transfer, and
  cross-regulation pass
- tests/test_batch_dedup_runner.py: 21 tests (all passing)

Modified:
- control_dedup.py: optional collection param on Qdrant functions
- crosswalk_routes.py: POST/GET batch-dedup endpoints

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 07:06:38 +01:00
Benjamin Admin
cce2707c03 fix: update 61 outdated test mocks to match current schemas
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 41s
CI/CD / test-python-backend-compliance (push) Successful in 31s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 16s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Successful in 4s
Tests were failing due to stale mock objects after schema extensions:
- DSFA: add _mapping property to _DictRow, use proper mock instead of MagicMock
- Company Profile: add 6 missing fields (project_id, offering_urls, etc.)
- Legal Templates/Policy: update document type count 52→58
- VVT: add 13 missing attributes to activity mock
- Legal Documents: align consent test assertions with production behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 06:40:42 +01:00
Benjamin Admin
2efc738803 Merge branch 'feature/anti-fake-evidence' into main
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 34s
CI/CD / test-python-backend-compliance (push) Successful in 34s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 17s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Successful in 3s
Anti-Fake-Evidence System (Phase 1-4b): Confidence levels, assertion engine,
four-eyes approval, audit trail, traceability matrix UI, evidence distribution dashboard.
2026-03-23 21:12:45 +01:00
Benjamin Admin
e6201d5239 feat: Anti-Fake-Evidence System (Phase 1-4b)
Implement full evidence integrity pipeline to prevent compliance theater:
- Confidence levels (E0-E4), truth status tracking, assertion engine
- Four-Eyes approval workflow, audit trail, reject endpoint
- Evidence distribution dashboard, LLM audit routes
- Traceability matrix (backend endpoint + Compliance Hub UI tab)
- Anti-fake badges, control status machine, normative patterns
- 2 migrations, 4 test suites, MkDocs documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 17:15:45 +01:00
Benjamin Admin
48ca0a6bef feat: Framework Decomposition Engine + Composite Detection for Pass 0b
Adds a routing layer between Pass 0a and Pass 0b that classifies obligations
into atomic/compound/framework_container. Framework-container obligations
(e.g. "CCM-Praktiken fuer AIS") are decomposed into concrete sub-obligations
via an internal framework registry before Pass 0b composition.

- New: framework_decomposition.py with routing, matching, decomposition
- New: Framework registry (NIST SP 800-53, OWASP ASVS, CSA CCM) as JSON
- New: Composite detection flags on atomic controls (is_composite, atomicity)
- New: gen_meta fields: framework_ref, framework_domain, decomposition_source
- Integration: _route_and_compose() in run_pass0b() deterministic path
- 248 tests (198 decomposition + 50 framework), all passing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 12:11:55 +01:00
Benjamin Admin
1a63f5857b feat: Deterministic Control Composition Engine v2 for Pass 0b
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 38s
CI/CD / test-python-backend-compliance (push) Successful in 39s
CI/CD / test-python-document-crawler (push) Successful in 26s
CI/CD / test-python-dsms-gateway (push) Successful in 21s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 3s
Replace LLM-based Pass 0b with rule-based deterministic engine that
composes atomic controls from obligation data without any LLM call.

Engine features:
- 24 action types (implement, configure, define, document, maintain,
  review, monitor, assess, audit, test, verify, validate, report,
  notify, train, restrict_access, encrypt, delete, retain, ensure,
  approve, remediate, perform, obtain)
- 19 object classes (policy, procedure, register, record, report,
  technical_control, access_control, cryptographic_control,
  configuration, account, system, data, interface, role, training,
  incident, risk_artifact, process, consent)
- Compound action splitting with no-split phrases
- Title pattern: "{Object} {state_suffix}" (24 state suffixes)
- Statement field: "{condition} {object} ist {trigger} {action}"
- Pattern candidates for downstream categorization (26 specific
  combos + 24 action fallbacks)
- Structured timing: deadline_hours + frequency extraction
- Confidence scoring (0.3 base + action/object/trigger/template)
- Merge group hints for dedup: "{action}:{norm_obj}:{trigger}"
- Synonym-based object normalization (50+ German synonyms)
- 16 specific (action_type, object_class) template overrides
- Output validator with Pflichtfelder + Negativregeln + Warnregeln
- All new fields serialized into gen_meta JSONB (no migration needed)

Tests: 185 passed (33 new tests covering all engine components)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 11:05:48 +01:00
Benjamin Admin
295c18c6f7 feat: add DECOMPOSITION_LLM_MODEL env var for runtime model switching
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 46s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 23s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Successful in 6s
Allows switching between Haiku 4.5 and Sonnet 4.6 for Pass 0b
without rebuilding the backend container.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 09:20:10 +01:00
Benjamin Admin
649a3c5e4e perf: switch Pass 0b default model to Haiku 4.5
Benchmark shows Haiku is 2.5x faster than Sonnet at 5x lower cost
for this JSON structuring task. Quality is equivalent.
$142 vs $705 for 75K obligations, ~2.8 days vs ~7 days.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 09:12:01 +01:00
Benjamin Admin
bdd2f6fa0f fix: cap Anthropic max_tokens to 16384 for Pass 0b batches
Previous formula (batch_size * 1500) exceeded Claude's 16K output limit
for batch_size > 10, causing API failures and Ollama fallback.
New formula: min(16384, max(4096, batch_size * 500))

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 08:50:45 +01:00
Benjamin Admin
ac6134ce6d feat: control_parent_links population + traceability API + frontend
- _write_atomic_control() now uses RETURNING id and inserts into
  control_parent_links (M:N) with source_regulation, source_article,
  and obligation_candidate_id parsed from parent's source_citation
- New _parse_citation() helper for JSONB source_citation extraction
- New GET /controls/{id}/traceability endpoint returning full chain:
  parent links with obligations, child controls, source_count
- Backend: control_type filter (atomic/rich) for controls + count
- Frontend: Rechtsgrundlagen section in ControlDetail showing all
  parent links per source regulation with obligation text + strength
- Frontend: Atomic/Rich filter dropdown in Control Library list
- Frontend: GenerationStrategyBadge recognizes 'pass0b' strategy
- Tests: 3 new tests for parent_link creation + citation parsing,
  existing batch test mock updated for RETURNING clause

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 08:14:29 +01:00
Benjamin Admin
0027f78fc5 fix(ci): sync AllowedCollections test with current whitelist
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 42s
CI/CD / test-python-backend-compliance (push) Successful in 32s
CI/CD / test-python-document-crawler (push) Successful in 23s
CI/CD / test-python-dsms-gateway (push) Successful in 18s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Successful in 4s
TestAllowedCollections was asserting bp_compliance_recht which was
removed from the handler whitelist. Updated test to match the actual
AllowedCollections map (added bp_compliance_gdpr, bp_dsfa_templates,
bp_dsfa_risks, bp_iace_libraries; removed bp_compliance_recht).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 09:23:23 +01:00
Benjamin Admin
b29a7caee7 feat(scripts): add Phase I ingestion script for 12 new documents
Covers NIST SP 800-160/30/82, SPDX 3.0, CVSS v4.0, SLSA v1.0,
CycloneDX 1.6, OpenTelemetry, EU Machinery Guide 2006/42/EC,
FDA Human Factors, and 5 GPAI documents (Scope Guidelines,
Communication, CoP Safety/Transparency/Copyright).

All documents include license metadata in regulation payloads.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 09:18:30 +01:00
Benjamin Admin
a14e2f3a00 feat(decomposition): add merge pass, enrichment, and Pass 0b refinements
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 51s
CI/CD / test-python-backend-compliance (push) Successful in 34s
CI/CD / test-python-document-crawler (push) Successful in 23s
CI/CD / test-python-dsms-gateway (push) Successful in 20s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Has been skipped
Add obligation refinement pipeline between Pass 0a and 0b:
- Merge pass: rule-based dedup of implementation-level duplicate obligations
  within the same parent control (Jaccard similarity on action+object)
- Enrich pass: classify trigger_type (event/periodic/continuous) and detect
  is_implementation_specific from obligation text (regex-based, no LLM)
- Pass 0b: skip merged obligations, cap severity for impl-specific, override
  category to 'testing' for test obligations
- Migration 075: merged_into_id, trigger_type, is_implementation_specific
- Two new API endpoints: merge-obligations, enrich-obligations
- 30+ new tests (122 total, all passing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 22:27:09 +01:00
Benjamin Admin
71b8c33270 fix(docker): make torch/sentence-transformers optional to unblock builds
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 34s
CI/CD / test-python-backend-compliance (push) Successful in 34s
CI/CD / test-python-document-crawler (push) Successful in 24s
CI/CD / test-python-dsms-gateway (push) Successful in 18s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Has been skipped
Move torch and sentence-transformers to requirements-reranker.txt so
the main Docker build succeeds even if these large packages fail to
install. The reranker code already handles missing imports gracefully
when RERANK_ENABLED=false (the default).

This fixes the production deployment — builds were failing because of
the ~800MB torch dependency, preventing ALL new code from deploying.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 15:06:51 +01:00
Benjamin Admin
f2924a58ed debug: add /debug/routers endpoint to diagnose import failures
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 32s
CI/CD / test-python-backend-compliance (push) Successful in 1m37s
CI/CD / test-python-document-crawler (push) Successful in 20s
CI/CD / test-python-dsms-gateway (push) Successful in 17s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Has been skipped
Crosswalk routes returning 404 on production. This adds a diagnostic
endpoint that reports which sub-routers failed to load and why.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 12:23:26 +01:00
Benjamin Admin
643b26618f feat: Control Library UI, dedup migration, QA tooling, docs
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 31s
CI/CD / test-python-backend-compliance (push) Successful in 1m35s
CI/CD / test-python-document-crawler (push) Successful in 20s
CI/CD / test-python-dsms-gateway (push) Successful in 17s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Has been skipped
- Control Library: parent control display, ObligationTypeBadge,
  GenerationStrategyBadge variants, evidence string fallback
- API: expose parent_control_uuid/id/title in canonical controls
- Fix: DSFA SQLAlchemy 2.0 Row._mapping compatibility
- Migration 074: control_parent_links + control_dedup_reviews tables
- QA scripts: benchmark, gap analysis, OSCAL import, OWASP cleanup,
  phase5 normalize, phase74 gap fill, sync_db, run_job
- Docs: dedup engine, RAG benchmark, lessons learned, pipeline docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 11:56:08 +01:00
Benjamin Admin
c52dbdb8f1 feat(rag): optimize RAG pipeline — JSON-Mode, CoT, Hybrid Search, Re-Ranking, Cross-Reg Dedup, chunk 1024
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 42s
CI/CD / test-python-backend-compliance (push) Successful in 1m38s
CI/CD / test-python-document-crawler (push) Successful in 20s
CI/CD / test-python-dsms-gateway (push) Successful in 17s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Has been skipped
Phase 1 (LLM Quality):
- Add format=json to all Ollama payloads (obligation_extractor, control_generator, citation_backfill)
- Add Chain-of-Thought analysis steps to Pass 0a/0b system prompts

Phase 2 (Retrieval Quality):
- Hybrid search via Qdrant Query API with RRF fusion + automatic text index (legal_rag.go)
- Fallback to dense-only search if Query API unavailable
- Cross-encoder re-ranking with BGE Reranker v2 (RERANK_ENABLED=false by default)
- CPU-only PyTorch dependency to keep Docker image small

Phase 3 (Data Layer):
- Cross-regulation dedup pass (threshold 0.95) links controls across regulations
- DedupResult.link_type field distinguishes dedup_merge vs cross_regulation
- Chunk size defaults updated 512/50 → 1024/128 for new ingestions only
- Existing collections and controls are NOT affected

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 11:49:43 +01:00
Benjamin Admin
c3a53fe5d2 fix(tts): upgrade edge-tts 6.1.12 → 7.2.7 (fixes 403 token expiry)
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 35s
CI/CD / test-python-backend-compliance (push) Successful in 32s
CI/CD / test-python-document-crawler (push) Successful in 22s
CI/CD / test-python-dsms-gateway (push) Successful in 20s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Has been skipped
v6.1.12 had an expired TrustedClientToken causing 403 on all Edge TTS
requests. v7.2.7 uses a valid token and same Communicate API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 16:23:44 +01:00
Benjamin Admin
df5b6d69ef feat(tts): add Edge TTS (Microsoft Neural Voices) as primary engine with Piper fallback
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 32s
CI/CD / test-python-backend-compliance (push) Successful in 31s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 20s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Has been skipped
Edge TTS provides near-human quality voices (de-DE-ConradNeural, en-US-GuyNeural).
Falls back to Piper TTS when Edge TTS is unavailable (e.g. no internet).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 16:13:10 +01:00
Benjamin Admin
4f6ac9b23a feat(tts): add English voice (lessac-high) + language-based model selection
- Download en_US-lessac-high Piper model in Dockerfile
- Select TTS engine based on request language (de/en)
- Include language in cache key to avoid collisions
- List both voices in /voices endpoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 14:07:23 +01:00
Benjamin Admin
5ea31a3236 feat(tts): add /synthesize-direct endpoint for real-time audio streaming
- Returns MP3 audio directly in response body (no MinIO upload)
- Disk cache (/tmp/tts-cache) avoids re-synthesis of identical text
- Used by pitch-deck presenter for real-time TTS playback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 12:25:25 +01:00
Benjamin Admin
95c371e9a5 feat(sdk): update SDK Flow, Architecture, and StepHeader for vendor-compliance integration
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 32s
CI/CD / test-python-backend-compliance (push) Successful in 30s
CI/CD / test-python-document-crawler (push) Successful in 19s
CI/CD / test-python-dsms-gateway (push) Successful in 16s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Has been skipped
- flow-data.ts: vendor-compliance moved from betrieb/seq:4200 to
  dokumentation/seq:2500, prerequisite changed to vvt, added 5 DB tables
- architecture-data.ts: added vendor tables and API endpoints to
  backend-compliance service definition
- StepHeader.tsx: added vendor-compliance explanation with 4 tips
  (Art. 28, cross-module integration, third-country transfers, controls
  library). Updated obligations (12 checks, vendor-link, document),
  loeschfristen (vendor picker), tom (vendor-controls cross-ref),
  vvt (processor tab from vendor API)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 09:12:11 +01:00
Benjamin Admin
b1627252ee fix(obligations): show linked vendor IDs in Pflichtenregister document
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 34s
CI/CD / test-python-backend-compliance (push) Successful in 32s
CI/CD / test-python-document-crawler (push) Successful in 22s
CI/CD / test-python-dsms-gateway (push) Successful in 17s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Has been skipped
The HTML document builder was missing linked_vendor_ids in the detailed
obligation cards. Art. 28 obligations with linked vendors now display
them in the audit-ready PDF/HTML output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 08:55:01 +01:00
Benjamin Admin
2a0449c9b7 docs(qa): add Control Quality Pipeline documentation
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 33s
CI/CD / test-python-backend-compliance (push) Successful in 32s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 18s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Has been skipped
QA process, article types, match rates, preamble dedup rules,
and next steps documented in MkDocs under Entwicklung.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 08:16:07 +01:00
Benjamin Admin
92d37a1660 chore(qa): preamble vs article dedup — 190 duplicates marked
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 33s
CI/CD / test-python-backend-compliance (push) Successful in 32s
CI/CD / test-python-document-crawler (push) Successful in 20s
CI/CD / test-python-dsms-gateway (push) Successful in 16s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Has been skipped
Preamble controls that duplicate article controls (same regulation,
Jaccard title similarity >= 0.40) are marked as duplicate.
Article controls always take priority.

Result: 6,183 active controls (was 6,373), 648 unique preamble controls remain.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 08:08:04 +01:00
Benjamin Admin
0e16640c28 chore(qa): PDF QA v3 — 6,259/7,943 controls matched (79%)
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 43s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 22s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Has been skipped
- Added NIST 800-53, OWASP Top 10/ASVS/SAMM/API/MASVS, ENISA ICS PDFs
- Improved normalize() for ligatures, smart quotes, dashes
- Added OWASP-specific index builder (A01:2021, V1.1, MASVS-*)
- 6,259 article assignments in DB (1,817 article, 1,355 preamble,
  1,173 control, 790 annex, 666 section)
- Remaining 1,651 unmatched: Blue Guide (EN text vs DE PDF),
  OWASP multilingual translations (PT/AR/ID/ES)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 07:57:52 +01:00
Benjamin Admin
24f02b52ed refactor: remove 473 lines of dead code across 5 SDK modules
- obligations: unused vendors state/fetch, unreachable filter==='ai' path
- tom: unused vendorControlsLoading state, unused bulkUpdateTOMs import
- loeschfristen: unused BASELINE_TEMPLATES imports, sdk hook, managingLegalHolds state
- vvt: unused apiGetCompleteness/apiGetLibrary, 7 unused VVTLib* interfaces
- vendor-compliance: 11 unused context methods, 6 unused selector hooks, ContractUploadData type

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 06:57:01 +01:00
Benjamin Admin
9b0f25c105 chore(qa): add PDF-based control QA scripts and results
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 36s
CI/CD / test-python-backend-compliance (push) Successful in 32s
CI/CD / test-python-document-crawler (push) Successful in 22s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Has been skipped
QA pipeline that matches control source_original_text directly against
original PDF documents to verify article/paragraph assignments. Covers
backfill, dedup, source normalization, Qdrant cleanup, and prod sync.

Key results (2026-03-20):
- 4,110/7,943 controls matched to PDF (100% for major EU regs)
- 3,366 article corrections, 705 new assignments
- 1,290 controls from Erwägungsgründe (preamble) identified
- 779 controls from Anhänge (annexes) identified

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 00:56:13 +01:00
Benjamin Admin
1cc34c23d9 feat(document-generator): 33 policy + module document templates
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 36s
CI/CD / test-python-backend-compliance (push) Successful in 34s
CI/CD / test-python-document-crawler (push) Successful in 25s
CI/CD / test-python-dsms-gateway (push) Successful in 18s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Has been skipped
- Migration 071: 14 IT-Security policy templates (ISO 27001/BSI)
  information_security, access_control, password, encryption, logging,
  backup, incident_response, change_management, patch_management,
  asset_management, cloud_security, devsecops, secrets_management,
  vulnerability_management
- Migration 072: 15 Data/HR/Vendor/BCM policy templates
  data_protection, data_classification, data_retention, data_transfer,
  privacy_incident, employee_security, security_awareness, remote_work,
  offboarding, vendor_risk_management, third_party_security,
  supplier_security, business_continuity, disaster_recovery,
  crisis_management
- Migration 073: 4 module document reference templates
  vvt_register, tom_documentation, loeschkonzept, pflichtenregister
- TemplateType union: 17 → 61 types with German labels
- VALID_DOCUMENT_TYPES: +6 types (cybersecurity_policy, dsfa, 4 module docs)
- CATEGORIES: new "DSGVO-Dokumente" category for module documents

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 23:27:25 +01:00
Benjamin Admin
5dd7a27336 fix(pipeline): add missing regulation codes to LICENSE_MAP
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 41s
CI/CD / test-python-backend-compliance (push) Successful in 1m0s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 17s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Has been skipped
eu_2023_1542 (Batterieverordnung), eu_2023_988 (GPSR), nist_sp800_218,
nist_privacy_1_0, owasp_mobile_top10 were defaulting to Rule 3 (restricted)
instead of their correct rules. This caused 68/71 controls to be flagged
as too_close in the last pipeline run.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 22:14:10 +01:00
Benjamin Admin
c3afa628ed feat(sdk): vendor-compliance cross-module integration — VVT, obligations, TOM, loeschfristen
Integrate the vendor-compliance module with four DSGVO modules to eliminate
data silos and resolve the VVT processor tab's ephemeral state problem.

- Reposition vendor-compliance sidebar from seq 4200 to 2500 (after VVT)
- VVT: replace ephemeral ProcessorRecord state with Vendor-API fetch (read-only)
- Obligations: add linked_vendor_ids (JSONB) + compliance check #12 MISSING_VENDOR_LINK
- TOM: add vendor TOM-controls cross-reference table in overview tab
- Loeschfristen: add linked_vendor_ids (JSONB) + vendor picker + document section
- Migrations: 069_obligations_vendor_link.sql, 070_loeschfristen_vendor_link.sql
- Tests: 12 new backend tests (125 total pass)
- Docs: update obligations.md + vendors.md with cross-module integration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 13:59:43 +01:00
Benjamin Admin
4b1eede45b feat(tom): audit document, compliance checks, 25 controls, canonical control mapping
Phase A: TOM document HTML generator (12 sections, inline CSS, A4 print)
Phase B: TOMDocumentTab component (org-header form, revisions, print/download)
Phase C: 11 compliance checks with severity-weighted scoring
Phase D: MkDocs documentation for TOM module
Phase E: 25 new controls (63 → 88) in 13 categories

Canonical Control Mapping (three-layer architecture):
- Migration 068: tom_control_mappings + tom_control_sync_state tables
- 6 API endpoints: sync, list, by-tom, stats, manual add, delete
- Category mapping: 13 TOM categories → 17 canonical categories
- Frontend: sync button + coverage card (Overview), drill-down (Editor),
  belegende Controls count (Document)
- 20 tests (unit + API with mocked DB)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:56:53 +01:00
Benjamin Admin
2a70441eaa feat(sdk): VVT master libraries, process templates, Loeschfristen profiling + document
VVT: Master library tables (7 catalogs), 500+ seed entries, process templates
with instantiation, library API endpoints + 18 tests.
Loeschfristen: Baseline catalog, compliance checks, profiling engine, HTML document
generator, MkDocs documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:56:25 +01:00
Benjamin Admin
f2819b99af feat(pipeline): v3 — scoped control applicability + source_type classification
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 36s
CI/CD / test-python-backend-compliance (push) Successful in 36s
CI/CD / test-python-document-crawler (push) Successful in 27s
CI/CD / test-python-dsms-gateway (push) Successful in 18s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Has been skipped
Phase 4: source_type (law/guideline/standard/restricted) on source_citation
- NIST/OWASP/ENISA correctly shown as "Standard" instead of "Gesetzliche Grundlage"
- Dynamic frontend labels based on source_type
- Backfill endpoint POST /v1/canonical/generate/backfill-source-type

Phase v3: Scoped Control Applicability
- 3 new fields: applicable_industries, applicable_company_size, scope_conditions
- LLM prompt extended with 39 industries, 5 company sizes, 10 scope signals
- All 5 generation paths (Rule 1/2/3, batch structure, batch reform) updated
- _build_control_from_json: parsing + validation (string→list, size validation)
- _store_control: writes 3 new JSONB columns
- API: response models, create/update requests, SELECT queries extended
- Migration 063: 3 new JSONB columns with GIN indexes
- 110 generator tests + 28 route tests = 138 total, all passing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 16:28:05 +01:00
Benjamin Admin
3bb9fffab6 docs: update control library taxonomy, add provenance wiki page
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 36s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 24s
CI/CD / test-python-dsms-gateway (push) Successful in 16s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Has been skipped
- canonical-control-library.md: add 7 release_states (was 4), target_audience
  (17 values), generation_strategy, missing API endpoints (controls-customer,
  backfill-citations, backfill-domain), updated test counts (81+)
- NEW control-provenance.md: extracted from frontend page.tsx — methodology,
  legal basis, filters, badges, taxonomy, open/restricted sources,
  verification methods, 23 categories, license matrix, source registry
- mkdocs.yml: add Control Provenance Wiki to navigation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 08:49:42 +01:00
Benjamin Admin
148c7ba3af feat(qa): recital detection, review split, duplicate comparison
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 42s
CI/CD / test-python-backend-compliance (push) Successful in 34s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 20s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Has been skipped
Add _detect_recital() to QA pipeline — flags controls where
source_original_text contains Erwägungsgrund markers instead of
article text (28% of controls with source text affected).

- Recital detection via regex + phrase matching in QA validation
- 10 new tests (TestRecitalDetection), 81 total
- ReviewCompare component for side-by-side duplicate comparison
- Review mode split: Duplikat-Verdacht vs Rule-3-ohne-Anchor tabs
- MkDocs: recital detection documentation
- Detection script for bulk analysis (scripts/find_recital_controls.py)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 08:20:02 +01:00
Benjamin Admin
a9e0869205 feat(pipeline): pipeline_version v2, migration 062, docs + 71 tests
- Add PIPELINE_VERSION=2 constant and pipeline_version column to
  canonical_controls and canonical_processed_chunks (migration 062)
- Anthropic API decides chunk relevance via null-returns (skip_prefilter)
- Annex/appendix chunks explicitly protected in prompts
- Fix 6 failing tests (CRYP domain, _process_batch tuple return)
- Add TestPipelineVersion + TestRegulationFilter test classes (10 new tests)
- Add MkDocs page: control-generator-pipeline.md (541 lines)
- Update canonical-control-library.md with v2 pipeline diagram
- Update testing.md with 71-test breakdown table

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 17:31:11 +01:00
Benjamin Admin
653aad57e3 Let Anthropic API decide chunk relevance instead of local prefilter
Updated both structure_batch and reformulate_batch prompts to return null
for chunks without actionable requirements (definitions, TOCs, scope-only).
Explicit instruction to always process annexes/appendices as they often
contain concrete technical requirements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 16:44:01 +01:00
Benjamin Admin
a7f7e57dd7 Add skip_prefilter option to control generator
Local LLM prefilter (llama3.2 3B) was incorrectly skipping annex chunks
that contain concrete requirements. Added skip_prefilter flag to bypass
the local pre-filter and send all chunks directly to Anthropic API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 16:30:57 +01:00
Benjamin Admin
567e82ddf5 Fix stale DB session after long embedding pre-load
The embedding pre-load for 4998 existing controls takes ~16 minutes,
causing the SQLAlchemy session to become invalid. Added rollback after
pre-load completes to reset the session before subsequent DB operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 14:34:44 +01:00
Benjamin Admin
36ef34169a Fix regulation_filter bypass for chunks without regulation_code
Chunks without a regulation_code were silently passing through the filter
in _scan_rag(), causing unrelated documents (e.g. Data Act, legal templates)
to be included in filtered generation jobs. Now chunks without reg_code are
skipped when regulation_filter is active.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 13:38:25 +01:00
Benjamin Admin
d22c47c9eb feat(pipeline): Anthropic Batch API, source/regulation filter, cost optimization
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 35s
CI/CD / test-python-backend-compliance (push) Successful in 34s
CI/CD / test-python-document-crawler (push) Successful in 22s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Has been skipped
- Add Anthropic API support to decomposition Pass 0a/0b (prompt caching, content batching)
- Add Anthropic Batch API (50% cost reduction, async 24h processing)
- Add source_filter (ILIKE on source_citation) for regulation-based filtering
- Add category_filter to Pass 0a for selective decomposition
- Add regulation_filter to control_generator for RAG scan phase filtering
  (prefix match on regulation_code — enables CE + Code Review focus)
- New API endpoints: batch-submit-0a, batch-submit-0b, batch-status, batch-process
- 83 new tests (all passing)

Cost reduction: $2,525 → ~$600-700 with all optimizations combined.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 13:22:01 +01:00
Benjamin Admin
825e070ed9 feat(multi-layer): complete Multi-Layer Control Architecture (Phases 1-8 + Pass 0)
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 47s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 24s
CI/CD / test-python-dsms-gateway (push) Successful in 18s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Has been skipped
Implements the full Multi-Layer Control Architecture for migrating ~25,000
Rich Controls into atomic, deduplicated Master Controls with full traceability.

Architecture: Legal Source → Obligation → Control Pattern → Master Control → Customer Instance

New services:
- ObligationExtractor: 3-tier extraction (exact → embedding → LLM)
- PatternMatcher: 2-tier matching (keyword + embedding + domain-bonus)
- ControlComposer: Pattern + Obligation → Master Control
- PipelineAdapter: Pipeline integration + Migration Passes 1-5
- DecompositionPass: Pass 0a/0b — Rich Control → atomic Controls
- CrosswalkRoutes: 15 API endpoints under /v1/canonical/

New DB schema:
- Migration 060: obligation_extractions, control_patterns, crosswalk_matrix
- Migration 061: obligation_candidates, parent_control_uuid tracking

Pattern Library: 50 YAML patterns (30 core + 20 IT-security)
Go SDK: Pattern loader with YAML validation and indexing
Documentation: MkDocs updated with full architecture overview

500 Python tests passing across all components.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 09:00:37 +01:00
Benjamin Admin
4f6bc8f6f6 feat(training+controls): interactive video pipeline, training blocks, control generator, CE libraries
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Failing after 37s
CI/CD / test-python-backend-compliance (push) Successful in 39s
CI/CD / test-python-document-crawler (push) Successful in 26s
CI/CD / test-python-dsms-gateway (push) Successful in 23s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Has been skipped
Interactive Training Videos (CP-TRAIN):
- DB migration 022: training_checkpoints + checkpoint_progress tables
- NarratorScript generation via Anthropic (AI Teacher persona, German)
- TTS batch synthesis + interactive video pipeline (slides + checkpoint slides + FFmpeg)
- 4 new API endpoints: generate-interactive, interactive-manifest, checkpoint submit, checkpoint progress
- InteractiveVideoPlayer component (HTML5 Video, quiz overlay, seek protection, progress tracking)
- Learner portal integration with automatic completion on all checkpoints passed
- 30 new tests (handler validation + grading logic + manifest/progress + seek protection)

Training Blocks:
- Block generator, block store, block config CRUD + preview/generate endpoints
- Migration 021: training_blocks schema

Control Generator + Canonical Library:
- Control generator routes + service enhancements
- Canonical control library helpers, sidebar entry
- Citation backfill service + tests
- CE libraries data (hazard, protection, evidence, lifecycle, components)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 21:41:48 +01:00
Benjamin Admin
d2133dbfa2 test+docs(iace): add handler tests, error-handling tests, JSON export tests, TipTap docs
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 38s
CI/CD / test-python-backend-compliance (push) Successful in 34s
CI/CD / test-python-document-crawler (push) Successful in 29s
CI/CD / test-python-dsms-gateway (push) Successful in 20s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Successful in 2s
- Create iace_handler_test.go (22 tests): input validation for InitFromProfile,
  GenerateSingleSection, ExportTechFile, CheckCompleteness, getTenantID,
  CreateProject, ListProjects, Component CRUD handlers
- Add error-handling tests to tech_file_generator_test.go: nil context, nil project,
  empty components/hazards/classifications/evidence, unknown section type,
  all 19 getSystemPrompt types, AI-specific section prompts
- Add JSON export tests to document_export_test.go: valid output, empty project,
  nil project error, special character handling (German text, XML escapes)
- Add iace-hazard-library.md to mkdocs.yml navigation
- Add TipTap Rich-Text-Editor section to iace.md documentation

Total: 181 tests passing (was 165), 0 failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 13:15:31 +01:00
Benjamin Admin
6d2de9b897 feat(iace): complete CE risk assessment — LLM tech-file generation, multi-format export, TipTap editor
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 36s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 24s
CI/CD / test-python-dsms-gateway (push) Successful in 21s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 2s
Phase 1: Fix completeness gates G23 (require verified/rejected mitigations) and G09 (audit trail check)
Phase 2: LLM-based tech-file section generation with 19 German prompts and RAG enrichment
Phase 3: Multi-format document export (PDF/Excel/DOCX/Markdown/JSON)
Phase 4: Company profile → IACE data flow with auto component/classification creation
Phase 5: TipTap WYSIWYG editor replacing textarea for tech-file sections
Phase 6: User journey tests, developer portal API reference, updated documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 12:50:53 +01:00
Benjamin Admin
5adb1c5f16 feat(iace): integrate Rule Library as 58 extended hazard patterns (HP045-HP102)
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 39s
CI/CD / test-python-backend-compliance (push) Successful in 38s
CI/CD / test-python-document-crawler (push) Successful in 25s
CI/CD / test-python-dsms-gateway (push) Successful in 20s
CI/CD / validate-canonical-controls (push) Successful in 14s
CI/CD / Deploy (push) Successful in 2s
Parsed 171 explicit rules from 4 Rule Library Word documents (R051-R1550),
deduplicated into 58 unique (component, energy_source) patterns, and mapped
to existing IACE IDs (component tags, M-IDs, E-IDs).

Changes:
- hazard_patterns_extended.go: 58 new patterns derived from Rule Library
- pattern_engine.go: combines builtin (44) + extended (58) = 102 total patterns
- iace_handler.go: ListHazardPatterns returns all 102 patterns
- iace.md: updated documentation for 102 patterns
- scripts/generate-rule-patterns.py: mapping + Go code generator
- scripts/parsed-rule-library.json: extracted rule data

Tests: 132 passing (9 new extended pattern tests)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 11:24:07 +01:00
Benjamin Admin
9c1355c05f feat(iace): Phase 5+6 — frontend integration, RAG library search, comprehensive tests
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 34s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 23s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 2s
Phase 5 — Frontend Integration:
- components/page.tsx: ComponentLibraryModal with 120 components + 20 energy sources
- hazards/page.tsx: AutoSuggestPanel with 3-column pattern matching review
- mitigations/page.tsx: SuggestMeasuresModal per hazard with 3-level grouping
- verification/page.tsx: SuggestEvidenceModal per mitigation with evidence types

Phase 6 — RAG Library Search:
- Added bp_iace_libraries to AllowedCollections whitelist in rag_handlers.go
- SearchLibrary endpoint: POST /iace/library-search (semantic search across libraries)
- EnrichTechFileSection endpoint: POST /projects/:id/tech-file/:section/enrich
- Created ingest-iace-libraries.sh ingestion script for Qdrant collection

Tests (123 passing):
- tag_taxonomy_test.go: 8 tests for taxonomy entries, domains, essential tags
- controls_library_test.go: 7 tests for measures, reduction types, subtypes
- integration_test.go: 7 integration tests for full match flow and library consistency
- Extended tag_resolver_test.go: 9 new tests for FindByTags and cross-category resolution

Documentation:
- Updated iace.md with Hazard-Matching-Engine, RAG enrichment, and new DB tables

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 10:22:49 +01:00
Benjamin Admin
3b2006ebce feat(iace): add hazard-matching-engine with component library, tag system, and pattern engine
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 44s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 22s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 4s
Implements Phases 1-4 of the IACE Hazard-Matching-Engine:
- 120 machine components (C001-C120) in 11 categories
- 20 energy sources (EN01-EN20)
- ~85 tag taxonomy across 5 domains
- 44 hazard patterns with AND/NOT matching logic
- Pattern engine with tag resolution and confidence scoring
- 8 new API endpoints (component-library, energy-sources, tags, patterns, match/apply)
- Completeness gate G09 for pattern matching
- 320 tests passing (36 new)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 08:50:11 +01:00
Benjamin Admin
c7651796c9 feat(iace): integrate ISO 12100 machine risk model with 4-factor assessment
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 36s
CI/CD / test-python-backend-compliance (push) Successful in 36s
CI/CD / test-python-document-crawler (push) Successful in 22s
CI/CD / test-python-dsms-gateway (push) Successful in 18s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Successful in 2s
Add dual-mode risk engine: legacy S×E×P (avoidance=0) and ISO mode S×F×P×A
(avoidance>=1) with new thresholds (low/medium/high/very_high/not_acceptable).

- 150+ hazard library entries across 28 categories incl. physical hazards
  (mechanical, electrical, thermal, pneumatic/hydraulic, noise/vibration,
  ergonomic, material/environmental)
- 160-entry protective measures library with 3-step hierarchy validation
  (design → protective → information)
- 25 lifecycle phases, 20 affected person roles, 50 evidence types
- 10 verification methods (expanded from 7)
- New API endpoints: lifecycle-phases, roles, evidence-types,
  protective-measures-library, validate-mitigation-hierarchy
- DB migrations 018+019 for extended schema
- Frontend: 4-slider risk assessment, hierarchy warnings, measures library modal
- MkDocs wiki updated with ISO mode docs and legal notice (no norm text)

All content uses original wording — norms referenced as methodology only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 23:13:41 +01:00
Benjamin Admin
c8fd9cc780 feat(control-library): document-grouped batching, generation strategy tracking, sort by source
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 31s
CI/CD / test-python-backend-compliance (push) Successful in 31s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 18s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Successful in 2s
- Group chunks by regulation_code before batching for better LLM context
- Add generation_strategy column (ungrouped=v1, document_grouped=v2)
- Add v1/v2 badge to control cards in frontend
- Add sort-by-source option with visual group headers
- Add frontend page tests (18 tests)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 15:10:52 +01:00
Benjamin Admin
0d95c3bb44 feat(control-provenance): add filter explanations, badges, and updated taxonomy
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 33s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Successful in 2s
- Add new "Filter in der Control Library" section explaining all 7 dropdowns
- Add "Badges & Lizenzregeln" section explaining Rule 1/2/3 and all badges
- Update taxonomy with actual top-10 domains and counts (~3100+ controls)
- Update Master Library strategy with current numbers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 10:45:21 +01:00
Benjamin Admin
f066cf1a03 feat(control-library): add document source dropdown filter
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 44s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 25s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Successful in 6s
Add "Dokumentenursprung" filter dropdown to the control library page.
Extracts unique source_citation.source values from controls, sorted by
frequency. Includes "Ohne Quelle" option for controls without source info.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 09:03:21 +01:00
Benjamin Admin
dd09fa7a46 feat: CRA wiki, cybersecurity policy template, Phase H RAG ingestion
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 35s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 22s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Successful in 2s
- Wiki: add CRA category with 3 articles (Grundlagen, 35 Security Controls,
  CRA+NIS2+AI Act Framework)
- Document Generator: add CRA-konforme Cybersecurity Policy template with
  21 sections covering governance, SSDLC, vulnerability management,
  incident response (24h/72h), SBOM, patch management
- RAG: ingest Phase H — 17 EU regulations + 2 NIST frameworks now in Qdrant
  (CRA, AI Act, NIS2, DSGVO, DMA, GPSR, Batterieverordnung, etc.)
- Phase H script: add scripts/ingest-phase-h.sh for reproducible ingestion
- rag-sources.md: update status to ingestiert, add CRA entry

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 00:43:46 +01:00
Benjamin Admin
f3e05c1bf7 feat: enhance whistleblower HinSchG content, fix control-library filter layout
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 34s
CI/CD / test-python-backend-compliance (push) Successful in 35s
CI/CD / test-python-document-crawler (push) Successful in 26s
CI/CD / test-python-dsms-gateway (push) Successful in 21s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Successful in 2s
- Whistleblower page: expand overview tab with comprehensive HinSchG legal info
  (Gesetzliche Grundlage, Fristen-Cards, Anwendungsbereich, Schutz des Hinweisgebers)
- StepHeader: enrich whistleblower tips with detailed HinSchG paragraphs and sanctions
- Wiki: add migration 054 with 5 new/updated HinSchG articles (Anwendungsbereich,
  Hinweisgeberschutz, Meldestellen, Verfahrensablauf, Datenschutz-Anforderungen)
- MKDocs: rewrite whistleblower docs with full legal basis, architecture, API, DB schema
- Control library: fix filter dropdown overflow by splitting into search + filter rows

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 00:23:19 +01:00
Benjamin Admin
2ed1c08acf feat: enhance legal basis display, add batch processing tests and docs
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 32s
CI/CD / test-python-backend-compliance (push) Successful in 31s
CI/CD / test-python-document-crawler (push) Successful in 23s
CI/CD / test-python-dsms-gateway (push) Successful in 17s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Successful in 2s
- Backfill 81 controls with empty source_citation.source from generation_metadata
- Add fallback to generation_metadata.source_regulation in ControlDetail blue box
- Improve Rule 3 amber box text for reformulated controls
- Add 30 new tests for batch processing (TestParseJsonArray, TestBatchSizeConfig,
  TestBatchProcessingLoop) — all 61 control generator tests passing
- Fix stale test_config_defaults assertion (max_controls 50→0)
- Update canonical-control-library.md with batch processing pipeline docs,
  processed chunks tracking, migration guide, and stats endpoint
- Update testing.md with canonical control generator test section

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 23:51:52 +01:00
Benjamin Admin
4018b9af9b chore: add coverage.out to .gitignore
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 34s
CI/CD / test-python-backend-compliance (push) Successful in 35s
CI/CD / test-python-document-crawler (push) Successful in 23s
CI/CD / test-python-dsms-gateway (push) Successful in 20s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Successful in 2s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 22:55:05 +01:00
Benjamin Admin
a9f291ff49 test+docs: add policy library tests (67 tests) and MKDocs documentation
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 34s
CI/CD / test-python-backend-compliance (push) Successful in 35s
CI/CD / test-python-document-crawler (push) Successful in 23s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Successful in 2s
- New test_policy_templates.py: 67 tests covering all 29 policy types,
  API creation, filtering, placeholders, seed script validation
- Updated test_legal_template_routes.py: fix type count 16→52
- New MKDocs page policy-bibliothek.md with full template reference
- Updated dokumentengenerierung.md and rechtliche-texte.md with cross-refs
- Added policy-bibliothek to mkdocs.yml navigation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 22:50:50 +01:00
Benjamin Admin
0171d611f6 feat: add policy library with 29 German policy templates
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 34s
CI/CD / test-python-backend-compliance (push) Successful in 35s
CI/CD / test-python-document-crawler (push) Successful in 26s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Successful in 2s
Add 29 new document types (IT security, data, personnel, vendor, BCM
policies) to VALID_DOCUMENT_TYPES and 5 category pills to the document
generator UI. Include seed script for production DB population.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 22:37:33 +01:00
Benjamin Admin
637fab6fdb fix: migration runner strips BEGIN/COMMIT and guards missing tables
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 33s
CI/CD / test-python-backend-compliance (push) Successful in 39s
CI/CD / test-python-document-crawler (push) Successful in 22s
CI/CD / test-python-dsms-gateway (push) Successful in 21s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Successful in 2s
Root cause: migrations 046-047 used explicit BEGIN/COMMIT which
conflicts with psycopg2 implicit transactions, and ALTER TABLE
on canonical_controls fails when the table doesn't exist on
production. This blocked all subsequent migrations (048-053).

Changes:
- migration_runner.py: strip BEGIN/COMMIT from SQL before executing
- 046: wrap canonical_controls ALTER in DO $$ IF EXISTS block
- 047: wrap canonical_controls ALTER in DO $$ IF EXISTS block

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 21:59:10 +01:00
Benjamin Admin
d462141ccd fix: migration runner continues on failure instead of aborting
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 32s
CI/CD / test-python-backend-compliance (push) Successful in 35s
CI/CD / test-python-document-crawler (push) Successful in 23s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Successful in 2s
Previously, a single failed migration would abort all subsequent
migrations via raise RuntimeError. Now the runner logs the failure
and continues with remaining migrations, so independent schema
changes (e.g. 050-053) are not blocked by an unrelated failure
in an earlier migration (e.g. 048).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 21:54:08 +01:00
Benjamin Admin
5f8aebf5b1 fix: make migrations 048/049 safe for environments without canonical tables
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 32s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 17s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Successful in 2s
Migrations 048 and 049 reference canonical_processed_chunks and
canonical_controls tables which may not exist on all environments.
Wrap ALTER TABLE statements in DO blocks that check for table
existence first. This unblocks migrations 050-053 on production.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 21:45:00 +01:00
Benjamin Admin
c74f506415 fix: add API proxy routes for process-tasks and evidence-checks
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 31s
CI/CD / test-python-backend-compliance (push) Successful in 34s
CI/CD / test-python-document-crawler (push) Successful in 23s
CI/CD / test-python-dsms-gateway (push) Successful in 17s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Successful in 2s
The frontend pages were calling /api/sdk/v1/compliance/process-tasks/*
and /api/sdk/v1/compliance/evidence-checks/* but no Next.js proxy
routes existed for these paths, causing 404s and empty data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 21:40:57 +01:00
Benjamin Admin
49ce417428 feat: add compliance modules 2-5 (dashboard, security templates, process manager, evidence collector)
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 32s
CI/CD / test-python-backend-compliance (push) Successful in 34s
CI/CD / test-python-document-crawler (push) Successful in 23s
CI/CD / test-python-dsms-gateway (push) Successful in 21s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Successful in 2s
Module 2: Extended Compliance Dashboard with roadmap, module-status, next-actions, snapshots, score-history
Module 3: 7 German security document templates (IT-Sicherheitskonzept, Datenschutz, Backup, Logging, Incident-Response, Zugriff, Risikomanagement)
Module 4: Compliance Process Manager with CRUD, complete/skip/seed, ~50 seed tasks, 3-tab UI
Module 5: Evidence Collector Extended with automated checks, control-mapping, coverage report, 4-tab UI

Also includes: canonical control library enhancements (verification method, categories, dedup), control generator improvements, RAG client extensions

52 tests pass, frontend builds clean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 21:03:04 +01:00
Benjamin Admin
13d13c8226 fix: add all RAG regulation codes to license mapping
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 34s
CI/CD / test-python-backend-compliance (push) Successful in 32s
CI/CD / test-python-document-crawler (push) Successful in 24s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Successful in 1s
Many regulation codes (nist_sp800_53r5, eucsa, owasp_top10_2021, EDPB
guidelines, EU laws, AT/FR/ES/NL/IT/HU laws) were defaulting to Rule 3
(restricted) because they weren't in REGULATION_LICENSE_MAP. Now all
~100 regulation codes from RAG are properly mapped to Rule 1 or 2.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 08:38:31 +01:00
Benjamin Admin
b6e6ffaaee feat: add verification method, categories, and dedup UI to control library
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 44s
CI/CD / test-python-backend-compliance (push) Successful in 40s
CI/CD / test-python-document-crawler (push) Successful in 22s
CI/CD / test-python-dsms-gateway (push) Successful in 17s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Successful in 4s
- Migration 047: verification_method + category columns, 17 category lookup table
- Backend: new filters, GET /categories, GET /controls/{id}/similar (embedding-based)
- Frontend: filter dropdowns, badges, dedup UI in ControlDetail with merge workflow
- ControlForm: verification method + category selects
- Provenance: verification methods, categories, master library strategy sections
- Fix UUID cast syntax in generator routes (::uuid -> CAST)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 07:55:22 +01:00
375 changed files with 173055 additions and 2777 deletions

1
.gitignore vendored
View File

@@ -18,6 +18,7 @@ __pycache__/
venv/
.venv/
.coverage
coverage.out
test_*.db
# Docker

View File

@@ -48,12 +48,12 @@ describe('Ingestion Script: ingest-industry-compliance.sh', () => {
expect(scriptContent).toContain('chunk_strategy=recursive')
})
it('should use chunk_size=512', () => {
expect(scriptContent).toContain('chunk_size=512')
it('should use chunk_size=1024', () => {
expect(scriptContent).toContain('chunk_size=1024')
})
it('should use chunk_overlap=50', () => {
expect(scriptContent).toContain('chunk_overlap=50')
it('should use chunk_overlap=128', () => {
expect(scriptContent).toContain('chunk_overlap=128')
})
it('should validate minimum file size', () => {

View File

@@ -591,12 +591,43 @@ async function handleV2Draft(body: Record<string, unknown>): Promise<NextRespons
cacheStats: proseCache.getStats(),
}
// Anti-Fake-Evidence: Truth label for all LLM-generated content
const truthLabel = {
generation_mode: 'draft_assistance',
truth_status: 'generated',
may_be_used_as_evidence: false,
generated_by: 'system',
}
// Fire-and-forget: persist LLM audit trail to backend
try {
const BACKEND_URL = process.env.BACKEND_COMPLIANCE_URL || 'http://backend-compliance:8002'
fetch(`${BACKEND_URL}/api/compliance/llm-audit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
entity_type: 'document',
entity_id: null,
generation_mode: 'draft_assistance',
truth_status: 'generated',
may_be_used_as_evidence: false,
llm_model: LLM_MODEL,
llm_provider: 'ollama',
input_summary: `${documentType} draft generation`,
output_summary: draft?.sections?.length ? `${draft.sections.length} sections generated` : 'draft generated',
}),
}).catch(() => {/* fire-and-forget */})
} catch {
// LLM audit persistence failure should not block the response
}
return NextResponse.json({
draft,
constraintCheck,
tokensUsed: Math.round(totalTokens),
pipelineVersion: 'v2',
auditTrail,
truthLabel,
})
}

View File

@@ -14,6 +14,76 @@ import { buildCrossCheckPrompt } from '@/lib/sdk/drafting-engine/prompts/validat
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434'
const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b'
/**
* Anti-Fake-Evidence: Verbotene Formulierungen
*
* Flags formulations that falsely claim compliance without evidence.
* Only allowed when: control_status=pass AND confidence >= E2 AND
* truth_status in (validated_internal, accepted_by_auditor).
*/
interface EvidenceContext {
controlStatus?: string
confidenceLevel?: string
truthStatus?: string
}
const FORBIDDEN_PATTERNS: Array<{
pattern: RegExp
label: string
safeAlternative: string
}> = [
{ pattern: /ist\s+compliant/gi, label: 'ist compliant', safeAlternative: 'soll compliant sein' },
{ pattern: /erfüllt\s+vollständig/gi, label: 'erfüllt vollständig', safeAlternative: 'soll vollständig erfüllt werden' },
{ pattern: /wurde\s+geprüft/gi, label: 'wurde geprüft', safeAlternative: 'soll geprüft werden' },
{ pattern: /wurde\s+umgesetzt/gi, label: 'wurde umgesetzt', safeAlternative: 'ist zur Umsetzung vorgesehen' },
{ pattern: /ist\s+auditiert/gi, label: 'ist auditiert', safeAlternative: 'soll auditiert werden' },
{ pattern: /vollständig\s+implementiert/gi, label: 'vollständig implementiert', safeAlternative: 'Implementierung ist vorgesehen' },
{ pattern: /nachweislich\s+konform/gi, label: 'nachweislich konform', safeAlternative: 'Konformität ist nachzuweisen' },
]
const CONFIDENCE_ORDER: Record<string, number> = { E0: 0, E1: 1, E2: 2, E3: 3, E4: 4 }
const VALID_TRUTH_STATUSES = new Set(['validated_internal', 'accepted_by_auditor'])
function checkForbiddenFormulations(
content: string,
evidenceContext?: EvidenceContext,
): ValidationFinding[] {
const findings: ValidationFinding[] = []
if (!content) return findings
// If evidence context shows sufficient proof, allow the formulations
if (evidenceContext) {
const { controlStatus, confidenceLevel, truthStatus } = evidenceContext
const confLevel = CONFIDENCE_ORDER[confidenceLevel ?? 'E0'] ?? 0
if (
controlStatus === 'pass' &&
confLevel >= CONFIDENCE_ORDER.E2 &&
VALID_TRUTH_STATUSES.has(truthStatus ?? '')
) {
return findings // Formulations are backed by real evidence
}
}
for (const { pattern, label, safeAlternative } of FORBIDDEN_PATTERNS) {
// Reset regex state for global patterns
pattern.lastIndex = 0
if (pattern.test(content)) {
findings.push({
id: `AFE-FORBIDDEN-${label.replace(/\s+/g, '_').toUpperCase()}`,
severity: 'error',
category: 'forbidden_formulation' as ValidationFinding['category'],
title: `Verbotene Formulierung: "${label}"`,
description: `Die Formulierung "${label}" impliziert eine nachgewiesene Compliance, die ohne ausreichenden Nachweis (Evidence >= E2, validiert) nicht verwendet werden darf.`,
documentType: 'vvt' as ScopeDocumentType,
suggestion: `Verwende stattdessen: "${safeAlternative}"`,
})
}
}
return findings
}
/**
* Stufe 1: Deterministische Pruefung
*/
@@ -221,10 +291,18 @@ export async function POST(request: NextRequest) {
// LLM unavailable, continue with deterministic results only
}
// ---------------------------------------------------------------
// Stufe 1b: Verbotene Formulierungen (Anti-Fake-Evidence)
// ---------------------------------------------------------------
const forbiddenFindings = checkForbiddenFormulations(
draftContent || '',
validationContext.evidenceContext,
)
// ---------------------------------------------------------------
// Combine results
// ---------------------------------------------------------------
const allFindings = [...deterministicFindings, ...llmFindings]
const allFindings = [...deterministicFindings, ...forbiddenFindings, ...llmFindings]
const errors = allFindings.filter(f => f.severity === 'error')
const warnings = allFindings.filter(f => f.severity === 'warning')
const suggestions = allFindings.filter(f => f.severity === 'suggestion')

View File

@@ -0,0 +1,47 @@
import { NextRequest, NextResponse } from 'next/server'
const SDK_URL = process.env.SDK_URL || 'http://ai-compliance-sdk:8090'
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const { id } = await params
const resp = await fetch(`${SDK_URL}/sdk/v1/ai-registration/${id}`)
const data = await resp.json()
return NextResponse.json(data)
} catch (err) {
return NextResponse.json({ error: 'Failed to fetch registration' }, { status: 500 })
}
}
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const { id } = await params
const tenantId = request.headers.get('x-tenant-id') || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
const body = await request.json()
const resp = await fetch(`${SDK_URL}/sdk/v1/ai-registration/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenantId },
body: JSON.stringify(body),
})
const data = await resp.json()
return NextResponse.json(data, { status: resp.status })
} catch (err) {
return NextResponse.json({ error: 'Failed to update registration' }, { status: 500 })
}
}
export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const { id } = await params
const body = await request.json()
const resp = await fetch(`${SDK_URL}/sdk/v1/ai-registration/${id}/status`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const data = await resp.json()
return NextResponse.json(data, { status: resp.status })
} catch (err) {
return NextResponse.json({ error: 'Failed to update status' }, { status: 500 })
}
}

View File

@@ -0,0 +1,32 @@
import { NextRequest, NextResponse } from 'next/server'
const SDK_URL = process.env.SDK_URL || 'http://ai-compliance-sdk:8090'
export async function GET(request: NextRequest) {
try {
const tenantId = request.headers.get('x-tenant-id') || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
const resp = await fetch(`${SDK_URL}/sdk/v1/ai-registration`, {
headers: { 'X-Tenant-ID': tenantId },
})
const data = await resp.json()
return NextResponse.json(data)
} catch (err) {
return NextResponse.json({ error: 'Failed to fetch registrations' }, { status: 500 })
}
}
export async function POST(request: NextRequest) {
try {
const tenantId = request.headers.get('x-tenant-id') || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
const body = await request.json()
const resp = await fetch(`${SDK_URL}/sdk/v1/ai-registration`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenantId },
body: JSON.stringify(body),
})
const data = await resp.json()
return NextResponse.json(data, { status: resp.status })
} catch (err) {
return NextResponse.json({ error: 'Failed to create registration' }, { status: 500 })
}
}

View File

@@ -25,16 +25,44 @@ export async function GET(request: NextRequest) {
break
case 'controls': {
const severity = searchParams.get('severity')
const domain = searchParams.get('domain')
const params = new URLSearchParams()
if (severity) params.set('severity', severity)
if (domain) params.set('domain', domain)
const qs = params.toString()
const controlParams = new URLSearchParams()
const passthrough = ['severity', 'domain', 'release_state', 'verification_method', 'category', 'evidence_type',
'target_audience', 'source', 'search', 'control_type', 'exclude_duplicates', 'sort', 'order', 'limit', 'offset']
for (const key of passthrough) {
const val = searchParams.get(key)
if (val) controlParams.set(key, val)
}
const qs = controlParams.toString()
backendPath = `/api/compliance/v1/canonical/controls${qs ? `?${qs}` : ''}`
break
}
case 'controls-count': {
const countParams = new URLSearchParams()
const countPassthrough = ['severity', 'domain', 'release_state', 'verification_method', 'category', 'evidence_type',
'target_audience', 'source', 'search', 'control_type', 'exclude_duplicates']
for (const key of countPassthrough) {
const val = searchParams.get(key)
if (val) countParams.set(key, val)
}
const countQs = countParams.toString()
backendPath = `/api/compliance/v1/canonical/controls-count${countQs ? `?${countQs}` : ''}`
break
}
case 'controls-meta': {
const metaParams = new URLSearchParams()
const metaPassthrough = ['severity', 'domain', 'release_state', 'verification_method', 'category', 'evidence_type',
'target_audience', 'source', 'search', 'control_type', 'exclude_duplicates']
for (const key of metaPassthrough) {
const val = searchParams.get(key)
if (val) metaParams.set(key, val)
}
const metaQs = metaParams.toString()
backendPath = `/api/compliance/v1/canonical/controls-meta${metaQs ? `?${metaQs}` : ''}`
break
}
case 'control': {
const controlId = searchParams.get('id')
if (!controlId) {
@@ -76,10 +104,63 @@ export async function GET(request: NextRequest) {
backendPath = '/api/compliance/v1/canonical/generate/processed-stats'
break
case 'categories':
backendPath = '/api/compliance/v1/canonical/categories'
break
case 'traceability': {
const traceId = searchParams.get('id')
if (!traceId) {
return NextResponse.json({ error: 'Missing control id' }, { status: 400 })
}
backendPath = `/api/compliance/v1/canonical/controls/${encodeURIComponent(traceId)}/traceability`
break
}
case 'provenance': {
const provId = searchParams.get('id')
if (!provId) {
return NextResponse.json({ error: 'Missing control id' }, { status: 400 })
}
backendPath = `/api/compliance/v1/canonical/controls/${encodeURIComponent(provId)}/provenance`
break
}
case 'atomic-stats':
backendPath = '/api/compliance/v1/canonical/controls/atomic-stats'
break
case 'similar': {
const simControlId = searchParams.get('id')
if (!simControlId) {
return NextResponse.json({ error: 'Missing control id' }, { status: 400 })
}
const simThreshold = searchParams.get('threshold') || '0.85'
backendPath = `/api/compliance/v1/canonical/controls/${encodeURIComponent(simControlId)}/similar?threshold=${simThreshold}`
break
}
case 'blocked-sources':
backendPath = '/api/compliance/v1/canonical/blocked-sources'
break
case 'v1-matches': {
const matchId = searchParams.get('id')
if (!matchId) {
return NextResponse.json({ error: 'Missing control id' }, { status: 400 })
}
backendPath = `/api/compliance/v1/canonical/controls/${encodeURIComponent(matchId)}/v1-matches`
break
}
case 'v1-enrichment-stats':
backendPath = '/api/compliance/v1/canonical/controls/v1-enrichment-stats'
break
case 'obligation-dedup-stats':
backendPath = '/api/compliance/v1/canonical/obligations/dedup-stats'
break
case 'controls-customer': {
const custSeverity = searchParams.get('severity')
const custDomain = searchParams.get('domain')
@@ -142,8 +223,20 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Missing control id' }, { status: 400 })
}
backendPath = `/api/compliance/v1/canonical/generate/review/${encodeURIComponent(controlId)}`
} else if (endpoint === 'bulk-review') {
backendPath = '/api/compliance/v1/canonical/generate/bulk-review'
} else if (endpoint === 'blocked-sources-cleanup') {
backendPath = '/api/compliance/v1/canonical/blocked-sources/cleanup'
} else if (endpoint === 'enrich-v1-matches') {
const dryRun = searchParams.get('dry_run') ?? 'true'
const batchSize = searchParams.get('batch_size') ?? '100'
const enrichOffset = searchParams.get('offset') ?? '0'
backendPath = `/api/compliance/v1/canonical/controls/enrich-v1-matches?dry_run=${dryRun}&batch_size=${batchSize}&offset=${enrichOffset}`
} else if (endpoint === 'obligation-dedup') {
const dryRun = searchParams.get('dry_run') ?? 'true'
const batchSize = searchParams.get('batch_size') ?? '0'
const dedupOffset = searchParams.get('offset') ?? '0'
backendPath = `/api/compliance/v1/canonical/obligations/dedup?dry_run=${dryRun}&batch_size=${batchSize}&offset=${dedupOffset}`
} else if (endpoint === 'similarity-check') {
const controlId = searchParams.get('id')
if (!controlId) {

View File

@@ -0,0 +1,129 @@
/**
* Evidence Checks API Proxy - Catch-all route
* Proxies all /api/sdk/v1/compliance/evidence-checks/* requests to backend-compliance
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${BACKEND_URL}/api/compliance/evidence-checks`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
'X-Tenant-Id': '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e',
'X-User-Id': 'admin',
}
const authHeader = request.headers.get('authorization')
if (authHeader) {
headers['Authorization'] = authHeader
}
const tenantHeader = request.headers.get('x-tenant-id')
if (tenantHeader) {
headers['X-Tenant-Id'] = tenantHeader
}
const userIdHeader = request.headers.get('x-user-id')
if (userIdHeader) {
headers['X-User-Id'] = userIdHeader
}
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(30000),
}
if (['POST', 'PUT', 'PATCH'].includes(method)) {
const contentType = request.headers.get('content-type')
if (contentType?.includes('application/json')) {
try {
const text = await request.text()
if (text && text.trim()) {
fetchOptions.body = text
}
} catch {
// Empty or invalid body
}
}
}
const response = await fetch(url, fetchOptions)
if (!response.ok) {
const errorText = await response.text()
let errorJson
try {
errorJson = JSON.parse(errorText)
} catch {
errorJson = { error: errorText }
}
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, ...errorJson },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Evidence Checks API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'GET')
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'POST')
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PUT')
}
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PATCH')
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'DELETE')
}

View File

@@ -0,0 +1,129 @@
/**
* Process Tasks API Proxy - Catch-all route
* Proxies all /api/sdk/v1/compliance/process-tasks/* requests to backend-compliance
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${BACKEND_URL}/api/compliance/process-tasks`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
'X-Tenant-Id': '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e',
'X-User-Id': 'admin',
}
const authHeader = request.headers.get('authorization')
if (authHeader) {
headers['Authorization'] = authHeader
}
const tenantHeader = request.headers.get('x-tenant-id')
if (tenantHeader) {
headers['X-Tenant-Id'] = tenantHeader
}
const userIdHeader = request.headers.get('x-user-id')
if (userIdHeader) {
headers['X-User-Id'] = userIdHeader
}
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(30000),
}
if (['POST', 'PUT', 'PATCH'].includes(method)) {
const contentType = request.headers.get('content-type')
if (contentType?.includes('application/json')) {
try {
const text = await request.text()
if (text && text.trim()) {
fetchOptions.body = text
}
} catch {
// Empty or invalid body
}
}
}
const response = await fetch(url, fetchOptions)
if (!response.ok) {
const errorText = await response.text()
let errorJson
try {
errorJson = JSON.parse(errorText)
} catch {
errorJson = { error: errorText }
}
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, ...errorJson },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Process Tasks API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'GET')
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'POST')
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PUT')
}
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PATCH')
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'DELETE')
}

View File

@@ -53,7 +53,18 @@ async function proxyRequest(
}
}
const response = await fetch(url, fetchOptions)
const response = await fetch(url, {
...fetchOptions,
redirect: 'manual',
})
// Handle redirects (e.g. media stream presigned URL)
if (response.status === 307 || response.status === 302) {
const location = response.headers.get('location')
if (location) {
return NextResponse.redirect(location)
}
}
if (!response.ok) {
const errorText = await response.text()
@@ -69,6 +80,19 @@ async function proxyRequest(
)
}
// Handle binary responses (PDF, octet-stream)
const contentType = response.headers.get('content-type') || ''
if (contentType.includes('application/pdf') || contentType.includes('application/octet-stream')) {
const buffer = await response.arrayBuffer()
return new NextResponse(buffer, {
status: response.status,
headers: {
'Content-Type': contentType,
'Content-Disposition': response.headers.get('content-disposition') || '',
},
})
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {

View File

@@ -0,0 +1,57 @@
import { NextRequest, NextResponse } from 'next/server'
const SDK_URL = process.env.SDK_URL || 'http://ai-compliance-sdk:8090'
const DEFAULT_TENANT = process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
/**
* Proxy: /api/sdk/v1/ucca/decision-tree/... → Go Backend /sdk/v1/ucca/decision-tree/...
*/
async function proxyRequest(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
const { path } = await params
const subPath = path ? path.join('/') : ''
const search = request.nextUrl.search || ''
const targetUrl = `${SDK_URL}/sdk/v1/ucca/decision-tree/${subPath}${search}`
const tenantID = request.headers.get('X-Tenant-ID') || DEFAULT_TENANT
try {
const headers: Record<string, string> = {
'X-Tenant-ID': tenantID,
}
const fetchOptions: RequestInit = {
method: request.method,
headers,
}
if (request.method === 'POST' || request.method === 'PUT' || request.method === 'PATCH') {
const body = await request.json()
headers['Content-Type'] = 'application/json'
fetchOptions.body = JSON.stringify(body)
}
const response = await fetch(targetUrl, fetchOptions)
if (!response.ok) {
const errorText = await response.text()
console.error(`Decision tree proxy error [${request.method} ${subPath}]:`, errorText)
return NextResponse.json(
{ error: 'Backend error', details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data, { status: response.status })
} catch (error) {
console.error('Decision tree proxy connection error:', error)
return NextResponse.json(
{ error: 'Failed to connect to AI compliance backend' },
{ status: 503 }
)
}
}
export const GET = proxyRequest
export const POST = proxyRequest
export const DELETE = proxyRequest

View File

@@ -0,0 +1,36 @@
import { NextRequest, NextResponse } from 'next/server'
const SDK_URL = process.env.SDK_URL || 'http://ai-compliance-sdk:8090'
const DEFAULT_TENANT = process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
/**
* Proxy: GET /api/sdk/v1/ucca/decision-tree → Go Backend GET /sdk/v1/ucca/decision-tree
* Returns the decision tree definition (questions, structure)
*/
export async function GET(request: NextRequest) {
const tenantID = request.headers.get('X-Tenant-ID') || DEFAULT_TENANT
try {
const response = await fetch(`${SDK_URL}/sdk/v1/ucca/decision-tree`, {
headers: { 'X-Tenant-ID': tenantID },
})
if (!response.ok) {
const errorText = await response.text()
console.error('Decision tree GET error:', errorText)
return NextResponse.json(
{ error: 'Backend error', details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Decision tree proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to AI compliance backend' },
{ status: 503 }
)
}
}

View File

@@ -333,6 +333,71 @@ function AdvisoryBoardPageInner() {
purposes: [] as string[],
// Automation (single-select tile)
automation: '' as string,
// BetrVG / works council
employee_monitoring: false,
hr_decision_support: false,
works_council_consulted: false,
// Domain-specific contexts (Annex III)
hr_automated_screening: false,
hr_automated_rejection: false,
hr_candidate_ranking: false,
hr_bias_audits: false,
hr_agg_visible: false,
hr_human_review: false,
hr_performance_eval: false,
edu_grade_influence: false,
edu_exam_evaluation: false,
edu_student_selection: false,
edu_minors: false,
edu_teacher_review: false,
hc_diagnosis: false,
hc_treatment: false,
hc_triage: false,
hc_patient_data: false,
hc_medical_device: false,
hc_clinical_validation: false,
// Legal
leg_legal_advice: false, leg_court_prediction: false, leg_client_confidential: false,
// Public Sector
pub_admin_decision: false, pub_benefit_allocation: false, pub_transparency: false,
// Critical Infrastructure
crit_grid_control: false, crit_safety_critical: false, crit_redundancy: false,
// Automotive
auto_autonomous: false, auto_safety: false, auto_functional_safety: false,
// Retail
ret_pricing: false, ret_profiling: false, ret_credit_scoring: false, ret_dark_patterns: false,
// IT Security
its_surveillance: false, its_threat_detection: false, its_data_retention: false,
// Logistics
log_driver_tracking: false, log_workload_scoring: false,
// Construction
con_tenant_screening: false, con_worker_safety: false,
// Marketing
mkt_deepfake: false, mkt_minors: false, mkt_targeting: false, mkt_labeled: false,
// Manufacturing
mfg_machine_safety: false, mfg_ce_required: false, mfg_validated: false,
// Agriculture
agr_pesticide: false, agr_animal_welfare: false, agr_environmental: false,
// Social Services
soc_vulnerable: false, soc_benefit: false, soc_case_mgmt: false,
// Hospitality
hos_guest_profiling: false, hos_dynamic_pricing: false, hos_review_manipulation: false,
// Insurance
ins_risk_class: false, ins_claims: false, ins_premium: false, ins_fraud: false,
// Investment
inv_algo_trading: false, inv_advice: false, inv_robo: false,
// Defense
def_dual_use: false, def_export: false, def_classified: false,
// Supply Chain
sch_supplier: false, sch_human_rights: false, sch_environmental: false,
// Facility
fac_access: false, fac_occupancy: false, fac_energy: false,
// Sports
spo_athlete: false, spo_fan: false, spo_doping: false,
// Finance / Banking
fin_credit_scoring: false, fin_aml_kyc: false, fin_algo_decisions: false, fin_customer_profiling: false,
// General
gen_affects_people: false, gen_automated_decisions: false, gen_sensitive_data: false,
// Hosting (single-select tile)
hosting_provider: '' as string,
hosting_region: '' as string,
@@ -420,7 +485,131 @@ function AdvisoryBoardPageInner() {
retention_purpose: form.retention_purpose,
contracts_list: form.contracts,
subprocessors: form.subprocessors,
employee_monitoring: form.employee_monitoring,
hr_decision_support: form.hr_decision_support,
works_council_consulted: form.works_council_consulted,
// Domain-specific contexts
hr_context: ['hr', 'recruiting'].includes(form.domain) ? {
automated_screening: form.hr_automated_screening,
automated_rejection: form.hr_automated_rejection,
candidate_ranking: form.hr_candidate_ranking,
bias_audits_done: form.hr_bias_audits,
agg_categories_visible: form.hr_agg_visible,
human_review_enforced: form.hr_human_review,
performance_evaluation: form.hr_performance_eval,
} : undefined,
education_context: ['education', 'higher_education', 'vocational_training', 'research'].includes(form.domain) ? {
grade_influence: form.edu_grade_influence,
exam_evaluation: form.edu_exam_evaluation,
student_selection: form.edu_student_selection,
minors_involved: form.edu_minors,
teacher_review_required: form.edu_teacher_review,
} : undefined,
healthcare_context: ['healthcare', 'medical_devices', 'pharma', 'elderly_care'].includes(form.domain) ? {
diagnosis_support: form.hc_diagnosis,
treatment_recommendation: form.hc_treatment,
triage_decision: form.hc_triage,
patient_data_processed: form.hc_patient_data,
medical_device: form.hc_medical_device,
clinical_validation: form.hc_clinical_validation,
} : undefined,
legal_context: ['legal', 'consulting', 'tax_advisory'].includes(form.domain) ? {
legal_advice: form.leg_legal_advice,
court_prediction: form.leg_court_prediction,
client_confidential: form.leg_client_confidential,
} : undefined,
public_sector_context: ['public_sector', 'defense', 'justice'].includes(form.domain) ? {
admin_decision: form.pub_admin_decision,
benefit_allocation: form.pub_benefit_allocation,
transparency_ensured: form.pub_transparency,
} : undefined,
critical_infra_context: ['energy', 'utilities', 'oil_gas'].includes(form.domain) ? {
grid_control: form.crit_grid_control,
safety_critical: form.crit_safety_critical,
redundancy_exists: form.crit_redundancy,
} : undefined,
automotive_context: ['automotive', 'aerospace'].includes(form.domain) ? {
autonomous_driving: form.auto_autonomous,
safety_relevant: form.auto_safety,
functional_safety: form.auto_functional_safety,
} : undefined,
retail_context: ['retail', 'ecommerce', 'wholesale'].includes(form.domain) ? {
pricing_personalized: form.ret_pricing,
credit_scoring: form.ret_credit_scoring,
dark_patterns: form.ret_dark_patterns,
} : undefined,
it_security_context: ['it_services', 'cybersecurity', 'telecom'].includes(form.domain) ? {
employee_surveillance: form.its_surveillance,
threat_detection: form.its_threat_detection,
data_retention_logs: form.its_data_retention,
} : undefined,
logistics_context: ['logistics'].includes(form.domain) ? {
driver_tracking: form.log_driver_tracking,
workload_scoring: form.log_workload_scoring,
} : undefined,
construction_context: ['construction', 'real_estate', 'facility_management'].includes(form.domain) ? {
tenant_screening: form.con_tenant_screening,
worker_safety: form.con_worker_safety,
} : undefined,
marketing_context: ['marketing', 'media', 'entertainment'].includes(form.domain) ? {
deepfake_content: form.mkt_deepfake,
behavioral_targeting: form.mkt_targeting,
minors_targeted: form.mkt_minors,
ai_content_labeled: form.mkt_labeled,
} : undefined,
manufacturing_context: ['mechanical_engineering', 'electrical_engineering', 'plant_engineering', 'chemicals', 'food_beverage'].includes(form.domain) ? {
machine_safety: form.mfg_machine_safety,
ce_marking_required: form.mfg_ce_required,
safety_validated: form.mfg_validated,
} : undefined,
agriculture_context: ['agriculture', 'forestry', 'fishing'].includes(form.domain) ? {
pesticide_ai: form.agr_pesticide,
animal_welfare: form.agr_animal_welfare,
environmental_data: form.agr_environmental,
} : undefined,
social_services_context: ['social_services', 'nonprofit'].includes(form.domain) ? {
vulnerable_groups: form.soc_vulnerable,
benefit_decision: form.soc_benefit,
case_management: form.soc_case_mgmt,
} : undefined,
hospitality_context: ['hospitality', 'tourism'].includes(form.domain) ? {
guest_profiling: form.hos_guest_profiling,
dynamic_pricing: form.hos_dynamic_pricing,
review_manipulation: form.hos_review_manipulation,
} : undefined,
insurance_context: ['insurance'].includes(form.domain) ? {
risk_classification: form.ins_risk_class,
claims_automation: form.ins_claims,
premium_calculation: form.ins_premium,
fraud_detection: form.ins_fraud,
} : undefined,
investment_context: ['investment'].includes(form.domain) ? {
algo_trading: form.inv_algo_trading,
investment_advice: form.inv_advice,
robo_advisor: form.inv_robo,
} : undefined,
defense_context: ['defense'].includes(form.domain) ? {
dual_use: form.def_dual_use,
export_controlled: form.def_export,
classified_data: form.def_classified,
} : undefined,
supply_chain_context: ['textiles', 'packaging'].includes(form.domain) ? {
supplier_monitoring: form.sch_supplier,
human_rights_check: form.sch_human_rights,
environmental_impact: form.sch_environmental,
} : undefined,
facility_context: ['facility_management'].includes(form.domain) ? {
access_control_ai: form.fac_access,
occupancy_tracking: form.fac_occupancy,
energy_optimization: form.fac_energy,
} : undefined,
sports_context: ['sports'].includes(form.domain) ? {
athlete_tracking: form.spo_athlete,
fan_profiling: form.spo_fan,
} : undefined,
store_raw_text: true,
// Finance/Banking and General don't need separate context structs —
// their fields are evaluated via existing FinancialContext or generic rules
}
const url = isEditMode
@@ -777,6 +966,567 @@ function AdvisoryBoardPageInner() {
Informationspflicht, Recht auf menschliche Ueberpruefung und Anfechtungsmoeglichkeit.
</p>
</div>
{/* BetrVG Section */}
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Betriebsrat & Beschaeftigtendaten</h3>
<p className="text-xs text-gray-500 mb-4">
Relevant fuer deutsche Unternehmen mit Betriebsrat (§87 Abs.1 Nr.6 BetrVG).
</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input
type="checkbox"
checked={form.employee_monitoring}
onChange={(e) => updateForm({ employee_monitoring: e.target.checked })}
className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
/>
<div>
<span className="text-sm font-medium text-gray-900">System kann Verhalten/Leistung ueberwachen</span>
<p className="text-xs text-gray-500">Nutzungslogs, Produktivitaetskennzahlen, Kommunikationsanalyse</p>
</div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input
type="checkbox"
checked={form.hr_decision_support}
onChange={(e) => updateForm({ hr_decision_support: e.target.checked })}
className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
/>
<div>
<span className="text-sm font-medium text-gray-900">System unterstuetzt HR-Entscheidungen</span>
<p className="text-xs text-gray-500">Recruiting, Bewertung, Befoerderung, Kuendigung</p>
</div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input
type="checkbox"
checked={form.works_council_consulted}
onChange={(e) => updateForm({ works_council_consulted: e.target.checked })}
className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
/>
<div>
<span className="text-sm font-medium text-gray-900">Betriebsrat wurde konsultiert</span>
<p className="text-xs text-gray-500">Betriebsvereinbarung liegt vor oder ist in Verhandlung</p>
</div>
</label>
</div>
</div>
{/* Domain-specific questions — HR/Recruiting */}
{['hr', 'recruiting'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">HR & Recruiting Hochrisiko-Pruefung</h3>
<p className="text-xs text-gray-500 mb-4">AI Act Annex III Nr. 4 + AGG Pflichtfragen bei KI im Personalbereich.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.hr_automated_screening} onChange={(e) => updateForm({ hr_automated_screening: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Bewerber werden automatisch vorsortiert/gerankt</span><p className="text-xs text-gray-500">CV-Screening, Score-basierte Vorauswahl</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.hr_automated_rejection} onChange={(e) => updateForm({ hr_automated_rejection: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">Absagen werden automatisch versendet</span><p className="text-xs text-red-700">Art. 22 DSGVO: Vollautomatische Absagen grundsaetzlich unzulaessig!</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.hr_agg_visible} onChange={(e) => updateForm({ hr_agg_visible: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">System kann AGG-Merkmale erkennen (Name, Foto, Alter)</span><p className="text-xs text-gray-500">Proxy-Diskriminierung: NameHerkunft, FotoGeschlecht</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.hr_performance_eval} onChange={(e) => updateForm({ hr_performance_eval: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">System bewertet Mitarbeiterleistung</span><p className="text-xs text-gray-500">Performance Reviews, KPI-Tracking</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-green-200 bg-green-50 hover:bg-green-100 cursor-pointer">
<input type="checkbox" checked={form.hr_bias_audits} onChange={(e) => updateForm({ hr_bias_audits: e.target.checked })} className="w-4 h-4 rounded border-green-300 text-green-600 focus:ring-green-500" />
<div><span className="text-sm font-medium text-green-900">Regelmaessige Bias-Audits durchgefuehrt</span><p className="text-xs text-green-700">Analyse nach Geschlecht, Alter, Herkunft</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-green-200 bg-green-50 hover:bg-green-100 cursor-pointer">
<input type="checkbox" checked={form.hr_human_review} onChange={(e) => updateForm({ hr_human_review: e.target.checked })} className="w-4 h-4 rounded border-green-300 text-green-600 focus:ring-green-500" />
<div><span className="text-sm font-medium text-green-900">Mensch prueft jede KI-Empfehlung</span><p className="text-xs text-green-700">Kein Rubber Stamping echte Pruefung</p></div>
</label>
</div>
</div>
)}
{/* Domain-specific questions — Education */}
{['education', 'higher_education', 'vocational_training', 'research'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Bildung Hochrisiko-Pruefung</h3>
<p className="text-xs text-gray-500 mb-4">AI Act Annex III Nr. 3 bei KI in Bildung und Ausbildung.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.edu_grade_influence} onChange={(e) => updateForm({ edu_grade_influence: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI beeinflusst Noten oder Bewertungen</span><p className="text-xs text-gray-500">Notenvorschlaege, Bewertungsunterstuetzung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.edu_exam_evaluation} onChange={(e) => updateForm({ edu_exam_evaluation: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI bewertet Pruefungen/Klausuren</span><p className="text-xs text-gray-500">Automatische Korrektur, Bewertungsvorschlaege</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.edu_student_selection} onChange={(e) => updateForm({ edu_student_selection: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI beeinflusst Zugang zu Bildungsangeboten</span><p className="text-xs text-gray-500">Zulassung, Kursempfehlungen, Einstufung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.edu_minors} onChange={(e) => updateForm({ edu_minors: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">Minderjaehrige sind betroffen</span><p className="text-xs text-red-700">Besonderer Schutz (Art. 24 EU-Grundrechtecharta)</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-green-200 bg-green-50 hover:bg-green-100 cursor-pointer">
<input type="checkbox" checked={form.edu_teacher_review} onChange={(e) => updateForm({ edu_teacher_review: e.target.checked })} className="w-4 h-4 rounded border-green-300 text-green-600 focus:ring-green-500" />
<div><span className="text-sm font-medium text-green-900">Lehrkraft prueft jedes KI-Ergebnis</span><p className="text-xs text-green-700">Human Oversight vor Mitteilung an Schueler</p></div>
</label>
</div>
</div>
)}
{/* Domain-specific questions — Healthcare */}
{['healthcare', 'medical_devices', 'pharma', 'elderly_care'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Gesundheitswesen Hochrisiko-Pruefung</h3>
<p className="text-xs text-gray-500 mb-4">AI Act Annex III Nr. 5 + MDR (EU) 2017/745.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.hc_diagnosis} onChange={(e) => updateForm({ hc_diagnosis: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI unterstuetzt Diagnosen</span><p className="text-xs text-gray-500">Diagnosevorschlaege, Bildgebungsauswertung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.hc_treatment} onChange={(e) => updateForm({ hc_treatment: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI empfiehlt Behandlungen</span><p className="text-xs text-gray-500">Therapievorschlaege, Medikation</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.hc_triage} onChange={(e) => updateForm({ hc_triage: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">KI priorisiert Patienten (Triage)</span><p className="text-xs text-red-700">Lebenskritisch erhoehte Anforderungen</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.hc_patient_data} onChange={(e) => updateForm({ hc_patient_data: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Gesundheitsdaten verarbeitet</span><p className="text-xs text-gray-500">Art. 9 DSGVO besondere Kategorie</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.hc_medical_device} onChange={(e) => updateForm({ hc_medical_device: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">System ist Medizinprodukt (MDR)</span><p className="text-xs text-gray-500">MDR (EU) 2017/745 Zertifizierung erforderlich</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-green-200 bg-green-50 hover:bg-green-100 cursor-pointer">
<input type="checkbox" checked={form.hc_clinical_validation} onChange={(e) => updateForm({ hc_clinical_validation: e.target.checked })} className="w-4 h-4 rounded border-green-300 text-green-600 focus:ring-green-500" />
<div><span className="text-sm font-medium text-green-900">Klinisch validiert</span><p className="text-xs text-green-700">System wurde in klinischer Studie geprueft</p></div>
</label>
</div>
</div>
)}
{/* Legal / Justice */}
{['legal', 'consulting', 'tax_advisory'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Recht & Beratung Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">AI Act Annex III Nr. 8 KI in Rechtspflege und Demokratie.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.leg_legal_advice} onChange={(e) => updateForm({ leg_legal_advice: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI gibt Rechtsberatung oder rechtliche Empfehlungen</span><p className="text-xs text-gray-500">Vertragsanalyse, rechtliche Einschaetzungen, Compliance-Checks</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.leg_court_prediction} onChange={(e) => updateForm({ leg_court_prediction: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI prognostiziert Verfahrensausgaenge</span><p className="text-xs text-gray-500">Urteilsprognosen, Risikoeinschaetzung von Rechtsstreitigkeiten</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.leg_client_confidential} onChange={(e) => updateForm({ leg_client_confidential: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Mandantengeheimnis betroffen</span><p className="text-xs text-gray-500">Vertrauliche Mandantendaten werden durch KI verarbeitet (§ 203 StGB)</p></div>
</label>
</div>
</div>
)}
{/* Public Sector */}
{['public_sector', 'defense', 'justice'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Oeffentlicher Sektor Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">Art. 27 AI Act FRIA-Pflicht fuer oeffentliche Stellen.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.pub_admin_decision} onChange={(e) => updateForm({ pub_admin_decision: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">KI beeinflusst Verwaltungsentscheidungen</span><p className="text-xs text-red-700">Bescheide, Bewilligungen, Genehmigungen FRIA erforderlich</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.pub_benefit_allocation} onChange={(e) => updateForm({ pub_benefit_allocation: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI verteilt Leistungen oder Foerderung</span><p className="text-xs text-gray-500">Sozialleistungen, Subventionen, Zuteilungen</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-green-200 bg-green-50 hover:bg-green-100 cursor-pointer">
<input type="checkbox" checked={form.pub_transparency} onChange={(e) => updateForm({ pub_transparency: e.target.checked })} className="w-4 h-4 rounded border-green-300 text-green-600 focus:ring-green-500" />
<div><span className="text-sm font-medium text-green-900">Transparenz gegenueber Buergern sichergestellt</span><p className="text-xs text-green-700">Buerger werden ueber KI-Nutzung informiert</p></div>
</label>
</div>
</div>
)}
{/* Critical Infrastructure */}
{['energy', 'utilities', 'oil_gas'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Kritische Infrastruktur Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">AI Act Annex III Nr. 2 + NIS2.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.crit_grid_control} onChange={(e) => updateForm({ crit_grid_control: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI steuert Netz oder Infrastruktur</span><p className="text-xs text-gray-500">Stromnetz, Wasserversorgung, Gasverteilung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.crit_safety_critical} onChange={(e) => updateForm({ crit_safety_critical: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">Sicherheitskritische Steuerung</span><p className="text-xs text-red-700">Fehler koennen Menschenleben gefaehrden</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-green-200 bg-green-50 hover:bg-green-100 cursor-pointer">
<input type="checkbox" checked={form.crit_redundancy} onChange={(e) => updateForm({ crit_redundancy: e.target.checked })} className="w-4 h-4 rounded border-green-300 text-green-600 focus:ring-green-500" />
<div><span className="text-sm font-medium text-green-900">Redundante Systeme vorhanden</span><p className="text-xs text-green-700">Fallback bei KI-Ausfall sichergestellt</p></div>
</label>
</div>
</div>
)}
{/* Automotive / Aerospace */}
{['automotive', 'aerospace'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Automotive / Aerospace Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">Safety-critical AI Typgenehmigung + Functional Safety.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.auto_autonomous} onChange={(e) => updateForm({ auto_autonomous: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">Autonomes Fahren / ADAS</span><p className="text-xs text-red-700">Hochrisiko erfordert Typgenehmigung und extensive Validierung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.auto_safety} onChange={(e) => updateForm({ auto_safety: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Sicherheitsrelevante Funktion</span><p className="text-xs text-gray-500">Bremsen, Lenkung, Kollisionsvermeidung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-green-200 bg-green-50 hover:bg-green-100 cursor-pointer">
<input type="checkbox" checked={form.auto_functional_safety} onChange={(e) => updateForm({ auto_functional_safety: e.target.checked })} className="w-4 h-4 rounded border-green-300 text-green-600 focus:ring-green-500" />
<div><span className="text-sm font-medium text-green-900">ISO 26262 Functional Safety beruecksichtigt</span><p className="text-xs text-green-700">ASIL-Einstufung und Sicherheitsvalidierung durchgefuehrt</p></div>
</label>
</div>
</div>
)}
{/* Retail / E-Commerce */}
{['retail', 'ecommerce', 'wholesale'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Handel & E-Commerce Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">DSA, Verbraucherrecht, DSGVO Art. 22.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.ret_pricing} onChange={(e) => updateForm({ ret_pricing: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Personalisierte Preisgestaltung</span><p className="text-xs text-gray-500">Individuelle Preise basierend auf Nutzerprofil</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.ret_credit_scoring} onChange={(e) => updateForm({ ret_credit_scoring: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Bonitaetspruefung bei Kauf auf Rechnung</span><p className="text-xs text-gray-500">Kredit-Scoring beeinflusst Zugang zu Zahlungsarten</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.ret_dark_patterns} onChange={(e) => updateForm({ ret_dark_patterns: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Manipulative UI-Muster moeglich (Dark Patterns)</span><p className="text-xs text-gray-500">Kuenstliche Verknappung, Social Proof, versteckte Kosten</p></div>
</label>
</div>
</div>
)}
{/* IT / Cybersecurity / Telecom */}
{['it_services', 'cybersecurity', 'telecom'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">IT & Cybersecurity Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">NIS2, DSGVO, BetrVG §87.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.its_surveillance} onChange={(e) => updateForm({ its_surveillance: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Mitarbeiterueberwachung (SIEM, DLP, UBA)</span><p className="text-xs text-gray-500">User Behavior Analytics, Data Loss Prevention mit Personenbezug</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.its_threat_detection} onChange={(e) => updateForm({ its_threat_detection: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI-gestuetzte Bedrohungserkennung</span><p className="text-xs text-gray-500">Anomalie-Erkennung, Intrusion Detection</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.its_data_retention} onChange={(e) => updateForm({ its_data_retention: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Umfangreiche Log-Speicherung</span><p className="text-xs text-gray-500">Security-Logs mit Personenbezug werden langfristig gespeichert</p></div>
</label>
</div>
</div>
)}
{/* Logistics */}
{['logistics'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Logistik Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">BetrVG §87, DSGVO Worker Tracking.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.log_driver_tracking} onChange={(e) => updateForm({ log_driver_tracking: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Fahrer-/Kurier-Tracking (GPS)</span><p className="text-xs text-gray-500">Standortverfolgung von Mitarbeitern</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.log_workload_scoring} onChange={(e) => updateForm({ log_workload_scoring: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Leistungsbewertung von Lager-/Liefermitarbeitern</span><p className="text-xs text-gray-500">Picks/Stunde, Liefergeschwindigkeit, Performance-Scores</p></div>
</label>
</div>
</div>
)}
{/* Construction / Real Estate */}
{['construction', 'real_estate', 'facility_management'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Bau & Immobilien Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">AGG, DSGVO, Arbeitsschutz.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.con_tenant_screening} onChange={(e) => updateForm({ con_tenant_screening: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI-gestuetzte Mieterauswahl</span><p className="text-xs text-gray-500">Bonitaetspruefung, Bewerber-Ranking fuer Wohnungen</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.con_worker_safety} onChange={(e) => updateForm({ con_worker_safety: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI-Arbeitsschutzueberwachung auf Baustellen</span><p className="text-xs text-gray-500">Kamera-basierte Sicherheitsueberwachung, Helm-Erkennung</p></div>
</label>
</div>
</div>
)}
{/* Marketing / Media */}
{['marketing', 'media', 'entertainment'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Marketing & Medien Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">Art. 50 AI Act (Deepfakes), DSA, DSGVO.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.mkt_deepfake} onChange={(e) => updateForm({ mkt_deepfake: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">Synthetische Inhalte (Deepfakes)</span><p className="text-xs text-red-700">KI-generierte Bilder, Videos oder Stimmen Kennzeichnungspflicht!</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.mkt_targeting} onChange={(e) => updateForm({ mkt_targeting: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Verhaltensbasiertes Targeting</span><p className="text-xs text-gray-500">Personalisierte Werbung basierend auf Nutzerverhalten</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.mkt_minors} onChange={(e) => updateForm({ mkt_minors: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">Minderjaehrige als Zielgruppe</span><p className="text-xs text-red-700">Besonderer Schutz DSA Art. 28 verbietet Profiling Minderjaehriger</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-green-200 bg-green-50 hover:bg-green-100 cursor-pointer">
<input type="checkbox" checked={form.mkt_labeled} onChange={(e) => updateForm({ mkt_labeled: e.target.checked })} className="w-4 h-4 rounded border-green-300 text-green-600 focus:ring-green-500" />
<div><span className="text-sm font-medium text-green-900">KI-Inhalte werden als solche gekennzeichnet</span><p className="text-xs text-green-700">Art. 50 AI Act: Pflicht zur Kennzeichnung synthetischer Inhalte</p></div>
</label>
</div>
</div>
)}
{/* Manufacturing */}
{['mechanical_engineering', 'electrical_engineering', 'plant_engineering', 'chemicals', 'food_beverage', 'textiles', 'packaging'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Fertigung Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">Maschinenverordnung (EU) 2023/1230, CE-Kennzeichnung.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.mfg_machine_safety} onChange={(e) => updateForm({ mfg_machine_safety: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">KI in Maschinensicherheit</span><p className="text-xs text-red-700">Sicherheitsrelevante Steuerung Validierung erforderlich</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.mfg_ce_required} onChange={(e) => updateForm({ mfg_ce_required: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">CE-Kennzeichnung erforderlich</span><p className="text-xs text-gray-500">Maschinenverordnung (EU) 2023/1230</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-green-200 bg-green-50 hover:bg-green-100 cursor-pointer">
<input type="checkbox" checked={form.mfg_validated} onChange={(e) => updateForm({ mfg_validated: e.target.checked })} className="w-4 h-4 rounded border-green-300 text-green-600 focus:ring-green-500" />
<div><span className="text-sm font-medium text-green-900">Sicherheitsvalidierung durchgefuehrt</span><p className="text-xs text-green-700">Konformitaetsbewertung nach Maschinenverordnung abgeschlossen</p></div>
</label>
</div>
</div>
)}
{/* Agriculture */}
{['agriculture', 'forestry', 'fishing'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Landwirtschaft Compliance-Fragen</h3>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.agr_pesticide} onChange={(e) => updateForm({ agr_pesticide: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI steuert Pestizideinsatz</span><p className="text-xs text-gray-500">Precision Farming, automatisierte Ausbringung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.agr_animal_welfare} onChange={(e) => updateForm({ agr_animal_welfare: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI beeinflusst Tierhaltungsentscheidungen</span><p className="text-xs text-gray-500">Fuetterung, Gesundheit, Stallmanagement</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.agr_environmental} onChange={(e) => updateForm({ agr_environmental: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Umweltdaten werden verarbeitet</span><p className="text-xs text-gray-500">Boden, Wasser, Emissionen</p></div>
</label>
</div>
</div>
)}
{/* Social Services */}
{['social_services', 'nonprofit'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Soziales Compliance-Fragen</h3>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.soc_vulnerable} onChange={(e) => updateForm({ soc_vulnerable: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">Schutzbeduerftiger Personenkreis betroffen</span><p className="text-xs text-red-700">Kinder, Senioren, Gefluechtete, Menschen mit Behinderung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.soc_benefit} onChange={(e) => updateForm({ soc_benefit: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI beeinflusst Leistungszuteilung</span><p className="text-xs text-gray-500">Sozialleistungen, Hilfsangebote, Foerderung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.soc_case_mgmt} onChange={(e) => updateForm({ soc_case_mgmt: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI in Fallmanagement</span><p className="text-xs text-gray-500">Priorisierung, Zuordnung, Verlaufsprognose</p></div>
</label>
</div>
</div>
)}
{/* Hospitality / Tourism */}
{['hospitality', 'tourism'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Tourismus & Gastronomie Compliance-Fragen</h3>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.hos_guest_profiling} onChange={(e) => updateForm({ hos_guest_profiling: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Gaeste-Profilbildung</span><p className="text-xs text-gray-500">Praeferenzen, Buchungsverhalten, Segmentierung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.hos_dynamic_pricing} onChange={(e) => updateForm({ hos_dynamic_pricing: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Dynamische Preisgestaltung</span><p className="text-xs text-gray-500">Personalisierte Zimmer-/Flugreise</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.hos_review_manipulation} onChange={(e) => updateForm({ hos_review_manipulation: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">KI manipuliert oder generiert Bewertungen</span><p className="text-xs text-red-700">Fake Reviews sind unzulaessig (UWG, DSA)</p></div>
</label>
</div>
</div>
)}
{/* Insurance */}
{['insurance'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Versicherung Compliance-Fragen</h3>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.ins_premium} onChange={(e) => updateForm({ ins_premium: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI berechnet individuelle Praemien</span><p className="text-xs text-gray-500">Risikoadjustierte Preisgestaltung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.ins_claims} onChange={(e) => updateForm({ ins_claims: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Automatisierte Schadenbearbeitung</span><p className="text-xs text-gray-500">KI entscheidet ueber Schadenregulierung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.ins_fraud} onChange={(e) => updateForm({ ins_fraud: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI-Betrugserkennung</span><p className="text-xs text-gray-500">Automatische Verdachtsfallerkennung</p></div>
</label>
</div>
</div>
)}
{/* Investment */}
{['investment'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Investment Compliance-Fragen</h3>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.inv_algo_trading} onChange={(e) => updateForm({ inv_algo_trading: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Algorithmischer Handel</span><p className="text-xs text-gray-500">Automated Trading, HFT MiFID II relevant</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.inv_robo} onChange={(e) => updateForm({ inv_robo: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Robo Advisor / KI-Anlageberatung</span><p className="text-xs text-gray-500">Automatisierte Vermoegensberatung WpHG-Pflichten</p></div>
</label>
</div>
</div>
)}
{/* Defense */}
{['defense'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Verteidigung Compliance-Fragen</h3>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 cursor-pointer">
<input type="checkbox" checked={form.def_dual_use} onChange={(e) => updateForm({ def_dual_use: e.target.checked })} className="w-4 h-4 rounded border-red-300 text-red-600 focus:ring-red-500" />
<div><span className="text-sm font-medium text-red-900">Dual-Use KI-Technologie</span><p className="text-xs text-red-700">Exportkontrolle (EU VO 2021/821) beachten</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.def_classified} onChange={(e) => updateForm({ def_classified: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Verschlusssachen werden verarbeitet</span><p className="text-xs text-gray-500">VS-NfD oder hoeher besondere Schutzmassnahmen</p></div>
</label>
</div>
</div>
)}
{/* Supply Chain (Textiles, Packaging) */}
{['textiles', 'packaging'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Lieferkette Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">LkSG Lieferkettensorgfaltspflichtengesetz.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.sch_supplier} onChange={(e) => updateForm({ sch_supplier: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI ueberwacht Lieferanten</span><p className="text-xs text-gray-500">Lieferantenbewertung, Risikoanalyse</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.sch_human_rights} onChange={(e) => updateForm({ sch_human_rights: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI prueft Menschenrechte in Lieferkette</span><p className="text-xs text-gray-500">LkSG-Sorgfaltspflichten, Kinderarbeit, Zwangsarbeit</p></div>
</label>
</div>
</div>
)}
{/* Sports */}
{['sports'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Sport Compliance-Fragen</h3>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.spo_athlete} onChange={(e) => updateForm({ spo_athlete: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Athleten-Performance-Tracking</span><p className="text-xs text-gray-500">GPS, Biometrie, Leistungsdaten</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.spo_fan} onChange={(e) => updateForm({ spo_fan: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Fan-/Zuschauer-Profilbildung</span><p className="text-xs text-gray-500">Ticketing, Merchandising, Stadion-Tracking</p></div>
</label>
</div>
</div>
)}
{/* Finance / Banking */}
{['finance', 'banking'].includes(form.domain) && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Finanzdienstleistungen Compliance-Fragen</h3>
<p className="text-xs text-gray-500 mb-4">DORA, MaRisk, BAIT, AI Act Annex III Nr. 5.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.fin_credit_scoring} onChange={(e) => updateForm({ fin_credit_scoring: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">KI-gestuetztes Kredit-Scoring</span><p className="text-xs text-gray-500">Bonitaetsbewertung, Kreditwuerdigkeitspruefung Art. 22 DSGVO + AGG</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.fin_aml_kyc} onChange={(e) => updateForm({ fin_aml_kyc: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">AML/KYC Automatisierung</span><p className="text-xs text-gray-500">Geldwaeschebekacmpfung, Kundenidentifizierung durch KI</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.fin_algo_decisions} onChange={(e) => updateForm({ fin_algo_decisions: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Automatisierte Finanzentscheidungen</span><p className="text-xs text-gray-500">Kreditvergabe, Kontosperrung, Limitaenderungen</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.fin_customer_profiling} onChange={(e) => updateForm({ fin_customer_profiling: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Kunden-Profilbildung / Segmentierung</span><p className="text-xs text-gray-500">Risikoklassifikation, Produkt-Empfehlungen</p></div>
</label>
</div>
</div>
)}
{/* General — universal AI governance questions */}
{form.domain === 'general' && (
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Allgemeine KI-Governance</h3>
<p className="text-xs text-gray-500 mb-4">Grundlegende Compliance-Fragen fuer jeden KI-Einsatz.</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.gen_affects_people} onChange={(e) => updateForm({ gen_affects_people: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">System hat Auswirkungen auf natuerliche Personen</span><p className="text-xs text-gray-500">Entscheidungen, Empfehlungen oder Bewertungen betreffen Menschen direkt</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.gen_automated_decisions} onChange={(e) => updateForm({ gen_automated_decisions: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Automatisierte Entscheidungen werden getroffen</span><p className="text-xs text-gray-500">KI trifft oder beeinflusst Entscheidungen ohne menschliche Pruefung</p></div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.gen_sensitive_data} onChange={(e) => updateForm({ gen_sensitive_data: e.target.checked })} className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<div><span className="text-sm font-medium text-gray-900">Sensible oder vertrauliche Daten verarbeitet</span><p className="text-xs text-gray-500">Geschaeftsgeheimnisse, personenbezogene Daten, vertrauliche Informationen</p></div>
</label>
</div>
</div>
)}
</div>
)}

View File

@@ -3,6 +3,7 @@
import React, { useState, useEffect } from 'react'
import { useSDK } from '@/lib/sdk'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
import DecisionTreeWizard from '@/components/sdk/ai-act/DecisionTreeWizard'
// =============================================================================
// TYPES
@@ -21,6 +22,8 @@ interface AISystem {
assessmentResult: Record<string, unknown> | null
}
type TabId = 'overview' | 'decision-tree' | 'results'
// =============================================================================
// LOADING SKELETON
// =============================================================================
@@ -306,12 +309,178 @@ function AISystemCard({
)
}
// =============================================================================
// SAVED RESULTS TAB
// =============================================================================
interface SavedResult {
id: string
system_name: string
system_description?: string
high_risk_result: string
gpai_result: { gpai_category: string; is_systemic_risk: boolean }
combined_obligations: string[]
created_at: string
}
function SavedResultsTab() {
const [results, setResults] = useState<SavedResult[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const load = async () => {
try {
const res = await fetch('/api/sdk/v1/ucca/decision-tree/results')
if (res.ok) {
const data = await res.json()
setResults(data.results || [])
}
} catch {
// Ignore
} finally {
setLoading(false)
}
}
load()
}, [])
const handleDelete = async (id: string) => {
if (!confirm('Ergebnis wirklich löschen?')) return
try {
const res = await fetch(`/api/sdk/v1/ucca/decision-tree/results/${id}`, { method: 'DELETE' })
if (res.ok) {
setResults(prev => prev.filter(r => r.id !== id))
}
} catch {
// Ignore
}
}
const riskLabels: Record<string, string> = {
unacceptable: 'Unzulässig',
high_risk: 'Hochrisiko',
limited_risk: 'Begrenztes Risiko',
minimal_risk: 'Minimales Risiko',
not_applicable: 'Nicht anwendbar',
}
const riskColors: Record<string, string> = {
unacceptable: 'bg-red-100 text-red-700',
high_risk: 'bg-orange-100 text-orange-700',
limited_risk: 'bg-yellow-100 text-yellow-700',
minimal_risk: 'bg-green-100 text-green-700',
not_applicable: 'bg-gray-100 text-gray-500',
}
const gpaiLabels: Record<string, string> = {
none: 'Kein GPAI',
standard: 'GPAI Standard',
systemic: 'GPAI Systemisch',
}
const gpaiColors: Record<string, string> = {
none: 'bg-gray-100 text-gray-500',
standard: 'bg-blue-100 text-blue-700',
systemic: 'bg-purple-100 text-purple-700',
}
if (loading) {
return <LoadingSkeleton />
}
if (results.length === 0) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
<div className="w-16 h-16 mx-auto bg-purple-100 rounded-full flex items-center justify-center mb-4">
<svg className="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">Keine Ergebnisse vorhanden</h3>
<p className="mt-2 text-gray-500">Nutzen Sie den Entscheidungsbaum, um KI-Systeme zu klassifizieren.</p>
</div>
)
}
return (
<div className="space-y-4">
{results.map(r => (
<div key={r.id} className="bg-white rounded-xl border border-gray-200 p-5">
<div className="flex items-start justify-between">
<div>
<h4 className="font-semibold text-gray-900">{r.system_name}</h4>
{r.system_description && (
<p className="text-sm text-gray-500 mt-0.5">{r.system_description}</p>
)}
<div className="flex items-center gap-2 mt-2">
<span className={`px-2 py-1 text-xs rounded-full ${riskColors[r.high_risk_result] || 'bg-gray-100 text-gray-500'}`}>
{riskLabels[r.high_risk_result] || r.high_risk_result}
</span>
<span className={`px-2 py-1 text-xs rounded-full ${gpaiColors[r.gpai_result?.gpai_category] || 'bg-gray-100 text-gray-500'}`}>
{gpaiLabels[r.gpai_result?.gpai_category] || 'Kein GPAI'}
</span>
{r.gpai_result?.is_systemic_risk && (
<span className="px-2 py-1 text-xs rounded-full bg-red-100 text-red-700">Systemisch</span>
)}
</div>
<div className="text-xs text-gray-400 mt-2">
{r.combined_obligations?.length || 0} Pflichten &middot; {new Date(r.created_at).toLocaleDateString('de-DE')}
</div>
</div>
<button
onClick={() => handleDelete(r.id)}
className="px-3 py-1 text-xs text-red-600 hover:bg-red-50 rounded transition-colors"
>
Löschen
</button>
</div>
</div>
))}
</div>
)
}
// =============================================================================
// TABS
// =============================================================================
const TABS: { id: TabId; label: string; icon: React.ReactNode }[] = [
{
id: 'overview',
label: 'Übersicht',
icon: (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
</svg>
),
},
{
id: 'decision-tree',
label: 'Entscheidungsbaum',
icon: (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 010 3.75H5.625a1.875 1.875 0 010-3.75z" />
</svg>
),
},
{
id: 'results',
label: 'Ergebnisse',
icon: (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25z" />
</svg>
),
},
]
// =============================================================================
// MAIN PAGE
// =============================================================================
export default function AIActPage() {
const { state } = useSDK()
const [activeTab, setActiveTab] = useState<TabId>('overview')
const [systems, setSystems] = useState<AISystem[]>([])
const [filter, setFilter] = useState<string>('all')
const [showAddForm, setShowAddForm] = useState(false)
@@ -354,7 +523,6 @@ export default function AIActPage() {
const handleAddSystem = async (data: Omit<AISystem, 'id' | 'assessmentDate' | 'assessmentResult'>) => {
setError(null)
if (editingSystem) {
// Edit existing system via PUT
try {
const res = await fetch(`/api/sdk/v1/compliance/ai/systems/${editingSystem.id}`, {
method: 'PUT',
@@ -380,14 +548,12 @@ export default function AIActPage() {
setError('Speichern fehlgeschlagen')
}
} catch {
// Fallback: update locally
setSystems(prev => prev.map(s =>
s.id === editingSystem.id ? { ...s, ...data } : s
))
}
setEditingSystem(null)
} else {
// Create new system via POST
try {
const res = await fetch('/api/sdk/v1/compliance/ai/systems', {
method: 'POST',
@@ -415,7 +581,6 @@ export default function AIActPage() {
setError('Registrierung fehlgeschlagen')
}
} catch {
// Fallback: add locally
const newSystem: AISystem = {
...data,
id: `ai-${Date.now()}`,
@@ -503,17 +668,37 @@ export default function AIActPage() {
explanation={stepInfo.explanation}
tips={stepInfo.tips}
>
<button
onClick={() => setShowAddForm(true)}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
KI-System registrieren
</button>
{activeTab === 'overview' && (
<button
onClick={() => setShowAddForm(true)}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
KI-System registrieren
</button>
)}
</StepHeader>
{/* Tabs */}
<div className="flex items-center gap-1 bg-gray-100 p-1 rounded-lg w-fit">
{TABS.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md transition-colors ${
activeTab === tab.id
? 'bg-white text-purple-700 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
{tab.icon}
{tab.label}
</button>
))}
</div>
{/* Error Banner */}
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 flex items-center justify-between">
@@ -522,90 +707,105 @@ export default function AIActPage() {
</div>
)}
{/* Add/Edit System Form */}
{showAddForm && (
<AddSystemForm
onSubmit={handleAddSystem}
onCancel={() => { setShowAddForm(false); setEditingSystem(null) }}
initialData={editingSystem}
/>
)}
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="text-sm text-gray-500">KI-Systeme gesamt</div>
<div className="text-3xl font-bold text-gray-900">{systems.length}</div>
</div>
<div className="bg-white rounded-xl border border-orange-200 p-6">
<div className="text-sm text-orange-600">Hochrisiko</div>
<div className="text-3xl font-bold text-orange-600">{highRiskCount}</div>
</div>
<div className="bg-white rounded-xl border border-green-200 p-6">
<div className="text-sm text-green-600">Konform</div>
<div className="text-3xl font-bold text-green-600">{compliantCount}</div>
</div>
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="text-sm text-gray-500">Nicht klassifiziert</div>
<div className="text-3xl font-bold text-gray-500">{unclassifiedCount}</div>
</div>
</div>
{/* Risk Pyramid */}
<RiskPyramid systems={systems} />
{/* Filter */}
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm text-gray-500">Filter:</span>
{['all', 'high-risk', 'limited-risk', 'minimal-risk', 'unclassified', 'compliant', 'non-compliant'].map(f => (
<button
key={f}
onClick={() => setFilter(f)}
className={`px-3 py-1 text-sm rounded-full transition-colors ${
filter === f
? 'bg-purple-600 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
{f === 'all' ? 'Alle' :
f === 'high-risk' ? 'Hochrisiko' :
f === 'limited-risk' ? 'Begrenztes Risiko' :
f === 'minimal-risk' ? 'Minimales Risiko' :
f === 'unclassified' ? 'Nicht klassifiziert' :
f === 'compliant' ? 'Konform' : 'Nicht konform'}
</button>
))}
</div>
{/* Loading */}
{loading && <LoadingSkeleton />}
{/* AI Systems List */}
{!loading && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{filteredSystems.map(system => (
<AISystemCard
key={system.id}
system={system}
onAssess={() => handleAssess(system.id)}
onEdit={() => handleEdit(system)}
onDelete={() => handleDelete(system.id)}
assessing={assessingId === system.id}
{/* Tab: Overview */}
{activeTab === 'overview' && (
<>
{/* Add/Edit System Form */}
{showAddForm && (
<AddSystemForm
onSubmit={handleAddSystem}
onCancel={() => { setShowAddForm(false); setEditingSystem(null) }}
initialData={editingSystem}
/>
))}
</div>
)}
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="text-sm text-gray-500">KI-Systeme gesamt</div>
<div className="text-3xl font-bold text-gray-900">{systems.length}</div>
</div>
<div className="bg-white rounded-xl border border-orange-200 p-6">
<div className="text-sm text-orange-600">Hochrisiko</div>
<div className="text-3xl font-bold text-orange-600">{highRiskCount}</div>
</div>
<div className="bg-white rounded-xl border border-green-200 p-6">
<div className="text-sm text-green-600">Konform</div>
<div className="text-3xl font-bold text-green-600">{compliantCount}</div>
</div>
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="text-sm text-gray-500">Nicht klassifiziert</div>
<div className="text-3xl font-bold text-gray-500">{unclassifiedCount}</div>
</div>
</div>
{/* Risk Pyramid */}
<RiskPyramid systems={systems} />
{/* Filter */}
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm text-gray-500">Filter:</span>
{['all', 'high-risk', 'limited-risk', 'minimal-risk', 'unclassified', 'compliant', 'non-compliant'].map(f => (
<button
key={f}
onClick={() => setFilter(f)}
className={`px-3 py-1 text-sm rounded-full transition-colors ${
filter === f
? 'bg-purple-600 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
{f === 'all' ? 'Alle' :
f === 'high-risk' ? 'Hochrisiko' :
f === 'limited-risk' ? 'Begrenztes Risiko' :
f === 'minimal-risk' ? 'Minimales Risiko' :
f === 'unclassified' ? 'Nicht klassifiziert' :
f === 'compliant' ? 'Konform' : 'Nicht konform'}
</button>
))}
</div>
{/* Loading */}
{loading && <LoadingSkeleton />}
{/* AI Systems List */}
{!loading && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{filteredSystems.map(system => (
<AISystemCard
key={system.id}
system={system}
onAssess={() => handleAssess(system.id)}
onEdit={() => handleEdit(system)}
onDelete={() => handleDelete(system.id)}
assessing={assessingId === system.id}
/>
))}
</div>
)}
{!loading && filteredSystems.length === 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
<div className="w-16 h-16 mx-auto bg-purple-100 rounded-full flex items-center justify-center mb-4">
<svg className="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">Keine KI-Systeme gefunden</h3>
<p className="mt-2 text-gray-500">Passen Sie den Filter an oder registrieren Sie ein neues KI-System.</p>
</div>
)}
</>
)}
{!loading && filteredSystems.length === 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
<div className="w-16 h-16 mx-auto bg-purple-100 rounded-full flex items-center justify-center mb-4">
<svg className="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">Keine KI-Systeme gefunden</h3>
<p className="mt-2 text-gray-500">Passen Sie den Filter an oder registrieren Sie ein neues KI-System.</p>
</div>
{/* Tab: Decision Tree */}
{activeTab === 'decision-tree' && (
<DecisionTreeWizard />
)}
{/* Tab: Results */}
{activeTab === 'results' && (
<SavedResultsTab />
)}
</div>
)

View File

@@ -0,0 +1,491 @@
'use client'
import React, { useState, useEffect } from 'react'
interface Registration {
id: string
system_name: string
system_version: string
risk_classification: string
gpai_classification: string
registration_status: string
eu_database_id: string
provider_name: string
created_at: string
}
const STATUS_STYLES: Record<string, { bg: string; text: string; label: string }> = {
draft: { bg: 'bg-gray-100', text: 'text-gray-700', label: 'Entwurf' },
ready: { bg: 'bg-blue-100', text: 'text-blue-700', label: 'Bereit' },
submitted: { bg: 'bg-yellow-100', text: 'text-yellow-700', label: 'Eingereicht' },
registered: { bg: 'bg-green-100', text: 'text-green-700', label: 'Registriert' },
update_required: { bg: 'bg-orange-100', text: 'text-orange-700', label: 'Update noetig' },
withdrawn: { bg: 'bg-red-100', text: 'text-red-700', label: 'Zurueckgezogen' },
}
const RISK_STYLES: Record<string, { bg: string; text: string }> = {
high_risk: { bg: 'bg-red-100', text: 'text-red-700' },
limited_risk: { bg: 'bg-yellow-100', text: 'text-yellow-700' },
minimal_risk: { bg: 'bg-green-100', text: 'text-green-700' },
not_classified: { bg: 'bg-gray-100', text: 'text-gray-500' },
}
const INITIAL_FORM = {
system_name: '',
system_version: '1.0',
system_description: '',
intended_purpose: '',
provider_name: '',
provider_legal_form: '',
provider_address: '',
provider_country: 'DE',
eu_representative_name: '',
eu_representative_contact: '',
risk_classification: 'not_classified',
annex_iii_category: '',
gpai_classification: 'none',
conformity_assessment_type: 'internal',
notified_body_name: '',
notified_body_id: '',
ce_marking: false,
training_data_summary: '',
}
export default function AIRegistrationPage() {
const [registrations, setRegistrations] = useState<Registration[]>([])
const [loading, setLoading] = useState(true)
const [showWizard, setShowWizard] = useState(false)
const [wizardStep, setWizardStep] = useState(1)
const [form, setForm] = useState({ ...INITIAL_FORM })
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => { loadRegistrations() }, [])
async function loadRegistrations() {
try {
setLoading(true)
const resp = await fetch('/api/sdk/v1/ai-registration')
if (resp.ok) {
const data = await resp.json()
setRegistrations(data.registrations || [])
}
} catch {
setError('Fehler beim Laden')
} finally {
setLoading(false)
}
}
async function handleSubmit() {
setSubmitting(true)
try {
const resp = await fetch('/api/sdk/v1/ai-registration', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form),
})
if (resp.ok) {
setShowWizard(false)
setForm({ ...INITIAL_FORM })
setWizardStep(1)
loadRegistrations()
} else {
const data = await resp.json()
setError(data.error || 'Fehler beim Erstellen')
}
} catch {
setError('Netzwerkfehler')
} finally {
setSubmitting(false)
}
}
async function handleExport(id: string) {
try {
const resp = await fetch(`/api/sdk/v1/ai-registration/${id}`)
if (resp.ok) {
const reg = await resp.json()
// Build export JSON client-side
const exportData = {
schema_version: '1.0',
submission_type: 'ai_system_registration',
regulation: 'EU AI Act (EU) 2024/1689',
article: 'Art. 49',
provider: { name: reg.provider_name, address: reg.provider_address, country: reg.provider_country },
system: { name: reg.system_name, version: reg.system_version, description: reg.system_description, purpose: reg.intended_purpose },
classification: { risk_level: reg.risk_classification, annex_iii: reg.annex_iii_category, gpai: reg.gpai_classification },
conformity: { type: reg.conformity_assessment_type, ce_marking: reg.ce_marking },
}
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `eu_ai_registration_${reg.system_name.replace(/\s+/g, '_')}.json`
a.click()
URL.revokeObjectURL(url)
}
} catch {
setError('Export fehlgeschlagen')
}
}
async function handleStatusChange(id: string, status: string) {
try {
await fetch(`/api/sdk/v1/ai-registration/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status }),
})
loadRegistrations()
} catch {
setError('Status-Aenderung fehlgeschlagen')
}
}
const updateForm = (updates: Partial<typeof form>) => setForm(prev => ({ ...prev, ...updates }))
const STEPS = [
{ id: 1, title: 'Anbieter', desc: 'Unternehmensangaben' },
{ id: 2, title: 'System', desc: 'KI-System Details' },
{ id: 3, title: 'Klassifikation', desc: 'Risikoeinstufung' },
{ id: 4, title: 'Konformitaet', desc: 'CE & Notified Body' },
{ id: 5, title: 'Trainingsdaten', desc: 'Datenzusammenfassung' },
{ id: 6, title: 'Pruefung', desc: 'Zusammenfassung & Export' },
]
return (
<div className="max-w-5xl mx-auto p-6">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-2xl font-bold text-gray-900">EU AI Database Registrierung</h1>
<p className="text-sm text-gray-500 mt-1">Art. 49 KI-Verordnung (EU) 2024/1689 Registrierung von Hochrisiko-KI-Systemen</p>
</div>
<button
onClick={() => setShowWizard(true)}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
+ Neue Registrierung
</button>
</div>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
{error}
<button onClick={() => setError(null)} className="ml-2 underline">Schliessen</button>
</div>
)}
{/* Stats */}
<div className="grid grid-cols-4 gap-4 mb-8">
{['draft', 'ready', 'submitted', 'registered'].map(status => {
const count = registrations.filter(r => r.registration_status === status).length
const style = STATUS_STYLES[status]
return (
<div key={status} className={`p-4 rounded-xl border ${style.bg}`}>
<div className={`text-2xl font-bold ${style.text}`}>{count}</div>
<div className="text-sm text-gray-600">{style.label}</div>
</div>
)
})}
</div>
{/* Registrations List */}
{loading ? (
<div className="text-center py-12 text-gray-500">Lade...</div>
) : registrations.length === 0 ? (
<div className="text-center py-12 text-gray-400">
<p className="text-lg mb-2">Noch keine Registrierungen</p>
<p className="text-sm">Erstelle eine neue Registrierung fuer dein Hochrisiko-KI-System.</p>
</div>
) : (
<div className="space-y-4">
{registrations.map(reg => {
const status = STATUS_STYLES[reg.registration_status] || STATUS_STYLES.draft
const risk = RISK_STYLES[reg.risk_classification] || RISK_STYLES.not_classified
return (
<div key={reg.id} className="bg-white rounded-xl border border-gray-200 p-6 hover:border-purple-300 transition-all">
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-2 mb-1">
<h3 className="text-lg font-semibold text-gray-900">{reg.system_name}</h3>
<span className="text-sm text-gray-400">v{reg.system_version}</span>
<span className={`px-2 py-0.5 text-xs rounded-full ${status.bg} ${status.text}`}>{status.label}</span>
<span className={`px-2 py-0.5 text-xs rounded-full ${risk.bg} ${risk.text}`}>{reg.risk_classification.replace('_', ' ')}</span>
{reg.gpai_classification !== 'none' && (
<span className="px-2 py-0.5 text-xs rounded-full bg-blue-100 text-blue-700">GPAI: {reg.gpai_classification}</span>
)}
</div>
<div className="text-sm text-gray-500">
{reg.provider_name && <span>{reg.provider_name} · </span>}
{reg.eu_database_id && <span>EU-ID: {reg.eu_database_id} · </span>}
<span>{new Date(reg.created_at).toLocaleDateString('de-DE')}</span>
</div>
</div>
<div className="flex gap-2">
<button onClick={() => handleExport(reg.id)} className="px-3 py-1.5 text-sm border border-gray-300 rounded-lg hover:bg-gray-50">
JSON Export
</button>
{reg.registration_status === 'draft' && (
<button onClick={() => handleStatusChange(reg.id, 'ready')} className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700">
Bereit markieren
</button>
)}
{reg.registration_status === 'ready' && (
<button onClick={() => handleStatusChange(reg.id, 'submitted')} className="px-3 py-1.5 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700">
Als eingereicht markieren
</button>
)}
</div>
</div>
</div>
)
})}
</div>
)}
{/* Wizard Modal */}
{showWizard && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-3xl max-h-[90vh] overflow-y-auto">
<div className="p-6 border-b">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-gray-900">Neue EU AI Registrierung</h2>
<button onClick={() => { setShowWizard(false); setWizardStep(1) }} className="text-gray-400 hover:text-gray-600 text-2xl">&times;</button>
</div>
{/* Step Indicator */}
<div className="flex gap-1">
{STEPS.map(step => (
<button key={step.id} onClick={() => setWizardStep(step.id)}
className={`flex-1 py-2 text-xs rounded-lg transition-all ${
wizardStep === step.id ? 'bg-purple-100 text-purple-700 font-medium' :
wizardStep > step.id ? 'bg-green-50 text-green-700' : 'bg-gray-50 text-gray-400'
}`}>
{wizardStep > step.id ? '✓ ' : ''}{step.title}
</button>
))}
</div>
</div>
<div className="p-6 space-y-4">
{/* Step 1: Provider */}
{wizardStep === 1 && (
<>
<h3 className="font-semibold text-gray-900">Anbieter-Informationen</h3>
<p className="text-sm text-gray-500">Angaben zum Anbieter des KI-Systems gemaess Art. 49 KI-VO.</p>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Firmenname *</label>
<input value={form.provider_name} onChange={e => updateForm({ provider_name: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="Acme GmbH" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Rechtsform</label>
<input value={form.provider_legal_form} onChange={e => updateForm({ provider_legal_form: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="GmbH" />
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Adresse</label>
<input value={form.provider_address} onChange={e => updateForm({ provider_address: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="Musterstr. 1, 20095 Hamburg" />
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Land</label>
<select value={form.provider_country} onChange={e => updateForm({ provider_country: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
<option value="DE">Deutschland</option>
<option value="AT">Oesterreich</option>
<option value="CH">Schweiz</option>
<option value="OTHER">Anderes Land</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">EU-Repraesentant (falls Non-EU)</label>
<input value={form.eu_representative_name} onChange={e => updateForm({ eu_representative_name: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="Optional" />
</div>
</div>
</>
)}
{/* Step 2: System */}
{wizardStep === 2 && (
<>
<h3 className="font-semibold text-gray-900">KI-System Details</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Systemname *</label>
<input value={form.system_name} onChange={e => updateForm({ system_name: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="z.B. HR Copilot" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Version</label>
<input value={form.system_version} onChange={e => updateForm({ system_version: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="1.0" />
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Systembeschreibung</label>
<textarea value={form.system_description} onChange={e => updateForm({ system_description: e.target.value })} rows={3}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="Beschreibe was das KI-System tut..." />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Einsatzzweck (Intended Purpose)</label>
<textarea value={form.intended_purpose} onChange={e => updateForm({ intended_purpose: e.target.value })} rows={2}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="Wofuer wird das System eingesetzt?" />
</div>
</>
)}
{/* Step 3: Classification */}
{wizardStep === 3 && (
<>
<h3 className="font-semibold text-gray-900">Risiko-Klassifikation</h3>
<p className="text-sm text-gray-500">Basierend auf dem AI Act Decision Tree oder manueller Einstufung.</p>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Risikoklasse</label>
<select value={form.risk_classification} onChange={e => updateForm({ risk_classification: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
<option value="not_classified">Noch nicht klassifiziert</option>
<option value="minimal_risk">Minimal Risk</option>
<option value="limited_risk">Limited Risk</option>
<option value="high_risk">High Risk</option>
</select>
</div>
{form.risk_classification === 'high_risk' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Annex III Kategorie</label>
<select value={form.annex_iii_category} onChange={e => updateForm({ annex_iii_category: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
<option value="">Bitte waehlen...</option>
<option value="biometric">1. Biometrische Identifizierung</option>
<option value="critical_infrastructure">2. Kritische Infrastruktur</option>
<option value="education">3. Bildung und Berufsausbildung</option>
<option value="employment">4. Beschaeftigung und Arbeitnehmerverwaltung</option>
<option value="essential_services">5. Zugang zu wesentlichen Diensten</option>
<option value="law_enforcement">6. Strafverfolgung</option>
<option value="migration">7. Migration und Grenzkontrolle</option>
<option value="justice">8. Rechtspflege und Demokratie</option>
</select>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">GPAI Klassifikation</label>
<select value={form.gpai_classification} onChange={e => updateForm({ gpai_classification: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
<option value="none">Kein GPAI</option>
<option value="standard">GPAI (Standard)</option>
<option value="systemic">GPAI mit systemischem Risiko</option>
</select>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 text-sm text-blue-800">
<strong>Tipp:</strong> Nutze den <a href="/sdk/ai-act" className="underline">AI Act Decision Tree</a> fuer eine strukturierte Klassifikation.
</div>
</>
)}
{/* Step 4: Conformity */}
{wizardStep === 4 && (
<>
<h3 className="font-semibold text-gray-900">Konformitaetsbewertung</h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Art der Konformitaetsbewertung</label>
<select value={form.conformity_assessment_type} onChange={e => updateForm({ conformity_assessment_type: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
<option value="not_required">Nicht erforderlich</option>
<option value="internal">Interne Konformitaetsbewertung</option>
<option value="third_party">Drittpartei-Bewertung (Notified Body)</option>
</select>
</div>
{form.conformity_assessment_type === 'third_party' && (
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Notified Body Name</label>
<input value={form.notified_body_name} onChange={e => updateForm({ notified_body_name: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Notified Body ID</label>
<input value={form.notified_body_id} onChange={e => updateForm({ notified_body_id: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
</div>
</div>
)}
<label className="flex items-center gap-3 p-3 rounded-lg border hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.ce_marking} onChange={e => updateForm({ ce_marking: e.target.checked })}
className="w-4 h-4 rounded border-gray-300 text-purple-600" />
<span className="text-sm font-medium text-gray-900">CE-Kennzeichnung angebracht</span>
</label>
</>
)}
{/* Step 5: Training Data */}
{wizardStep === 5 && (
<>
<h3 className="font-semibold text-gray-900">Trainingsdaten-Zusammenfassung</h3>
<p className="text-sm text-gray-500">Art. 10 KI-VO Keine vollstaendige Offenlegung, sondern Kategorien und Herkunft.</p>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Zusammenfassung der Trainingsdaten</label>
<textarea value={form.training_data_summary} onChange={e => updateForm({ training_data_summary: e.target.value })} rows={5}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
placeholder="Beschreibe die verwendeten Datenquellen:&#10;- Oeffentliche Daten (z.B. Wikipedia, Common Crawl)&#10;- Lizenzierte Daten (z.B. Fachpublikationen)&#10;- Synthetische Daten&#10;- Unternehmensinterne Daten" />
</div>
</>
)}
{/* Step 6: Review */}
{wizardStep === 6 && (
<>
<h3 className="font-semibold text-gray-900">Zusammenfassung</h3>
<div className="space-y-3 text-sm">
<div className="grid grid-cols-2 gap-4 p-4 bg-gray-50 rounded-lg">
<div><span className="text-gray-500">Anbieter:</span> <strong>{form.provider_name || ''}</strong></div>
<div><span className="text-gray-500">Land:</span> <strong>{form.provider_country}</strong></div>
<div><span className="text-gray-500">System:</span> <strong>{form.system_name || ''}</strong></div>
<div><span className="text-gray-500">Version:</span> <strong>{form.system_version}</strong></div>
<div><span className="text-gray-500">Risiko:</span> <strong>{form.risk_classification}</strong></div>
<div><span className="text-gray-500">GPAI:</span> <strong>{form.gpai_classification}</strong></div>
<div><span className="text-gray-500">Konformitaet:</span> <strong>{form.conformity_assessment_type}</strong></div>
<div><span className="text-gray-500">CE:</span> <strong>{form.ce_marking ? 'Ja' : 'Nein'}</strong></div>
</div>
{form.intended_purpose && (
<div className="p-4 bg-gray-50 rounded-lg">
<span className="text-gray-500">Zweck:</span> {form.intended_purpose}
</div>
)}
</div>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 text-sm text-yellow-800">
<strong>Hinweis:</strong> Die EU AI Datenbank befindet sich noch im Aufbau. Die Registrierung wird lokal gespeichert und kann spaeter uebermittelt werden.
</div>
</>
)}
</div>
{/* Navigation */}
<div className="p-6 border-t flex justify-between">
<button onClick={() => wizardStep > 1 ? setWizardStep(wizardStep - 1) : setShowWizard(false)}
className="px-4 py-2 text-gray-700 border rounded-lg hover:bg-gray-50">
{wizardStep === 1 ? 'Abbrechen' : 'Zurueck'}
</button>
{wizardStep < 6 ? (
<button onClick={() => setWizardStep(wizardStep + 1)}
disabled={wizardStep === 2 && !form.system_name}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50">
Weiter
</button>
) : (
<button onClick={handleSubmit} disabled={submitting || !form.system_name}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50">
{submitting ? 'Speichere...' : 'Registrierung erstellen'}
</button>
)}
</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -160,6 +160,8 @@ export const ARCH_SERVICES: ArchService[] = [
'security_backlog', 'quality_entries',
'notfallplan_incidents', 'notfallplan_templates',
'data_processing_agreement',
'vendor_vendors', 'vendor_contracts', 'vendor_findings',
'vendor_control_instances', 'compliance_templates',
'compliance_isms_scope', 'compliance_isms_context', 'compliance_isms_policy',
'compliance_security_objectives', 'compliance_soa',
'compliance_audit_findings', 'compliance_corrective_actions',
@@ -178,6 +180,10 @@ export const ARCH_SERVICES: ArchService[] = [
'CRUD /api/compliance/vvt',
'CRUD /api/compliance/loeschfristen',
'CRUD /api/compliance/obligations',
'CRUD /api/sdk/v1/vendor-compliance/vendors',
'CRUD /api/sdk/v1/vendor-compliance/contracts',
'CRUD /api/sdk/v1/vendor-compliance/findings',
'CRUD /api/sdk/v1/vendor-compliance/control-instances',
'CRUD /api/isms/scope',
'CRUD /api/isms/policies',
'CRUD /api/isms/objectives',

View File

@@ -0,0 +1,468 @@
'use client'
import React, { useState, useEffect } from 'react'
// =============================================================================
// TYPES
// =============================================================================
interface Assertion {
id: string
tenant_id: string | null
entity_type: string
entity_id: string
sentence_text: string
sentence_index: number
assertion_type: string // 'assertion' | 'fact' | 'rationale'
evidence_ids: string[]
confidence: number
normative_tier: string | null // 'pflicht' | 'empfehlung' | 'kann'
verified_by: string | null
verified_at: string | null
created_at: string | null
updated_at: string | null
}
interface AssertionSummary {
total_assertions: number
total_facts: number
total_rationale: number
unverified_count: number
}
// =============================================================================
// CONSTANTS
// =============================================================================
const TIER_COLORS: Record<string, string> = {
pflicht: 'bg-red-100 text-red-700',
empfehlung: 'bg-yellow-100 text-yellow-700',
kann: 'bg-blue-100 text-blue-700',
}
const TIER_LABELS: Record<string, string> = {
pflicht: 'Pflicht',
empfehlung: 'Empfehlung',
kann: 'Kann',
}
const TYPE_COLORS: Record<string, string> = {
assertion: 'bg-orange-100 text-orange-700',
fact: 'bg-green-100 text-green-700',
rationale: 'bg-purple-100 text-purple-700',
}
const TYPE_LABELS: Record<string, string> = {
assertion: 'Behauptung',
fact: 'Fakt',
rationale: 'Begruendung',
}
const API_BASE = '/api/sdk/v1/compliance'
type TabKey = 'overview' | 'list' | 'extract'
// =============================================================================
// ASSERTION CARD
// =============================================================================
function AssertionCard({
assertion,
onVerify,
}: {
assertion: Assertion
onVerify: (id: string) => void
}) {
const tierColor = assertion.normative_tier ? TIER_COLORS[assertion.normative_tier] || 'bg-gray-100 text-gray-600' : 'bg-gray-100 text-gray-600'
const tierLabel = assertion.normative_tier ? TIER_LABELS[assertion.normative_tier] || assertion.normative_tier : '—'
const typeColor = TYPE_COLORS[assertion.assertion_type] || 'bg-gray-100 text-gray-600'
const typeLabel = TYPE_LABELS[assertion.assertion_type] || assertion.assertion_type
return (
<div className="bg-white rounded-xl border border-gray-200 p-5">
<div className="flex items-start justify-between gap-3">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<span className={`px-2 py-0.5 text-xs rounded font-medium ${tierColor}`}>
{tierLabel}
</span>
<span className={`px-2 py-0.5 text-xs rounded ${typeColor}`}>
{typeLabel}
</span>
{assertion.entity_type && (
<span className="px-2 py-0.5 text-xs bg-gray-100 text-gray-500 rounded">
{assertion.entity_type}: {assertion.entity_id?.slice(0, 8) || '—'}
</span>
)}
{assertion.confidence > 0 && (
<span className="text-xs text-gray-400">
Konfidenz: {(assertion.confidence * 100).toFixed(0)}%
</span>
)}
</div>
<p className="text-sm text-gray-900 leading-relaxed">
&ldquo;{assertion.sentence_text}&rdquo;
</p>
<div className="mt-2 flex items-center gap-3 text-xs text-gray-400">
{assertion.verified_by && (
<span className="text-green-600">
Verifiziert von {assertion.verified_by} am {assertion.verified_at ? new Date(assertion.verified_at).toLocaleDateString('de-DE') : '—'}
</span>
)}
{assertion.evidence_ids.length > 0 && (
<span>
{assertion.evidence_ids.length} Evidence verknuepft
</span>
)}
</div>
</div>
<div className="flex flex-col gap-1">
{assertion.assertion_type !== 'fact' && (
<button
onClick={() => onVerify(assertion.id)}
className="px-3 py-1.5 text-xs bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors whitespace-nowrap"
>
Als Fakt pruefen
</button>
)}
</div>
</div>
</div>
)
}
// =============================================================================
// MAIN PAGE
// =============================================================================
export default function AssertionsPage() {
const [activeTab, setActiveTab] = useState<TabKey>('overview')
const [summary, setSummary] = useState<AssertionSummary | null>(null)
const [assertions, setAssertions] = useState<Assertion[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Filters
const [filterEntityType, setFilterEntityType] = useState('')
const [filterAssertionType, setFilterAssertionType] = useState('')
// Extract tab
const [extractText, setExtractText] = useState('')
const [extractEntityType, setExtractEntityType] = useState('control')
const [extractEntityId, setExtractEntityId] = useState('')
const [extracting, setExtracting] = useState(false)
const [extractedAssertions, setExtractedAssertions] = useState<Assertion[]>([])
// Verify dialog
const [verifyingId, setVerifyingId] = useState<string | null>(null)
const [verifyEmail, setVerifyEmail] = useState('')
useEffect(() => {
loadSummary()
}, [])
useEffect(() => {
if (activeTab === 'list') loadAssertions()
}, [activeTab, filterEntityType, filterAssertionType]) // eslint-disable-line react-hooks/exhaustive-deps
const loadSummary = async () => {
try {
const res = await fetch(`${API_BASE}/assertions/summary`)
if (res.ok) setSummary(await res.json())
} catch { /* silent */ }
finally { setLoading(false) }
}
const loadAssertions = async () => {
setLoading(true)
try {
const params = new URLSearchParams()
if (filterEntityType) params.set('entity_type', filterEntityType)
if (filterAssertionType) params.set('assertion_type', filterAssertionType)
params.set('limit', '200')
const res = await fetch(`${API_BASE}/assertions?${params}`)
if (res.ok) {
const data = await res.json()
setAssertions(data.assertions || [])
}
} catch {
setError('Assertions konnten nicht geladen werden')
} finally {
setLoading(false)
}
}
const handleExtract = async () => {
if (!extractText.trim()) { setError('Bitte Text eingeben'); return }
setExtracting(true)
setError(null)
setExtractedAssertions([])
try {
const res = await fetch(`${API_BASE}/assertions/extract`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: extractText,
entity_type: extractEntityType || 'control',
entity_id: extractEntityId || undefined,
}),
})
if (!res.ok) {
const err = await res.json().catch(() => ({ detail: 'Extraktion fehlgeschlagen' }))
throw new Error(typeof err.detail === 'string' ? err.detail : JSON.stringify(err.detail))
}
const data = await res.json()
setExtractedAssertions(data.assertions || [])
// Refresh summary
loadSummary()
} catch (err) {
setError(err instanceof Error ? err.message : 'Extraktion fehlgeschlagen')
} finally {
setExtracting(false)
}
}
const handleVerify = async (assertionId: string) => {
setVerifyingId(assertionId)
}
const submitVerify = async () => {
if (!verifyingId || !verifyEmail.trim()) return
try {
const res = await fetch(`${API_BASE}/assertions/${verifyingId}/verify?verified_by=${encodeURIComponent(verifyEmail)}`, {
method: 'POST',
})
if (res.ok) {
setVerifyingId(null)
setVerifyEmail('')
loadAssertions()
loadSummary()
} else {
const err = await res.json().catch(() => ({ detail: 'Verifizierung fehlgeschlagen' }))
setError(typeof err.detail === 'string' ? err.detail : 'Verifizierung fehlgeschlagen')
}
} catch {
setError('Netzwerkfehler')
}
}
const tabs: { key: TabKey; label: string }[] = [
{ key: 'overview', label: 'Uebersicht' },
{ key: 'list', label: 'Assertion-Liste' },
{ key: 'extract', label: 'Extraktion' },
]
return (
<div className="space-y-6">
{/* Header */}
<div className="bg-white rounded-xl shadow-sm border p-6">
<h1 className="text-2xl font-bold text-slate-900">Assertions</h1>
<p className="text-slate-500 mt-1">
Behauptungen vs. Fakten in Compliance-Texten trennen und verifizieren.
</p>
</div>
{/* Tabs */}
<div className="bg-white rounded-xl shadow-sm border">
<div className="flex border-b">
{tabs.map(tab => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`px-6 py-3 text-sm font-medium transition-colors ${
activeTab === tab.key
? 'text-purple-600 border-b-2 border-purple-600'
: 'text-slate-500 hover:text-slate-700'
}`}
>
{tab.label}
</button>
))}
</div>
</div>
{/* Error */}
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 flex items-center justify-between">
<span>{error}</span>
<button onClick={() => setError(null)} className="text-red-500 hover:text-red-700">&times;</button>
</div>
)}
{/* ============================================================ */}
{/* TAB: Uebersicht */}
{/* ============================================================ */}
{activeTab === 'overview' && (
<>
{loading ? (
<div className="flex justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
</div>
) : summary ? (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="text-sm text-gray-500">Gesamt Assertions</div>
<div className="text-3xl font-bold text-gray-900">{summary.total_assertions}</div>
</div>
<div className="bg-white rounded-xl border border-green-200 p-6">
<div className="text-sm text-green-600">Verifizierte Fakten</div>
<div className="text-3xl font-bold text-green-600">{summary.total_facts}</div>
</div>
<div className="bg-white rounded-xl border border-purple-200 p-6">
<div className="text-sm text-purple-600">Begruendungen</div>
<div className="text-3xl font-bold text-purple-600">{summary.total_rationale}</div>
</div>
<div className="bg-white rounded-xl border border-orange-200 p-6">
<div className="text-sm text-orange-600">Unverifizizt</div>
<div className="text-3xl font-bold text-orange-600">{summary.unverified_count}</div>
</div>
</div>
) : (
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
<p className="text-gray-500">Keine Assertions vorhanden. Nutzen Sie die Extraktion, um Behauptungen aus Texten zu identifizieren.</p>
</div>
)}
</>
)}
{/* ============================================================ */}
{/* TAB: Assertion-Liste */}
{/* ============================================================ */}
{activeTab === 'list' && (
<>
{/* Filters */}
<div className="flex items-center gap-4 flex-wrap">
<div>
<label className="block text-xs text-gray-500 mb-1">Entity-Typ</label>
<select value={filterEntityType} onChange={e => setFilterEntityType(e.target.value)}
className="px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent">
<option value="">Alle</option>
<option value="control">Control</option>
<option value="evidence">Evidence</option>
<option value="requirement">Requirement</option>
</select>
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">Assertion-Typ</label>
<select value={filterAssertionType} onChange={e => setFilterAssertionType(e.target.value)}
className="px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent">
<option value="">Alle</option>
<option value="assertion">Behauptung</option>
<option value="fact">Fakt</option>
<option value="rationale">Begruendung</option>
</select>
</div>
</div>
{loading ? (
<div className="flex justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
</div>
) : assertions.length === 0 ? (
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
<p className="text-gray-500">Keine Assertions gefunden.</p>
</div>
) : (
<div className="space-y-3">
<p className="text-sm text-gray-500">{assertions.length} Assertions</p>
{assertions.map(a => (
<AssertionCard key={a.id} assertion={a} onVerify={handleVerify} />
))}
</div>
)}
</>
)}
{/* ============================================================ */}
{/* TAB: Extraktion */}
{/* ============================================================ */}
{activeTab === 'extract' && (
<div className="bg-white rounded-xl shadow-sm border p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Assertions aus Text extrahieren</h3>
<p className="text-sm text-gray-500 mb-4">
Geben Sie einen Compliance-Text ein. Das System identifiziert automatisch Behauptungen, Fakten und Begruendungen.
</p>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Entity-Typ</label>
<select value={extractEntityType} onChange={e => setExtractEntityType(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent">
<option value="control">Control</option>
<option value="evidence">Evidence</option>
<option value="requirement">Requirement</option>
<option value="policy">Policy</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Entity-ID (optional)</label>
<input type="text" value={extractEntityId} onChange={e => setExtractEntityId(e.target.value)}
placeholder="z.B. GOV-001 oder UUID"
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
</div>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">Text</label>
<textarea
value={extractText}
onChange={e => setExtractText(e.target.value)}
placeholder="Die Organisation muss ein ISMS gemaess ISO 27001 implementieren. Es sollte regelmaessig ein internes Audit durchgefuehrt werden. Optional kann ein externer Auditor hinzugezogen werden."
rows={6}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent resize-none"
/>
</div>
<button
onClick={handleExtract}
disabled={extracting || !extractText.trim()}
className={`px-5 py-2 rounded-lg font-medium transition-colors ${
extracting || !extractText.trim()
? 'bg-gray-200 text-gray-400 cursor-not-allowed'
: 'bg-purple-600 text-white hover:bg-purple-700'
}`}
>
{extracting ? 'Extrahiere...' : 'Extrahieren'}
</button>
{/* Extracted results */}
{extractedAssertions.length > 0 && (
<div className="mt-6">
<h4 className="text-sm font-semibold text-gray-800 mb-3">{extractedAssertions.length} Assertions extrahiert:</h4>
<div className="space-y-3">
{extractedAssertions.map(a => (
<AssertionCard key={a.id} assertion={a} onVerify={handleVerify} />
))}
</div>
</div>
)}
</div>
)}
{/* Verify Dialog */}
{verifyingId && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={() => setVerifyingId(null)}>
<div className="bg-white rounded-2xl shadow-xl w-full max-w-md mx-4 p-6" onClick={e => e.stopPropagation()}>
<h2 className="text-lg font-bold text-gray-900 mb-4">Als Fakt verifizieren</h2>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">Verifiziert von (E-Mail)</label>
<input type="email" value={verifyEmail} onChange={e => setVerifyEmail(e.target.value)}
placeholder="auditor@unternehmen.de"
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
</div>
<div className="flex justify-end gap-3">
<button onClick={() => setVerifyingId(null)} className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
Abbrechen
</button>
<button onClick={submitVerify} disabled={!verifyEmail.trim()}
className="px-4 py-2 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50">
Verifizieren
</button>
</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,413 @@
'use client'
import { useState, useEffect, useCallback, useRef } from 'react'
import {
Atom, Search, ChevronRight, ChevronLeft, Filter,
BarChart3, ChevronsLeft, ChevronsRight, ArrowUpDown,
Clock, RefreshCw,
} from 'lucide-react'
import {
CanonicalControl, BACKEND_URL,
SeverityBadge, StateBadge, CategoryBadge, TargetAudienceBadge,
GenerationStrategyBadge, ObligationTypeBadge, RegulationCountBadge,
CATEGORY_OPTIONS,
} from '../control-library/components/helpers'
import { ControlDetail } from '../control-library/components/ControlDetail'
// =============================================================================
// TYPES
// =============================================================================
interface AtomicStats {
total_active: number
total_duplicate: number
by_domain: Array<{ domain: string; count: number }>
by_regulation: Array<{ regulation: string; count: number }>
avg_regulation_coverage: number
}
// =============================================================================
// ATOMIC CONTROLS PAGE
// =============================================================================
const PAGE_SIZE = 50
export default function AtomicControlsPage() {
const [controls, setControls] = useState<CanonicalControl[]>([])
const [totalCount, setTotalCount] = useState(0)
const [stats, setStats] = useState<AtomicStats | null>(null)
const [selectedControl, setSelectedControl] = useState<CanonicalControl | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Filters
const [searchQuery, setSearchQuery] = useState('')
const [debouncedSearch, setDebouncedSearch] = useState('')
const [severityFilter, setSeverityFilter] = useState<string>('')
const [domainFilter, setDomainFilter] = useState<string>('')
const [categoryFilter, setCategoryFilter] = useState<string>('')
const [sortBy, setSortBy] = useState<'id' | 'newest' | 'oldest'>('id')
// Pagination
const [currentPage, setCurrentPage] = useState(1)
// Mode
const [mode, setMode] = useState<'list' | 'detail'>('list')
// Debounce search
const searchTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
useEffect(() => {
if (searchTimer.current) clearTimeout(searchTimer.current)
searchTimer.current = setTimeout(() => setDebouncedSearch(searchQuery), 400)
return () => { if (searchTimer.current) clearTimeout(searchTimer.current) }
}, [searchQuery])
// Build query params
const buildParams = useCallback((extra?: Record<string, string>) => {
const p = new URLSearchParams()
p.set('control_type', 'atomic')
// Exclude duplicates — show only active masters
if (!extra?.release_state) {
// Don't filter by state for count queries that already have it
}
if (severityFilter) p.set('severity', severityFilter)
if (domainFilter) p.set('domain', domainFilter)
if (categoryFilter) p.set('category', categoryFilter)
if (debouncedSearch) p.set('search', debouncedSearch)
if (extra) for (const [k, v] of Object.entries(extra)) p.set(k, v)
return p.toString()
}, [severityFilter, domainFilter, categoryFilter, debouncedSearch])
// Load stats
const loadStats = useCallback(async () => {
try {
const res = await fetch(`${BACKEND_URL}?endpoint=atomic-stats`)
if (res.ok) setStats(await res.json())
} catch { /* ignore */ }
}, [])
// Load controls page
const loadControls = useCallback(async () => {
try {
setLoading(true)
const sortField = sortBy === 'id' ? 'control_id' : 'created_at'
const sortOrder = sortBy === 'newest' ? 'desc' : 'asc'
const offset = (currentPage - 1) * PAGE_SIZE
const qs = buildParams({
sort: sortField,
order: sortOrder,
limit: String(PAGE_SIZE),
offset: String(offset),
})
const countQs = buildParams()
const [ctrlRes, countRes] = await Promise.all([
fetch(`${BACKEND_URL}?endpoint=controls&${qs}`),
fetch(`${BACKEND_URL}?endpoint=controls-count&${countQs}`),
])
if (ctrlRes.ok) setControls(await ctrlRes.json())
if (countRes.ok) {
const data = await countRes.json()
setTotalCount(data.total || 0)
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Fehler beim Laden')
} finally {
setLoading(false)
}
}, [buildParams, sortBy, currentPage])
// Initial load
useEffect(() => { loadStats() }, [loadStats])
useEffect(() => { loadControls() }, [loadControls])
useEffect(() => { setCurrentPage(1) }, [severityFilter, domainFilter, categoryFilter, debouncedSearch, sortBy])
const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE))
// Loading
if (loading && controls.length === 0) {
return (
<div className="flex items-center justify-center h-96">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-violet-600 border-t-transparent" />
</div>
)
}
if (error) {
return (
<div className="flex items-center justify-center h-96">
<p className="text-red-600">{error}</p>
</div>
)
}
// DETAIL MODE
if (mode === 'detail' && selectedControl) {
return (
<div className="flex flex-col h-full">
<div className="flex-1 overflow-hidden">
<ControlDetail
ctrl={selectedControl}
onBack={() => { setMode('list'); setSelectedControl(null) }}
onEdit={() => {}}
onDelete={() => {}}
onReview={() => {}}
onNavigateToControl={async (controlId: string) => {
try {
const res = await fetch(`${BACKEND_URL}?endpoint=control&id=${controlId}`)
if (res.ok) {
const data = await res.json()
setSelectedControl(data)
}
} catch { /* ignore */ }
}}
/>
</div>
</div>
)
}
// =========================================================================
// LIST VIEW
// =========================================================================
return (
<div className="flex flex-col h-full">
{/* Header */}
<div className="border-b border-gray-200 bg-white px-6 py-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<Atom className="w-6 h-6 text-violet-600" />
<div>
<h1 className="text-lg font-semibold text-gray-900">Atomare Controls</h1>
<p className="text-xs text-gray-500">
Deduplizierte atomare Controls mit Herkunftsnachweis
</p>
</div>
</div>
<button
onClick={() => { loadControls(); loadStats() }}
className="p-2 text-gray-400 hover:text-violet-600"
title="Aktualisieren"
>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
</button>
</div>
{/* Stats Bar */}
{stats && (
<div className="grid grid-cols-4 gap-3 mb-4">
<div className="bg-violet-50 border border-violet-200 rounded-lg p-3">
<div className="text-2xl font-bold text-violet-700">{stats.total_active.toLocaleString('de-DE')}</div>
<div className="text-xs text-violet-500">Master Controls</div>
</div>
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3">
<div className="text-2xl font-bold text-gray-600">{stats.total_duplicate.toLocaleString('de-DE')}</div>
<div className="text-xs text-gray-500">Duplikate (entfernt)</div>
</div>
<div className="bg-indigo-50 border border-indigo-200 rounded-lg p-3">
<div className="text-2xl font-bold text-indigo-700">{stats.by_regulation.length}</div>
<div className="text-xs text-indigo-500">Regulierungen</div>
</div>
<div className="bg-emerald-50 border border-emerald-200 rounded-lg p-3">
<div className="text-2xl font-bold text-emerald-700">{stats.avg_regulation_coverage}</div>
<div className="text-xs text-emerald-500">Avg. Regulierungen / Control</div>
</div>
</div>
)}
{/* Filters */}
<div className="space-y-3">
<div className="flex items-center gap-3">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Atomare Controls durchsuchen (ID, Titel, Objective)..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
className="w-full pl-9 pr-4 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-violet-500"
/>
</div>
</div>
<div className="flex items-center gap-2 flex-wrap">
<Filter className="w-4 h-4 text-gray-400" />
<select
value={domainFilter}
onChange={e => setDomainFilter(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-violet-500"
>
<option value="">Domain</option>
{stats?.by_domain.map(d => (
<option key={d.domain} value={d.domain}>{d.domain} ({d.count})</option>
))}
</select>
<select
value={severityFilter}
onChange={e => setSeverityFilter(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-violet-500"
>
<option value="">Schweregrad</option>
<option value="critical">Kritisch</option>
<option value="high">Hoch</option>
<option value="medium">Mittel</option>
<option value="low">Niedrig</option>
</select>
<select
value={categoryFilter}
onChange={e => setCategoryFilter(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-violet-500"
>
<option value="">Kategorie</option>
{CATEGORY_OPTIONS.map(c => (
<option key={c.value} value={c.value}>{c.label}</option>
))}
</select>
<span className="text-gray-300 mx-1">|</span>
<ArrowUpDown className="w-4 h-4 text-gray-400" />
<select
value={sortBy}
onChange={e => setSortBy(e.target.value as 'id' | 'newest' | 'oldest')}
className="text-sm border border-gray-300 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-violet-500"
>
<option value="id">Sortierung: ID</option>
<option value="newest">Neueste zuerst</option>
<option value="oldest">Aelteste zuerst</option>
</select>
</div>
</div>
</div>
{/* Pagination Header */}
<div className="px-6 py-2 bg-gray-50 border-b border-gray-200 flex items-center justify-between text-xs text-gray-500">
<span>
{totalCount} Controls gefunden
{stats && totalCount !== stats.total_active && ` (von ${stats.total_active.toLocaleString('de-DE')} Master Controls)`}
{loading && <span className="ml-2 text-violet-500">Lade...</span>}
</span>
<span>Seite {currentPage} von {totalPages}</span>
</div>
{/* Control List */}
<div className="flex-1 overflow-y-auto p-6">
<div className="space-y-3">
{controls.map((ctrl) => (
<button
key={ctrl.control_id}
onClick={() => { setSelectedControl(ctrl); setMode('detail') }}
className="w-full text-left bg-white border border-gray-200 rounded-lg p-4 hover:border-violet-300 hover:shadow-sm transition-all group"
>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1 flex-wrap">
<span className="text-xs font-mono text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded">{ctrl.control_id}</span>
<SeverityBadge severity={ctrl.severity} />
<StateBadge state={ctrl.release_state} />
<CategoryBadge category={ctrl.category} />
<TargetAudienceBadge audience={ctrl.target_audience} />
<GenerationStrategyBadge strategy={ctrl.generation_strategy} pipelineInfo={ctrl} />
<ObligationTypeBadge type={ctrl.generation_metadata?.obligation_type as string} />
</div>
<h3 className="text-sm font-medium text-gray-900 group-hover:text-violet-700">{ctrl.title}</h3>
<p className="text-xs text-gray-500 mt-1 line-clamp-2">{ctrl.objective}</p>
<div className="flex items-center gap-2 mt-2">
{ctrl.source_citation?.source && (
<>
<span className="text-xs text-blue-600">
{ctrl.source_citation.source}
{ctrl.source_citation.article && ` ${ctrl.source_citation.article}`}
</span>
<span className="text-gray-300">|</span>
</>
)}
{ctrl.parent_control_id && (
<>
<span className="text-xs text-violet-500">via {ctrl.parent_control_id}</span>
<span className="text-gray-300">|</span>
</>
)}
<Clock className="w-3 h-3 text-gray-400" />
<span className="text-xs text-gray-400" title={ctrl.created_at}>
{ctrl.created_at ? new Date(ctrl.created_at).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit' }) : '-'}
</span>
</div>
</div>
<ChevronRight className="w-4 h-4 text-gray-300 group-hover:text-violet-500 flex-shrink-0 mt-1 ml-4" />
</div>
</button>
))}
{controls.length === 0 && !loading && (
<div className="text-center py-12 text-gray-400 text-sm">
Keine atomaren Controls gefunden.
</div>
)}
</div>
{/* Pagination Controls */}
{totalPages > 1 && (
<div className="flex items-center justify-center gap-2 mt-6 pb-4">
<button
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
className="p-2 text-gray-500 hover:text-violet-600 disabled:opacity-30 disabled:cursor-not-allowed"
>
<ChevronsLeft className="w-4 h-4" />
</button>
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="p-2 text-gray-500 hover:text-violet-600 disabled:opacity-30 disabled:cursor-not-allowed"
>
<ChevronLeft className="w-4 h-4" />
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1)
.filter(p => p === 1 || p === totalPages || Math.abs(p - currentPage) <= 2)
.reduce<(number | 'dots')[]>((acc, p, i, arr) => {
if (i > 0 && p - (arr[i - 1] as number) > 1) acc.push('dots')
acc.push(p)
return acc
}, [])
.map((p, i) =>
p === 'dots' ? (
<span key={`dots-${i}`} className="px-1 text-gray-400">...</span>
) : (
<button
key={p}
onClick={() => setCurrentPage(p as number)}
className={`w-8 h-8 text-sm rounded-lg ${
currentPage === p
? 'bg-violet-600 text-white'
: 'text-gray-600 hover:bg-violet-50 hover:text-violet-600'
}`}
>
{p}
</button>
)
)
}
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
className="p-2 text-gray-500 hover:text-violet-600 disabled:opacity-30 disabled:cursor-not-allowed"
>
<ChevronRight className="w-4 h-4" />
</button>
<button
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages}
className="p-2 text-gray-500 hover:text-violet-600 disabled:opacity-30 disabled:cursor-not-allowed"
>
<ChevronsRight className="w-4 h-4" />
</button>
</div>
)}
</div>
</div>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,322 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { render, screen, waitFor, fireEvent, act } from '@testing-library/react'
import ControlLibraryPage from '../page'
// ============================================================================
// Mock data
// ============================================================================
const MOCK_FRAMEWORK = {
id: 'fw-1',
framework_id: 'bp_security_v1',
name: 'BreakPilot Security',
version: '1.0',
description: 'Test framework',
release_state: 'draft',
}
const MOCK_CONTROL = {
id: 'ctrl-1',
framework_id: 'fw-1',
control_id: 'AUTH-001',
title: 'Multi-Factor Authentication',
objective: 'Require MFA for all admin accounts.',
rationale: 'Passwords alone are insufficient.',
scope: {},
requirements: ['MFA for admin'],
test_procedure: ['Test admin login'],
evidence: [{ type: 'config', description: 'MFA enabled' }],
severity: 'high',
risk_score: 4.0,
implementation_effort: 'm',
evidence_confidence: null,
open_anchors: [{ framework: 'OWASP', ref: 'V2.8', url: 'https://owasp.org' }],
release_state: 'draft',
tags: ['mfa'],
license_rule: 1,
source_original_text: null,
source_citation: { source: 'DSGVO' },
customer_visible: true,
verification_method: 'automated',
category: 'authentication',
target_audience: 'developer',
generation_metadata: null,
generation_strategy: 'ungrouped',
created_at: '2026-03-15T10:00:00+00:00',
updated_at: '2026-03-15T10:00:00+00:00',
}
const MOCK_META = {
total: 1,
domains: [{ domain: 'AUTH', count: 1 }],
sources: [{ source: 'DSGVO', count: 1 }],
no_source_count: 0,
}
// ============================================================================
// Fetch mock
// ============================================================================
function createFetchMock(overrides?: Record<string, unknown>) {
const responses: Record<string, unknown> = {
frameworks: [MOCK_FRAMEWORK],
controls: [MOCK_CONTROL],
'controls-count': { total: 1 },
'controls-meta': MOCK_META,
...overrides,
}
return vi.fn((url: string) => {
const urlStr = typeof url === 'string' ? url : ''
// Match endpoint param
const match = urlStr.match(/endpoint=([^&]+)/)
const endpoint = match?.[1] || ''
const data = responses[endpoint] ?? []
return Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve(data),
})
})
}
// ============================================================================
// Tests
// ============================================================================
describe('ControlLibraryPage', () => {
let fetchMock: ReturnType<typeof createFetchMock>
beforeEach(() => {
fetchMock = createFetchMock()
global.fetch = fetchMock as unknown as typeof fetch
})
afterEach(() => {
vi.restoreAllMocks()
})
it('renders the page header', async () => {
render(<ControlLibraryPage />)
await waitFor(() => {
expect(screen.getByText('Canonical Control Library')).toBeInTheDocument()
})
})
it('shows control count from meta', async () => {
fetchMock = createFetchMock({ 'controls-meta': { ...MOCK_META, total: 42 } })
global.fetch = fetchMock as unknown as typeof fetch
render(<ControlLibraryPage />)
await waitFor(() => {
expect(screen.getByText(/42 Security Controls/)).toBeInTheDocument()
})
})
it('renders control list with data', async () => {
render(<ControlLibraryPage />)
await waitFor(() => {
expect(screen.getByText('AUTH-001')).toBeInTheDocument()
expect(screen.getByText('Multi-Factor Authentication')).toBeInTheDocument()
})
})
it('shows timestamp on control cards', async () => {
render(<ControlLibraryPage />)
await waitFor(() => {
// The date should be rendered in German locale format
expect(screen.getByText(/15\.03\.26/)).toBeInTheDocument()
})
})
it('shows source citation on control cards', async () => {
render(<ControlLibraryPage />)
await waitFor(() => {
expect(screen.getByText('DSGVO')).toBeInTheDocument()
})
})
it('fetches with limit and offset params', async () => {
render(<ControlLibraryPage />)
await waitFor(() => {
expect(fetchMock).toHaveBeenCalled()
})
// Find the controls fetch call
const controlsCalls = fetchMock.mock.calls.filter(
(call: unknown[]) => typeof call[0] === 'string' && (call[0] as string).includes('endpoint=controls&')
)
expect(controlsCalls.length).toBeGreaterThan(0)
const url = controlsCalls[0][0] as string
expect(url).toContain('limit=50')
expect(url).toContain('offset=0')
})
it('fetches controls-count alongside controls', async () => {
render(<ControlLibraryPage />)
await waitFor(() => {
const countCalls = fetchMock.mock.calls.filter(
(call: unknown[]) => typeof call[0] === 'string' && (call[0] as string).includes('endpoint=controls-count')
)
expect(countCalls.length).toBeGreaterThan(0)
})
})
it('fetches controls-meta on mount', async () => {
render(<ControlLibraryPage />)
await waitFor(() => {
const metaCalls = fetchMock.mock.calls.filter(
(call: unknown[]) => typeof call[0] === 'string' && (call[0] as string).includes('endpoint=controls-meta')
)
expect(metaCalls.length).toBeGreaterThan(0)
})
})
it('renders domain dropdown from meta', async () => {
render(<ControlLibraryPage />)
await waitFor(() => {
expect(screen.getByText('AUTH (1)')).toBeInTheDocument()
})
})
it('renders source dropdown from meta', async () => {
render(<ControlLibraryPage />)
await waitFor(() => {
// The source option should appear in the dropdown
expect(screen.getByText('DSGVO (1)')).toBeInTheDocument()
})
})
it('has sort dropdown with all sort options', async () => {
render(<ControlLibraryPage />)
await waitFor(() => {
expect(screen.getByText('Sortierung: ID')).toBeInTheDocument()
expect(screen.getByText('Nach Quelle')).toBeInTheDocument()
expect(screen.getByText('Neueste zuerst')).toBeInTheDocument()
expect(screen.getByText('Aelteste zuerst')).toBeInTheDocument()
})
})
it('sends sort params when sorting by newest', async () => {
render(<ControlLibraryPage />)
await waitFor(() => {
expect(screen.getByText('AUTH-001')).toBeInTheDocument()
})
// Clear previous calls
fetchMock.mockClear()
// Change sort to newest
const sortSelect = screen.getByDisplayValue('Sortierung: ID')
await act(async () => {
fireEvent.change(sortSelect, { target: { value: 'newest' } })
})
await waitFor(() => {
const controlsCalls = fetchMock.mock.calls.filter(
(call: unknown[]) => typeof call[0] === 'string' && (call[0] as string).includes('endpoint=controls&')
)
expect(controlsCalls.length).toBeGreaterThan(0)
const url = controlsCalls[0][0] as string
expect(url).toContain('sort=created_at')
expect(url).toContain('order=desc')
})
})
it('sends search param after debounce', async () => {
render(<ControlLibraryPage />)
await waitFor(() => {
expect(screen.getByText('AUTH-001')).toBeInTheDocument()
})
fetchMock.mockClear()
const searchInput = screen.getByPlaceholderText(/Controls durchsuchen/)
await act(async () => {
fireEvent.change(searchInput, { target: { value: 'encryption' } })
})
// Wait for debounce (400ms)
await waitFor(
() => {
const controlsCalls = fetchMock.mock.calls.filter(
(call: unknown[]) => typeof call[0] === 'string' && (call[0] as string).includes('search=encryption')
)
expect(controlsCalls.length).toBeGreaterThan(0)
},
{ timeout: 1000 }
)
})
it('shows empty state when no controls', async () => {
fetchMock = createFetchMock({
controls: [],
'controls-count': { total: 0 },
'controls-meta': { ...MOCK_META, total: 0 },
})
global.fetch = fetchMock as unknown as typeof fetch
render(<ControlLibraryPage />)
await waitFor(() => {
expect(screen.getByText(/Noch keine Controls/)).toBeInTheDocument()
})
})
it('shows "Keine Controls gefunden" when filter matches nothing', async () => {
fetchMock = createFetchMock({
controls: [],
'controls-count': { total: 0 },
'controls-meta': { ...MOCK_META, total: 50 },
})
global.fetch = fetchMock as unknown as typeof fetch
render(<ControlLibraryPage />)
// Wait for initial load to finish
await waitFor(() => {
expect(screen.getByPlaceholderText(/Controls durchsuchen/)).toBeInTheDocument()
})
// Trigger a search to have a filter active
const searchInput = screen.getByPlaceholderText(/Controls durchsuchen/)
await act(async () => {
fireEvent.change(searchInput, { target: { value: 'zzzzzzz' } })
})
await waitFor(
() => {
expect(screen.getByText('Keine Controls gefunden.')).toBeInTheDocument()
},
{ timeout: 1000 }
)
})
it('has a refresh button', async () => {
render(<ControlLibraryPage />)
await waitFor(() => {
expect(screen.getByTitle('Aktualisieren')).toBeInTheDocument()
})
})
it('renders pagination info', async () => {
render(<ControlLibraryPage />)
await waitFor(() => {
expect(screen.getByText(/Seite 1 von 1/)).toBeInTheDocument()
})
})
it('shows pagination buttons for many controls', async () => {
fetchMock = createFetchMock({
'controls-count': { total: 150 },
'controls-meta': { ...MOCK_META, total: 150 },
})
global.fetch = fetchMock as unknown as typeof fetch
render(<ControlLibraryPage />)
await waitFor(() => {
expect(screen.getByText(/Seite 1 von 3/)).toBeInTheDocument()
})
})
})

View File

@@ -1,11 +1,83 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import {
ArrowLeft, ExternalLink, BookOpen, Scale, FileText,
Eye, CheckCircle2, Trash2, Pencil, Clock,
ChevronLeft, SkipForward,
ChevronLeft, SkipForward, GitMerge, Search, Landmark,
} from 'lucide-react'
import { CanonicalControl, EFFORT_LABELS, SeverityBadge, StateBadge, LicenseRuleBadge } from './helpers'
import {
CanonicalControl, EFFORT_LABELS, BACKEND_URL,
SeverityBadge, StateBadge, LicenseRuleBadge, VerificationMethodBadge, CategoryBadge, EvidenceTypeBadge, TargetAudienceBadge,
ObligationTypeBadge, GenerationStrategyBadge, isEigenentwicklung,
ExtractionMethodBadge, RegulationCountBadge,
VERIFICATION_METHODS, CATEGORY_OPTIONS, EVIDENCE_TYPE_OPTIONS,
ObligationInfo, DocumentReference, MergedDuplicate, RegulationSummary,
} from './helpers'
interface SimilarControl {
control_id: string
title: string
severity: string
release_state: string
tags: string[]
license_rule: number | null
verification_method: string | null
category: string | null
similarity: number
}
interface ParentLink {
parent_control_id: string
parent_title: string
link_type: string
confidence: number
source_regulation: string | null
source_article: string | null
parent_citation: Record<string, string> | null
obligation: {
text: string
action: string
object: string
normative_strength: string
} | null
}
interface TraceabilityData {
control_id: string
title: string
is_atomic: boolean
parent_links: ParentLink[]
children: Array<{
control_id: string
title: string
category: string
severity: string
decomposition_method: string
}>
source_count: number
// Extended provenance fields
obligations?: ObligationInfo[]
obligation_count?: number
document_references?: DocumentReference[]
merged_duplicates?: MergedDuplicate[]
merged_duplicates_count?: number
regulations_summary?: RegulationSummary[]
}
interface V1Match {
matched_control_id: string
matched_title: string
matched_objective: string
matched_severity: string
matched_category: string
matched_source: string | null
matched_article: string | null
matched_source_citation: Record<string, string> | null
similarity_score: number
match_rank: number
match_method: string
}
interface ControlDetailProps {
ctrl: CanonicalControl
@@ -13,6 +85,9 @@ interface ControlDetailProps {
onEdit: () => void
onDelete: (controlId: string) => void
onReview: (controlId: string, action: string) => void
onRefresh?: () => void
onNavigateToControl?: (controlId: string) => void
onCompare?: (ctrl: CanonicalControl, matches: V1Match[]) => void
// Review mode navigation
reviewMode?: boolean
reviewIndex?: number
@@ -27,12 +102,104 @@ export function ControlDetail({
onEdit,
onDelete,
onReview,
onRefresh,
onNavigateToControl,
onCompare,
reviewMode,
reviewIndex = 0,
reviewTotal = 0,
onReviewPrev,
onReviewNext,
}: ControlDetailProps) {
const [similarControls, setSimilarControls] = useState<SimilarControl[]>([])
const [loadingSimilar, setLoadingSimilar] = useState(false)
const [selectedDuplicates, setSelectedDuplicates] = useState<Set<string>>(new Set())
const [merging, setMerging] = useState(false)
const [traceability, setTraceability] = useState<TraceabilityData | null>(null)
const [loadingTrace, setLoadingTrace] = useState(false)
const [v1Matches, setV1Matches] = useState<V1Match[]>([])
const [loadingV1, setLoadingV1] = useState(false)
const eigenentwicklung = isEigenentwicklung(ctrl)
const loadTraceability = useCallback(async () => {
setLoadingTrace(true)
try {
// Try provenance first (extended data), fall back to traceability
let res = await fetch(`${BACKEND_URL}?endpoint=provenance&id=${ctrl.control_id}`)
if (!res.ok) {
res = await fetch(`${BACKEND_URL}?endpoint=traceability&id=${ctrl.control_id}`)
}
if (res.ok) {
setTraceability(await res.json())
}
} catch { /* ignore */ }
finally { setLoadingTrace(false) }
}, [ctrl.control_id])
const loadV1Matches = useCallback(async () => {
if (!eigenentwicklung) { setV1Matches([]); return }
setLoadingV1(true)
try {
const res = await fetch(`${BACKEND_URL}?endpoint=v1-matches&id=${ctrl.control_id}`)
if (res.ok) setV1Matches(await res.json())
else setV1Matches([])
} catch { setV1Matches([]) }
finally { setLoadingV1(false) }
}, [ctrl.control_id, eigenentwicklung])
useEffect(() => {
loadSimilarControls()
loadTraceability()
loadV1Matches()
setSelectedDuplicates(new Set())
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ctrl.control_id])
const loadSimilarControls = async () => {
setLoadingSimilar(true)
try {
const res = await fetch(`${BACKEND_URL}?endpoint=similar&id=${ctrl.control_id}`)
if (res.ok) {
setSimilarControls(await res.json())
}
} catch { /* ignore */ }
finally { setLoadingSimilar(false) }
}
const toggleDuplicate = (controlId: string) => {
setSelectedDuplicates(prev => {
const next = new Set(prev)
if (next.has(controlId)) next.delete(controlId)
else next.add(controlId)
return next
})
}
const handleMergeDuplicates = async () => {
if (selectedDuplicates.size === 0) return
if (!confirm(`${selectedDuplicates.size} Controls als Duplikate markieren und Tags/Anchors in ${ctrl.control_id} zusammenfuehren?`)) return
setMerging(true)
try {
// For each duplicate: mark as deprecated
for (const dupId of selectedDuplicates) {
await fetch(`${BACKEND_URL}?endpoint=update-control&id=${dupId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ release_state: 'deprecated' }),
})
}
// Refresh to show updated state
if (onRefresh) onRefresh()
setSelectedDuplicates(new Set())
loadSimilarControls()
} catch {
alert('Fehler beim Zusammenfuehren')
} finally {
setMerging(false)
}
}
return (
<div className="flex flex-col h-full">
{/* Header */}
@@ -47,6 +214,12 @@ export function ControlDetail({
<SeverityBadge severity={ctrl.severity} />
<StateBadge state={ctrl.release_state} />
<LicenseRuleBadge rule={ctrl.license_rule} />
<VerificationMethodBadge method={ctrl.verification_method} />
<CategoryBadge category={ctrl.category} />
<EvidenceTypeBadge type={ctrl.evidence_type} />
<TargetAudienceBadge audience={ctrl.target_audience} />
<GenerationStrategyBadge strategy={ctrl.generation_strategy} pipelineInfo={ctrl} />
<ObligationTypeBadge type={ctrl.generation_metadata?.obligation_type as string} />
</div>
<h2 className="text-lg font-semibold text-gray-900 mt-1">{ctrl.title}</h2>
</div>
@@ -86,22 +259,74 @@ export function ControlDetail({
<p className="text-sm text-gray-700 leading-relaxed">{ctrl.rationale}</p>
</section>
{/* Source Info (Rule 1 + 2) */}
{/* Quellennachweis (Rule 1 + 2) — dynamic label based on source_type */}
{ctrl.source_citation && (
<section className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<Scale className="w-4 h-4 text-blue-600" />
<h3 className="text-sm font-semibold text-blue-900">Quellenangabe</h3>
<section className={`border rounded-lg p-4 ${
ctrl.source_citation.source_type === 'law' ? 'bg-blue-50 border-blue-200' :
ctrl.source_citation.source_type === 'guideline' ? 'bg-indigo-50 border-indigo-200' :
'bg-teal-50 border-teal-200'
}`}>
<div className="flex items-center gap-2 mb-3">
<Scale className={`w-4 h-4 ${
ctrl.source_citation.source_type === 'law' ? 'text-blue-600' :
ctrl.source_citation.source_type === 'guideline' ? 'text-indigo-600' :
'text-teal-600'
}`} />
<h3 className={`text-sm font-semibold ${
ctrl.source_citation.source_type === 'law' ? 'text-blue-900' :
ctrl.source_citation.source_type === 'guideline' ? 'text-indigo-900' :
'text-teal-900'
}`}>{
ctrl.source_citation.source_type === 'law' ? 'Gesetzliche Grundlage' :
ctrl.source_citation.source_type === 'guideline' ? 'Behoerdliche Leitlinie' :
'Standard / Best Practice'
}</h3>
{ctrl.source_citation.source_type === 'law' && (
<span className="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full">Direkte gesetzliche Pflicht</span>
)}
{ctrl.source_citation.source_type === 'guideline' && (
<span className="text-xs bg-indigo-100 text-indigo-700 px-2 py-0.5 rounded-full">Aufsichtsbehoerdliche Empfehlung</span>
)}
{(ctrl.source_citation.source_type === 'standard' || (!ctrl.source_citation.source_type && ctrl.license_rule === 2)) && (
<span className="text-xs bg-teal-100 text-teal-700 px-2 py-0.5 rounded-full">Freiwilliger Standard</span>
)}
{(!ctrl.source_citation.source_type && ctrl.license_rule === 1) && (
<span className="text-xs bg-gray-100 text-gray-600 px-2 py-0.5 rounded-full">Noch nicht klassifiziert</span>
)}
</div>
<div className="text-xs text-blue-700 space-y-1">
{Object.entries(ctrl.source_citation).map(([k, v]) => (
<p key={k}><span className="font-medium">{k}:</span> {v}</p>
))}
<div className="flex items-start gap-3">
<div className="flex-1">
{ctrl.source_citation.source ? (
<p className="text-sm font-medium text-blue-900 mb-1">
{ctrl.source_citation.source}
{ctrl.source_citation.article && `${ctrl.source_citation.article}`}
{ctrl.source_citation.paragraph && ` ${ctrl.source_citation.paragraph}`}
</p>
) : ctrl.generation_metadata?.source_regulation ? (
<p className="text-sm font-medium text-blue-900 mb-1">{String(ctrl.generation_metadata.source_regulation)}</p>
) : null}
{ctrl.source_citation.license && (
<p className="text-xs text-blue-600">Lizenz: {ctrl.source_citation.license}</p>
)}
{ctrl.source_citation.license_notice && (
<p className="text-xs text-blue-600 mt-0.5">{ctrl.source_citation.license_notice}</p>
)}
</div>
{ctrl.source_citation.url && (
<a
href={ctrl.source_citation.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-xs text-blue-600 hover:text-blue-800 whitespace-nowrap"
>
<ExternalLink className="w-3.5 h-3.5" />Quelle
</a>
)}
</div>
{ctrl.source_original_text && (
<details className="mt-3">
<summary className="text-xs text-blue-600 cursor-pointer hover:text-blue-800">Originaltext anzeigen</summary>
<p className="text-xs text-gray-600 mt-2 p-2 bg-white rounded border border-blue-100 leading-relaxed max-h-40 overflow-y-auto">
<p className="text-xs text-gray-600 mt-2 p-2 bg-white rounded border border-blue-100 leading-relaxed max-h-40 overflow-y-auto whitespace-pre-wrap">
{ctrl.source_original_text}
</p>
</details>
@@ -109,6 +334,320 @@ export function ControlDetail({
</section>
)}
{/* Regulatorische Abdeckung (Eigenentwicklung) */}
{eigenentwicklung && (
<section className="bg-orange-50 border border-orange-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<Scale className="w-4 h-4 text-orange-600" />
<h3 className="text-sm font-semibold text-orange-900">
Regulatorische Abdeckung
</h3>
{loadingV1 && <span className="text-xs text-orange-400">Laden...</span>}
</div>
{v1Matches.length > 0 ? (
<div className="space-y-2">
{v1Matches.map((match, i) => (
<div key={i} className="bg-white/60 border border-orange-100 rounded-lg p-3">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap mb-1">
{match.matched_source && (
<span className="text-xs font-semibold text-blue-800 bg-blue-100 px-1.5 py-0.5 rounded">
{match.matched_source}
</span>
)}
{match.matched_article && (
<span className="text-xs text-blue-700 bg-blue-50 px-1.5 py-0.5 rounded">
{match.matched_article}
</span>
)}
<span className={`text-xs font-medium px-1.5 py-0.5 rounded ${
match.similarity_score >= 0.85 ? 'bg-green-100 text-green-700' :
match.similarity_score >= 0.80 ? 'bg-yellow-100 text-yellow-700' :
'bg-gray-100 text-gray-600'
}`}>
{(match.similarity_score * 100).toFixed(0)}%
</span>
</div>
<p className="text-sm text-gray-800">
{onNavigateToControl ? (
<button
onClick={() => onNavigateToControl(match.matched_control_id)}
className="font-mono text-xs text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded hover:bg-purple-100 hover:underline mr-1.5"
>
{match.matched_control_id}
</button>
) : (
<span className="font-mono text-xs text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded mr-1.5">
{match.matched_control_id}
</span>
)}
{match.matched_title}
</p>
</div>
{onCompare && (
<button
onClick={() => onCompare(ctrl, v1Matches)}
className="text-xs text-orange-600 border border-orange-300 rounded px-2 py-1 hover:bg-orange-100 whitespace-nowrap flex-shrink-0"
>
Vergleichen
</button>
)}
</div>
</div>
))}
</div>
) : !loadingV1 ? (
<p className="text-sm text-orange-600">Keine regulatorische Abdeckung gefunden. Dieses Control ist eine reine Eigenentwicklung.</p>
) : null}
</section>
)}
{/* Rechtsgrundlagen / Traceability (atomic controls) */}
{traceability && traceability.parent_links.length > 0 && (
<section className="bg-violet-50 border border-violet-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<Landmark className="w-4 h-4 text-violet-600" />
<h3 className="text-sm font-semibold text-violet-900">
Rechtsgrundlagen ({traceability.source_count} {traceability.source_count === 1 ? 'Quelle' : 'Quellen'})
</h3>
<ObligationTypeBadge type={ctrl.generation_metadata?.obligation_type as string} />
{traceability.regulations_summary && traceability.regulations_summary.map(rs => (
<span key={rs.regulation_code} className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-violet-200 text-violet-800">
{rs.regulation_code}
</span>
))}
{loadingTrace && <span className="text-xs text-violet-400">Laden...</span>}
</div>
<div className="space-y-3">
{traceability.parent_links.map((link, i) => (
<div key={i} className="bg-white/60 border border-violet-100 rounded-lg p-3">
<div className="flex items-start gap-2">
<Scale className="w-4 h-4 text-violet-500 mt-0.5 flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
{link.source_regulation && (
<span className="text-sm font-semibold text-violet-900">{link.source_regulation}</span>
)}
{link.source_article && (
<span className="text-sm text-violet-700">{link.source_article}</span>
)}
{!link.source_regulation && link.parent_citation?.source && (
<span className="text-sm font-semibold text-violet-900">
{link.parent_citation.source}
{link.parent_citation.article && `${link.parent_citation.article}`}
</span>
)}
<span className={`text-xs px-1.5 py-0.5 rounded ${
link.link_type === 'decomposition' ? 'bg-violet-100 text-violet-600' :
link.link_type === 'dedup_merge' ? 'bg-blue-100 text-blue-600' :
'bg-gray-100 text-gray-600'
}`}>
{link.link_type === 'decomposition' ? 'Ableitung' :
link.link_type === 'dedup_merge' ? 'Dedup' :
link.link_type}
</span>
</div>
<p className="text-xs text-violet-600 mt-1">
via{' '}
{onNavigateToControl ? (
<button
onClick={() => onNavigateToControl(link.parent_control_id)}
className="font-mono font-medium text-purple-700 bg-purple-50 px-1 py-0.5 rounded hover:bg-purple-100 hover:underline"
>
{link.parent_control_id}
</button>
) : (
<span className="font-mono font-medium text-purple-700 bg-purple-50 px-1 py-0.5 rounded">
{link.parent_control_id}
</span>
)}
{link.parent_title && (
<span className="text-violet-500 ml-1"> {link.parent_title}</span>
)}
</p>
{link.obligation && (
<p className="text-xs text-violet-500 mt-1.5 bg-violet-50 rounded p-2">
<span className={`inline-block mr-1.5 px-1.5 py-0.5 rounded text-xs font-medium ${
link.obligation.normative_strength === 'must' ? 'bg-red-100 text-red-700' :
link.obligation.normative_strength === 'should' ? 'bg-amber-100 text-amber-700' :
'bg-green-100 text-green-700'
}`}>
{link.obligation.normative_strength === 'must' ? 'MUSS' :
link.obligation.normative_strength === 'should' ? 'SOLL' : 'KANN'}
</span>
{link.obligation.text.slice(0, 200)}
{link.obligation.text.length > 200 ? '...' : ''}
</p>
)}
</div>
</div>
</div>
))}
</div>
</section>
)}
{/* Fallback: simple parent display when traceability not loaded yet */}
{ctrl.parent_control_uuid && (!traceability || traceability.parent_links.length === 0) && !loadingTrace && (
<section className="bg-violet-50 border border-violet-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-1">
<GitMerge className="w-4 h-4 text-violet-600" />
<h3 className="text-sm font-semibold text-violet-900">Atomares Control</h3>
<ObligationTypeBadge type={ctrl.generation_metadata?.obligation_type as string} />
</div>
<p className="text-sm text-violet-800">
Abgeleitet aus Eltern-Control{' '}
<span className="font-mono font-semibold text-purple-700 bg-purple-100 px-1.5 py-0.5 rounded">
{ctrl.parent_control_id || ctrl.parent_control_uuid}
</span>
{ctrl.parent_control_title && (
<span className="text-violet-700 ml-1"> {ctrl.parent_control_title}</span>
)}
</p>
</section>
)}
{/* Document References (atomic controls) */}
{traceability && traceability.is_atomic && traceability.document_references && traceability.document_references.length > 0 && (
<section className="bg-indigo-50 border border-indigo-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<FileText className="w-4 h-4 text-indigo-600" />
<h3 className="text-sm font-semibold text-indigo-900">
Original-Dokumente ({traceability.document_references.length})
</h3>
</div>
<div className="space-y-2">
{traceability.document_references.map((dr, i) => (
<div key={i} className="flex items-center gap-2 text-sm bg-white/60 border border-indigo-100 rounded-lg p-2">
<span className="font-semibold text-indigo-900">{dr.regulation_code}</span>
{dr.article && <span className="text-indigo-700">{dr.article}</span>}
{dr.paragraph && <span className="text-indigo-600 text-xs">{dr.paragraph}</span>}
<span className="ml-auto flex items-center gap-1.5">
<ExtractionMethodBadge method={dr.extraction_method} />
{dr.confidence !== null && (
<span className="text-xs text-gray-500">{(dr.confidence * 100).toFixed(0)}%</span>
)}
</span>
</div>
))}
</div>
</section>
)}
{/* Obligations (rich controls) */}
{traceability && !traceability.is_atomic && traceability.obligations && traceability.obligations.length > 0 && (
<section className="bg-amber-50 border border-amber-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<Scale className="w-4 h-4 text-amber-600" />
<h3 className="text-sm font-semibold text-amber-900">
Abgeleitete Pflichten ({traceability.obligation_count ?? traceability.obligations.length})
</h3>
</div>
<div className="space-y-2">
{traceability.obligations.map((ob) => (
<div key={ob.candidate_id} className="bg-white/60 border border-amber-100 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1 flex-wrap">
<span className="font-mono text-xs text-amber-700 bg-amber-100 px-1.5 py-0.5 rounded">{ob.candidate_id}</span>
<span className={`inline-block px-1.5 py-0.5 rounded text-xs font-medium ${
ob.normative_strength === 'must' ? 'bg-red-100 text-red-700' :
ob.normative_strength === 'should' ? 'bg-amber-100 text-amber-700' :
'bg-green-100 text-green-700'
}`}>
{ob.normative_strength === 'must' ? 'MUSS' :
ob.normative_strength === 'should' ? 'SOLL' : 'KANN'}
</span>
{ob.action && <span className="text-xs text-amber-600">{ob.action}</span>}
{ob.object && <span className="text-xs text-amber-500"> {ob.object}</span>}
</div>
<p className="text-xs text-gray-700 leading-relaxed">
{ob.obligation_text.slice(0, 300)}
{ob.obligation_text.length > 300 ? '...' : ''}
</p>
</div>
))}
</div>
</section>
)}
{/* Merged Duplicates */}
{traceability && traceability.merged_duplicates && traceability.merged_duplicates.length > 0 && (
<section className="bg-slate-50 border border-slate-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<GitMerge className="w-4 h-4 text-slate-600" />
<h3 className="text-sm font-semibold text-slate-900">
Zusammengefuehrte Duplikate ({traceability.merged_duplicates_count ?? traceability.merged_duplicates.length})
</h3>
</div>
<div className="space-y-1.5">
{traceability.merged_duplicates.map((dup) => (
<div key={dup.control_id} className="flex items-center gap-2 text-sm">
{onNavigateToControl ? (
<button
onClick={() => onNavigateToControl(dup.control_id)}
className="font-mono text-xs text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded hover:bg-purple-100 hover:underline"
>
{dup.control_id}
</button>
) : (
<span className="font-mono text-xs text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded">{dup.control_id}</span>
)}
<span className="text-gray-700 flex-1 truncate">{dup.title}</span>
{dup.source_regulation && (
<span className="text-xs text-slate-500 bg-slate-100 px-1.5 py-0.5 rounded">{dup.source_regulation}</span>
)}
</div>
))}
</div>
</section>
)}
{/* Child controls (rich controls that have atomic children) */}
{traceability && traceability.children.length > 0 && (
<section className="bg-emerald-50 border border-emerald-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<GitMerge className="w-4 h-4 text-emerald-600" />
<h3 className="text-sm font-semibold text-emerald-900">
Abgeleitete Controls ({traceability.children.length})
</h3>
</div>
<div className="space-y-1.5">
{traceability.children.map((child) => (
<div key={child.control_id} className="flex items-center gap-2 text-sm">
{onNavigateToControl ? (
<button
onClick={() => onNavigateToControl(child.control_id)}
className="font-mono text-xs text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded hover:bg-purple-100 hover:underline"
>
{child.control_id}
</button>
) : (
<span className="font-mono text-xs text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded">{child.control_id}</span>
)}
<span className="text-gray-700 flex-1 truncate">{child.title}</span>
<SeverityBadge severity={child.severity} />
</div>
))}
</div>
</section>
)}
{/* Impliziter Gesetzesbezug (Rule 3 — reformuliert, kein Originaltext) */}
{!ctrl.source_citation && ctrl.open_anchors.length > 0 && (
<section className="bg-amber-50 border border-amber-200 rounded-lg p-3">
<div className="flex items-center gap-2">
<Scale className="w-4 h-4 text-amber-600" />
<div className="flex-1">
<p className="text-xs text-amber-800 font-medium">Abgeleitet aus regulatorischen Anforderungen</p>
<p className="text-xs text-amber-700 mt-0.5">
Dieser Control wurde aus geschuetzten Quellen reformuliert (z.B. BSI Grundschutz, ISO 27001).
Die konkreten Massnahmen leiten sich aus den Open-Source-Referenzen unten ab.
</p>
</div>
</div>
</section>
)}
{/* Scope */}
{(ctrl.scope.platforms?.length || ctrl.scope.components?.length || ctrl.scope.data_classes?.length) ? (
<section>
@@ -151,7 +690,7 @@ export function ControlDetail({
</section>
)}
{/* Evidence */}
{/* Evidence — handles both {type, description} objects and plain strings */}
{ctrl.evidence.length > 0 && (
<section>
<h3 className="text-sm font-semibold text-gray-900 mb-2">Nachweise</h3>
@@ -159,7 +698,11 @@ export function ControlDetail({
{ctrl.evidence.map((ev, i) => (
<div key={i} className="flex items-start gap-2 text-sm text-gray-700">
<FileText className="w-4 h-4 text-gray-400 flex-shrink-0 mt-0.5" />
<div><span className="font-medium">{ev.type}:</span> {ev.description}</div>
{typeof ev === 'string' ? (
<div>{ev}</div>
) : (
<div><span className="font-medium">{ev.type}:</span> {ev.description}</div>
)}
</div>
))}
</div>
@@ -213,7 +756,18 @@ export function ControlDetail({
<h3 className="text-sm font-semibold text-gray-700">Generierungsdetails (intern)</h3>
</div>
<div className="text-xs text-gray-600 space-y-1">
<p>Pfad: {String(ctrl.generation_metadata.processing_path || '-')}</p>
{ctrl.generation_metadata.processing_path && (
<p>Pfad: {String(ctrl.generation_metadata.processing_path)}</p>
)}
{ctrl.generation_metadata.decomposition_method && (
<p>Methode: {String(ctrl.generation_metadata.decomposition_method)}</p>
)}
{ctrl.generation_metadata.pass0b_model && (
<p>LLM: {String(ctrl.generation_metadata.pass0b_model)}</p>
)}
{ctrl.generation_metadata.obligation_type && (
<p>Obligation-Typ: {String(ctrl.generation_metadata.obligation_type)}</p>
)}
{ctrl.generation_metadata.similarity_status && (
<p className="text-red-600">Similarity: {String(ctrl.generation_metadata.similarity_status)}</p>
)}
@@ -229,6 +783,60 @@ export function ControlDetail({
</section>
)}
{/* Similar Controls (Dedup) */}
<section className="bg-gray-50 border border-gray-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<Search className="w-4 h-4 text-gray-600" />
<h3 className="text-sm font-semibold text-gray-800">Aehnliche Controls</h3>
{loadingSimilar && <span className="text-xs text-gray-400">Laden...</span>}
</div>
{similarControls.length > 0 ? (
<>
<div className="mb-3 p-2 bg-white border border-gray-100 rounded flex items-center gap-2">
<input type="radio" checked readOnly className="text-purple-600" />
<span className="text-sm font-medium text-purple-700">{ctrl.control_id} {ctrl.title}</span>
<span className="text-xs text-gray-400 ml-auto">Behalten (Haupt-Control)</span>
</div>
<div className="space-y-2">
{similarControls.map(sim => (
<div key={sim.control_id} className="p-2 bg-white border border-gray-100 rounded flex items-center gap-2">
<input
type="checkbox"
checked={selectedDuplicates.has(sim.control_id)}
onChange={() => toggleDuplicate(sim.control_id)}
className="text-red-600"
/>
<span className="text-xs font-mono text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded">{sim.control_id}</span>
<span className="text-sm text-gray-700 flex-1">{sim.title}</span>
<span className="text-xs font-medium text-amber-600 bg-amber-50 px-1.5 py-0.5 rounded">
{(sim.similarity * 100).toFixed(1)}%
</span>
<LicenseRuleBadge rule={sim.license_rule} />
<VerificationMethodBadge method={sim.verification_method} />
</div>
))}
</div>
{selectedDuplicates.size > 0 && (
<button
onClick={handleMergeDuplicates}
disabled={merging}
className="mt-3 flex items-center gap-1.5 px-3 py-1.5 text-sm text-white bg-red-600 rounded-lg hover:bg-red-700 disabled:opacity-50"
>
<GitMerge className="w-3.5 h-3.5" />
{merging ? 'Zusammenfuehren...' : `${selectedDuplicates.size} Duplikat(e) zusammenfuehren`}
</button>
)}
</>
) : (
<p className="text-sm text-gray-500">
{loadingSimilar ? 'Suche aehnliche Controls...' : 'Keine aehnlichen Controls gefunden.'}
</p>
)}
</section>
{/* Review Actions */}
{['needs_review', 'too_close', 'duplicate'].includes(ctrl.release_state) && (
<section className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">

View File

@@ -2,7 +2,7 @@
import { useState } from 'react'
import { BookOpen, Trash2, Save, X } from 'lucide-react'
import { EMPTY_CONTROL } from './helpers'
import { EMPTY_CONTROL, VERIFICATION_METHODS, CATEGORY_OPTIONS, TARGET_AUDIENCE_OPTIONS } from './helpers'
export function ControlForm({
initial,
@@ -267,6 +267,51 @@ export function ControlForm({
</select>
</div>
</div>
{/* Verification Method, Category & Target Audience */}
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Nachweismethode</label>
<select
value={form.verification_method || ''}
onChange={e => setForm({ ...form, verification_method: e.target.value || null })}
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg"
>
<option value=""> Nicht zugewiesen </option>
{Object.entries(VERIFICATION_METHODS).map(([k, v]) => (
<option key={k} value={k}>{v.label}</option>
))}
</select>
<p className="text-xs text-gray-400 mt-1">Wie wird dieses Control nachgewiesen?</p>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Kategorie</label>
<select
value={form.category || ''}
onChange={e => setForm({ ...form, category: e.target.value || null })}
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg"
>
<option value=""> Nicht zugewiesen </option>
{CATEGORY_OPTIONS.map(c => (
<option key={c.value} value={c.value}>{c.label}</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Zielgruppe</label>
<select
value={form.target_audience || ''}
onChange={e => setForm({ ...form, target_audience: e.target.value || null })}
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg"
>
<option value=""> Nicht zugewiesen </option>
{Object.entries(TARGET_AUDIENCE_OPTIONS).map(([k, v]) => (
<option key={k} value={k}>{v.label}</option>
))}
</select>
<p className="text-xs text-gray-400 mt-1">Fuer wen ist dieses Control relevant?</p>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,264 @@
'use client'
import { useState, useEffect } from 'react'
import {
ArrowLeft, CheckCircle2, Trash2, Pencil, SkipForward,
ChevronLeft, Scale, BookOpen, ExternalLink, AlertTriangle,
FileText, Clock,
} from 'lucide-react'
import {
CanonicalControl, BACKEND_URL,
SeverityBadge, StateBadge, LicenseRuleBadge, CategoryBadge, TargetAudienceBadge,
} from './helpers'
// =============================================================================
// Compact Control Panel (used on both sides of the comparison)
// =============================================================================
export function ControlPanel({ ctrl, label, highlight }: { ctrl: CanonicalControl; label: string; highlight?: boolean }) {
return (
<div className={`flex flex-col h-full overflow-y-auto ${highlight ? 'bg-yellow-50' : 'bg-white'}`}>
{/* Panel Header */}
<div className={`sticky top-0 z-10 px-4 py-3 border-b ${highlight ? 'bg-yellow-100 border-yellow-200' : 'bg-gray-50 border-gray-200'}`}>
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 mb-1">{label}</div>
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-mono text-purple-600 bg-purple-50 px-2 py-0.5 rounded">{ctrl.control_id}</span>
<SeverityBadge severity={ctrl.severity} />
<StateBadge state={ctrl.release_state} />
<LicenseRuleBadge rule={ctrl.license_rule} />
<CategoryBadge category={ctrl.category} />
<TargetAudienceBadge audience={ctrl.target_audience} />
</div>
<h3 className="text-sm font-semibold text-gray-900 mt-1 leading-snug">{ctrl.title}</h3>
</div>
{/* Panel Content */}
<div className="p-4 space-y-4 text-sm">
{/* Objective */}
<section>
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">Ziel</h4>
<p className="text-gray-700 leading-relaxed">{ctrl.objective}</p>
</section>
{/* Rationale */}
{ctrl.rationale && (
<section>
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">Begruendung</h4>
<p className="text-gray-700 leading-relaxed">{ctrl.rationale}</p>
</section>
)}
{/* Source Citation (Rule 1+2) */}
{ctrl.source_citation && (
<section className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<div className="flex items-center gap-1.5 mb-1">
<Scale className="w-3.5 h-3.5 text-blue-600" />
<span className="text-xs font-semibold text-blue-900">Gesetzliche Grundlage</span>
</div>
{ctrl.source_citation.source && (
<p className="text-xs text-blue-800">
{ctrl.source_citation.source}
{ctrl.source_citation.article && `${ctrl.source_citation.article}`}
{ctrl.source_citation.paragraph && ` ${ctrl.source_citation.paragraph}`}
</p>
)}
</section>
)}
{/* Requirements */}
{ctrl.requirements.length > 0 && (
<section>
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">Anforderungen</h4>
<ol className="list-decimal list-inside space-y-1">
{ctrl.requirements.map((r, i) => (
<li key={i} className="text-gray-700 text-xs leading-relaxed">{r}</li>
))}
</ol>
</section>
)}
{/* Test Procedure */}
{ctrl.test_procedure.length > 0 && (
<section>
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">Pruefverfahren</h4>
<ol className="list-decimal list-inside space-y-1">
{ctrl.test_procedure.map((s, i) => (
<li key={i} className="text-gray-700 text-xs leading-relaxed">{s}</li>
))}
</ol>
</section>
)}
{/* Open Anchors */}
{ctrl.open_anchors.length > 0 && (
<section className="bg-green-50 border border-green-200 rounded-lg p-3">
<div className="flex items-center gap-1.5 mb-2">
<BookOpen className="w-3.5 h-3.5 text-green-700" />
<span className="text-xs font-semibold text-green-900">Referenzen ({ctrl.open_anchors.length})</span>
</div>
<div className="space-y-1">
{ctrl.open_anchors.map((a, i) => (
<div key={i} className="flex items-center gap-1.5 text-xs">
<ExternalLink className="w-3 h-3 text-green-600 flex-shrink-0" />
<span className="font-medium text-green-800">{a.framework}</span>
<span className="text-green-700">{a.ref}</span>
</div>
))}
</div>
</section>
)}
{/* Tags */}
{ctrl.tags.length > 0 && (
<div className="flex items-center gap-1 flex-wrap">
{ctrl.tags.map(t => (
<span key={t} className="px-2 py-0.5 bg-gray-100 text-gray-600 rounded text-xs">{t}</span>
))}
</div>
)}
</div>
</div>
)
}
// =============================================================================
// ReviewCompare — Side-by-Side Duplicate Comparison
// =============================================================================
interface ReviewCompareProps {
ctrl: CanonicalControl
onBack: () => void
onReview: (controlId: string, action: string) => void
onEdit: () => void
reviewIndex: number
reviewTotal: number
onReviewPrev: () => void
onReviewNext: () => void
}
export function ReviewCompare({
ctrl,
onBack,
onReview,
onEdit,
reviewIndex,
reviewTotal,
onReviewPrev,
onReviewNext,
}: ReviewCompareProps) {
const [suspectedDuplicate, setSuspectedDuplicate] = useState<CanonicalControl | null>(null)
const [loading, setLoading] = useState(false)
const [similarity, setSimilarity] = useState<number | null>(null)
// Load the suspected duplicate from generation_metadata.similar_controls
useEffect(() => {
const loadDuplicate = async () => {
const similarControls = ctrl.generation_metadata?.similar_controls as Array<{ control_id: string; title: string; similarity: number }> | undefined
if (!similarControls || similarControls.length === 0) {
setSuspectedDuplicate(null)
setSimilarity(null)
return
}
const suspect = similarControls[0]
setSimilarity(suspect.similarity)
setLoading(true)
try {
const res = await fetch(`${BACKEND_URL}?endpoint=control&id=${encodeURIComponent(suspect.control_id)}`)
if (res.ok) {
const data = await res.json()
setSuspectedDuplicate(data)
} else {
setSuspectedDuplicate(null)
}
} catch {
setSuspectedDuplicate(null)
} finally {
setLoading(false)
}
}
loadDuplicate()
}, [ctrl.control_id, ctrl.generation_metadata])
return (
<div className="flex flex-col h-full">
{/* Header */}
<div className="border-b border-gray-200 bg-white px-6 py-3 flex items-center justify-between">
<div className="flex items-center gap-3">
<button onClick={onBack} className="text-gray-400 hover:text-gray-600">
<ArrowLeft className="w-5 h-5" />
</button>
<div>
<div className="flex items-center gap-2">
<AlertTriangle className="w-4 h-4 text-amber-500" />
<span className="text-sm font-semibold text-gray-900">Duplikat-Vergleich</span>
{similarity !== null && (
<span className="text-xs font-medium text-amber-600 bg-amber-50 px-2 py-0.5 rounded-full">
{(similarity * 100).toFixed(1)}% Aehnlichkeit
</span>
)}
</div>
</div>
</div>
<div className="flex items-center gap-2">
{/* Navigation */}
<div className="flex items-center gap-1 mr-3">
<button onClick={onReviewPrev} disabled={reviewIndex === 0} className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-30">
<ChevronLeft className="w-4 h-4" />
</button>
<span className="text-xs text-gray-500 font-medium">{reviewIndex + 1} / {reviewTotal}</span>
<button onClick={onReviewNext} disabled={reviewIndex >= reviewTotal - 1} className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-30">
<SkipForward className="w-4 h-4" />
</button>
</div>
{/* Actions */}
<button
onClick={() => onReview(ctrl.control_id, 'approve')}
className="px-3 py-1.5 text-sm text-white bg-green-600 rounded-lg hover:bg-green-700"
>
<CheckCircle2 className="w-3.5 h-3.5 inline mr-1" />Behalten
</button>
<button
onClick={() => onReview(ctrl.control_id, 'reject')}
className="px-3 py-1.5 text-sm text-white bg-red-600 rounded-lg hover:bg-red-700"
>
<Trash2 className="w-3.5 h-3.5 inline mr-1" />Duplikat
</button>
<button
onClick={onEdit}
className="px-3 py-1.5 text-sm text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50"
>
<Pencil className="w-3.5 h-3.5 inline mr-1" />Bearbeiten
</button>
</div>
</div>
{/* Side-by-Side Panels */}
<div className="flex-1 flex overflow-hidden">
{/* Left: Control to review */}
<div className="w-1/2 border-r border-gray-200 overflow-y-auto">
<ControlPanel ctrl={ctrl} label="Zu pruefen" highlight />
</div>
{/* Right: Suspected duplicate */}
<div className="w-1/2 overflow-y-auto">
{loading ? (
<div className="flex items-center justify-center h-full">
<div className="animate-spin rounded-full h-6 w-6 border-2 border-purple-600 border-t-transparent" />
</div>
) : suspectedDuplicate ? (
<ControlPanel ctrl={suspectedDuplicate} label="Bestehendes Control (Verdacht)" />
) : (
<div className="flex items-center justify-center h-full text-gray-400 text-sm">
Kein Duplikat-Kandidat gefunden
</div>
)}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,155 @@
'use client'
import { useState, useEffect } from 'react'
import {
ArrowLeft, ChevronLeft, SkipForward, Scale,
} from 'lucide-react'
import { CanonicalControl, BACKEND_URL } from './helpers'
import { ControlPanel } from './ReviewCompare'
interface V1Match {
matched_control_id: string
matched_title: string
matched_objective: string
matched_severity: string
matched_category: string
matched_source: string | null
matched_article: string | null
matched_source_citation: Record<string, string> | null
similarity_score: number
match_rank: number
match_method: string
}
interface V1CompareViewProps {
v1Control: CanonicalControl
matches: V1Match[]
onBack: () => void
onNavigateToControl?: (controlId: string) => void
}
export function V1CompareView({ v1Control, matches, onBack, onNavigateToControl }: V1CompareViewProps) {
const [currentMatchIndex, setCurrentMatchIndex] = useState(0)
const [matchedControl, setMatchedControl] = useState<CanonicalControl | null>(null)
const [loading, setLoading] = useState(false)
const currentMatch = matches[currentMatchIndex]
// Load the full matched control when index changes
useEffect(() => {
if (!currentMatch) return
const load = async () => {
setLoading(true)
try {
const res = await fetch(`${BACKEND_URL}?endpoint=control&id=${encodeURIComponent(currentMatch.matched_control_id)}`)
if (res.ok) {
setMatchedControl(await res.json())
} else {
setMatchedControl(null)
}
} catch {
setMatchedControl(null)
} finally {
setLoading(false)
}
}
load()
}, [currentMatch])
return (
<div className="flex flex-col h-full">
{/* Header */}
<div className="border-b border-gray-200 bg-white px-6 py-3 flex items-center justify-between">
<div className="flex items-center gap-3">
<button onClick={onBack} className="text-gray-400 hover:text-gray-600">
<ArrowLeft className="w-5 h-5" />
</button>
<div>
<div className="flex items-center gap-2">
<Scale className="w-4 h-4 text-orange-500" />
<span className="text-sm font-semibold text-gray-900">V1-Vergleich</span>
{currentMatch && (
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${
currentMatch.similarity_score >= 0.85 ? 'bg-green-100 text-green-700' :
currentMatch.similarity_score >= 0.80 ? 'bg-yellow-100 text-yellow-700' :
'bg-gray-100 text-gray-600'
}`}>
{(currentMatch.similarity_score * 100).toFixed(1)}% Aehnlichkeit
</span>
)}
</div>
</div>
</div>
<div className="flex items-center gap-2">
{/* Navigation */}
<div className="flex items-center gap-1">
<button
onClick={() => setCurrentMatchIndex(Math.max(0, currentMatchIndex - 1))}
disabled={currentMatchIndex === 0}
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-30"
>
<ChevronLeft className="w-4 h-4" />
</button>
<span className="text-xs text-gray-500 font-medium">
{currentMatchIndex + 1} / {matches.length}
</span>
<button
onClick={() => setCurrentMatchIndex(Math.min(matches.length - 1, currentMatchIndex + 1))}
disabled={currentMatchIndex >= matches.length - 1}
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-30"
>
<SkipForward className="w-4 h-4" />
</button>
</div>
{/* Navigate to matched control */}
{onNavigateToControl && matchedControl && (
<button
onClick={() => { onBack(); onNavigateToControl(matchedControl.control_id) }}
className="px-3 py-1.5 text-sm text-purple-600 border border-purple-300 rounded-lg hover:bg-purple-50"
>
Zum Control
</button>
)}
</div>
</div>
{/* Source info bar */}
{currentMatch && (currentMatch.matched_source || currentMatch.matched_article) && (
<div className="px-6 py-2 bg-blue-50 border-b border-blue-200 flex items-center gap-2 text-sm">
<Scale className="w-3.5 h-3.5 text-blue-600" />
{currentMatch.matched_source && (
<span className="font-semibold text-blue-900">{currentMatch.matched_source}</span>
)}
{currentMatch.matched_article && (
<span className="text-blue-700">{currentMatch.matched_article}</span>
)}
</div>
)}
{/* Side-by-Side Panels */}
<div className="flex-1 flex overflow-hidden">
{/* Left: V1 Eigenentwicklung */}
<div className="w-1/2 border-r border-gray-200 overflow-y-auto">
<ControlPanel ctrl={v1Control} label="Eigenentwicklung" highlight />
</div>
{/* Right: Regulatory match */}
<div className="w-1/2 overflow-y-auto">
{loading ? (
<div className="flex items-center justify-center h-full">
<div className="animate-spin rounded-full h-6 w-6 border-2 border-purple-600 border-t-transparent" />
</div>
) : matchedControl ? (
<ControlPanel ctrl={matchedControl} label="Regulatorisch gedeckt" />
) : (
<div className="flex items-center justify-center h-full text-gray-400 text-sm">
Control konnte nicht geladen werden
</div>
)}
</div>
</div>
</div>
)
}

View File

@@ -30,7 +30,7 @@ export interface CanonicalControl {
}
requirements: string[]
test_procedure: string[]
evidence: EvidenceItem[]
evidence: (EvidenceItem | string)[]
severity: string
risk_score: number | null
implementation_effort: string | null
@@ -42,7 +42,17 @@ export interface CanonicalControl {
source_original_text?: string | null
source_citation?: Record<string, string> | null
customer_visible?: boolean
verification_method: string | null
category: string | null
evidence_type: string | null
target_audience: string | string[] | null
generation_metadata?: Record<string, unknown> | null
generation_strategy?: string | null
parent_control_uuid?: string | null
parent_control_id?: string | null
parent_control_title?: string | null
decomposition_method?: string | null
pipeline_version?: number | string | null
created_at: string
updated_at: string
}
@@ -92,6 +102,10 @@ export const EMPTY_CONTROL = {
open_anchors: [{ framework: '', ref: '', url: '' }],
release_state: 'draft',
tags: [] as string[],
verification_method: null as string | null,
category: null as string | null,
evidence_type: null as string | null,
target_audience: null as string | null,
}
export const DOMAIN_OPTIONS = [
@@ -107,6 +121,69 @@ export const DOMAIN_OPTIONS = [
{ value: 'COMP', label: 'COMP — Compliance' },
]
export const VERIFICATION_METHODS: Record<string, { bg: string; label: string }> = {
code_review: { bg: 'bg-blue-100 text-blue-700', label: 'Code Review' },
document: { bg: 'bg-amber-100 text-amber-700', label: 'Dokument' },
tool: { bg: 'bg-teal-100 text-teal-700', label: 'Tool' },
hybrid: { bg: 'bg-purple-100 text-purple-700', label: 'Hybrid' },
}
export const CATEGORY_OPTIONS = [
{ value: 'encryption', label: 'Verschluesselung & Kryptographie' },
{ value: 'authentication', label: 'Authentisierung & Zugriffskontrolle' },
{ value: 'network', label: 'Netzwerksicherheit' },
{ value: 'data_protection', label: 'Datenschutz & Datensicherheit' },
{ value: 'logging', label: 'Logging & Monitoring' },
{ value: 'incident', label: 'Vorfallmanagement' },
{ value: 'continuity', label: 'Notfall & Wiederherstellung' },
{ value: 'compliance', label: 'Compliance & Audit' },
{ value: 'supply_chain', label: 'Lieferkettenmanagement' },
{ value: 'physical', label: 'Physische Sicherheit' },
{ value: 'personnel', label: 'Personal & Schulung' },
{ value: 'application', label: 'Anwendungssicherheit' },
{ value: 'system', label: 'Systemhaertung & -betrieb' },
{ value: 'risk', label: 'Risikomanagement' },
{ value: 'governance', label: 'Sicherheitsorganisation' },
{ value: 'hardware', label: 'Hardware & Plattformsicherheit' },
{ value: 'identity', label: 'Identitaetsmanagement' },
]
export const EVIDENCE_TYPE_CONFIG: Record<string, { bg: string; label: string }> = {
code: { bg: 'bg-sky-100 text-sky-700', label: 'Code' },
process: { bg: 'bg-amber-100 text-amber-700', label: 'Prozess' },
hybrid: { bg: 'bg-violet-100 text-violet-700', label: 'Hybrid' },
}
export const EVIDENCE_TYPE_OPTIONS = [
{ value: 'code', label: 'Code — Technisch (Source Code, IaC, CI/CD)' },
{ value: 'process', label: 'Prozess — Organisatorisch (Dokumente, Policies)' },
{ value: 'hybrid', label: 'Hybrid — Code + Prozess' },
]
export const TARGET_AUDIENCE_OPTIONS: Record<string, { bg: string; label: string }> = {
// Legacy English keys
enterprise: { bg: 'bg-cyan-100 text-cyan-700', label: 'Unternehmen' },
authority: { bg: 'bg-rose-100 text-rose-700', label: 'Behoerden' },
provider: { bg: 'bg-violet-100 text-violet-700', label: 'Anbieter' },
all: { bg: 'bg-gray-100 text-gray-700', label: 'Alle' },
// German keys from LLM generation
unternehmen: { bg: 'bg-cyan-100 text-cyan-700', label: 'Unternehmen' },
behoerden: { bg: 'bg-rose-100 text-rose-700', label: 'Behoerden' },
entwickler: { bg: 'bg-sky-100 text-sky-700', label: 'Entwickler' },
datenschutzbeauftragte: { bg: 'bg-purple-100 text-purple-700', label: 'DSB' },
geschaeftsfuehrung: { bg: 'bg-amber-100 text-amber-700', label: 'GF' },
'it-abteilung': { bg: 'bg-blue-100 text-blue-700', label: 'IT' },
rechtsabteilung: { bg: 'bg-fuchsia-100 text-fuchsia-700', label: 'Recht' },
'compliance-officer': { bg: 'bg-indigo-100 text-indigo-700', label: 'Compliance' },
personalwesen: { bg: 'bg-pink-100 text-pink-700', label: 'Personal' },
einkauf: { bg: 'bg-lime-100 text-lime-700', label: 'Einkauf' },
produktion: { bg: 'bg-orange-100 text-orange-700', label: 'Produktion' },
vertrieb: { bg: 'bg-teal-100 text-teal-700', label: 'Vertrieb' },
gesundheitswesen: { bg: 'bg-red-100 text-red-700', label: 'Gesundheit' },
finanzwesen: { bg: 'bg-emerald-100 text-emerald-700', label: 'Finanzen' },
oeffentlicher_dienst: { bg: 'bg-rose-100 text-rose-700', label: 'Oeffentl. Dienst' },
}
export const COLLECTION_OPTIONS = [
{ value: 'bp_compliance_ce', label: 'CE (OWASP, ENISA, BSI)' },
{ value: 'bp_compliance_gesetze', label: 'Gesetze (EU, DE, BSI)' },
@@ -165,6 +242,167 @@ export function LicenseRuleBadge({ rule }: { rule: number | null | undefined })
return <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${c.bg}`}>{c.label}</span>
}
export function VerificationMethodBadge({ method }: { method: string | null }) {
if (!method) return null
const config = VERIFICATION_METHODS[method]
if (!config) return null
return <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${config.bg}`}>{config.label}</span>
}
export function CategoryBadge({ category }: { category: string | null }) {
if (!category) return null
const opt = CATEGORY_OPTIONS.find(c => c.value === category)
return (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-indigo-50 text-indigo-700">
{opt?.label || category}
</span>
)
}
export function EvidenceTypeBadge({ type }: { type: string | null }) {
if (!type) return null
const config = EVIDENCE_TYPE_CONFIG[type]
if (!config) return null
return <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${config.bg}`}>{config.label}</span>
}
export function TargetAudienceBadge({ audience }: { audience: string | string[] | null }) {
if (!audience) return null
// Parse JSON array string from DB (e.g. '["unternehmen", "einkauf"]')
let items: string[] = []
if (typeof audience === 'string') {
if (audience.startsWith('[')) {
try { items = JSON.parse(audience) } catch { items = [audience] }
} else {
items = [audience]
}
} else if (Array.isArray(audience)) {
items = audience
}
if (items.length === 0) return null
return (
<span className="inline-flex items-center gap-1 flex-wrap">
{items.map((item, i) => {
const config = TARGET_AUDIENCE_OPTIONS[item]
if (!config) return <span key={i} className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-600">{item}</span>
return <span key={i} className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${config.bg}`}>{config.label}</span>
})}
</span>
)
}
export interface CanonicalControlPipelineInfo {
pipeline_version?: number | string | null
source_citation?: Record<string, string> | null
parent_control_uuid?: string | null
}
export function isEigenentwicklung(ctrl: CanonicalControlPipelineInfo & { generation_strategy?: string | null }): boolean {
return (
(!ctrl.generation_strategy || ctrl.generation_strategy === 'ungrouped') &&
(!ctrl.pipeline_version || String(ctrl.pipeline_version) === '1') &&
!ctrl.source_citation &&
!ctrl.parent_control_uuid
)
}
export function GenerationStrategyBadge({ strategy, pipelineInfo }: {
strategy: string | null | undefined
pipelineInfo?: CanonicalControlPipelineInfo & { generation_strategy?: string | null }
}) {
// Eigenentwicklung detection: v1 + no source + no parent
if (pipelineInfo && isEigenentwicklung(pipelineInfo)) {
return <span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-orange-100 text-orange-700">Eigenentwicklung</span>
}
if (!strategy || strategy === 'ungrouped') {
return <span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-500">v1</span>
}
if (strategy === 'document_grouped') {
return <span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-emerald-100 text-emerald-700">v2</span>
}
if (strategy === 'phase74_gap_fill') {
return <span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-700">v5 Gap</span>
}
if (strategy === 'pass0b_atomic' || strategy === 'pass0b') {
return <span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-violet-100 text-violet-700">Atomar</span>
}
return <span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-500">{strategy}</span>
}
export const OBLIGATION_TYPE_CONFIG: Record<string, { bg: string; label: string }> = {
pflicht: { bg: 'bg-red-100 text-red-700', label: 'Pflicht' },
empfehlung: { bg: 'bg-amber-100 text-amber-700', label: 'Empfehlung' },
kann: { bg: 'bg-green-100 text-green-700', label: 'Kann' },
}
export function ObligationTypeBadge({ type }: { type: string | null | undefined }) {
if (!type) return null
const config = OBLIGATION_TYPE_CONFIG[type]
if (!config) return null
return <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${config.bg}`}>{config.label}</span>
}
export function getDomain(controlId: string): string {
return controlId.split('-')[0] || ''
}
// =============================================================================
// PROVENANCE TYPES
// =============================================================================
export interface ObligationInfo {
candidate_id: string
obligation_text: string
action: string | null
object: string | null
normative_strength: string
release_state: string
}
export interface DocumentReference {
regulation_code: string
article: string | null
paragraph: string | null
extraction_method: string
confidence: number | null
}
export interface MergedDuplicate {
control_id: string
title: string
source_regulation: string | null
}
export interface RegulationSummary {
regulation_code: string
articles: string[]
link_types: string[]
}
// =============================================================================
// PROVENANCE BADGES
// =============================================================================
const EXTRACTION_METHOD_CONFIG: Record<string, { bg: string; label: string }> = {
exact_match: { bg: 'bg-green-100 text-green-700', label: 'Exakt' },
embedding_match: { bg: 'bg-blue-100 text-blue-700', label: 'Embedding' },
llm_extracted: { bg: 'bg-violet-100 text-violet-700', label: 'LLM' },
inferred: { bg: 'bg-gray-100 text-gray-600', label: 'Abgeleitet' },
}
export function ExtractionMethodBadge({ method }: { method: string }) {
const config = EXTRACTION_METHOD_CONFIG[method] || EXTRACTION_METHOD_CONFIG.inferred
return <span className={`inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium ${config.bg}`}>{config.label}</span>
}
export function RegulationCountBadge({ count }: { count: number }) {
if (count <= 0) return null
return (
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-violet-100 text-violet-700">
{count} {count === 1 ? 'Regulierung' : 'Regulierungen'}
</span>
)
}

View File

@@ -1,34 +1,73 @@
'use client'
import { useState, useEffect, useMemo, useCallback } from 'react'
import { useState, useEffect, useCallback, useRef } from 'react'
import {
Shield, Search, ChevronRight, Filter, Lock,
BookOpen, Plus, Zap, BarChart3, ListChecks,
Shield, Search, ChevronRight, ChevronLeft, Filter, Lock,
BookOpen, Plus, Zap, BarChart3, ListChecks, Trash2,
ChevronsLeft, ChevronsRight, ArrowUpDown, Clock, RefreshCw,
} from 'lucide-react'
import {
CanonicalControl, Framework, BACKEND_URL, EMPTY_CONTROL,
SeverityBadge, StateBadge, LicenseRuleBadge, getDomain,
SeverityBadge, StateBadge, LicenseRuleBadge, VerificationMethodBadge, CategoryBadge, EvidenceTypeBadge, TargetAudienceBadge,
GenerationStrategyBadge, ObligationTypeBadge,
VERIFICATION_METHODS, CATEGORY_OPTIONS, EVIDENCE_TYPE_OPTIONS, TARGET_AUDIENCE_OPTIONS,
} from './components/helpers'
import { ControlForm } from './components/ControlForm'
import { ControlDetail } from './components/ControlDetail'
import { ReviewCompare } from './components/ReviewCompare'
import { V1CompareView } from './components/V1CompareView'
import { GeneratorModal } from './components/GeneratorModal'
// =============================================================================
// CONTROL LIBRARY PAGE
// TYPES
// =============================================================================
interface ControlsMeta {
total: number
domains: Array<{ domain: string; count: number }>
sources: Array<{ source: string; count: number }>
no_source_count: number
type_counts?: {
rich: number
atomic: number
eigenentwicklung: number
}
severity_counts?: Record<string, number>
verification_method_counts?: Record<string, number>
category_counts?: Record<string, number>
evidence_type_counts?: Record<string, number>
release_state_counts?: Record<string, number>
}
// =============================================================================
// CONTROL LIBRARY PAGE — Server-Side Pagination
// =============================================================================
const PAGE_SIZE = 50
export default function ControlLibraryPage() {
const [frameworks, setFrameworks] = useState<Framework[]>([])
const [controls, setControls] = useState<CanonicalControl[]>([])
const [totalCount, setTotalCount] = useState(0)
const [meta, setMeta] = useState<ControlsMeta | null>(null)
const [selectedControl, setSelectedControl] = useState<CanonicalControl | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Filters
const [searchQuery, setSearchQuery] = useState('')
const [debouncedSearch, setDebouncedSearch] = useState('')
const [severityFilter, setSeverityFilter] = useState<string>('')
const [domainFilter, setDomainFilter] = useState<string>('')
const [stateFilter, setStateFilter] = useState<string>('')
const [verificationFilter, setVerificationFilter] = useState<string>('')
const [categoryFilter, setCategoryFilter] = useState<string>('')
const [evidenceTypeFilter, setEvidenceTypeFilter] = useState<string>('')
const [audienceFilter, setAudienceFilter] = useState<string>('')
const [sourceFilter, setSourceFilter] = useState<string>('')
const [typeFilter, setTypeFilter] = useState<string>('')
const [hideDuplicates, setHideDuplicates] = useState(true)
const [sortBy, setSortBy] = useState<'id' | 'newest' | 'oldest' | 'source'>('id')
// CRUD state
const [mode, setMode] = useState<'list' | 'detail' | 'create' | 'edit'>('list')
@@ -39,59 +78,153 @@ export default function ControlLibraryPage() {
const [processedStats, setProcessedStats] = useState<Array<Record<string, unknown>>>([])
const [showStats, setShowStats] = useState(false)
// Pagination
const [currentPage, setCurrentPage] = useState(1)
// Review mode
const [reviewMode, setReviewMode] = useState(false)
const [reviewIndex, setReviewIndex] = useState(0)
const [reviewItems, setReviewItems] = useState<CanonicalControl[]>([])
const [reviewCount, setReviewCount] = useState(0)
const [reviewTab, setReviewTab] = useState<'duplicates' | 'rule3'>('duplicates')
const [reviewDuplicates, setReviewDuplicates] = useState<CanonicalControl[]>([])
const [reviewRule3, setReviewRule3] = useState<CanonicalControl[]>([])
// Load data
const loadData = useCallback(async () => {
// V1 Compare mode
const [compareMode, setCompareMode] = useState(false)
const [compareV1Control, setCompareV1Control] = useState<CanonicalControl | null>(null)
const [compareMatches, setCompareMatches] = useState<Array<{
matched_control_id: string; matched_title: string; matched_objective: string
matched_severity: string; matched_category: string
matched_source: string | null; matched_article: string | null
matched_source_citation: Record<string, string> | null
similarity_score: number; match_rank: number; match_method: string
}>>([])
// Abort controllers for cancelling stale requests
const metaAbortRef = useRef<AbortController | null>(null)
const controlsAbortRef = useRef<AbortController | null>(null)
// Debounce search
const searchTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
useEffect(() => {
if (searchTimer.current) clearTimeout(searchTimer.current)
searchTimer.current = setTimeout(() => setDebouncedSearch(searchQuery), 400)
return () => { if (searchTimer.current) clearTimeout(searchTimer.current) }
}, [searchQuery])
// Build query params for backend
const buildParams = useCallback((extra?: Record<string, string>) => {
const p = new URLSearchParams()
if (severityFilter) p.set('severity', severityFilter)
if (domainFilter) p.set('domain', domainFilter)
if (stateFilter) p.set('release_state', stateFilter)
if (verificationFilter) p.set('verification_method', verificationFilter)
if (categoryFilter) p.set('category', categoryFilter)
if (evidenceTypeFilter) p.set('evidence_type', evidenceTypeFilter)
if (audienceFilter) p.set('target_audience', audienceFilter)
if (sourceFilter) p.set('source', sourceFilter)
if (typeFilter) p.set('control_type', typeFilter)
if (hideDuplicates) p.set('exclude_duplicates', 'true')
if (debouncedSearch) p.set('search', debouncedSearch)
if (extra) for (const [k, v] of Object.entries(extra)) p.set(k, v)
return p.toString()
}, [severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, evidenceTypeFilter, audienceFilter, sourceFilter, typeFilter, hideDuplicates, debouncedSearch])
// Load frameworks (once)
const loadFrameworks = useCallback(async () => {
try {
setLoading(true)
const [fwRes, ctrlRes] = await Promise.all([
fetch(`${BACKEND_URL}?endpoint=frameworks`),
fetch(`${BACKEND_URL}?endpoint=controls`),
])
if (fwRes.ok) setFrameworks(await fwRes.json())
if (ctrlRes.ok) setControls(await ctrlRes.json())
} catch (err) {
setError(err instanceof Error ? err.message : 'Fehler beim Laden')
} finally {
setLoading(false)
}
const res = await fetch(`${BACKEND_URL}?endpoint=frameworks`)
if (res.ok) setFrameworks(await res.json())
} catch { /* ignore */ }
}, [])
useEffect(() => { loadData() }, [loadData])
// Load faceted metadata (reloads when filters change, cancels stale requests)
const loadMeta = useCallback(async () => {
if (metaAbortRef.current) metaAbortRef.current.abort()
const controller = new AbortController()
metaAbortRef.current = controller
try {
const qs = buildParams()
const res = await fetch(`${BACKEND_URL}?endpoint=controls-meta${qs ? `&${qs}` : ''}`, { signal: controller.signal })
if (res.ok && !controller.signal.aborted) setMeta(await res.json())
} catch (e) {
if (e instanceof DOMException && e.name === 'AbortError') return
}
}, [buildParams])
// Derived: unique domains
const domains = useMemo(() => {
const set = new Set(controls.map(c => getDomain(c.control_id)))
return Array.from(set).sort()
}, [controls])
// Load controls page (cancels stale requests)
const loadControls = useCallback(async () => {
if (controlsAbortRef.current) controlsAbortRef.current.abort()
const controller = new AbortController()
controlsAbortRef.current = controller
try {
setLoading(true)
// Filtered controls
const filteredControls = useMemo(() => {
return controls.filter(c => {
if (severityFilter && c.severity !== severityFilter) return false
if (domainFilter && getDomain(c.control_id) !== domainFilter) return false
if (stateFilter && c.release_state !== stateFilter) return false
if (searchQuery) {
const q = searchQuery.toLowerCase()
return (
c.control_id.toLowerCase().includes(q) ||
c.title.toLowerCase().includes(q) ||
c.objective.toLowerCase().includes(q) ||
c.tags.some(t => t.toLowerCase().includes(q))
)
// Determine sort
const sortField = sortBy === 'id' ? 'control_id' : sortBy === 'source' ? 'source' : 'created_at'
const sortOrder = sortBy === 'newest' ? 'desc' : sortBy === 'oldest' ? 'asc' : 'asc'
const offset = (currentPage - 1) * PAGE_SIZE
const qs = buildParams({
sort: sortField,
order: sortOrder,
limit: String(PAGE_SIZE),
offset: String(offset),
})
const countQs = buildParams()
const [ctrlRes, countRes] = await Promise.all([
fetch(`${BACKEND_URL}?endpoint=controls&${qs}`, { signal: controller.signal }),
fetch(`${BACKEND_URL}?endpoint=controls-count&${countQs}`, { signal: controller.signal }),
])
if (!controller.signal.aborted) {
if (ctrlRes.ok) setControls(await ctrlRes.json())
if (countRes.ok) {
const data = await countRes.json()
setTotalCount(data.total || 0)
}
}
return true
})
}, [controls, severityFilter, domainFilter, stateFilter, searchQuery])
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') return
setError(err instanceof Error ? err.message : 'Fehler beim Laden')
} finally {
if (!controller.signal.aborted) setLoading(false)
}
}, [buildParams, sortBy, currentPage])
// Review queue items
const reviewItems = useMemo(() => {
return controls.filter(c => ['needs_review', 'too_close', 'duplicate'].includes(c.release_state))
}, [controls])
// Load review count
const loadReviewCount = useCallback(async () => {
try {
const res = await fetch(`${BACKEND_URL}?endpoint=controls-count&release_state=needs_review`)
if (res.ok) {
const data = await res.json()
setReviewCount(data.total || 0)
}
} catch { /* ignore */ }
}, [])
// Initial load (frameworks only once)
useEffect(() => { loadFrameworks(); loadReviewCount() }, [loadFrameworks, loadReviewCount])
// Load faceted meta when filters change
useEffect(() => { loadMeta() }, [loadMeta])
// Load controls when filters/page/sort change
useEffect(() => { loadControls() }, [loadControls])
// Reset page when filters change
useEffect(() => { setCurrentPage(1) }, [severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, evidenceTypeFilter, audienceFilter, sourceFilter, typeFilter, hideDuplicates, debouncedSearch, sortBy])
// Pagination
const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE))
// Full reload (after CRUD)
const fullReload = useCallback(async () => {
await Promise.all([loadControls(), loadMeta(), loadFrameworks(), loadReviewCount()])
}, [loadControls, loadMeta, loadFrameworks, loadReviewCount])
// CRUD handlers
const handleCreate = async (data: typeof EMPTY_CONTROL) => {
@@ -107,7 +240,7 @@ export default function ControlLibraryPage() {
alert(`Fehler: ${err.error || err.details || 'Unbekannt'}`)
return
}
await loadData()
await fullReload()
setMode('list')
} catch {
alert('Netzwerkfehler')
@@ -130,7 +263,7 @@ export default function ControlLibraryPage() {
alert(`Fehler: ${err.error || err.details || 'Unbekannt'}`)
return
}
await loadData()
await fullReload()
setSelectedControl(null)
setMode('list')
} catch {
@@ -148,7 +281,7 @@ export default function ControlLibraryPage() {
alert('Fehler beim Loeschen')
return
}
await loadData()
await fullReload()
setSelectedControl(null)
setMode('list')
} catch {
@@ -164,11 +297,10 @@ export default function ControlLibraryPage() {
body: JSON.stringify({ action }),
})
if (res.ok) {
await loadData()
await fullReload()
if (reviewMode) {
const remaining = controls.filter(c =>
['needs_review', 'too_close', 'duplicate'].includes(c.release_state) && c.control_id !== controlId
)
const remaining = reviewItems.filter(c => c.control_id !== controlId)
setReviewItems(remaining)
if (remaining.length > 0) {
const nextIdx = Math.min(reviewIndex, remaining.length - 1)
setReviewIndex(nextIdx)
@@ -186,6 +318,33 @@ export default function ControlLibraryPage() {
} catch { /* ignore */ }
}
const [bulkProcessing, setBulkProcessing] = useState(false)
const handleBulkReject = async (sourceState: string) => {
const count = stateFilter === sourceState ? totalCount : reviewCount
if (!confirm(`Alle ${count} Controls mit Status "${sourceState}" auf "deprecated" setzen? Diese Aktion kann nicht rueckgaengig gemacht werden.`)) return
setBulkProcessing(true)
try {
const res = await fetch(`${BACKEND_URL}?endpoint=bulk-review`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ release_state: sourceState, action: 'reject' }),
})
if (res.ok) {
const data = await res.json()
alert(`${data.affected_count} Controls auf "deprecated" gesetzt.`)
await fullReload()
} else {
const err = await res.json()
alert(`Fehler: ${err.error || err.details || 'Unbekannt'}`)
}
} catch {
alert('Netzwerkfehler')
} finally {
setBulkProcessing(false)
}
}
const loadProcessedStats = async () => {
try {
const res = await fetch(`${BACKEND_URL}?endpoint=processed-stats`)
@@ -196,16 +355,52 @@ export default function ControlLibraryPage() {
} catch { /* ignore */ }
}
const enterReviewMode = () => {
if (reviewItems.length === 0) return
setReviewMode(true)
const enterReviewMode = async () => {
// Load review items from backend
try {
const res = await fetch(`${BACKEND_URL}?endpoint=controls&release_state=needs_review&limit=1000`)
if (res.ok) {
const items: CanonicalControl[] = await res.json()
if (items.length > 0) {
// Split into duplicate suspects vs rule 3 without anchor
const dupes = items.filter(c =>
c.generation_metadata?.similar_controls &&
Array.isArray(c.generation_metadata.similar_controls) &&
(c.generation_metadata.similar_controls as unknown[]).length > 0
)
const rule3 = items.filter(c =>
!c.generation_metadata?.similar_controls ||
!Array.isArray(c.generation_metadata.similar_controls) ||
(c.generation_metadata.similar_controls as unknown[]).length === 0
)
setReviewDuplicates(dupes)
setReviewRule3(rule3)
// Start with duplicates tab if any, otherwise rule3
const startTab = dupes.length > 0 ? 'duplicates' : 'rule3'
const startItems = startTab === 'duplicates' ? dupes : rule3
setReviewTab(startTab)
setReviewItems(startItems)
setReviewMode(true)
setReviewIndex(0)
setSelectedControl(startItems[0])
setMode('detail')
}
}
} catch { /* ignore */ }
}
const switchReviewTab = (tab: 'duplicates' | 'rule3') => {
const items = tab === 'duplicates' ? reviewDuplicates : reviewRule3
setReviewTab(tab)
setReviewItems(items)
setReviewIndex(0)
setSelectedControl(reviewItems[0])
setMode('detail')
if (items.length > 0) {
setSelectedControl(items[0])
}
}
// Loading
if (loading) {
if (loading && controls.length === 0) {
return (
<div className="flex items-center justify-center h-96">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-purple-600 border-t-transparent" />
@@ -248,32 +443,130 @@ export default function ControlLibraryPage() {
)
}
// DETAIL MODE
if (mode === 'detail' && selectedControl) {
// V1 COMPARE MODE
if (compareMode && compareV1Control) {
return (
<ControlDetail
ctrl={selectedControl}
onBack={() => { setMode('list'); setSelectedControl(null); setReviewMode(false) }}
onEdit={() => setMode('edit')}
onDelete={handleDelete}
onReview={handleReview}
reviewMode={reviewMode}
reviewIndex={reviewIndex}
reviewTotal={reviewItems.length}
onReviewPrev={() => {
const idx = Math.max(0, reviewIndex - 1)
setReviewIndex(idx)
setSelectedControl(reviewItems[idx])
}}
onReviewNext={() => {
const idx = Math.min(reviewItems.length - 1, reviewIndex + 1)
setReviewIndex(idx)
setSelectedControl(reviewItems[idx])
<V1CompareView
v1Control={compareV1Control}
matches={compareMatches}
onBack={() => { setCompareMode(false) }}
onNavigateToControl={async (controlId: string) => {
try {
const res = await fetch(`${BACKEND_URL}?endpoint=control&id=${controlId}`)
if (res.ok) {
setCompareMode(false)
setSelectedControl(await res.json())
setMode('detail')
}
} catch { /* ignore */ }
}}
/>
)
}
// DETAIL MODE
if (mode === 'detail' && selectedControl) {
const isDuplicateReview = reviewMode && reviewTab === 'duplicates'
// Review tab bar (shown above the detail/compare view in review mode)
const reviewTabBar = reviewMode ? (
<div className="border-b border-gray-200 bg-white px-6 py-2 flex items-center gap-4">
<button
onClick={() => switchReviewTab('duplicates')}
className={`px-3 py-1.5 text-sm rounded-lg font-medium ${
reviewTab === 'duplicates'
? 'bg-amber-100 text-amber-800 border border-amber-300'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
}`}
>
Duplikat-Verdacht ({reviewDuplicates.length})
</button>
<button
onClick={() => switchReviewTab('rule3')}
className={`px-3 py-1.5 text-sm rounded-lg font-medium ${
reviewTab === 'rule3'
? 'bg-purple-100 text-purple-800 border border-purple-300'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
}`}
>
Rule 3 ohne Anchor ({reviewRule3.length})
</button>
</div>
) : null
if (isDuplicateReview) {
return (
<div className="flex flex-col h-full">
{reviewTabBar}
<div className="flex-1 overflow-hidden">
<ReviewCompare
ctrl={selectedControl}
onBack={() => { setMode('list'); setSelectedControl(null); setReviewMode(false) }}
onReview={handleReview}
onEdit={() => setMode('edit')}
reviewIndex={reviewIndex}
reviewTotal={reviewItems.length}
onReviewPrev={() => {
const idx = Math.max(0, reviewIndex - 1)
setReviewIndex(idx)
setSelectedControl(reviewItems[idx])
}}
onReviewNext={() => {
const idx = Math.min(reviewItems.length - 1, reviewIndex + 1)
setReviewIndex(idx)
setSelectedControl(reviewItems[idx])
}}
/>
</div>
</div>
)
}
return (
<div className="flex flex-col h-full">
{reviewTabBar}
<div className="flex-1 overflow-hidden">
<ControlDetail
ctrl={selectedControl}
onBack={() => { setMode('list'); setSelectedControl(null); setReviewMode(false) }}
onEdit={() => setMode('edit')}
onDelete={handleDelete}
onReview={handleReview}
onRefresh={fullReload}
onCompare={(ctrl, matches) => {
setCompareV1Control(ctrl)
setCompareMatches(matches)
setCompareMode(true)
}}
onNavigateToControl={async (controlId: string) => {
try {
const res = await fetch(`${BACKEND_URL}?endpoint=control&id=${controlId}`)
if (res.ok) {
const data = await res.json()
setSelectedControl(data)
setMode('detail')
}
} catch { /* ignore */ }
}}
reviewMode={reviewMode}
reviewIndex={reviewIndex}
reviewTotal={reviewItems.length}
onReviewPrev={() => {
const idx = Math.max(0, reviewIndex - 1)
setReviewIndex(idx)
setSelectedControl(reviewItems[idx])
}}
onReviewNext={() => {
const idx = Math.min(reviewItems.length - 1, reviewIndex + 1)
setReviewIndex(idx)
setSelectedControl(reviewItems[idx])
}}
/>
</div>
</div>
)
}
// =========================================================================
// LIST VIEW
// =========================================================================
@@ -288,20 +581,29 @@ export default function ControlLibraryPage() {
<div>
<h1 className="text-lg font-semibold text-gray-900">Canonical Control Library</h1>
<p className="text-xs text-gray-500">
{controls.length} unabhaengig formulierte Security Controls {' '}
{controls.reduce((sum, c) => sum + c.open_anchors.length, 0)} Open-Source-Referenzen
{meta?.total ?? totalCount} Security Controls
</p>
</div>
</div>
<div className="flex items-center gap-2">
{reviewItems.length > 0 && (
<button
onClick={enterReviewMode}
className="flex items-center gap-1.5 px-3 py-2 text-sm text-white bg-yellow-600 rounded-lg hover:bg-yellow-700"
>
<ListChecks className="w-4 h-4" />
Review ({reviewItems.length})
</button>
{reviewCount > 0 && (
<>
<button
onClick={enterReviewMode}
className="flex items-center gap-1.5 px-3 py-2 text-sm text-white bg-yellow-600 rounded-lg hover:bg-yellow-700"
>
<ListChecks className="w-4 h-4" />
Review ({reviewCount})
</button>
<button
onClick={() => handleBulkReject('needs_review')}
disabled={bulkProcessing}
className="flex items-center gap-1.5 px-3 py-2 text-sm text-white bg-red-600 rounded-lg hover:bg-red-700 disabled:opacity-50"
>
<Trash2 className="w-4 h-4" />
{bulkProcessing ? 'Wird verarbeitet...' : `Alle ${reviewCount} ablehnen`}
</button>
</>
)}
<button
onClick={() => { setShowStats(!showStats); if (!showStats) loadProcessedStats() }}
@@ -340,53 +642,160 @@ export default function ControlLibraryPage() {
)}
{/* Filters */}
<div className="flex items-center gap-3">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Controls durchsuchen..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
className="w-full pl-9 pr-4 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
<div className="space-y-3">
<div className="flex items-center gap-3">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Controls durchsuchen (ID, Titel, Objective)..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
className="w-full pl-9 pr-4 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
</div>
<button
onClick={() => { loadControls(); loadMeta(); loadFrameworks(); loadReviewCount() }}
className="p-2 text-gray-400 hover:text-purple-600"
title="Aktualisieren"
>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
</button>
</div>
<div className="flex items-center gap-1">
<div className="flex items-center gap-2 flex-wrap">
<Filter className="w-4 h-4 text-gray-400" />
<select
value={severityFilter}
onChange={e => setSeverityFilter(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">Schweregrad</option>
<option value="critical">Kritisch{meta?.severity_counts?.critical ? ` (${meta.severity_counts.critical})` : ''}</option>
<option value="high">Hoch{meta?.severity_counts?.high ? ` (${meta.severity_counts.high})` : ''}</option>
<option value="medium">Mittel{meta?.severity_counts?.medium ? ` (${meta.severity_counts.medium})` : ''}</option>
<option value="low">Niedrig{meta?.severity_counts?.low ? ` (${meta.severity_counts.low})` : ''}</option>
</select>
<select
value={domainFilter}
onChange={e => setDomainFilter(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">Domain</option>
{(meta?.domains || []).map(d => (
<option key={d.domain} value={d.domain}>{d.domain} ({d.count})</option>
))}
</select>
<select
value={stateFilter}
onChange={e => setStateFilter(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">Status</option>
<option value="draft">Draft{meta?.release_state_counts?.draft ? ` (${meta.release_state_counts.draft})` : ''}</option>
<option value="approved">Approved{meta?.release_state_counts?.approved ? ` (${meta.release_state_counts.approved})` : ''}</option>
<option value="needs_review">Review noetig{meta?.release_state_counts?.needs_review ? ` (${meta.release_state_counts.needs_review})` : ''}</option>
<option value="too_close">Zu aehnlich{meta?.release_state_counts?.too_close ? ` (${meta.release_state_counts.too_close})` : ''}</option>
<option value="duplicate">Duplikat{meta?.release_state_counts?.duplicate ? ` (${meta.release_state_counts.duplicate})` : ''}</option>
<option value="deprecated">Deprecated{meta?.release_state_counts?.deprecated ? ` (${meta.release_state_counts.deprecated})` : ''}</option>
</select>
<label className="flex items-center gap-1.5 text-sm text-gray-600 cursor-pointer whitespace-nowrap">
<input
type="checkbox"
checked={hideDuplicates}
onChange={e => setHideDuplicates(e.target.checked)}
className="rounded border-gray-300 text-purple-600 focus:ring-purple-500"
/>
Duplikate ausblenden
</label>
<select
value={verificationFilter}
onChange={e => setVerificationFilter(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">Nachweis</option>
{Object.entries(VERIFICATION_METHODS).map(([k, v]) => (
<option key={k} value={k}>{v.label}{meta?.verification_method_counts?.[k] ? ` (${meta.verification_method_counts[k]})` : ''}</option>
))}
{meta?.verification_method_counts?.['__none__'] ? <option value="__none__">Ohne Nachweis ({meta.verification_method_counts['__none__']})</option> : null}
</select>
<select
value={categoryFilter}
onChange={e => setCategoryFilter(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">Kategorie</option>
{CATEGORY_OPTIONS.map(c => (
<option key={c.value} value={c.value}>{c.label}{meta?.category_counts?.[c.value] ? ` (${meta.category_counts[c.value]})` : ''}</option>
))}
{meta?.category_counts?.['__none__'] ? <option value="__none__">Ohne Kategorie ({meta.category_counts['__none__']})</option> : null}
</select>
<select
value={evidenceTypeFilter}
onChange={e => setEvidenceTypeFilter(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">Nachweisart</option>
{EVIDENCE_TYPE_OPTIONS.map(c => (
<option key={c.value} value={c.value}>{c.label}{meta?.evidence_type_counts?.[c.value] ? ` (${meta.evidence_type_counts[c.value]})` : ''}</option>
))}
{meta?.evidence_type_counts?.['__none__'] ? <option value="__none__">Ohne Nachweisart ({meta.evidence_type_counts['__none__']})</option> : null}
</select>
<select
value={audienceFilter}
onChange={e => setAudienceFilter(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">Zielgruppe</option>
<option value="unternehmen">Unternehmen</option>
<option value="behoerden">Behoerden</option>
<option value="entwickler">Entwickler</option>
<option value="datenschutzbeauftragte">DSB</option>
<option value="geschaeftsfuehrung">Geschaeftsfuehrung</option>
<option value="it-abteilung">IT-Abteilung</option>
<option value="rechtsabteilung">Rechtsabteilung</option>
<option value="compliance-officer">Compliance Officer</option>
<option value="personalwesen">Personalwesen</option>
<option value="einkauf">Einkauf</option>
<option value="produktion">Produktion</option>
<option value="vertrieb">Vertrieb</option>
<option value="gesundheitswesen">Gesundheitswesen</option>
<option value="finanzwesen">Finanzwesen</option>
<option value="oeffentlicher_dienst">Oeffentl. Dienst</option>
</select>
<select
value={sourceFilter}
onChange={e => setSourceFilter(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-purple-500 max-w-[220px]"
>
<option value="">Dokumentenursprung</option>
{meta && <option value="__none__">Ohne Quelle ({meta.no_source_count})</option>}
{(meta?.sources || []).map(s => (
<option key={s.source} value={s.source}>{s.source} ({s.count})</option>
))}
</select>
<select
value={typeFilter}
onChange={e => setTypeFilter(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">Alle Typen</option>
<option value="rich">Rich Controls{meta?.type_counts ? ` (${meta.type_counts.rich})` : ''}</option>
<option value="atomic">Atomare Controls{meta?.type_counts ? ` (${meta.type_counts.atomic})` : ''}</option>
<option value="eigenentwicklung">Eigenentwicklung{meta?.type_counts ? ` (${meta.type_counts.eigenentwicklung})` : ''}</option>
</select>
<span className="text-gray-300 mx-1">|</span>
<ArrowUpDown className="w-4 h-4 text-gray-400" />
<select
value={sortBy}
onChange={e => setSortBy(e.target.value as 'id' | 'newest' | 'oldest' | 'source')}
className="text-sm border border-gray-300 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="id">Sortierung: ID</option>
<option value="source">Nach Quelle</option>
<option value="newest">Neueste zuerst</option>
<option value="oldest">Aelteste zuerst</option>
</select>
</div>
<select
value={severityFilter}
onChange={e => setSeverityFilter(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">Alle Schweregrade</option>
<option value="critical">Kritisch</option>
<option value="high">Hoch</option>
<option value="medium">Mittel</option>
<option value="low">Niedrig</option>
</select>
<select
value={domainFilter}
onChange={e => setDomainFilter(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">Alle Domains</option>
{domains.map(d => (
<option key={d} value={d}>{d}</option>
))}
</select>
<select
value={stateFilter}
onChange={e => setStateFilter(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">Alle Status</option>
<option value="draft">Draft</option>
<option value="approved">Approved</option>
<option value="needs_review">Review noetig</option>
<option value="too_close">Zu aehnlich</option>
<option value="duplicate">Duplikat</option>
</select>
</div>
{/* Processing Stats */}
@@ -413,26 +822,67 @@ export default function ControlLibraryPage() {
{showGenerator && (
<GeneratorModal
onClose={() => setShowGenerator(false)}
onComplete={() => loadData()}
onComplete={() => fullReload()}
/>
)}
{/* Pagination Header */}
<div className="px-6 py-2 bg-gray-50 border-b border-gray-200 flex items-center justify-between text-xs text-gray-500">
<div className="flex items-center gap-3">
<span>
{totalCount} Controls gefunden
{totalCount !== (meta?.total ?? totalCount) && ` (von ${meta?.total} gesamt)`}
{loading && <span className="ml-2 text-purple-500">Lade...</span>}
</span>
{stateFilter && ['needs_review', 'too_close', 'duplicate'].includes(stateFilter) && totalCount > 0 && (
<button
onClick={() => handleBulkReject(stateFilter)}
disabled={bulkProcessing}
className="flex items-center gap-1 px-2 py-1 text-xs text-white bg-red-600 rounded hover:bg-red-700 disabled:opacity-50"
>
<Trash2 className="w-3 h-3" />
{bulkProcessing ? '...' : `Alle ${totalCount} ablehnen`}
</button>
)}
</div>
<span>Seite {currentPage} von {totalPages}</span>
</div>
{/* Control List */}
<div className="flex-1 overflow-y-auto p-6">
<div className="space-y-3">
{filteredControls.map(ctrl => (
{controls.map((ctrl, idx) => {
// Show source group header when sorting by source
const prevSource = idx > 0 ? (controls[idx - 1].source_citation?.source || 'Ohne Quelle') : null
const curSource = ctrl.source_citation?.source || 'Ohne Quelle'
const showSourceHeader = sortBy === 'source' && curSource !== prevSource
return (
<div key={ctrl.control_id}>
{showSourceHeader && (
<div className="flex items-center gap-2 pt-3 pb-1">
<div className="h-px flex-1 bg-blue-200" />
<span className="text-xs font-semibold text-blue-700 bg-blue-50 px-2 py-0.5 rounded whitespace-nowrap">{curSource}</span>
<div className="h-px flex-1 bg-blue-200" />
</div>
)}
<button
key={ctrl.control_id}
onClick={() => { setSelectedControl(ctrl); setMode('detail') }}
className="w-full text-left bg-white border border-gray-200 rounded-lg p-4 hover:border-purple-300 hover:shadow-sm transition-all group"
>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<div className="flex items-center gap-2 mb-1 flex-wrap">
<span className="text-xs font-mono text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded">{ctrl.control_id}</span>
<SeverityBadge severity={ctrl.severity} />
<StateBadge state={ctrl.release_state} />
<LicenseRuleBadge rule={ctrl.license_rule} />
<VerificationMethodBadge method={ctrl.verification_method} />
<CategoryBadge category={ctrl.category} />
<EvidenceTypeBadge type={ctrl.evidence_type} />
<TargetAudienceBadge audience={ctrl.target_audience} />
<GenerationStrategyBadge strategy={ctrl.generation_strategy} pipelineInfo={ctrl} />
<ObligationTypeBadge type={ctrl.generation_metadata?.obligation_type as string} />
{ctrl.risk_score !== null && (
<span className="text-xs text-gray-400">Score: {ctrl.risk_score}</span>
)}
@@ -440,30 +890,110 @@ export default function ControlLibraryPage() {
<h3 className="text-sm font-medium text-gray-900 group-hover:text-purple-700">{ctrl.title}</h3>
<p className="text-xs text-gray-500 mt-1 line-clamp-2">{ctrl.objective}</p>
{/* Open anchors summary */}
{/* Open anchors summary + timestamp */}
<div className="flex items-center gap-2 mt-2">
<BookOpen className="w-3 h-3 text-green-600" />
<span className="text-xs text-green-700">
{ctrl.open_anchors.length} Open-Source-Referenzen:
{ctrl.open_anchors.length} Referenzen
</span>
<span className="text-xs text-gray-500">
{ctrl.open_anchors.map(a => a.framework).filter((v, i, arr) => arr.indexOf(v) === i).join(', ')}
{ctrl.source_citation?.source && (
<>
<span className="text-gray-300">|</span>
<span className="text-xs text-blue-600">
{ctrl.source_citation.source}
{ctrl.source_citation.article && ` ${ctrl.source_citation.article}`}
{ctrl.source_citation.paragraph && ` ${ctrl.source_citation.paragraph}`}
</span>
</>
)}
<span className="text-gray-300">|</span>
<Clock className="w-3 h-3 text-gray-400" />
<span className="text-xs text-gray-400" title={ctrl.created_at}>
{ctrl.created_at ? new Date(ctrl.created_at).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' }) : ''}
</span>
</div>
</div>
<ChevronRight className="w-4 h-4 text-gray-300 group-hover:text-purple-500 flex-shrink-0 mt-1 ml-4" />
</div>
</button>
))}
</div>
)
})}
{filteredControls.length === 0 && (
{controls.length === 0 && !loading && (
<div className="text-center py-12 text-gray-400 text-sm">
{controls.length === 0
{totalCount === 0 && !debouncedSearch && !severityFilter && !domainFilter
? 'Noch keine Controls vorhanden. Klicke auf "Neues Control" um zu starten.'
: 'Keine Controls gefunden.'}
</div>
)}
</div>
{/* Pagination Controls */}
{totalPages > 1 && (
<div className="flex items-center justify-center gap-2 mt-6 pb-4">
<button
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
className="p-2 text-gray-500 hover:text-purple-600 disabled:opacity-30 disabled:cursor-not-allowed"
title="Erste Seite"
>
<ChevronsLeft className="w-4 h-4" />
</button>
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="p-2 text-gray-500 hover:text-purple-600 disabled:opacity-30 disabled:cursor-not-allowed"
title="Vorherige Seite"
>
<ChevronLeft className="w-4 h-4" />
</button>
{/* Page numbers */}
{Array.from({ length: totalPages }, (_, i) => i + 1)
.filter(p => p === 1 || p === totalPages || Math.abs(p - currentPage) <= 2)
.reduce<(number | 'dots')[]>((acc, p, i, arr) => {
if (i > 0 && p - (arr[i - 1] as number) > 1) acc.push('dots')
acc.push(p)
return acc
}, [])
.map((p, i) =>
p === 'dots' ? (
<span key={`dots-${i}`} className="px-1 text-gray-400">...</span>
) : (
<button
key={p}
onClick={() => setCurrentPage(p as number)}
className={`w-8 h-8 text-sm rounded-lg ${
currentPage === p
? 'bg-purple-600 text-white'
: 'text-gray-600 hover:bg-purple-50 hover:text-purple-600'
}`}
>
{p}
</button>
)
)
}
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
className="p-2 text-gray-500 hover:text-purple-600 disabled:opacity-30 disabled:cursor-not-allowed"
title="Naechste Seite"
>
<ChevronRight className="w-4 h-4" />
</button>
<button
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages}
className="p-2 text-gray-500 hover:text-purple-600 disabled:opacity-30 disabled:cursor-not-allowed"
title="Letzte Seite"
>
<ChevronsRight className="w-4 h-4" />
</button>
</div>
)}
</div>
</div>
)

View File

@@ -72,6 +72,132 @@ aus geschuetzten Quellen uebernommen.
- **UrhG §23** — Hinreichender Abstand zum Originalwerk durch eigene Formulierung
- **BSI Nutzungsbedingungen** — Kommerzielle Nutzung nur mit Zustimmung; wir nutzen BSI-Dokumente
ausschliesslich als Analysegrundlage, nicht im Produkt`,
},
{
id: 'filters',
title: 'Filter in der Control Library',
content: `## Dropdown-Filter
Die Control Library bietet 7 Filter-Dropdowns, um die ueber 3.000 Controls effizient zu durchsuchen:
### Schweregrad (Severity)
| Stufe | Farbe | Bedeutung |
|-------|-------|-----------|
| **Kritisch** | Rot | Sicherheitskritische Controls — Verstoesse fuehren zu schwerwiegenden Risiken |
| **Hoch** | Orange | Wichtige Controls — sollten zeitnah umgesetzt werden |
| **Mittel** | Gelb | Standardmaessige Controls — empfohlene Umsetzung |
| **Niedrig** | Gruen | Nice-to-have Controls — zusaetzliche Haertung |
### Domain
Das Praefix der Control-ID (z.B. \`AUTH-001\`, \`SEC-042\`). Kennzeichnet den thematischen Bereich.
Die haeufigsten Domains:
| Domain | Anzahl | Thema |
|--------|--------|-------|
| SEC | ~700 | Allgemeine Sicherheit, Systemhaertung |
| COMP | ~470 | Compliance, Regulierung, Nachweispflichten |
| DATA | ~400 | Datenschutz, Datenklassifizierung, DSGVO |
| AI | ~290 | KI-Regulierung (AI Act, Transparenz, Erklaerbarkeit) |
| LOG | ~230 | Logging, Monitoring, SIEM |
| AUTH | ~200 | Authentifizierung, Zugriffskontrolle |
| NET | ~150 | Netzwerksicherheit, Transport, Firewall |
| CRYP | ~90 | Kryptographie, Schluesselmanagement |
| ACC | ~25 | Zugriffskontrolle (Access Control) |
| INC | ~25 | Incident Response, Vorfallmanagement |
Zusaetzlich existieren spezialisierte Domains wie CRA, ARC (Architektur), API, PKI, SUP (Supply Chain) u.v.m.
### Status (Release State)
| Status | Bedeutung |
|--------|-----------|
| **Draft** | Entwurf — noch nicht freigegeben |
| **Approved** | Freigegeben fuer Kunden |
| **Review noetig** | Muss manuell geprueft werden |
| **Zu aehnlich** | Too-Close-Check hat Warnung ausgeloest |
| **Duplikat** | Wurde als Duplikat eines anderen Controls erkannt |
### Nachweis (Verification Method)
| Methode | Farbe | Beschreibung |
|---------|-------|-------------|
| **Code Review** | Blau | Nachweis durch Quellcode-Inspektion |
| **Dokument** | Amber | Nachweis durch Richtlinien, Prozesse, Schulungen |
| **Tool** | Teal | Nachweis durch automatisierte Scans/Monitoring |
| **Hybrid** | Lila | Kombination aus mehreren Methoden |
### Kategorie
Thematische Einordnung (17 Kategorien). Kategorien sind **thematisch**, Domains **strukturell**.
Ein AUTH-Control kann z.B. die Kategorie "Netzwerksicherheit" haben.
### Zielgruppe (Target Audience)
| Zielgruppe | Bedeutung |
|------------|-----------|
| **Unternehmen** | Fuer Endkunden/Firmen relevant |
| **Behoerden** | Spezifisch fuer oeffentliche Verwaltung |
| **Anbieter** | Fuer SaaS/Plattform-Anbieter |
| **Alle** | Allgemein anwendbar |
### Dokumentenursprung (Source)
Filtert nach der Quelldokument-Herkunft des Controls. Zeigt alle Quellen sortiert nach
Haeufigkeit. Die wichtigsten Quellen:
| Quelle | Typ |
|--------|-----|
| KI-Verordnung (EU) 2024/1689 | EU-Recht |
| Cyber Resilience Act (EU) 2024/2847 | EU-Recht |
| DSGVO (EU) 2016/679 | EU-Recht |
| NIS2-Richtlinie (EU) 2022/2555 | EU-Recht |
| NIST SP 800-53, CSF 2.0, SSDF | US-Standards |
| OWASP Top 10, ASVS, SAMM | Open Source |
| ENISA Guidelines | EU-Agentur |
| CISA Secure by Design | US-Behoerde |
| BDSG, TKG, GewO, HGB | Deutsche Gesetze |
| EDPB Leitlinien | EU Datenschutz |`,
},
{
id: 'badges',
title: 'Badges & Lizenzregeln',
content: `## Badges in der Control Library
Jedes Control zeigt mehrere farbige Badges:
### Lizenzregel-Badge (Rule 1 / 2 / 3)
Die Lizenzregel bestimmt, wie ein Control erstellt und genutzt werden darf:
| Badge | Farbe | Regel | Bedeutung |
|-------|-------|-------|-----------|
| **Free Use** | Gruen | Rule 1 | Quelle ist Public Domain oder EU-Recht — Originaltext darf gezeigt werden |
| **Zitation** | Blau | Rule 2 | Quelle ist CC-BY oder aehnlich — Zitation + Quellenangabe erforderlich |
| **Reformuliert** | Amber | Rule 3 | Quelle hat eingeschraenkte Lizenz — Control wurde eigenstaendig reformuliert, kein Originaltext |
### Processing-Path
| Pfad | Bedeutung |
|------|-----------|
| **structured** | Control wurde direkt aus strukturierten Daten (Tabellen, Listen) extrahiert |
| **llm_reform** | Control wurde mit LLM eigenstaendig formuliert (bei Rule 3 zwingend) |
### Referenzen (Open Anchors)
Zeigt die Anzahl der verlinkten Open-Source-Referenzen (OWASP, NIST, ENISA etc.).
Jedes freigegebene Control muss mindestens 1 Open Anchor haben.
### Weitere Badges
| Badge | Bedeutung |
|-------|-----------|
| Score | Risiko-Score (0-10) |
| Severity-Badge | Schweregrad (Kritisch/Hoch/Mittel/Niedrig) |
| State-Badge | Freigabestatus (Draft/Approved/etc.) |
| Kategorie-Badge | Thematische Kategorie |
| Zielgruppe-Badge | Enterprise/Behoerden/Anbieter/Alle |`,
},
{
id: 'taxonomy',
@@ -79,22 +205,41 @@ aus geschuetzten Quellen uebernommen.
content: `## Eigenes Klassifikationssystem
Die Canonical Control Library verwendet ein **eigenes Domain-Schema**, das sich bewusst von
proprietaeren Frameworks unterscheidet:
proprietaeren Frameworks unterscheidet. Die Domains werden **automatisch** durch den
Control Generator vergeben, basierend auf dem Inhalt der Quelldokumente.
| Domain | Name | Abgrenzung |
|--------|------|------------|
| AUTH | Identity & Access Management | Eigene Struktur, nicht BSI O.Auth_* |
| NET | Network & Transport Security | Eigene Struktur, nicht BSI O.Netz_* |
| SUP | Software Supply Chain | NIST SSDF / SLSA-basiert |
| LOG | Security Operations & Logging | OWASP Logging Best Practices |
| WEB | Web Application Security | OWASP ASVS-basiert |
| DATA | Data Governance & Classification | NIST SP 800-60 basiert |
| CRYP | Cryptographic Operations | NIST SP 800-57 basiert |
| REL | Release & Change Governance | OWASP SAMM basiert |
### Top-10 Domains
| Domain | Anzahl | Thema | Hauptquellen |
|--------|--------|-------|-------------|
| SEC | ~700 | Allgemeine Sicherheit | CRA, NIS2, BSI, ENISA |
| COMP | ~470 | Compliance & Regulierung | DSGVO, AI Act, Richtlinien |
| DATA | ~400 | Datenschutz & Datenklassifizierung | DSGVO, BDSG, EDPB |
| AI | ~290 | KI-Regulierung & Ethik | AI Act, HLEG, OECD |
| LOG | ~230 | Logging & Monitoring | NIST, OWASP |
| AUTH | ~200 | Authentifizierung & Session | NIST SP 800-63, OWASP |
| NET | ~150 | Netzwerksicherheit | NIST, ENISA |
| CRYP | ~90 | Kryptographie & Schluessel | NIST SP 800-57 |
| ACC | ~25 | Zugriffskontrolle | OWASP ASVS |
| INC | ~25 | Incident Response | NIS2, CRA |
### Spezialisierte Domains
Neben den Top-10 gibt es ueber 90 weitere Domains fuer spezifische Themen:
- **CRA** — Cyber Resilience Act spezifisch
- **ARC** — Sichere Architektur
- **API** — API-Security
- **PKI** — Public Key Infrastructure
- **SUP** — Supply Chain Security
- **VUL** — Vulnerability Management
- **BCP** — Business Continuity
- **PHY** — Physische Sicherheit
- u.v.m.
### ID-Format
Control-IDs folgen dem Muster \`DOMAIN-NNN\` (z.B. AUTH-001, NET-002). Dieses Format ist
Control-IDs folgen dem Muster \`DOMAIN-NNN\` (z.B. AUTH-001, SEC-042). Dieses Format ist
**nicht von BSI oder anderen proprietaeren Standards abgeleitet**, sondern folgt einem
allgemein ueblichen Nummerierungsschema.`,
},
@@ -159,6 +304,104 @@ Kein Text, keine Struktur, keine Bezeichner aus diesen Quellen erscheinen im Pro
| Formulierung | ❌ Keine Uebernahme | ✅ Darf zitiert werden |
| Struktur | ❌ Keine Uebernahme | ✅ Darf verwendet werden |
| Produkttext | ❌ Nicht erlaubt | ✅ Erlaubt |`,
},
{
id: 'verification-methods',
title: 'Verifikationsmethoden',
content: `## Nachweis-Klassifizierung
Jedes Control wird einer von vier Verifikationsmethoden zugeordnet. Dies bestimmt,
**wie** ein Kunde den Nachweis fuer die Einhaltung erbringen kann:
| Methode | Beschreibung | Beispiele |
|---------|-------------|-----------|
| **Code Review** | Nachweis durch Quellcode-Inspektion | Input-Validierung, Encryption-Konfiguration, Auth-Logic |
| **Dokument** | Nachweis durch Richtlinien, Prozesse, Schulungen | Notfallplaene, Schulungsnachweise, Datenschutzkonzepte |
| **Tool** | Nachweis durch automatisierte Tools/Scans | SIEM-Logs, Vulnerability-Scans, Monitoring-Dashboards |
| **Hybrid** | Kombination aus mehreren Methoden | Zugriffskontrollen (Code + Policy + Tool) |
### Bedeutung fuer Kunden
- **Code Review Controls** koennen direkt im SDK-Scan geprueft werden
- **Dokument Controls** erfordern manuelle Uploads (PDFs, Links)
- **Tool Controls** koennen per API-Integration automatisch nachgewiesen werden
- **Hybrid Controls** benoetigen mehrere Nachweisarten`,
},
{
id: 'categories',
title: 'Thematische Kategorien',
content: `## 17 Sicherheitskategorien
Controls sind in thematische Kategorien gruppiert, um Kunden eine
uebersichtliche Navigation zu ermoeglichen:
| Kategorie | Beschreibung |
|-----------|-------------|
| Verschluesselung & Kryptographie | TLS, Key Management, Algorithmen |
| Authentisierung & Zugriffskontrolle | Login, MFA, RBAC, Session-Management |
| Netzwerksicherheit | Firewall, Segmentierung, VPN, DNS |
| Datenschutz & Datensicherheit | DSGVO, Datenklassifizierung, Anonymisierung |
| Logging & Monitoring | SIEM, Audit-Logs, Alerting |
| Vorfallmanagement | Incident Response, Meldepflichten |
| Notfall & Wiederherstellung | BCM, Disaster Recovery, Backups |
| Compliance & Audit | Zertifizierungen, Audits, Berichtspflichten |
| Lieferkettenmanagement | Vendor Risk, SBOM, Third-Party |
| Physische Sicherheit | Zutritt, Gebaeudesicherheit |
| Personal & Schulung | Security Awareness, Rollenkonzepte |
| Anwendungssicherheit | SAST, DAST, Secure Coding |
| Systemhaertung & -betrieb | Patching, Konfiguration, Hardening |
| Risikomanagement | Risikoanalyse, Bewertung, Massnahmen |
| Sicherheitsorganisation | ISMS, Richtlinien, Governance |
| Hardware & Plattformsicherheit | TPM, Secure Boot, Firmware |
| Identitaetsmanagement | SSO, Federation, Directory |
### Abgrenzung zu Domains
Kategorien sind **thematisch**, Domains (AUTH, NET, etc.) sind **strukturell**.
Ein Control AUTH-005 (Domain AUTH) hat die Kategorie "authentication",
aber ein Control NET-012 (Domain NET) koennte ebenfalls die Kategorie
"authentication" haben, wenn es um Netzwerk-Authentifizierung geht.`,
},
{
id: 'master-library',
title: 'Master Library Strategie',
content: `## RAG-First Ansatz
Die Canonical Control Library folgt einer **RAG-First-Strategie**:
### Schritt 1: Rule 1+2 Controls aus RAG generieren
Prioritaet haben Controls aus Quellen mit **Originaltext-Erlaubnis**:
| Welle | Quellen | Lizenzregel | Vorteil |
|-------|---------|------------|---------|
| 1 | OWASP (ASVS, MASVS, Top10) | Rule 2 (CC-BY-SA, Zitation) | Originaltext + Zitation |
| 2 | NIST (SP 800-53, CSF, SSDF) | Rule 1 (Public Domain) | Voller Text, keine Einschraenkungen |
| 3 | EU-Verordnungen (DSGVO, AI Act, NIS2, CRA) | Rule 1 (EU Law) | Gesetzestext + Erklaerung |
| 4 | Deutsche Gesetze (BDSG, TTDSG, TKG) | Rule 1 (DE Law) | Gesetzestext + Erklaerung |
### Schritt 2: Dedup gegen BSI Rule-3 Controls
Die ~880 BSI Rule-3 Controls werden **gegen** die neuen Rule 1+2 Controls abgeglichen:
- Wenn ein BSI-Control ein Duplikat eines OWASP/NIST-Controls ist → **OWASP/NIST bevorzugt**
(weil Originaltext + Zitation erlaubt)
- BSI-Duplikate werden als \`deprecated\` markiert
- Tags und Anchors werden in den behaltenen Control zusammengefuehrt
### Schritt 3: Aktueller Stand
Aktuell: **~3.100+ Controls** (Stand Maerz 2026), davon:
- Viele mit \`source_original_text\` (Originaltext fuer Kunden sichtbar)
- Viele mit \`source_citation\` (Quellenangabe mit Lizenz)
- Klare Nachweismethode (\`verification_method\`)
- Thematische Kategorie (\`category\`)
### Verstaendliche Texte
Zusaetzlich zum Originaltext (der oft juristisch/technisch formuliert ist)
enthaelt jedes Control ein eigenstaendig formuliertes **Ziel** (objective)
und eine **Begruendung** (rationale) in verstaendlicher Sprache.`,
},
{
id: 'validation',

View File

@@ -196,7 +196,15 @@ function ControlCard({
{/* Linked Evidence */}
{control.linkedEvidence.length > 0 && (
<div className="mt-3 pt-3 border-t border-gray-100">
<span className="text-xs text-gray-500 mb-1 block">Nachweise:</span>
<span className="text-xs text-gray-500 mb-1 block">
Nachweise: {control.linkedEvidence.length}
{(() => {
const e2plus = control.linkedEvidence.filter((ev: { confidenceLevel?: string }) =>
ev.confidenceLevel && ['E2', 'E3', 'E4'].includes(ev.confidenceLevel)
).length
return e2plus > 0 ? ` (${e2plus} E2+)` : ''
})()}
</span>
<div className="flex items-center gap-1 flex-wrap">
{control.linkedEvidence.map(ev => (
<span key={ev.id} className={`px-2 py-0.5 text-xs rounded ${
@@ -205,6 +213,9 @@ function ControlCard({
'bg-yellow-50 text-yellow-700'
}`}>
{ev.title}
{(ev as { confidenceLevel?: string }).confidenceLevel && (
<span className="ml-1 opacity-70">({(ev as { confidenceLevel?: string }).confidenceLevel})</span>
)}
</span>
))}
</div>
@@ -359,6 +370,49 @@ interface RAGControlSuggestion {
// MAIN PAGE
// =============================================================================
function TransitionErrorBanner({
controlId,
violations,
onDismiss,
}: {
controlId: string
violations: string[]
onDismiss: () => void
}) {
return (
<div className="p-4 bg-orange-50 border border-orange-200 rounded-lg">
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<svg className="w-5 h-5 text-orange-600 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div>
<h4 className="font-medium text-orange-800">
Status-Transition blockiert ({controlId})
</h4>
<ul className="mt-2 space-y-1">
{violations.map((v, i) => (
<li key={i} className="text-sm text-orange-700 flex items-start gap-2">
<span className="text-orange-400 mt-0.5"></span>
<span>{v}</span>
</li>
))}
</ul>
<a href="/sdk/evidence" className="mt-2 inline-block text-sm text-purple-600 hover:text-purple-700 font-medium">
Evidence hinzufuegen
</a>
</div>
</div>
<button onClick={onDismiss} className="text-orange-400 hover:text-orange-600 ml-4">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
)
}
export default function ControlsPage() {
const { state, dispatch } = useSDK()
const router = useRouter()
@@ -373,6 +427,9 @@ export default function ControlsPage() {
const [showRagPanel, setShowRagPanel] = useState(false)
const [selectedRequirementId, setSelectedRequirementId] = useState<string>('')
// Transition error from Anti-Fake-Evidence state machine (409 Conflict)
const [transitionError, setTransitionError] = useState<{ controlId: string; violations: string[] } | null>(null)
// Track effectiveness locally as it's not in the SDK state type
const [effectivenessMap, setEffectivenessMap] = useState<Record<string, number>>({})
// Track linked evidence per control
@@ -385,7 +442,7 @@ export default function ControlsPage() {
const data = await res.json()
const allEvidence = data.evidence || data
if (Array.isArray(allEvidence)) {
const map: Record<string, { id: string; title: string; status: string }[]> = {}
const map: Record<string, { id: string; title: string; status: string; confidenceLevel?: string }[]> = {}
for (const ev of allEvidence) {
const ctrlId = ev.control_id || ''
if (!map[ctrlId]) map[ctrlId] = []
@@ -393,6 +450,7 @@ export default function ControlsPage() {
id: ev.id,
title: ev.title || ev.name || 'Nachweis',
status: ev.status || 'pending',
confidenceLevel: ev.confidence_level || undefined,
})
}
setEvidenceMap(map)
@@ -483,20 +541,56 @@ export default function ControlsPage() {
: 0
const partialCount = displayControls.filter(c => c.displayStatus === 'partial').length
const handleStatusChange = async (controlId: string, status: ImplementationStatus) => {
const handleStatusChange = async (controlId: string, newStatus: ImplementationStatus) => {
// Remember old status for rollback
const oldControl = state.controls.find(c => c.id === controlId)
const oldStatus = oldControl?.implementationStatus
// Optimistic update
dispatch({
type: 'UPDATE_CONTROL',
payload: { id: controlId, data: { implementationStatus: status } },
payload: { id: controlId, data: { implementationStatus: newStatus } },
})
try {
await fetch(`/api/sdk/v1/compliance/controls/${controlId}`, {
const res = await fetch(`/api/sdk/v1/compliance/controls/${controlId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ implementation_status: status }),
body: JSON.stringify({ implementation_status: newStatus }),
})
if (!res.ok) {
// Rollback optimistic update
if (oldStatus) {
dispatch({
type: 'UPDATE_CONTROL',
payload: { id: controlId, data: { implementationStatus: oldStatus } },
})
}
const err = await res.json().catch(() => ({ detail: 'Status-Aenderung fehlgeschlagen' }))
if (res.status === 409 && err.detail?.violations) {
setTransitionError({ controlId, violations: err.detail.violations })
} else {
const msg = typeof err.detail === 'string' ? err.detail : err.detail?.error || 'Status-Aenderung fehlgeschlagen'
setError(msg)
}
} else {
// Clear any previous transition error for this control
if (transitionError?.controlId === controlId) {
setTransitionError(null)
}
}
} catch {
// Silently fail — SDK state is already updated
// Network error — rollback
if (oldStatus) {
dispatch({
type: 'UPDATE_CONTROL',
payload: { id: controlId, data: { implementationStatus: oldStatus } },
})
}
setError('Netzwerkfehler bei Status-Aenderung')
}
}
@@ -745,6 +839,15 @@ export default function ControlsPage() {
</div>
)}
{/* Transition Error Banner (Anti-Fake-Evidence 409 violations) */}
{transitionError && (
<TransitionErrorBanner
controlId={transitionError.controlId}
violations={transitionError.violations}
onDismiss={() => setTransitionError(null)}
/>
)}
{/* Requirements Alert */}
{state.requirements.length === 0 && !loading && (
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">

View File

@@ -32,18 +32,28 @@ import {
const CATEGORIES: { key: string; label: string; types: string[] | null }[] = [
{ key: 'all', label: 'Alle', types: null },
// Legal / Vertragsvorlagen
{ key: 'privacy_policy', label: 'Datenschutz', types: ['privacy_policy'] },
{ key: 'terms', label: 'AGB', types: ['terms_of_service', 'agb', 'clause'] },
{ key: 'impressum', label: 'Impressum', types: ['impressum'] },
{ key: 'dpa', label: 'AVV/DPA', types: ['dpa'] },
{ key: 'nda', label: 'NDA', types: ['nda'] },
{ key: 'sla', label: 'SLA', types: ['sla'] },
{ key: 'acceptable_use', label: 'AUP', types: ['acceptable_use'] },
{ key: 'widerruf', label: 'Widerruf', types: ['widerruf'] },
{ key: 'cookie', label: 'Cookie', types: ['cookie_policy', 'cookie_banner'] },
{ key: 'cloud', label: 'Cloud', types: ['cloud_service_agreement'] },
{ key: 'misc', label: 'Weitere', types: ['community_guidelines', 'copyright_policy', 'data_usage_clause'] },
{ key: 'dsfa', label: 'DSFA', types: ['dsfa'] },
// Sicherheitskonzepte (Migration 051)
{ key: 'security', label: 'Sicherheitskonzepte', types: ['it_security_concept', 'data_protection_concept', 'backup_recovery_concept', 'logging_concept', 'incident_response_plan', 'access_control_concept', 'risk_management_concept', 'cybersecurity_policy'] },
// Policy-Bibliothek (Migration 071/072)
{ key: 'it_security_policies', label: 'IT-Sicherheit Policies', types: ['information_security_policy', 'access_control_policy', 'password_policy', 'encryption_policy', 'logging_policy', 'backup_policy', 'incident_response_policy', 'change_management_policy', 'patch_management_policy', 'asset_management_policy', 'cloud_security_policy', 'devsecops_policy', 'secrets_management_policy', 'vulnerability_management_policy'] },
{ key: 'data_policies', label: 'Daten-Policies', types: ['data_protection_policy', 'data_classification_policy', 'data_retention_policy', 'data_transfer_policy', 'privacy_incident_policy'] },
{ key: 'hr_policies', label: 'Personal-Policies', types: ['employee_security_policy', 'security_awareness_policy', 'acceptable_use', 'remote_work_policy', 'offboarding_policy'] },
{ key: 'vendor_policies', label: 'Lieferanten-Policies', types: ['vendor_risk_management_policy', 'third_party_security_policy', 'supplier_security_policy'] },
{ key: 'bcm_policies', label: 'BCM/Notfall', types: ['business_continuity_policy', 'disaster_recovery_policy', 'crisis_management_policy'] },
// Modul-Dokumente (Migration 073)
{ key: 'module_docs', label: 'DSGVO-Dokumente', types: ['vvt_register', 'tom_documentation', 'loeschkonzept', 'pflichtenregister'] },
]
// =============================================================================

View File

@@ -0,0 +1,111 @@
"use client"
import React from "react"
const badgeBase = "inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
// ---------------------------------------------------------------------------
// Confidence Level Badge (E0E4)
// ---------------------------------------------------------------------------
const confidenceColors: Record<string, string> = {
E0: "bg-red-100 text-red-800",
E1: "bg-yellow-100 text-yellow-800",
E2: "bg-blue-100 text-blue-800",
E3: "bg-green-100 text-green-800",
E4: "bg-emerald-100 text-emerald-800",
}
const confidenceLabels: Record<string, string> = {
E0: "E0 — Generiert",
E1: "E1 — Manuell",
E2: "E2 — Intern validiert",
E3: "E3 — System-beobachtet",
E4: "E4 — Extern auditiert",
}
export function ConfidenceLevelBadge({ level }: { level?: string | null }) {
if (!level) return null
const color = confidenceColors[level] || "bg-gray-100 text-gray-800"
const label = confidenceLabels[level] || level
return <span className={`${badgeBase} ${color}`}>{label}</span>
}
// ---------------------------------------------------------------------------
// Truth Status Badge
// ---------------------------------------------------------------------------
const truthColors: Record<string, string> = {
generated: "bg-violet-100 text-violet-800",
uploaded: "bg-gray-100 text-gray-800",
observed: "bg-blue-100 text-blue-800",
validated: "bg-green-100 text-green-800",
rejected: "bg-red-100 text-red-800",
audited: "bg-emerald-100 text-emerald-800",
}
const truthLabels: Record<string, string> = {
generated: "Generiert",
uploaded: "Hochgeladen",
observed: "Beobachtet",
validated: "Validiert",
rejected: "Abgelehnt",
audited: "Auditiert",
}
export function TruthStatusBadge({ status }: { status?: string | null }) {
if (!status) return null
const color = truthColors[status] || "bg-gray-100 text-gray-800"
const label = truthLabels[status] || status
return <span className={`${badgeBase} ${color}`}>{label}</span>
}
// ---------------------------------------------------------------------------
// Generation Mode Badge (sparkles icon)
// ---------------------------------------------------------------------------
export function GenerationModeBadge({ mode }: { mode?: string | null }) {
if (!mode) return null
return (
<span className={`${badgeBase} bg-violet-100 text-violet-800`}>
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-2 0v-1H3a1 1 0 010-2h1v-1a1 1 0 011-1zm7-10a1 1 0 01.967.744L14.146 7.2 17.5 7.512a1 1 0 010 1.976l-3.354.313-1.18 4.456a1 1 0 01-1.932 0l-1.18-4.456-3.354-.313a1 1 0 010-1.976l3.354-.313 1.18-4.456A1 1 0 0112 2z" />
</svg>
KI-generiert
</span>
)
}
// ---------------------------------------------------------------------------
// Approval Status Badge (Four-Eyes)
// ---------------------------------------------------------------------------
const approvalColors: Record<string, string> = {
none: "bg-gray-100 text-gray-600",
pending_first: "bg-yellow-100 text-yellow-800",
first_approved: "bg-blue-100 text-blue-800",
approved: "bg-green-100 text-green-800",
rejected: "bg-red-100 text-red-800",
}
const approvalLabels: Record<string, string> = {
none: "Kein Review",
pending_first: "Warte auf 1. Review",
first_approved: "1. Review OK",
approved: "Genehmigt (4-Augen)",
rejected: "Abgelehnt",
}
export function ApprovalStatusBadge({
status,
requiresFourEyes,
}: {
status?: string | null
requiresFourEyes?: boolean | null
}) {
if (!requiresFourEyes) return null
const s = status || "none"
const color = approvalColors[s] || "bg-gray-100 text-gray-600"
const label = approvalLabels[s] || s
return <span className={`${badgeBase} ${color}`}>{label}</span>
}

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,46 @@ interface Component {
safety_relevant: boolean
parent_id: string | null
children: Component[]
library_component_id?: string
energy_source_ids?: string[]
}
interface LibraryComponent {
id: string
name_de: string
name_en: string
category: string
description_de: string
typical_hazard_categories: string[]
typical_energy_sources: string[]
maps_to_component_type: string
tags: string[]
sort_order: number
}
interface EnergySource {
id: string
name_de: string
name_en: string
description_de: string
typical_components: string[]
typical_hazard_categories: string[]
tags: string[]
sort_order: number
}
const LIBRARY_CATEGORIES: Record<string, string> = {
mechanical: 'Mechanik',
structural: 'Struktur',
drive: 'Antrieb',
hydraulic: 'Hydraulik',
pneumatic: 'Pneumatik',
electrical: 'Elektrik',
control: 'Steuerung',
sensor: 'Sensorik',
actuator: 'Aktorik',
safety: 'Sicherheit',
it_network: 'IT/Netzwerk',
}
const COMPONENT_TYPES = [
@@ -98,6 +138,11 @@ function ComponentTreeNode({
Sicherheitsrelevant
</span>
)}
{component.library_component_id && (
<span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-700">
Bibliothek
</span>
)}
</div>
{component.description && (
@@ -289,6 +334,289 @@ function buildTree(components: Component[]): Component[] {
return roots
}
// ============================================================================
// Component Library Modal (Phase 5)
// ============================================================================
function ComponentLibraryModal({
onAdd,
onClose,
}: {
onAdd: (components: LibraryComponent[], energySources: EnergySource[]) => void
onClose: () => void
}) {
const [libraryComponents, setLibraryComponents] = useState<LibraryComponent[]>([])
const [energySources, setEnergySources] = useState<EnergySource[]>([])
const [selectedComponents, setSelectedComponents] = useState<Set<string>>(new Set())
const [selectedEnergySources, setSelectedEnergySources] = useState<Set<string>>(new Set())
const [search, setSearch] = useState('')
const [filterCategory, setFilterCategory] = useState('')
const [activeTab, setActiveTab] = useState<'components' | 'energy'>('components')
const [loading, setLoading] = useState(true)
useEffect(() => {
async function fetchData() {
try {
const [compRes, enRes] = await Promise.all([
fetch('/api/sdk/v1/iace/component-library'),
fetch('/api/sdk/v1/iace/energy-sources'),
])
if (compRes.ok) {
const json = await compRes.json()
setLibraryComponents(json.components || [])
}
if (enRes.ok) {
const json = await enRes.json()
setEnergySources(json.energy_sources || [])
}
} catch (err) {
console.error('Failed to fetch library:', err)
} finally {
setLoading(false)
}
}
fetchData()
}, [])
function toggleComponent(id: string) {
setSelectedComponents(prev => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}
function toggleEnergySource(id: string) {
setSelectedEnergySources(prev => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}
function toggleAllInCategory(category: string) {
const items = libraryComponents.filter(c => c.category === category)
const allIds = items.map(i => i.id)
const allSelected = allIds.every(id => selectedComponents.has(id))
setSelectedComponents(prev => {
const next = new Set(prev)
allIds.forEach(id => allSelected ? next.delete(id) : next.add(id))
return next
})
}
function handleAdd() {
const selComps = libraryComponents.filter(c => selectedComponents.has(c.id))
const selEnergy = energySources.filter(e => selectedEnergySources.has(e.id))
onAdd(selComps, selEnergy)
}
const filtered = libraryComponents.filter(c => {
if (filterCategory && c.category !== filterCategory) return false
if (search) {
const q = search.toLowerCase()
return c.name_de.toLowerCase().includes(q) || c.name_en.toLowerCase().includes(q) || c.description_de.toLowerCase().includes(q)
}
return true
})
const grouped = filtered.reduce<Record<string, LibraryComponent[]>>((acc, c) => {
if (!acc[c.category]) acc[c.category] = []
acc[c.category].push(c)
return acc
}, {})
const categories = Object.keys(LIBRARY_CATEGORIES)
const totalSelected = selectedComponents.size + selectedEnergySources.size
if (loading) {
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-xl p-8 text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600 mx-auto" />
<p className="mt-3 text-sm text-gray-500">Bibliothek wird geladen...</p>
</div>
</div>
)
}
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-xl w-full max-w-4xl max-h-[85vh] flex flex-col">
{/* Header */}
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Komponentenbibliothek</h3>
<button onClick={onClose} className="p-1 text-gray-400 hover:text-gray-600 rounded">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Tabs */}
<div className="flex gap-2 mb-4">
<button
onClick={() => setActiveTab('components')}
className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors ${
activeTab === 'components' ? 'bg-purple-100 text-purple-700' : 'text-gray-500 hover:bg-gray-100'
}`}
>
Komponenten ({libraryComponents.length})
</button>
<button
onClick={() => setActiveTab('energy')}
className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors ${
activeTab === 'energy' ? 'bg-purple-100 text-purple-700' : 'text-gray-500 hover:bg-gray-100'
}`}
>
Energiequellen ({energySources.length})
</button>
</div>
{activeTab === 'components' && (
<div className="flex gap-3">
<input
type="text"
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Suchen..."
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white"
/>
<select
value={filterCategory}
onChange={e => setFilterCategory(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
>
<option value="">Alle Kategorien</option>
{categories.map(cat => (
<option key={cat} value={cat}>{LIBRARY_CATEGORIES[cat]}</option>
))}
</select>
</div>
)}
</div>
{/* Body */}
<div className="flex-1 overflow-auto p-4">
{activeTab === 'components' ? (
<div className="space-y-4">
{Object.entries(grouped)
.sort(([a], [b]) => categories.indexOf(a) - categories.indexOf(b))
.map(([category, items]) => (
<div key={category}>
<div className="flex items-center gap-2 mb-2 sticky top-0 bg-white dark:bg-gray-800 py-1 z-10">
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300">
{LIBRARY_CATEGORIES[category] || category}
</h4>
<span className="text-xs text-gray-400">({items.length})</span>
<button
onClick={() => toggleAllInCategory(category)}
className="text-xs text-purple-600 hover:text-purple-700 ml-auto"
>
{items.every(i => selectedComponents.has(i.id)) ? 'Alle abwaehlen' : 'Alle waehlen'}
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{items.map(comp => (
<label
key={comp.id}
className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${
selectedComponents.has(comp.id)
? 'border-purple-400 bg-purple-50 dark:bg-purple-900/20'
: 'border-gray-200 hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-750'
}`}
>
<input
type="checkbox"
checked={selectedComponents.has(comp.id)}
onChange={() => toggleComponent(comp.id)}
className="mt-0.5 accent-purple-600"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-xs font-mono text-gray-400">{comp.id}</span>
<ComponentTypeIcon type={comp.maps_to_component_type} />
</div>
<div className="text-sm font-medium text-gray-900 dark:text-white">{comp.name_de}</div>
{comp.description_de && (
<div className="text-xs text-gray-500 mt-0.5 line-clamp-2">{comp.description_de}</div>
)}
</div>
</label>
))}
</div>
</div>
))}
{filtered.length === 0 && (
<div className="text-center py-8 text-gray-500">Keine Komponenten gefunden</div>
)}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{energySources.map(es => (
<label
key={es.id}
className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${
selectedEnergySources.has(es.id)
? 'border-purple-400 bg-purple-50 dark:bg-purple-900/20'
: 'border-gray-200 hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-750'
}`}
>
<input
type="checkbox"
checked={selectedEnergySources.has(es.id)}
onChange={() => toggleEnergySource(es.id)}
className="mt-0.5 accent-purple-600"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-xs font-mono text-gray-400">{es.id}</span>
</div>
<div className="text-sm font-medium text-gray-900 dark:text-white">{es.name_de}</div>
{es.description_de && (
<div className="text-xs text-gray-500 mt-0.5 line-clamp-2">{es.description_de}</div>
)}
</div>
</label>
))}
</div>
)}
</div>
{/* Footer */}
<div className="p-4 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between">
<span className="text-sm text-gray-500">
{selectedComponents.size} Komponenten, {selectedEnergySources.size} Energiequellen ausgewaehlt
</span>
<div className="flex gap-3">
<button onClick={onClose} className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
Abbrechen
</button>
<button
onClick={handleAdd}
disabled={totalSelected === 0}
className={`px-6 py-2 rounded-lg font-medium transition-colors ${
totalSelected > 0
? 'bg-purple-600 text-white hover:bg-purple-700'
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
}`}
>
{totalSelected > 0 ? `${totalSelected} hinzufuegen` : 'Auswaehlen'}
</button>
</div>
</div>
</div>
</div>
)
}
// ============================================================================
// Main Page
// ============================================================================
export default function ComponentsPage() {
const params = useParams()
const projectId = params.projectId as string
@@ -297,6 +625,7 @@ export default function ComponentsPage() {
const [showForm, setShowForm] = useState(false)
const [editingComponent, setEditingComponent] = useState<Component | null>(null)
const [addingParentId, setAddingParentId] = useState<string | null>(null)
const [showLibrary, setShowLibrary] = useState(false)
useEffect(() => {
fetchComponents()
@@ -365,6 +694,32 @@ export default function ComponentsPage() {
setShowForm(true)
}
async function handleAddFromLibrary(libraryComps: LibraryComponent[], energySrcs: EnergySource[]) {
setShowLibrary(false)
const energySourceIds = energySrcs.map(e => e.id)
for (const comp of libraryComps) {
try {
await fetch(`/api/sdk/v1/iace/projects/${projectId}/components`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: comp.name_de,
type: comp.maps_to_component_type,
description: comp.description_de,
safety_relevant: false,
library_component_id: comp.id,
energy_source_ids: energySourceIds,
tags: comp.tags,
}),
})
} catch (err) {
console.error(`Failed to add component ${comp.id}:`, err)
}
}
await fetchComponents()
}
const tree = buildTree(components)
if (loading) {
@@ -386,22 +741,41 @@ export default function ComponentsPage() {
</p>
</div>
{!showForm && (
<button
onClick={() => {
setShowForm(true)
setEditingComponent(null)
setAddingParentId(null)
}}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Komponente hinzufuegen
</button>
<div className="flex items-center gap-2">
<button
onClick={() => setShowLibrary(true)}
className="flex items-center gap-2 px-3 py-2 border border-purple-300 text-purple-700 rounded-lg hover:bg-purple-50 transition-colors text-sm"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
Aus Bibliothek waehlen
</button>
<button
onClick={() => {
setShowForm(true)
setEditingComponent(null)
setAddingParentId(null)
}}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Komponente hinzufuegen
</button>
</div>
)}
</div>
{/* Library Modal */}
{showLibrary && (
<ComponentLibraryModal
onAdd={handleAddFromLibrary}
onClose={() => setShowLibrary(false)}
/>
)}
{/* Form */}
{showForm && (
<ComponentForm
@@ -454,12 +828,20 @@ export default function ComponentsPage() {
Beginnen Sie mit der Erfassung aller relevanten Komponenten Ihrer Maschine.
Erstellen Sie eine hierarchische Struktur aus Software, Firmware, KI-Modulen und Hardware.
</p>
<button
onClick={() => setShowForm(true)}
className="mt-6 px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
Erste Komponente hinzufuegen
</button>
<div className="mt-6 flex items-center justify-center gap-3">
<button
onClick={() => setShowLibrary(true)}
className="px-6 py-3 border border-purple-300 text-purple-700 rounded-lg hover:bg-purple-50 transition-colors"
>
Aus Bibliothek waehlen
</button>
<button
onClick={() => setShowForm(true)}
className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
Manuell hinzufuegen
</button>
</div>
</div>
)
)}

File diff suppressed because it is too large Load Diff

View File

@@ -14,20 +14,51 @@ interface Mitigation {
created_at: string
verified_at: string | null
verified_by: string | null
source?: string
}
interface Hazard {
id: string
name: string
risk_level: string
category?: string
}
interface ProtectiveMeasure {
id: string
reduction_type: string
sub_type: string
name: string
description: string
hazard_category: string
examples: string[]
}
interface SuggestedMeasure {
id: string
reduction_type: string
sub_type: string
name: string
description: string
hazard_category: string
examples: string[]
tags?: string[]
}
const REDUCTION_TYPES = {
design: {
label: 'Design',
label: 'Stufe 1: Design',
description: 'Inhaerent sichere Konstruktion',
color: 'border-blue-200 bg-blue-50',
headerColor: 'bg-blue-100 text-blue-800',
subTypes: [
{ value: 'geometry', label: 'Geometrie & Anordnung' },
{ value: 'force_energy', label: 'Kraft & Energie' },
{ value: 'material', label: 'Material & Stabilitaet' },
{ value: 'ergonomics', label: 'Ergonomie' },
{ value: 'control_design', label: 'Steuerungstechnik' },
{ value: 'fluid_design', label: 'Pneumatik / Hydraulik' },
],
icon: (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z" />
@@ -35,10 +66,21 @@ const REDUCTION_TYPES = {
),
},
protection: {
label: 'Schutz',
label: 'Stufe 2: Schutz',
description: 'Technische Schutzmassnahmen',
color: 'border-green-200 bg-green-50',
headerColor: 'bg-green-100 text-green-800',
subTypes: [
{ value: 'fixed_guard', label: 'Feststehende Schutzeinrichtung' },
{ value: 'movable_guard', label: 'Bewegliche Schutzeinrichtung' },
{ value: 'electro_sensitive', label: 'Optoelektronisch' },
{ value: 'pressure_sensitive', label: 'Druckempfindlich' },
{ value: 'emergency_stop', label: 'Not-Halt' },
{ value: 'electrical_protection', label: 'Elektrischer Schutz' },
{ value: 'thermal_protection', label: 'Thermischer Schutz' },
{ value: 'fluid_protection', label: 'Hydraulik/Pneumatik-Schutz' },
{ value: 'extraction', label: 'Absaugung / Kapselung' },
],
icon: (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
@@ -46,10 +88,18 @@ const REDUCTION_TYPES = {
),
},
information: {
label: 'Information',
label: 'Stufe 3: Information',
description: 'Hinweise und Schulungen',
color: 'border-yellow-200 bg-yellow-50',
headerColor: 'bg-yellow-100 text-yellow-800',
subTypes: [
{ value: 'signage', label: 'Beschilderung & Kennzeichnung' },
{ value: 'manual', label: 'Betriebsanleitung' },
{ value: 'training', label: 'Schulung & Unterweisung' },
{ value: 'ppe', label: 'PSA (Schutzausruestung)' },
{ value: 'organizational', label: 'Organisatorisch' },
{ value: 'marking', label: 'Markierung & Codierung' },
],
icon: (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
@@ -76,6 +126,281 @@ function StatusBadge({ status }: { status: string }) {
)
}
function HierarchyWarning({ onDismiss }: { onDismiss: () => void }) {
return (
<div className="bg-amber-50 border border-amber-300 rounded-xl p-4 flex items-start gap-3">
<svg className="w-6 h-6 text-amber-600 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div className="flex-1">
<h4 className="text-sm font-semibold text-amber-800">Hierarchie-Warnung: Massnahmen vom Typ &quot;Information&quot;</h4>
<p className="text-sm text-amber-700 mt-1">
Hinweismassnahmen (Stufe 3) duerfen <strong>nicht als Primaermassnahme</strong> akzeptiert werden, wenn konstruktive
(Stufe 1) oder technische (Stufe 2) Massnahmen moeglich und zumutbar sind. Pruefen Sie, ob hoeherwertige
Massnahmen ergaenzt werden koennen.
</p>
</div>
<button onClick={onDismiss} className="text-amber-400 hover:text-amber-600 transition-colors">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
)
}
function MeasuresLibraryModal({
measures,
onSelect,
onClose,
filterType,
}: {
measures: ProtectiveMeasure[]
onSelect: (measure: ProtectiveMeasure) => void
onClose: () => void
filterType?: string
}) {
const [search, setSearch] = useState('')
const [selectedSubType, setSelectedSubType] = useState('')
const filtered = measures.filter((m) => {
if (filterType && m.reduction_type !== filterType) return false
if (selectedSubType && m.sub_type !== selectedSubType) return false
if (search) {
const q = search.toLowerCase()
return m.name.toLowerCase().includes(q) || m.description.toLowerCase().includes(q)
}
return true
})
const subTypes = [...new Set(measures.filter((m) => !filterType || m.reduction_type === filterType).map((m) => m.sub_type))].filter(Boolean)
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-xl w-full max-w-3xl max-h-[80vh] flex flex-col">
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Massnahmen-Bibliothek</h3>
<button onClick={onClose} className="p-1 text-gray-400 hover:text-gray-600 rounded transition-colors">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="flex gap-3">
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Massnahme suchen..."
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white"
/>
{subTypes.length > 1 && (
<select
value={selectedSubType}
onChange={(e) => setSelectedSubType(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white text-sm"
>
<option value="">Alle Sub-Typen</option>
{subTypes.map((st) => (
<option key={st} value={st}>{st}</option>
))}
</select>
)}
</div>
<div className="mt-2 text-xs text-gray-500">{filtered.length} Massnahmen</div>
</div>
<div className="flex-1 overflow-y-auto p-6 space-y-3">
{filtered.map((m) => (
<div
key={m.id}
className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:border-purple-300 hover:bg-purple-50/30 transition-colors cursor-pointer"
onClick={() => onSelect(m)}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-mono text-gray-400">{m.id}</span>
{m.sub_type && (
<span className="text-xs px-1.5 py-0.5 rounded bg-gray-100 text-gray-600">{m.sub_type}</span>
)}
</div>
<h4 className="text-sm font-medium text-gray-900 dark:text-white">{m.name}</h4>
<p className="text-xs text-gray-500 mt-1">{m.description}</p>
{m.examples && m.examples.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{m.examples.map((ex, i) => (
<span key={i} className="text-xs px-1.5 py-0.5 rounded bg-purple-50 text-purple-600">
{ex}
</span>
))}
</div>
)}
</div>
<button className="ml-3 px-3 py-1.5 text-xs bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200 transition-colors flex-shrink-0">
Uebernehmen
</button>
</div>
</div>
))}
{filtered.length === 0 && (
<div className="text-center py-8 text-gray-500">Keine Massnahmen gefunden</div>
)}
</div>
</div>
</div>
)
}
// ============================================================================
// Suggest Measures Modal (Phase 5)
// ============================================================================
function SuggestMeasuresModal({
hazards,
projectId,
onAddMeasure,
onClose,
}: {
hazards: Hazard[]
projectId: string
onAddMeasure: (title: string, description: string, reductionType: string, hazardId: string) => void
onClose: () => void
}) {
const [selectedHazard, setSelectedHazard] = useState<string>('')
const [suggested, setSuggested] = useState<SuggestedMeasure[]>([])
const [loadingSuggestions, setLoadingSuggestions] = useState(false)
const riskColors: Record<string, string> = {
not_acceptable: 'border-red-400 bg-red-50',
very_high: 'border-red-300 bg-red-50',
critical: 'border-red-300 bg-red-50',
high: 'border-orange-300 bg-orange-50',
medium: 'border-yellow-300 bg-yellow-50',
low: 'border-green-300 bg-green-50',
}
async function handleSelectHazard(hazardId: string) {
setSelectedHazard(hazardId)
setSuggested([])
if (!hazardId) return
setLoadingSuggestions(true)
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/hazards/${hazardId}/suggest-measures`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
})
if (res.ok) {
const json = await res.json()
setSuggested(json.suggested_measures || [])
}
} catch (err) {
console.error('Failed to suggest measures:', err)
} finally {
setLoadingSuggestions(false)
}
}
const groupedByType = {
design: suggested.filter(m => m.reduction_type === 'design'),
protection: suggested.filter(m => m.reduction_type === 'protection'),
information: suggested.filter(m => m.reduction_type === 'information'),
}
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-xl w-full max-w-3xl max-h-[85vh] flex flex-col">
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Massnahmen-Vorschlaege</h3>
<button onClick={onClose} className="p-1 text-gray-400 hover:text-gray-600 rounded">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<p className="text-sm text-gray-500 mb-3">
Waehlen Sie eine Gefaehrdung, um passende Massnahmen vorgeschlagen zu bekommen.
</p>
<div className="flex flex-wrap gap-2">
{hazards.map(h => (
<button
key={h.id}
onClick={() => handleSelectHazard(h.id)}
className={`px-3 py-1.5 text-xs rounded-lg border transition-colors ${
selectedHazard === h.id
? 'border-purple-400 bg-purple-50 text-purple-700 font-medium'
: `${riskColors[h.risk_level] || 'border-gray-200 bg-white'} text-gray-700 hover:border-purple-300`
}`}
>
{h.name}
</button>
))}
</div>
</div>
<div className="flex-1 overflow-auto p-6">
{loadingSuggestions ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
</div>
) : suggested.length > 0 ? (
<div className="space-y-6">
{(['design', 'protection', 'information'] as const).map(type => {
const items = groupedByType[type]
if (items.length === 0) return null
const config = REDUCTION_TYPES[type]
return (
<div key={type}>
<div className={`flex items-center gap-2 px-3 py-2 rounded-lg ${config.headerColor} mb-3`}>
{config.icon}
<span className="text-sm font-semibold">{config.label}</span>
<span className="ml-auto text-sm font-bold">{items.length}</span>
</div>
<div className="space-y-2">
{items.map(m => (
<div key={m.id} className="border border-gray-200 rounded-lg p-3 hover:bg-gray-50 transition-colors">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-mono text-gray-400">{m.id}</span>
{m.sub_type && (
<span className="text-xs px-1.5 py-0.5 rounded bg-gray-100 text-gray-600">{m.sub_type}</span>
)}
</div>
<div className="text-sm font-medium text-gray-900 dark:text-white">{m.name}</div>
<div className="text-xs text-gray-500 mt-0.5">{m.description}</div>
</div>
<button
onClick={() => onAddMeasure(m.name, m.description, m.reduction_type, selectedHazard)}
className="ml-3 px-3 py-1.5 text-xs bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors flex-shrink-0"
>
Uebernehmen
</button>
</div>
</div>
))}
</div>
</div>
)
})}
</div>
) : selectedHazard ? (
<div className="text-center py-12 text-gray-500">
Keine Vorschlaege fuer diese Gefaehrdung gefunden.
</div>
) : (
<div className="text-center py-12 text-gray-500">
Waehlen Sie eine Gefaehrdung aus, um Vorschlaege zu erhalten.
</div>
)}
</div>
</div>
</div>
)
}
interface MitigationFormData {
title: string
description: string
@@ -88,11 +413,13 @@ function MitigationForm({
onCancel,
hazards,
preselectedType,
onOpenLibrary,
}: {
onSubmit: (data: MitigationFormData) => void
onCancel: () => void
hazards: Hazard[]
preselectedType?: 'design' | 'protection' | 'information'
onOpenLibrary: (type?: string) => void
}) {
const [formData, setFormData] = useState<MitigationFormData>({
title: '',
@@ -112,7 +439,15 @@ function MitigationForm({
return (
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">Neue Massnahme</h3>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Neue Massnahme</h3>
<button
onClick={() => onOpenLibrary(formData.reduction_type)}
className="text-sm px-3 py-1.5 bg-purple-50 text-purple-700 border border-purple-200 rounded-lg hover:bg-purple-100 transition-colors"
>
Aus Bibliothek waehlen
</button>
</div>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
@@ -132,9 +467,9 @@ function MitigationForm({
onChange={(e) => setFormData({ ...formData, reduction_type: e.target.value as MitigationFormData['reduction_type'] })}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white"
>
<option value="design">Design - Inhaerent sichere Konstruktion</option>
<option value="protection">Schutz - Technische Schutzmassnahmen</option>
<option value="information">Information - Hinweise und Schulungen</option>
<option value="design">Stufe 1: Design - Inhaerent sichere Konstruktion</option>
<option value="protection">Stufe 2: Schutz - Technische Schutzmassnahmen</option>
<option value="information">Stufe 3: Information - Hinweise und Schulungen</option>
</select>
</div>
</div>
@@ -201,7 +536,14 @@ function MitigationCard({
return (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="flex items-start justify-between mb-2">
<h4 className="text-sm font-medium text-gray-900 dark:text-white">{mitigation.title}</h4>
<div className="flex items-center gap-2">
<h4 className="text-sm font-medium text-gray-900 dark:text-white">{mitigation.title}</h4>
{mitigation.title.startsWith('Auto:') && (
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-700">
Auto
</span>
)}
</div>
<StatusBadge status={mitigation.status} />
</div>
{mitigation.description && (
@@ -246,6 +588,12 @@ export default function MitigationsPage() {
const [loading, setLoading] = useState(true)
const [showForm, setShowForm] = useState(false)
const [preselectedType, setPreselectedType] = useState<'design' | 'protection' | 'information' | undefined>()
const [hierarchyWarning, setHierarchyWarning] = useState<boolean>(false)
const [showLibrary, setShowLibrary] = useState(false)
const [libraryFilter, setLibraryFilter] = useState<string | undefined>()
const [measures, setMeasures] = useState<ProtectiveMeasure[]>([])
// Phase 5: Suggest measures
const [showSuggest, setShowSuggest] = useState(false)
useEffect(() => {
fetchData()
@@ -259,11 +607,14 @@ export default function MitigationsPage() {
])
if (mitRes.ok) {
const json = await mitRes.json()
setMitigations(json.mitigations || json || [])
const mits = json.mitigations || json || []
setMitigations(mits)
// Check hierarchy: if information-only measures exist without design/protection
validateHierarchy(mits)
}
if (hazRes.ok) {
const json = await hazRes.json()
setHazards((json.hazards || json || []).map((h: Hazard) => ({ id: h.id, name: h.name, risk_level: h.risk_level })))
setHazards((json.hazards || json || []).map((h: Hazard) => ({ id: h.id, name: h.name, risk_level: h.risk_level, category: h.category })))
}
} catch (err) {
console.error('Failed to fetch data:', err)
@@ -272,6 +623,55 @@ export default function MitigationsPage() {
}
}
async function validateHierarchy(mits: Mitigation[]) {
if (mits.length === 0) return
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/validate-mitigation-hierarchy`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
mitigations: mits.map((m) => ({
reduction_type: m.reduction_type,
linked_hazard_ids: m.linked_hazard_ids,
})),
}),
})
if (res.ok) {
const json = await res.json()
setHierarchyWarning(json.has_warning === true)
}
} catch {
// Non-critical, ignore
}
}
async function fetchMeasuresLibrary(type?: string) {
try {
const url = type
? `/api/sdk/v1/iace/protective-measures-library?reduction_type=${type}`
: '/api/sdk/v1/iace/protective-measures-library'
const res = await fetch(url)
if (res.ok) {
const json = await res.json()
setMeasures(json.protective_measures || [])
}
} catch (err) {
console.error('Failed to fetch measures library:', err)
}
}
function handleOpenLibrary(type?: string) {
setLibraryFilter(type)
fetchMeasuresLibrary(type)
setShowLibrary(true)
}
function handleSelectMeasure(measure: ProtectiveMeasure) {
setShowLibrary(false)
setShowForm(true)
setPreselectedType(measure.reduction_type as 'design' | 'protection' | 'information')
}
async function handleSubmit(data: MitigationFormData) {
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/mitigations`, {
@@ -289,6 +689,26 @@ export default function MitigationsPage() {
}
}
async function handleAddSuggestedMeasure(title: string, description: string, reductionType: string, hazardId: string) {
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/mitigations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title,
description,
reduction_type: reductionType,
linked_hazard_ids: [hazardId],
}),
})
if (res.ok) {
await fetchData()
}
} catch (err) {
console.error('Failed to add suggested measure:', err)
}
}
async function handleVerify(id: string) {
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/mitigations/${id}/verify`, {
@@ -341,23 +761,50 @@ export default function MitigationsPage() {
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Massnahmen</h1>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Risikominderung nach dem 3-Stufen-Verfahren: Design, Schutz, Information.
Risikominderung nach dem 3-Stufen-Verfahren: Design &rarr; Schutz &rarr; Information.
</p>
</div>
<button
onClick={() => {
setPreselectedType(undefined)
setShowForm(true)
}}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Massnahme hinzufuegen
</button>
<div className="flex items-center gap-3">
{hazards.length > 0 && (
<button
onClick={() => setShowSuggest(true)}
className="flex items-center gap-2 px-3 py-2 border border-green-300 text-green-700 rounded-lg hover:bg-green-50 transition-colors text-sm"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
Vorschlaege
</button>
)}
<button
onClick={() => handleOpenLibrary()}
className="flex items-center gap-2 px-4 py-2 bg-white border border-purple-300 text-purple-700 rounded-lg hover:bg-purple-50 transition-colors"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
Bibliothek
</button>
<button
onClick={() => {
setPreselectedType(undefined)
setShowForm(true)
}}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Massnahme hinzufuegen
</button>
</div>
</div>
{/* Hierarchy Warning */}
{hierarchyWarning && (
<HierarchyWarning onDismiss={() => setHierarchyWarning(false)} />
)}
{/* Form */}
{showForm && (
<MitigationForm
@@ -368,6 +815,27 @@ export default function MitigationsPage() {
}}
hazards={hazards}
preselectedType={preselectedType}
onOpenLibrary={handleOpenLibrary}
/>
)}
{/* Measures Library Modal */}
{showLibrary && (
<MeasuresLibraryModal
measures={measures}
onSelect={handleSelectMeasure}
onClose={() => setShowLibrary(false)}
filterType={libraryFilter}
/>
)}
{/* Suggest Measures Modal (Phase 5) */}
{showSuggest && (
<SuggestMeasuresModal
hazards={hazards}
projectId={projectId}
onAddMeasure={handleAddSuggestedMeasure}
onClose={() => setShowSuggest(false)}
/>
)}
@@ -378,7 +846,7 @@ export default function MitigationsPage() {
const items = byType[type]
return (
<div key={type} className={`rounded-xl border ${config.color} p-4`}>
<div className={`flex items-center gap-2 px-3 py-2 rounded-lg ${config.headerColor} mb-4`}>
<div className={`flex items-center gap-2 px-3 py-2 rounded-lg ${config.headerColor} mb-3`}>
{config.icon}
<div>
<h3 className="text-sm font-semibold">{config.label}</h3>
@@ -387,6 +855,15 @@ export default function MitigationsPage() {
<span className="ml-auto text-sm font-bold">{items.length}</span>
</div>
{/* Sub-types overview */}
<div className="mb-3 flex flex-wrap gap-1">
{config.subTypes.map((st) => (
<span key={st.value} className="text-xs px-1.5 py-0.5 rounded bg-white/60 text-gray-500 border border-gray-200/50">
{st.label}
</span>
))}
</div>
<div className="space-y-3">
{items.map((m) => (
<MitigationCard
@@ -398,12 +875,23 @@ export default function MitigationsPage() {
))}
</div>
<button
onClick={() => handleAddForType(type)}
className="mt-3 w-full py-2 text-sm text-gray-500 hover:text-purple-600 hover:bg-white rounded-lg border border-dashed border-gray-300 hover:border-purple-300 transition-colors"
>
+ Massnahme hinzufuegen
</button>
<div className="mt-3 flex gap-2">
<button
onClick={() => handleAddForType(type)}
className="flex-1 py-2 text-sm text-gray-500 hover:text-purple-600 hover:bg-white rounded-lg border border-dashed border-gray-300 hover:border-purple-300 transition-colors"
>
+ Hinzufuegen
</button>
<button
onClick={() => handleOpenLibrary(type)}
className="py-2 px-3 text-sm text-gray-400 hover:text-purple-600 hover:bg-white rounded-lg border border-dashed border-gray-300 hover:border-purple-300 transition-colors"
title="Aus Bibliothek waehlen"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
</button>
</div>
</div>
)
})}

View File

@@ -1,7 +1,8 @@
'use client'
import React, { useState, useEffect } from 'react'
import React, { useState, useEffect, useRef } from 'react'
import { useParams } from 'next/navigation'
import { TechFileEditor } from '@/components/sdk/iace/TechFileEditor'
interface TechFileSection {
id: string
@@ -67,6 +68,14 @@ const STATUS_CONFIG: Record<string, { label: string; color: string; bgColor: str
approved: { label: 'Freigegeben', color: 'text-green-700', bgColor: 'bg-green-100' },
}
const EXPORT_FORMATS: { value: string; label: string; extension: string }[] = [
{ value: 'pdf', label: 'PDF', extension: '.pdf' },
{ value: 'xlsx', label: 'Excel', extension: '.xlsx' },
{ value: 'docx', label: 'Word', extension: '.docx' },
{ value: 'md', label: 'Markdown', extension: '.md' },
{ value: 'json', label: 'JSON', extension: '.json' },
]
function StatusBadge({ status }: { status: string }) {
const config = STATUS_CONFIG[status] || STATUS_CONFIG.empty
return (
@@ -87,7 +96,6 @@ function SectionViewer({
onApprove: (id: string) => void
onSave: (id: string, content: string) => void
}) {
const [editedContent, setEditedContent] = useState(section.content || '')
const [editing, setEditing] = useState(false)
return (
@@ -111,13 +119,10 @@ function SectionViewer({
)}
{editing && (
<button
onClick={() => {
onSave(section.id, editedContent)
setEditing(false)
}}
className="text-sm px-3 py-1.5 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
onClick={() => setEditing(false)}
className="text-sm px-3 py-1.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
Speichern
Fertig
</button>
)}
{section.status !== 'approved' && section.content && !editing && (
@@ -136,19 +141,19 @@ function SectionViewer({
</div>
</div>
<div className="p-6">
{editing ? (
<textarea
value={editedContent}
onChange={(e) => setEditedContent(e.target.value)}
rows={20}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-white"
/>
) : section.content ? (
<div className="prose prose-sm max-w-none dark:prose-invert">
<pre className="whitespace-pre-wrap text-sm text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-750 p-4 rounded-lg">
{section.content}
</pre>
</div>
{section.content ? (
editing ? (
<TechFileEditor
content={section.content}
onSave={(html) => onSave(section.id, html)}
/>
) : (
<TechFileEditor
content={section.content}
onSave={() => {}}
readOnly
/>
)
) : (
<div className="text-center py-8 text-gray-500">
Kein Inhalt vorhanden. Klicken Sie &quot;Generieren&quot; um den Abschnitt zu erstellen.
@@ -167,6 +172,21 @@ export default function TechFilePage() {
const [generatingSection, setGeneratingSection] = useState<string | null>(null)
const [viewingSection, setViewingSection] = useState<TechFileSection | null>(null)
const [exporting, setExporting] = useState(false)
const [showExportMenu, setShowExportMenu] = useState(false)
const exportMenuRef = useRef<HTMLDivElement>(null)
// Close export menu when clicking outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (exportMenuRef.current && !exportMenuRef.current.contains(event.target as Node)) {
setShowExportMenu(false)
}
}
if (showExportMenu) {
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}
}, [showExportMenu])
useEffect(() => {
fetchSections()
@@ -236,18 +256,22 @@ export default function TechFilePage() {
}
}
async function handleExportZip() {
async function handleExport(format: string) {
setExporting(true)
setShowExportMenu(false)
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/tech-file/export`, {
method: 'POST',
})
const res = await fetch(
`/api/sdk/v1/iace/projects/${projectId}/tech-file/export?format=${format}`,
{ method: 'GET' }
)
if (res.ok) {
const blob = await res.blob()
const url = window.URL.createObjectURL(blob)
const formatConfig = EXPORT_FORMATS.find((f) => f.value === format)
const extension = formatConfig?.extension || `.${format}`
const a = document.createElement('a')
a.href = url
a.download = `CE-Akte-${projectId}.zip`
a.download = `CE-Akte-${projectId}${extension}`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
@@ -284,25 +308,45 @@ export default function TechFilePage() {
Sie alle erforderlichen Abschnitte.
</p>
</div>
<button
onClick={handleExportZip}
disabled={!allRequiredApproved || exporting}
title={!allRequiredApproved ? 'Alle Pflichtabschnitte muessen freigegeben sein' : 'CE-Akte als ZIP exportieren'}
className={`flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-colors ${
allRequiredApproved && !exporting
? 'bg-purple-600 text-white hover:bg-purple-700'
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
}`}
>
{exporting ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" />
) : (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
{/* Export Dropdown */}
<div className="relative" ref={exportMenuRef}>
<button
onClick={() => setShowExportMenu((prev) => !prev)}
disabled={!allRequiredApproved || exporting}
title={!allRequiredApproved ? 'Alle Pflichtabschnitte muessen freigegeben sein' : 'CE-Akte exportieren'}
className={`flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-colors ${
allRequiredApproved && !exporting
? 'bg-purple-600 text-white hover:bg-purple-700'
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
}`}
>
{exporting ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" />
) : (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
)}
Exportieren
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{showExportMenu && allRequiredApproved && !exporting && (
<div className="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 z-50">
{EXPORT_FORMATS.map((fmt) => (
<button
key={fmt.value}
onClick={() => handleExport(fmt.value)}
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex items-center gap-3"
>
<span className="text-xs font-mono uppercase w-10 text-gray-400">{fmt.extension}</span>
<span>{fmt.label}</span>
</button>
))}
</div>
)}
ZIP exportieren
</button>
</div>
</div>
{/* Progress */}

View File

@@ -19,14 +19,25 @@ interface VerificationItem {
created_at: string
}
interface SuggestedEvidence {
id: string
name: string
description: string
method: string
tags?: string[]
}
const VERIFICATION_METHODS = [
{ value: 'test', label: 'Test' },
{ value: 'analysis', label: 'Analyse' },
{ value: 'inspection', label: 'Inspektion' },
{ value: 'simulation', label: 'Simulation' },
{ value: 'review', label: 'Review' },
{ value: 'demonstration', label: 'Demonstration' },
{ value: 'certification', label: 'Zertifizierung' },
{ value: 'design_review', label: 'Design-Review', description: 'Systematische Pruefung der Konstruktionsunterlagen' },
{ value: 'calculation', label: 'Berechnung', description: 'Rechnerischer Nachweis (FEM, Festigkeit, Thermik)' },
{ value: 'test_report', label: 'Pruefbericht', description: 'Dokumentierter Test mit Messprotokoll' },
{ value: 'validation', label: 'Validierung', description: 'Nachweis der Eignung unter realen Betriebsbedingungen' },
{ value: 'electrical_test', label: 'Elektrische Pruefung', description: 'Isolationsmessung, Schutzleiter, Spannungsfestigkeit' },
{ value: 'software_test', label: 'Software-Test', description: 'Unit-, Integrations- oder Systemtest der Steuerungssoftware' },
{ value: 'penetration_test', label: 'Penetrationstest', description: 'Security-Test der Netzwerk- und Steuerungskomponenten' },
{ value: 'acceptance_protocol', label: 'Abnahmeprotokoll', description: 'Formelle Abnahme mit Checkliste und Unterschrift' },
{ value: 'user_test', label: 'Anwendertest', description: 'Pruefung durch Bediener unter realen Einsatzbedingungen' },
{ value: 'documentation_release', label: 'Dokumentenfreigabe', description: 'Formelle Freigabe der technischen Dokumentation' },
]
const STATUS_CONFIG: Record<string, { label: string; color: string }> = {
@@ -238,6 +249,130 @@ function CompleteModal({
)
}
// ============================================================================
// Suggest Evidence Modal (Phase 5)
// ============================================================================
function SuggestEvidenceModal({
mitigations,
projectId,
onAddEvidence,
onClose,
}: {
mitigations: { id: string; title: string }[]
projectId: string
onAddEvidence: (title: string, description: string, method: string, mitigationId: string) => void
onClose: () => void
}) {
const [selectedMitigation, setSelectedMitigation] = useState<string>('')
const [suggested, setSuggested] = useState<SuggestedEvidence[]>([])
const [loadingSuggestions, setLoadingSuggestions] = useState(false)
async function handleSelectMitigation(mitigationId: string) {
setSelectedMitigation(mitigationId)
setSuggested([])
if (!mitigationId) return
setLoadingSuggestions(true)
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/mitigations/${mitigationId}/suggest-evidence`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
})
if (res.ok) {
const json = await res.json()
setSuggested(json.suggested_evidence || [])
}
} catch (err) {
console.error('Failed to suggest evidence:', err)
} finally {
setLoadingSuggestions(false)
}
}
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-xl w-full max-w-3xl max-h-[85vh] flex flex-col">
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Nachweise vorschlagen</h3>
<button onClick={onClose} className="p-1 text-gray-400 hover:text-gray-600 rounded">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<p className="text-sm text-gray-500 mb-3">
Waehlen Sie eine Massnahme, um passende Nachweismethoden vorgeschlagen zu bekommen.
</p>
<div className="flex flex-wrap gap-2">
{mitigations.map(m => (
<button
key={m.id}
onClick={() => handleSelectMitigation(m.id)}
className={`px-3 py-1.5 text-xs rounded-lg border transition-colors ${
selectedMitigation === m.id
? 'border-purple-400 bg-purple-50 text-purple-700 font-medium'
: 'border-gray-200 bg-white text-gray-700 hover:border-purple-300'
}`}
>
{m.title}
</button>
))}
</div>
</div>
<div className="flex-1 overflow-auto p-6">
{loadingSuggestions ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
</div>
) : suggested.length > 0 ? (
<div className="space-y-3">
{suggested.map(ev => (
<div key={ev.id} className="border border-gray-200 rounded-lg p-4 hover:bg-gray-50 transition-colors">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-mono text-gray-400">{ev.id}</span>
{ev.method && (
<span className="text-xs px-1.5 py-0.5 rounded bg-blue-50 text-blue-600">
{VERIFICATION_METHODS.find(m => m.value === ev.method)?.label || ev.method}
</span>
)}
</div>
<div className="text-sm font-medium text-gray-900 dark:text-white">{ev.name}</div>
<div className="text-xs text-gray-500 mt-0.5">{ev.description}</div>
</div>
<button
onClick={() => onAddEvidence(ev.name, ev.description, ev.method || 'test_report', selectedMitigation)}
className="ml-3 px-3 py-1.5 text-xs bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors flex-shrink-0"
>
Uebernehmen
</button>
</div>
</div>
))}
</div>
) : selectedMitigation ? (
<div className="text-center py-12 text-gray-500">
Keine Vorschlaege fuer diese Massnahme gefunden.
</div>
) : (
<div className="text-center py-12 text-gray-500">
Waehlen Sie eine Massnahme aus, um Nachweise vorgeschlagen zu bekommen.
</div>
)}
</div>
</div>
</div>
)
}
// ============================================================================
// Main Page
// ============================================================================
export default function VerificationPage() {
const params = useParams()
const projectId = params.projectId as string
@@ -247,6 +382,8 @@ export default function VerificationPage() {
const [loading, setLoading] = useState(true)
const [showForm, setShowForm] = useState(false)
const [completingItem, setCompletingItem] = useState<VerificationItem | null>(null)
// Phase 5: Suggest evidence
const [showSuggest, setShowSuggest] = useState(false)
useEffect(() => {
fetchData()
@@ -294,6 +431,26 @@ export default function VerificationPage() {
}
}
async function handleAddSuggestedEvidence(title: string, description: string, method: string, mitigationId: string) {
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/verifications`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title,
description,
method,
linked_mitigation_id: mitigationId,
}),
})
if (res.ok) {
await fetchData()
}
} catch (err) {
console.error('Failed to add suggested evidence:', err)
}
}
async function handleComplete(id: string, result: string, passed: boolean) {
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/verifications/${id}/complete`, {
@@ -344,15 +501,28 @@ export default function VerificationPage() {
Nachweisfuehrung fuer alle Schutzmassnahmen und Sicherheitsanforderungen.
</p>
</div>
<button
onClick={() => setShowForm(true)}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Verifikation hinzufuegen
</button>
<div className="flex items-center gap-2">
{mitigations.length > 0 && (
<button
onClick={() => setShowSuggest(true)}
className="flex items-center gap-2 px-3 py-2 border border-green-300 text-green-700 rounded-lg hover:bg-green-50 transition-colors text-sm"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
Nachweise vorschlagen
</button>
)}
<button
onClick={() => setShowForm(true)}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Verifikation hinzufuegen
</button>
</div>
</div>
{/* Stats */}
@@ -396,6 +566,16 @@ export default function VerificationPage() {
/>
)}
{/* Suggest Evidence Modal (Phase 5) */}
{showSuggest && (
<SuggestEvidenceModal
mitigations={mitigations}
projectId={projectId}
onAddEvidence={handleAddSuggestedEvidence}
onClose={() => setShowSuggest(false)}
/>
)}
{/* Table */}
{items.length > 0 ? (
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
@@ -469,12 +649,22 @@ export default function VerificationPage() {
Definieren Sie Verifikationsschritte fuer Ihre Schutzmassnahmen.
Jede Massnahme sollte durch mindestens eine Verifikation abgedeckt sein.
</p>
<button
onClick={() => setShowForm(true)}
className="mt-6 px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
Erste Verifikation anlegen
</button>
<div className="mt-6 flex items-center justify-center gap-3">
{mitigations.length > 0 && (
<button
onClick={() => setShowSuggest(true)}
className="px-6 py-3 border border-green-300 text-green-700 rounded-lg hover:bg-green-50 transition-colors"
>
Nachweise vorschlagen
</button>
)}
<button
onClick={() => setShowForm(true)}
className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
Erste Verifikation anlegen
</button>
</div>
</div>
)
)}

View File

@@ -2,7 +2,6 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react'
import { useRouter } from 'next/navigation'
import { useSDK } from '@/lib/sdk'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
import {
LoeschfristPolicy, LegalHold, StorageLocation,
@@ -15,7 +14,6 @@ import {
formatRetentionDuration, isPolicyOverdue, getActiveLegalHolds,
getEffectiveDeletionTrigger,
} from '@/lib/sdk/loeschfristen-types'
import { BASELINE_TEMPLATES, templateToPolicy, getTemplateById, getAllTemplateTags } from '@/lib/sdk/loeschfristen-baseline-catalog'
import {
PROFILING_STEPS, ProfilingAnswer, ProfilingStep,
isStepComplete, getProfilingProgress, generatePoliciesFromProfile,
@@ -27,12 +25,18 @@ import {
exportPoliciesAsJSON, exportPoliciesAsCSV,
generateComplianceSummary, downloadFile,
} from '@/lib/sdk/loeschfristen-export'
import {
buildLoeschkonzeptHtml,
type LoeschkonzeptOrgHeader,
type LoeschkonzeptRevision,
createDefaultLoeschkonzeptOrgHeader,
} from '@/lib/sdk/loeschfristen-document'
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
type Tab = 'uebersicht' | 'editor' | 'generator' | 'export'
type Tab = 'uebersicht' | 'editor' | 'generator' | 'export' | 'loeschkonzept'
// ---------------------------------------------------------------------------
// Helper: TagInput
@@ -101,7 +105,6 @@ function TagInput({
export default function LoeschfristenPage() {
const router = useRouter()
const sdk = useSDK()
// ---- Core state ----
const [tab, setTab] = useState<Tab>('uebersicht')
@@ -121,15 +124,19 @@ export default function LoeschfristenPage() {
// ---- Compliance state ----
const [complianceResult, setComplianceResult] = useState<ComplianceCheckResult | null>(null)
// ---- Legal Hold management ----
const [managingLegalHolds, setManagingLegalHolds] = useState(false)
// ---- Saving state ----
const [saving, setSaving] = useState(false)
// ---- VVT data ----
const [vvtActivities, setVvtActivities] = useState<any[]>([])
// ---- Vendor data ----
const [vendorList, setVendorList] = useState<Array<{id: string, name: string}>>([])
// ---- Loeschkonzept document state ----
const [orgHeader, setOrgHeader] = useState<LoeschkonzeptOrgHeader>(createDefaultLoeschkonzeptOrgHeader())
const [revisions, setRevisions] = useState<LoeschkonzeptRevision[]>([])
// --------------------------------------------------------------------------
// Persistence (API-backed)
// --------------------------------------------------------------------------
@@ -184,6 +191,7 @@ export default function LoeschfristenPage() {
responsiblePerson: raw.responsible_person || '',
releaseProcess: raw.release_process || '',
linkedVVTActivityIds: raw.linked_vvt_activity_ids || [],
linkedVendorIds: raw.linked_vendor_ids || [],
status: raw.status || 'DRAFT',
lastReviewDate: raw.last_review_date || base.lastReviewDate,
nextReviewDate: raw.next_review_date || base.nextReviewDate,
@@ -218,6 +226,7 @@ export default function LoeschfristenPage() {
responsible_person: p.responsiblePerson,
release_process: p.releaseProcess,
linked_vvt_activity_ids: p.linkedVVTActivityIds,
linked_vendor_ids: p.linkedVendorIds,
status: p.status,
last_review_date: p.lastReviewDate || null,
next_review_date: p.nextReviewDate || null,
@@ -247,6 +256,59 @@ export default function LoeschfristenPage() {
})
}, [tab, editingId])
// Load vendor list from API
useEffect(() => {
fetch('/api/sdk/v1/vendor-compliance/vendors?limit=500')
.then(r => r.ok ? r.json() : null)
.then(data => {
const items = data?.data?.items || []
setVendorList(items.map((v: any) => ({ id: v.id, name: v.name })))
})
.catch(() => {})
}, [])
// Load Loeschkonzept org header from VVT organization data + revisions from localStorage
useEffect(() => {
// Load revisions from localStorage
try {
const raw = localStorage.getItem('bp_loeschkonzept_revisions')
if (raw) {
const parsed = JSON.parse(raw)
if (Array.isArray(parsed)) setRevisions(parsed)
}
} catch { /* ignore */ }
// Load org header from localStorage (user overrides)
try {
const raw = localStorage.getItem('bp_loeschkonzept_orgheader')
if (raw) {
const parsed = JSON.parse(raw)
if (parsed && typeof parsed === 'object') {
setOrgHeader(prev => ({ ...prev, ...parsed }))
return // User has saved org header, skip VVT fetch
}
}
} catch { /* ignore */ }
// Fallback: fetch from VVT organization API
fetch('/api/sdk/v1/compliance/vvt/organization')
.then(res => res.ok ? res.json() : null)
.then(data => {
if (data) {
setOrgHeader(prev => ({
...prev,
organizationName: data.organization_name || data.organizationName || prev.organizationName,
industry: data.industry || prev.industry,
dpoName: data.dpo_name || data.dpoName || prev.dpoName,
dpoContact: data.dpo_contact || data.dpoContact || prev.dpoContact,
responsiblePerson: data.responsible_person || data.responsiblePerson || prev.responsiblePerson,
employeeCount: data.employee_count || data.employeeCount || prev.employeeCount,
}))
}
})
.catch(() => { /* ignore */ })
}, [])
// --------------------------------------------------------------------------
// Derived
// --------------------------------------------------------------------------
@@ -489,6 +551,7 @@ export default function LoeschfristenPage() {
{ key: 'editor', label: 'Editor' },
{ key: 'generator', label: 'Generator' },
{ key: 'export', label: 'Export & Compliance' },
{ key: 'loeschkonzept', label: 'Loeschkonzept' },
]
// --------------------------------------------------------------------------
@@ -1355,13 +1418,13 @@ export default function LoeschfristenPage() {
Verarbeitungstaetigkeit aus Ihrem VVT.
</p>
<div className="space-y-2">
{policy.linkedVvtIds && policy.linkedVvtIds.length > 0 && (
{policy.linkedVVTActivityIds && policy.linkedVVTActivityIds.length > 0 && (
<div className="mb-3">
<label className="block text-xs font-medium text-gray-500 mb-1">
Verknuepfte Taetigkeiten:
</label>
<div className="flex flex-wrap gap-1">
{policy.linkedVvtIds.map((vvtId: string) => {
{policy.linkedVVTActivityIds.map((vvtId: string) => {
const activity = vvtActivities.find(
(a: any) => a.id === vvtId,
)
@@ -1376,8 +1439,8 @@ export default function LoeschfristenPage() {
onClick={() =>
updatePolicy(pid, (p) => ({
...p,
linkedVvtIds: (
p.linkedVvtIds || []
linkedVVTActivityIds: (
p.linkedVVTActivityIds || []
).filter((id: string) => id !== vvtId),
}))
}
@@ -1396,11 +1459,11 @@ export default function LoeschfristenPage() {
const val = e.target.value
if (
val &&
!(policy.linkedVvtIds || []).includes(val)
!(policy.linkedVVTActivityIds || []).includes(val)
) {
updatePolicy(pid, (p) => ({
...p,
linkedVvtIds: [...(p.linkedVvtIds || []), val],
linkedVVTActivityIds: [...(p.linkedVVTActivityIds || []), val],
}))
}
e.target.value = ''
@@ -1413,7 +1476,7 @@ export default function LoeschfristenPage() {
{vvtActivities
.filter(
(a: any) =>
!(policy.linkedVvtIds || []).includes(a.id),
!(policy.linkedVVTActivityIds || []).includes(a.id),
)
.map((a: any) => (
<option key={a.id} value={a.id}>
@@ -1432,6 +1495,95 @@ export default function LoeschfristenPage() {
)}
</div>
{/* Sektion 5b: Auftragsverarbeiter-Verknuepfung */}
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
<h3 className="text-lg font-semibold text-gray-900">
5b. Verknuepfte Auftragsverarbeiter
</h3>
{vendorList.length > 0 ? (
<div>
<p className="text-sm text-gray-500 mb-3">
Verknuepfen Sie diese Loeschfrist mit relevanten Auftragsverarbeitern.
</p>
<div className="space-y-2">
{policy.linkedVendorIds && policy.linkedVendorIds.length > 0 && (
<div className="mb-3">
<label className="block text-xs font-medium text-gray-500 mb-1">
Verknuepfte Auftragsverarbeiter:
</label>
<div className="flex flex-wrap gap-1">
{policy.linkedVendorIds.map((vendorId: string) => {
const vendor = vendorList.find(
(v) => v.id === vendorId,
)
return (
<span
key={vendorId}
className="inline-flex items-center gap-1 bg-orange-100 text-orange-800 text-xs font-medium px-2 py-0.5 rounded-full"
>
{vendor?.name || vendorId}
<button
type="button"
onClick={() =>
updatePolicy(pid, (p) => ({
...p,
linkedVendorIds: (
p.linkedVendorIds || []
).filter((id: string) => id !== vendorId),
}))
}
className="text-orange-600 hover:text-orange-900"
>
x
</button>
</span>
)
})}
</div>
</div>
)}
<select
onChange={(e) => {
const val = e.target.value
if (
val &&
!(policy.linkedVendorIds || []).includes(val)
) {
updatePolicy(pid, (p) => ({
...p,
linkedVendorIds: [...(p.linkedVendorIds || []), val],
}))
}
e.target.value = ''
}}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
>
<option value="">
Auftragsverarbeiter verknuepfen...
</option>
{vendorList
.filter(
(v) =>
!(policy.linkedVendorIds || []).includes(v.id),
)
.map((v) => (
<option key={v.id} value={v.id}>
{v.name || v.id}
</option>
))}
</select>
</div>
</div>
) : (
<p className="text-sm text-gray-400">
Keine Auftragsverarbeiter gefunden. Erstellen Sie zuerst
Auftragsverarbeiter im Vendor-Compliance-Modul, um hier Verknuepfungen
herstellen zu koennen.
</p>
)}
</div>
{/* Sektion 6: Review-Einstellungen */}
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
<h3 className="text-lg font-semibold text-gray-900">
@@ -2278,6 +2430,316 @@ export default function LoeschfristenPage() {
)
}
// ==========================================================================
// Tab 5: Loeschkonzept Document
// ==========================================================================
function handleOrgHeaderChange(field: keyof LoeschkonzeptOrgHeader, value: string | string[]) {
const updated = { ...orgHeader, [field]: value }
setOrgHeader(updated)
localStorage.setItem('bp_loeschkonzept_orgheader', JSON.stringify(updated))
}
function handleAddRevision() {
const newRev: LoeschkonzeptRevision = {
version: orgHeader.loeschkonzeptVersion,
date: new Date().toISOString().split('T')[0],
author: orgHeader.dpoName || orgHeader.responsiblePerson || '',
changes: '',
}
const updated = [...revisions, newRev]
setRevisions(updated)
localStorage.setItem('bp_loeschkonzept_revisions', JSON.stringify(updated))
}
function handleUpdateRevision(index: number, field: keyof LoeschkonzeptRevision, value: string) {
const updated = revisions.map((r, i) => i === index ? { ...r, [field]: value } : r)
setRevisions(updated)
localStorage.setItem('bp_loeschkonzept_revisions', JSON.stringify(updated))
}
function handleRemoveRevision(index: number) {
const updated = revisions.filter((_, i) => i !== index)
setRevisions(updated)
localStorage.setItem('bp_loeschkonzept_revisions', JSON.stringify(updated))
}
function handlePrintLoeschkonzept() {
const htmlContent = buildLoeschkonzeptHtml(policies, orgHeader, vvtActivities, complianceResult, revisions)
const printWindow = window.open('', '_blank')
if (printWindow) {
printWindow.document.write(htmlContent)
printWindow.document.close()
printWindow.focus()
setTimeout(() => printWindow.print(), 300)
}
}
function handleDownloadLoeschkonzeptHtml() {
const htmlContent = buildLoeschkonzeptHtml(policies, orgHeader, vvtActivities, complianceResult, revisions)
downloadFile(htmlContent, `loeschkonzept-${new Date().toISOString().split('T')[0]}.html`, 'text/html;charset=utf-8')
}
function renderLoeschkonzept() {
const activePolicies = policies.filter(p => p.status !== 'ARCHIVED')
return (
<div className="space-y-4">
{/* Action bar */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-gray-900">
Loeschkonzept (Art. 5/17/30 DSGVO)
</h3>
<p className="text-sm text-gray-500 mt-0.5">
Druckfertiges Loeschkonzept mit Deckblatt, Loeschregeln, VVT-Verknuepfung und Compliance-Status.
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleDownloadLoeschkonzeptHtml}
disabled={activePolicies.length === 0}
className="bg-gray-100 text-gray-700 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg px-4 py-2 text-sm font-medium transition flex items-center gap-1.5"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
HTML herunterladen
</button>
<button
onClick={handlePrintLoeschkonzept}
disabled={activePolicies.length === 0}
className="bg-purple-600 text-white hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg px-4 py-2 text-sm font-medium transition flex items-center gap-1.5"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" /></svg>
Als PDF drucken
</button>
</div>
</div>
{activePolicies.length === 0 && (
<div className="bg-yellow-50 text-yellow-700 text-sm rounded-lg p-3 border border-yellow-200">
Keine aktiven Policies vorhanden. Erstellen Sie mindestens eine Policy, um das Loeschkonzept zu generieren.
</div>
)}
</div>
{/* Org Header Form */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h4 className="text-sm font-semibold text-gray-900 mb-4">Organisationsdaten (Deckblatt)</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Organisation</label>
<input
type="text"
value={orgHeader.organizationName}
onChange={e => handleOrgHeaderChange('organizationName', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="Name der Organisation"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Branche</label>
<input
type="text"
value={orgHeader.industry}
onChange={e => handleOrgHeaderChange('industry', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="z.B. IT / Software"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Datenschutzbeauftragter</label>
<input
type="text"
value={orgHeader.dpoName}
onChange={e => handleOrgHeaderChange('dpoName', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="Name des DSB"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">DSB-Kontakt</label>
<input
type="text"
value={orgHeader.dpoContact}
onChange={e => handleOrgHeaderChange('dpoContact', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="E-Mail oder Telefon"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Verantwortlicher (Art. 4 Nr. 7)</label>
<input
type="text"
value={orgHeader.responsiblePerson}
onChange={e => handleOrgHeaderChange('responsiblePerson', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="Name des Verantwortlichen"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Mitarbeiter</label>
<input
type="text"
value={orgHeader.employeeCount}
onChange={e => handleOrgHeaderChange('employeeCount', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="z.B. 50-249"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Version</label>
<input
type="text"
value={orgHeader.loeschkonzeptVersion}
onChange={e => handleOrgHeaderChange('loeschkonzeptVersion', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="1.0"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Pruefintervall</label>
<select
value={orgHeader.reviewInterval}
onChange={e => handleOrgHeaderChange('reviewInterval', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
>
<option value="Vierteljaehrlich">Vierteljaehrlich</option>
<option value="Halbjaehrlich">Halbjaehrlich</option>
<option value="Jaehrlich">Jaehrlich</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Letzte Pruefung</label>
<input
type="date"
value={orgHeader.lastReviewDate}
onChange={e => handleOrgHeaderChange('lastReviewDate', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Naechste Pruefung</label>
<input
type="date"
value={orgHeader.nextReviewDate}
onChange={e => handleOrgHeaderChange('nextReviewDate', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
/>
</div>
</div>
</div>
{/* Revisions */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h4 className="text-sm font-semibold text-gray-900">Aenderungshistorie</h4>
<button
onClick={handleAddRevision}
className="text-xs bg-purple-50 text-purple-700 hover:bg-purple-100 rounded-lg px-3 py-1.5 font-medium transition"
>
+ Revision hinzufuegen
</button>
</div>
{revisions.length === 0 ? (
<p className="text-sm text-gray-400">
Noch keine Revisionen. Die Erstversion wird automatisch im Dokument eingefuegt.
</p>
) : (
<div className="space-y-3">
{revisions.map((rev, idx) => (
<div key={idx} className="grid grid-cols-[80px_120px_1fr_1fr_32px] gap-2 items-start">
<input
type="text"
value={rev.version}
onChange={e => handleUpdateRevision(idx, 'version', e.target.value)}
className="rounded-lg border border-gray-300 px-2 py-1.5 text-xs"
placeholder="1.1"
/>
<input
type="date"
value={rev.date}
onChange={e => handleUpdateRevision(idx, 'date', e.target.value)}
className="rounded-lg border border-gray-300 px-2 py-1.5 text-xs"
/>
<input
type="text"
value={rev.author}
onChange={e => handleUpdateRevision(idx, 'author', e.target.value)}
className="rounded-lg border border-gray-300 px-2 py-1.5 text-xs"
placeholder="Autor"
/>
<input
type="text"
value={rev.changes}
onChange={e => handleUpdateRevision(idx, 'changes', e.target.value)}
className="rounded-lg border border-gray-300 px-2 py-1.5 text-xs"
placeholder="Beschreibung der Aenderungen"
/>
<button
onClick={() => handleRemoveRevision(idx)}
className="text-red-400 hover:text-red-600 p-1"
title="Revision entfernen"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
</button>
</div>
))}
</div>
)}
</div>
{/* Document Preview */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h4 className="text-sm font-semibold text-gray-900 mb-4">Dokument-Vorschau</h4>
<div className="bg-gray-50 rounded-lg p-6 border border-gray-200">
{/* Cover preview */}
<div className="text-center mb-6">
<div className="text-2xl font-bold text-purple-700 mb-1">Loeschkonzept</div>
<div className="text-sm text-purple-500 mb-4">gemaess Art. 5/17/30 DSGVO</div>
<div className="text-sm text-gray-600">
{orgHeader.organizationName || <span className="text-gray-400 italic">Organisation nicht angegeben</span>}
</div>
<div className="text-xs text-gray-400 mt-2">
Version {orgHeader.loeschkonzeptVersion} | {new Date().toLocaleDateString('de-DE')}
</div>
</div>
{/* Section list */}
<div className="border-t border-gray-200 pt-4">
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">12 Sektionen</div>
<div className="grid grid-cols-2 gap-1 text-xs text-gray-600">
<div>1. Ziel und Zweck</div>
<div>7. Auftragsverarbeiter</div>
<div>2. Geltungsbereich</div>
<div>8. Legal Hold Verfahren</div>
<div>3. Grundprinzipien</div>
<div>9. Verantwortlichkeiten</div>
<div>4. Loeschregeln-Uebersicht</div>
<div>10. Pruef-/Revisionszyklus</div>
<div>5. Detaillierte Loeschregeln</div>
<div>11. Compliance-Status</div>
<div>6. VVT-Verknuepfung</div>
<div>12. Aenderungshistorie</div>
</div>
</div>
{/* Stats */}
<div className="border-t border-gray-200 pt-4 mt-4 flex gap-6 text-xs text-gray-500">
<span><strong className="text-gray-700">{activePolicies.length}</strong> Loeschregeln</span>
<span><strong className="text-gray-700">{policies.filter(p => p.linkedVVTActivityIds.length > 0).length}</strong> VVT-Verknuepfungen</span>
<span><strong className="text-gray-700">{policies.filter(p => p.linkedVendorIds.length > 0).length}</strong> Vendor-Verknuepfungen</span>
<span><strong className="text-gray-700">{revisions.length}</strong> Revisionen</span>
{complianceResult && (
<span>Compliance-Score: <strong className={complianceResult.score >= 75 ? 'text-green-600' : complianceResult.score >= 50 ? 'text-yellow-600' : 'text-red-600'}>{complianceResult.score}/100</strong></span>
)}
</div>
</div>
</div>
</div>
)
}
// ==========================================================================
// Main render
// ==========================================================================
@@ -2317,6 +2779,7 @@ export default function LoeschfristenPage() {
{tab === 'editor' && renderEditor()}
{tab === 'generator' && renderGenerator()}
{tab === 'export' && renderExport()}
{tab === 'loeschkonzept' && renderLoeschkonzept()}
</div>
)
}

View File

@@ -1,35 +1,20 @@
'use client'
import React, { useState, useEffect, useCallback } from 'react'
import React, { useState, useEffect, useCallback, useMemo } from 'react'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
import TOMControlPanel from '@/components/sdk/obligations/TOMControlPanel'
import GapAnalysisView from '@/components/sdk/obligations/GapAnalysisView'
import { ObligationDocumentTab } from '@/components/sdk/obligations/ObligationDocumentTab'
import { useSDK } from '@/lib/sdk'
import { buildAssessmentPayload } from '@/lib/sdk/scope-to-facts'
import type { ApplicableRegulation } from '@/lib/sdk/compliance-scope-types'
import type { Obligation, ObligationComplianceCheckResult } from '@/lib/sdk/obligations-compliance'
import { runObligationComplianceCheck } from '@/lib/sdk/obligations-compliance'
// =============================================================================
// Types
// Types (local only — Obligation imported from obligations-compliance.ts)
// =============================================================================
interface Obligation {
id: string
title: string
description: string
source: string
source_article: string
deadline: string | null
status: 'pending' | 'in-progress' | 'completed' | 'overdue'
priority: 'critical' | 'high' | 'medium' | 'low'
responsible: string
linked_systems: string[]
assessment_id?: string
rule_code?: string
notes?: string
created_at?: string
updated_at?: string
}
interface ObligationStats {
pending: number
in_progress: number
@@ -50,6 +35,7 @@ interface ObligationFormData {
priority: string
responsible: string
linked_systems: string
linked_vendor_ids: string
notes: string
}
@@ -63,11 +49,26 @@ const EMPTY_FORM: ObligationFormData = {
priority: 'medium',
responsible: '',
linked_systems: '',
linked_vendor_ids: '',
notes: '',
}
const API = '/api/sdk/v1/compliance/obligations'
// =============================================================================
// Tab definitions
// =============================================================================
type Tab = 'uebersicht' | 'editor' | 'profiling' | 'gap-analyse' | 'pflichtenregister'
const TABS: { key: Tab; label: string }[] = [
{ key: 'uebersicht', label: 'Uebersicht' },
{ key: 'editor', label: 'Detail-Editor' },
{ key: 'profiling', label: 'Profiling' },
{ key: 'gap-analyse', label: 'Gap-Analyse' },
{ key: 'pflichtenregister', label: 'Pflichtenregister' },
]
// =============================================================================
// Status helpers
// =============================================================================
@@ -262,6 +263,18 @@ function ObligationModal({
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Verknuepfte Auftragsverarbeiter</label>
<input
type="text"
value={form.linked_vendor_ids}
onChange={e => update('linked_vendor_ids', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500"
placeholder="Kommagetrennt: Vendor-ID-1, Vendor-ID-2"
/>
<p className="text-xs text-gray-400 mt-1">IDs der Auftragsverarbeiter aus dem Vendor Register</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Notizen</label>
<textarea
@@ -365,6 +378,19 @@ function ObligationDetail({ obligation, onClose, onStatusChange, onEdit, onDelet
</div>
)}
{obligation.linked_vendor_ids && obligation.linked_vendor_ids.length > 0 && (
<div>
<span className="text-gray-500">Verknuepfte Auftragsverarbeiter</span>
<div className="flex flex-wrap gap-1 mt-1">
{obligation.linked_vendor_ids.map(id => (
<a key={id} href="/sdk/vendor-compliance" className="px-2 py-0.5 text-xs bg-indigo-50 text-indigo-700 rounded hover:bg-indigo-100 transition-colors">
{id}
</a>
))}
</div>
</div>
)}
{obligation.notes && (
<div>
<span className="text-gray-500">Notizen</span>
@@ -559,9 +585,15 @@ export default function ObligationsPage() {
const [showModal, setShowModal] = useState(false)
const [editObligation, setEditObligation] = useState<Obligation | null>(null)
const [detailObligation, setDetailObligation] = useState<Obligation | null>(null)
const [showGapAnalysis, setShowGapAnalysis] = useState(false)
const [profiling, setProfiling] = useState(false)
const [applicableRegs, setApplicableRegs] = useState<ApplicableRegulation[]>([])
const [activeTab, setActiveTab] = useState<Tab>('uebersicht')
// Compliance check result — auto-computed when obligations change
const complianceResult = useMemo<ObligationComplianceCheckResult | null>(() => {
if (obligations.length === 0) return null
return runObligationComplianceCheck(obligations)
}, [obligations])
const loadData = useCallback(async () => {
setLoading(true)
@@ -613,6 +645,7 @@ export default function ObligationsPage() {
priority: form.priority,
responsible: form.responsible || null,
linked_systems: form.linked_systems ? form.linked_systems.split(',').map(s => s.trim()).filter(Boolean) : [],
linked_vendor_ids: form.linked_vendor_ids ? form.linked_vendor_ids.split(',').map(s => s.trim()).filter(Boolean) : [],
notes: form.notes || null,
}),
})
@@ -634,12 +667,12 @@ export default function ObligationsPage() {
priority: form.priority,
responsible: form.responsible || null,
linked_systems: form.linked_systems ? form.linked_systems.split(',').map(s => s.trim()).filter(Boolean) : [],
linked_vendor_ids: form.linked_vendor_ids ? form.linked_vendor_ids.split(',').map(s => s.trim()).filter(Boolean) : [],
notes: form.notes || null,
}),
})
if (!res.ok) throw new Error('Aktualisierung fehlgeschlagen')
await loadData()
// Refresh detail if open
if (detailObligation?.id === id) {
const updated = await fetch(`${API}/${id}`)
if (updated.ok) setDetailObligation(await updated.json())
@@ -656,7 +689,6 @@ export default function ObligationsPage() {
const updated = await res.json()
setObligations(prev => prev.map(o => o.id === id ? updated : o))
if (detailObligation?.id === id) setDetailObligation(updated)
// Refresh stats
fetch(`${API}/stats`).then(r => r.json()).then(setStats).catch(() => {})
}
@@ -672,7 +704,6 @@ export default function ObligationsPage() {
setProfiling(true)
setError(null)
try {
// Build payload from real CompanyProfile + Scope data
const profile = sdkState.companyProfile
const scopeState = sdkState.complianceScope
const scopeAnswers = scopeState?.answers || []
@@ -682,7 +713,6 @@ export default function ObligationsPage() {
if (profile) {
payload = buildAssessmentPayload(profile, scopeAnswers, scopeDecision) as unknown as Record<string, unknown>
} else {
// Fallback: Minimaldaten wenn kein Profil vorhanden
payload = {
employee_count: 50,
industry: 'technology',
@@ -702,11 +732,9 @@ export default function ObligationsPage() {
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
// Store applicable regulations for the info box
const regs: ApplicableRegulation[] = data.overview?.applicable_regulations || data.applicable_regulations || []
setApplicableRegs(regs)
// Extract obligations from response (can be nested under overview)
const rawObls = data.overview?.obligations || data.obligations || []
if (rawObls.length > 0) {
const autoObls: Obligation[] = rawObls.map((o: Record<string, unknown>) => ({
@@ -738,11 +766,6 @@ export default function ObligationsPage() {
const stepInfo = STEP_EXPLANATIONS['obligations']
const filteredObligations = obligations.filter(o => {
// Status/priority filter
if (filter === 'ai') {
if (!o.source.toLowerCase().includes('ai')) return false
}
// Regulation filter
if (regulationFilter !== 'all') {
const src = o.source?.toLowerCase() || ''
const key = regulationFilter.toLowerCase()
@@ -751,91 +774,12 @@ export default function ObligationsPage() {
return true
})
return (
<div className="space-y-6">
{/* Modals */}
{(showModal || editObligation) && !detailObligation && (
<ObligationModal
initial={editObligation ? {
title: editObligation.title,
description: editObligation.description,
source: editObligation.source,
source_article: editObligation.source_article,
deadline: editObligation.deadline ? editObligation.deadline.slice(0, 10) : '',
status: editObligation.status,
priority: editObligation.priority,
responsible: editObligation.responsible,
linked_systems: editObligation.linked_systems?.join(', ') || '',
notes: editObligation.notes || '',
} : undefined}
onClose={() => { setShowModal(false); setEditObligation(null) }}
onSave={async (form) => {
if (editObligation) {
await handleUpdate(editObligation.id, form)
setEditObligation(null)
} else {
await handleCreate(form)
setShowModal(false)
}
}}
/>
)}
{detailObligation && (
<ObligationDetail
obligation={detailObligation}
onClose={() => setDetailObligation(null)}
onStatusChange={handleStatusChange}
onDelete={handleDelete}
onEdit={() => {
setEditObligation(detailObligation)
setDetailObligation(null)
}}
/>
)}
{/* Header */}
<StepHeader
stepId="obligations"
title={stepInfo?.title || 'Pflichten-Management'}
description={stepInfo?.description || 'DSGVO & AI-Act Compliance-Pflichten verwalten'}
explanation={stepInfo?.explanation || ''}
tips={stepInfo?.tips || []}
>
<div className="flex items-center gap-2">
<button
onClick={handleAutoProfiling}
disabled={profiling}
className="flex items-center gap-2 px-4 py-2 bg-white border border-purple-300 text-purple-700 rounded-lg hover:bg-purple-50 transition-colors text-sm disabled:opacity-50"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
{profiling ? 'Profiling...' : 'Auto-Profiling'}
</button>
<button
onClick={() => setShowGapAnalysis(v => !v)}
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-colors text-sm ${
showGapAnalysis ? 'bg-purple-100 text-purple-700' : 'bg-white border border-gray-300 text-gray-700 hover:bg-gray-50'
}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Gap-Analyse
</button>
<button
onClick={() => setShowModal(true)}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors text-sm"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Pflicht hinzufuegen
</button>
</div>
</StepHeader>
// ---------------------------------------------------------------------------
// Tab Content Renderers
// ---------------------------------------------------------------------------
const renderUebersichtTab = () => (
<>
{/* Error */}
{error && (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3 text-sm text-amber-700">{error}</div>
@@ -872,12 +816,13 @@ export default function ObligationsPage() {
)}
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
{[
{ label: 'Ausstehend', value: stats?.pending ?? 0, color: 'text-gray-600', border: 'border-gray-200' },
{ label: 'In Bearbeitung',value: stats?.in_progress ?? 0, color: 'text-blue-600', border: 'border-blue-200' },
{ label: 'Ueberfaellig', value: stats?.overdue ?? 0, color: 'text-red-600', border: 'border-red-200' },
{ label: 'Abgeschlossen', value: stats?.completed ?? 0, color: 'text-green-600', border: 'border-green-200'},
{ label: 'Compliance-Score', value: complianceResult ? complianceResult.score : '—', color: 'text-purple-600', border: 'border-purple-200'},
].map(s => (
<div key={s.label} className={`bg-white rounded-xl border ${s.border} p-5`}>
<div className={`text-xs ${s.color}`}>{s.label}</div>
@@ -901,9 +846,26 @@ export default function ObligationsPage() {
</div>
)}
{/* Gap Analysis View */}
{showGapAnalysis && (
<GapAnalysisView />
{/* Compliance Issues Summary */}
{complianceResult && complianceResult.issues.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h3 className="text-sm font-semibold text-gray-900 mb-3">Compliance-Befunde ({complianceResult.issues.length})</h3>
<div className="space-y-2">
{complianceResult.issues.map((issue, i) => (
<div key={i} className="flex items-start gap-3 text-sm">
<span className={`px-2 py-0.5 text-xs rounded-full flex-shrink-0 ${
issue.severity === 'CRITICAL' ? 'bg-red-100 text-red-700' :
issue.severity === 'HIGH' ? 'bg-orange-100 text-orange-700' :
issue.severity === 'MEDIUM' ? 'bg-yellow-100 text-yellow-700' :
'bg-gray-100 text-gray-600'
}`}>
{issue.severity === 'CRITICAL' ? 'Kritisch' : issue.severity === 'HIGH' ? 'Hoch' : issue.severity === 'MEDIUM' ? 'Mittel' : 'Niedrig'}
</span>
<span className="text-gray-700">{issue.message}</span>
</div>
))}
</div>
</div>
)}
{/* Regulation Filter Chips */}
@@ -970,7 +932,7 @@ export default function ObligationsPage() {
</div>
<h3 className="text-base font-semibold text-gray-900">Keine Pflichten gefunden</h3>
<p className="mt-2 text-sm text-gray-500">
Klicken Sie auf "Pflicht hinzufuegen", um die erste Compliance-Pflicht zu erfassen.
Klicken Sie auf &quot;Pflicht hinzufuegen&quot;, um die erste Compliance-Pflicht zu erfassen.
</p>
<button
onClick={() => setShowModal(true)}
@@ -982,6 +944,220 @@ export default function ObligationsPage() {
)}
</div>
)}
</>
)
const renderEditorTab = () => (
<>
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-gray-900">Pflichten bearbeiten ({obligations.length})</h3>
<button
onClick={() => setShowModal(true)}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm"
>
Pflicht hinzufuegen
</button>
</div>
{loading && <p className="text-gray-500 text-sm">Lade...</p>}
{!loading && obligations.length === 0 && (
<p className="text-gray-500 text-sm">Noch keine Pflichten vorhanden. Erstellen Sie eine neue Pflicht oder nutzen Sie Auto-Profiling.</p>
)}
{!loading && obligations.length > 0 && (
<div className="space-y-2 max-h-[60vh] overflow-y-auto">
{obligations.map(o => (
<div
key={o.id}
className="flex items-center justify-between p-3 border border-gray-100 rounded-lg hover:bg-gray-50 cursor-pointer"
onClick={() => {
setEditObligation(o)
}}
>
<div className="flex items-center gap-3 min-w-0">
<span className={`px-2 py-0.5 text-xs rounded-full flex-shrink-0 ${STATUS_COLORS[o.status]}`}>
{STATUS_LABELS[o.status]}
</span>
<span className={`px-2 py-0.5 text-xs rounded-full flex-shrink-0 ${PRIORITY_COLORS[o.priority]}`}>
{PRIORITY_LABELS[o.priority]}
</span>
<span className="text-sm text-gray-900 truncate">{o.title}</span>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<span className="text-xs text-gray-400">{o.source}</span>
<button
onClick={(e) => { e.stopPropagation(); setEditObligation(o) }}
className="text-xs text-purple-600 hover:text-purple-700 font-medium"
>
Bearbeiten
</button>
</div>
</div>
))}
</div>
)}
</div>
</>
)
const renderProfilingTab = () => (
<>
{error && (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3 text-sm text-amber-700">{error}</div>
)}
{!sdkState.companyProfile && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 text-sm text-yellow-700">
Kein Unternehmensprofil vorhanden. Auto-Profiling verwendet Beispieldaten.{' '}
<a href="/sdk/company-profile" className="underline font-medium">Profil anlegen </a>
</div>
)}
<div className="bg-white rounded-xl border border-gray-200 p-6 text-center">
<div className="w-12 h-12 mx-auto bg-purple-50 rounded-full flex items-center justify-center mb-3">
<svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<h3 className="text-sm font-semibold text-gray-900">Auto-Profiling</h3>
<p className="text-xs text-gray-500 mt-1 mb-4">
Ermittelt automatisch anwendbare Regulierungen und Pflichten aus dem Unternehmensprofil und Compliance-Scope.
</p>
<button
onClick={handleAutoProfiling}
disabled={profiling}
className="px-5 py-2 bg-purple-600 text-white text-sm rounded-lg hover:bg-purple-700 disabled:opacity-50"
>
{profiling ? 'Profiling laeuft...' : 'Auto-Profiling starten'}
</button>
</div>
{applicableRegs.length > 0 && (
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
<h3 className="text-sm font-semibold text-blue-900 mb-2">Anwendbare Regulierungen</h3>
<div className="flex flex-wrap gap-2">
{applicableRegs.map(reg => (
<span
key={reg.id}
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-white border border-blue-300 text-blue-800"
>
<svg className="w-3.5 h-3.5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
{reg.name}
{reg.classification && <span className="text-blue-500">({reg.classification})</span>}
<span className="text-blue-400">{reg.obligation_count} Pflichten</span>
</span>
))}
</div>
</div>
)}
</>
)
const renderGapAnalyseTab = () => (
<GapAnalysisView />
)
const renderPflichtenregisterTab = () => (
<ObligationDocumentTab
obligations={obligations}
complianceResult={complianceResult}
/>
)
const renderTabContent = () => {
switch (activeTab) {
case 'uebersicht': return renderUebersichtTab()
case 'editor': return renderEditorTab()
case 'profiling': return renderProfilingTab()
case 'gap-analyse': return renderGapAnalyseTab()
case 'pflichtenregister': return renderPflichtenregisterTab()
}
}
return (
<div className="space-y-6">
{/* Modals */}
{(showModal || editObligation) && !detailObligation && (
<ObligationModal
initial={editObligation ? {
title: editObligation.title,
description: editObligation.description,
source: editObligation.source,
source_article: editObligation.source_article,
deadline: editObligation.deadline ? editObligation.deadline.slice(0, 10) : '',
status: editObligation.status,
priority: editObligation.priority,
responsible: editObligation.responsible,
linked_systems: editObligation.linked_systems?.join(', ') || '',
notes: editObligation.notes || '',
} : undefined}
onClose={() => { setShowModal(false); setEditObligation(null) }}
onSave={async (form) => {
if (editObligation) {
await handleUpdate(editObligation.id, form)
setEditObligation(null)
} else {
await handleCreate(form)
setShowModal(false)
}
}}
/>
)}
{detailObligation && (
<ObligationDetail
obligation={detailObligation}
onClose={() => setDetailObligation(null)}
onStatusChange={handleStatusChange}
onDelete={handleDelete}
onEdit={() => {
setEditObligation(detailObligation)
setDetailObligation(null)
}}
/>
)}
{/* Header */}
<StepHeader
stepId="obligations"
title={stepInfo?.title || 'Pflichten-Management'}
description={stepInfo?.description || 'DSGVO & AI-Act Compliance-Pflichten verwalten'}
explanation={stepInfo?.explanation || ''}
tips={stepInfo?.tips || []}
>
<div className="flex items-center gap-2">
<button
onClick={() => setShowModal(true)}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors text-sm"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Pflicht hinzufuegen
</button>
</div>
</StepHeader>
{/* Tab Navigation */}
<div className="flex gap-1 border-b border-gray-200">
{TABS.map(tab => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`px-4 py-2.5 text-sm font-medium transition-colors ${
activeTab === tab.key
? 'border-b-2 border-purple-500 text-purple-700'
: 'text-gray-500 hover:text-gray-700 hover:border-b-2 hover:border-gray-300'
}`}
>
{tab.label}
</button>
))}
</div>
{/* Tab Content */}
{renderTabContent()}
</div>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -142,8 +142,8 @@ export const SDK_FLOW_STEPS: SDKFlowStep[] = [
checkpointId: 'CP-UC',
checkpointType: 'REQUIRED',
checkpointReviewer: 'NONE',
description: 'Systematische Erfassung aller Datenverarbeitungs- und KI-Anwendungsfaelle ueber einen 8-Schritte-Wizard mit Kachel-Auswahl.',
descriptionLong: 'In einem 8-Schritte-Wizard werden alle Use Cases erfasst: (1) Grundlegendes — Titel, Beschreibung, KI-Kategorie (21 Kacheln), Branche wird automatisch aus dem Profil abgeleitet. (2) Datenkategorien — ~60 Kategorien in 10 Gruppen als Kacheln (inkl. Art. 9 hervorgehoben). (3) Verarbeitungszweck — 16 Zweck-Kacheln, Rechtsgrundlage wird vom SDK automatisch ermittelt. (4) Automatisierungsgrad — assistiv/teilautomatisiert/vollautomatisiert. (5) Hosting & Modell — Provider, Region, Modellnutzung (Inferenz/RAG/Fine-Tuning/Training). (6) Datentransfer — Transferziele und Schutzmechanismen. (7) Datenhaltung — Aufbewahrungsfristen. (8) Vertraege — vorhandene Compliance-Dokumente. Die RAG-Collection bp_compliance_ce wird verwendet, um relevante CE-Regulierungen automatisch den Use Cases zuzuordnen (UCCA).',
description: 'Systematische Erfassung aller Datenverarbeitungs- und KI-Anwendungsfaelle ueber einen 8-Schritte-Wizard mit Kachel-Auswahl. Inkl. BetrVG-Mitbestimmungspruefung und Betriebsrats-Konflikt-Score.',
descriptionLong: 'In einem 8-Schritte-Wizard werden alle Use Cases erfasst: (1) Grundlegendes — Titel, Beschreibung, KI-Kategorie (21 Kacheln), Branche wird automatisch aus dem Profil abgeleitet. (2) Datenkategorien — ~60 Kategorien in 10 Gruppen als Kacheln (inkl. Art. 9 hervorgehoben). (3) Verarbeitungszweck — 16 Zweck-Kacheln, Rechtsgrundlage wird vom SDK automatisch ermittelt. (4) Automatisierungsgrad + BetrVG — assistiv/teilautomatisiert/vollautomatisiert, plus 3 BetrVG-Toggles: Ueberwachungseignung, HR-Entscheidungsunterstuetzung, BR-Konsultation. Das SDK berechnet daraus einen Betriebsrats-Konflikt-Score (0-100) und leitet BetrVG-Pflichten ab (§87, §90, §94, §95). (5) Hosting & Modell — Provider, Region, Modellnutzung (Inferenz/RAG/Fine-Tuning/Training). (6) Datentransfer — Transferziele und Schutzmechanismen. (7) Datenhaltung — Aufbewahrungsfristen. (8) Vertraege — vorhandene Compliance-Dokumente. Die RAG-Collection bp_compliance_ce wird verwendet, um relevante CE-Regulierungen automatisch den Use Cases zuzuordnen (UCCA). Die Collection bp_compliance_datenschutz enthaelt 14 BAG-Urteile zu IT-Mitbestimmung (M365, SAP, SaaS, Video).',
legalBasis: 'Art. 30 DSGVO (Verzeichnis von Verarbeitungstaetigkeiten)',
inputs: ['companyProfile'],
outputs: ['useCases'],
@@ -155,6 +155,27 @@ export const SDK_FLOW_STEPS: SDKFlowStep[] = [
isOptional: false,
url: '/sdk/use-cases',
},
{
id: 'ai-registration',
name: 'EU AI Database Registrierung',
nameShort: 'EU-Reg',
package: 'vorbereitung',
seq: 350,
checkpointId: 'CP-REG',
checkpointType: 'CONDITIONAL',
checkpointReviewer: 'NONE',
description: 'Registrierung von Hochrisiko-KI-Systemen in der EU AI Database gemaess Art. 49 KI-Verordnung.',
descriptionLong: 'Fuer Hochrisiko-KI-Systeme (Annex III) ist eine Registrierung in der EU AI Database Pflicht. Ein 6-Schritte-Wizard fuehrt durch den Prozess: (1) Anbieter-Daten — Name, Rechtsform, Adresse, EU-Repraesentant. (2) System-Details — Name, Version, Beschreibung, Einsatzzweck (vorausgefuellt aus UCCA Assessment). (3) Klassifikation — Risikoklasse und Annex III Kategorie (aus Decision Tree). (4) Konformitaet — CE-Kennzeichnung, Notified Body. (5) Trainingsdaten — Zusammenfassung der Datenquellen (Art. 10). (6) Pruefung + Export — JSON-Download fuer EU-Datenbank-Submission. Der Status-Workflow ist: Entwurf → Bereit → Eingereicht → Registriert.',
legalBasis: 'Art. 49 KI-Verordnung (EU) 2024/1689',
inputs: ['useCases', 'companyProfile'],
outputs: ['euRegistration'],
prerequisiteSteps: ['use-case-assessment'],
dbTables: ['ai_system_registrations'],
dbMode: 'read/write',
ragCollections: [],
isOptional: true,
url: '/sdk/ai-registration',
},
{
id: 'import',
name: 'Dokument-Import',
@@ -672,19 +693,19 @@ export const SDK_FLOW_STEPS: SDKFlowStep[] = [
id: 'vendor-compliance',
name: 'Vendor Compliance',
nameShort: 'Vendor',
package: 'betrieb',
seq: 4200,
package: 'dokumentation',
seq: 2500,
checkpointId: 'CP-VEND',
checkpointType: 'REQUIRED',
checkpointReviewer: 'NONE',
description: 'Pruefung und Verwaltung aller Auftragsverarbeiter und Drittanbieter.',
descriptionLong: 'Vendor Compliance verwaltet alle externen Dienstleister, die im Auftrag personenbezogene Daten verarbeiten (Auftragsverarbeiter nach Art. 28 DSGVO). Fuer jeden Vendor wird geprueft: Gibt es einen AVV? Wo werden Daten gespeichert (EU/Drittland)? Welche TOMs hat der Vendor? Gibt es Subunternehmer? Die Pruefung umfasst auch regelmässige Re-Assessments und die Verwaltung von Standardvertragsklauseln (SCCs) fuer Drittlandtransfers.',
description: 'Pruefung und Verwaltung aller Auftragsverarbeiter und Drittanbieter — Cross-Modul-Integration mit VVT, Obligations, TOM und Loeschfristen.',
descriptionLong: 'Vendor Compliance verwaltet alle externen Dienstleister, die im Auftrag personenbezogene Daten verarbeiten (Auftragsverarbeiter nach Art. 28 DSGVO). Fuer jeden Vendor wird geprueft: Gibt es einen AVV? Wo werden Daten gespeichert (EU/Drittland)? Welche TOMs hat der Vendor? Gibt es Subunternehmer? Cross-Modul-Integration: VVT-Processor-Tab liest Vendors mit role=PROCESSOR direkt aus der Vendor-API, Obligations und Loeschfristen verknuepfen Vendors ueber linked_vendor_ids (JSONB), TOM zeigt Vendor-Controls als Querverweis.',
legalBasis: 'Art. 28 DSGVO (Auftragsverarbeiter), Art. 44-49 (Drittlandtransfer)',
inputs: ['modules', 'vvt'],
outputs: ['vendorAssessments'],
prerequisiteSteps: ['escalations'],
dbTables: [],
dbMode: 'none',
outputs: ['vendorAssessments', 'vendorControlInstances'],
prerequisiteSteps: ['vvt'],
dbTables: ['vendor_vendors', 'vendor_contracts', 'vendor_findings', 'vendor_control_instances', 'compliance_templates'],
dbMode: 'read/write',
ragCollections: ['bp_compliance_recht'],
ragPurpose: 'AVV-Vorlagen und Pruefkataloge',
isOptional: false,

View File

@@ -1,18 +1,19 @@
'use client'
import React, { useState, useCallback, useMemo } from 'react'
import React, { useState, useCallback, useMemo, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useSDK } from '@/lib/sdk'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
import { useTOMGenerator } from '@/lib/sdk/tom-generator/context'
import { DerivedTOM } from '@/lib/sdk/tom-generator/types'
import { TOMOverviewTab, TOMEditorTab, TOMGapExportTab } from '@/components/sdk/tom-dashboard'
import { TOMOverviewTab, TOMEditorTab, TOMGapExportTab, TOMDocumentTab } from '@/components/sdk/tom-dashboard'
import { runTOMComplianceCheck, type TOMComplianceCheckResult } from '@/lib/sdk/tom-compliance'
// =============================================================================
// TYPES
// =============================================================================
type Tab = 'uebersicht' | 'editor' | 'generator' | 'gap-export'
type Tab = 'uebersicht' | 'editor' | 'generator' | 'gap-export' | 'tom-dokument'
interface TabDefinition {
key: Tab
@@ -24,6 +25,7 @@ const TABS: TabDefinition[] = [
{ key: 'editor', label: 'Detail-Editor' },
{ key: 'generator', label: 'Generator' },
{ key: 'gap-export', label: 'Gap-Analyse & Export' },
{ key: 'tom-dokument', label: 'TOM-Dokument' },
]
// =============================================================================
@@ -33,7 +35,7 @@ const TABS: TabDefinition[] = [
export default function TOMPage() {
const router = useRouter()
const sdk = useSDK()
const { state, dispatch, bulkUpdateTOMs, runGapAnalysis } = useTOMGenerator()
const { state, dispatch, runGapAnalysis } = useTOMGenerator()
// ---------------------------------------------------------------------------
// Local state
@@ -41,6 +43,58 @@ export default function TOMPage() {
const [tab, setTab] = useState<Tab>('uebersicht')
const [selectedTOMId, setSelectedTOMId] = useState<string | null>(null)
const [complianceResult, setComplianceResult] = useState<TOMComplianceCheckResult | null>(null)
const [vendorControls, setVendorControls] = useState<Array<{
vendorId: string
vendorName: string
controlId: string
controlName: string
domain: string
status: string
lastTestedAt?: string
}>>([])
// ---------------------------------------------------------------------------
// Compliance check (auto-run when derivedTOMs change)
// ---------------------------------------------------------------------------
useEffect(() => {
if (state?.derivedTOMs && Array.isArray(state.derivedTOMs) && state.derivedTOMs.length > 0) {
setComplianceResult(runTOMComplianceCheck(state))
}
}, [state?.derivedTOMs])
// ---------------------------------------------------------------------------
// Vendor controls cross-reference (fetch when overview tab is active)
// ---------------------------------------------------------------------------
useEffect(() => {
if (tab !== 'uebersicht') return
Promise.all([
fetch('/api/sdk/v1/vendor-compliance/control-instances?limit=500').then(r => r.ok ? r.json() : null),
fetch('/api/sdk/v1/vendor-compliance/vendors?limit=500').then(r => r.ok ? r.json() : null),
]).then(([ciData, vendorData]) => {
const instances = ciData?.data?.items || []
const vendors = vendorData?.data?.items || []
const vendorMap = new Map<string, string>()
for (const v of vendors) {
vendorMap.set(v.id, v.name)
}
// Filter for TOM-domain controls
const tomControls = instances
.filter((ci: any) => ci.domain === 'TOM' || ci.controlId?.startsWith('VND-TOM'))
.map((ci: any) => ({
vendorId: ci.vendorId || ci.vendor_id,
vendorName: vendorMap.get(ci.vendorId || ci.vendor_id) || 'Unbekannt',
controlId: ci.controlId || ci.control_id,
controlName: ci.controlName || ci.control_name || ci.controlId || ci.control_id,
domain: ci.domain || 'TOM',
status: ci.status || 'UNKNOWN',
lastTestedAt: ci.lastTestedAt || ci.last_tested_at,
}))
setVendorControls(tomControls)
}).catch(() => {})
}, [tab])
// ---------------------------------------------------------------------------
// Computed / memoised values
@@ -316,6 +370,17 @@ export default function TOMPage() {
/>
)
// ---------------------------------------------------------------------------
// Tab 5 TOM-Dokument
// ---------------------------------------------------------------------------
const renderTOMDokument = () => (
<TOMDocumentTab
state={state}
complianceResult={complianceResult}
/>
)
// ---------------------------------------------------------------------------
// Tab content router
// ---------------------------------------------------------------------------
@@ -330,6 +395,8 @@ export default function TOMPage() {
return renderGenerator()
case 'gap-export':
return renderGapExport()
case 'tom-dokument':
return renderTOMDokument()
default:
return renderUebersicht()
}
@@ -351,6 +418,60 @@ export default function TOMPage() {
{/* Active tab content */}
<div>{renderActiveTab()}</div>
{/* Vendor-Controls cross-reference (only on overview tab) */}
{tab === 'uebersicht' && vendorControls.length > 0 && (
<div className="mt-6 bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-base font-semibold text-gray-900">Auftragsverarbeiter-Controls (Art. 28)</h3>
<p className="text-sm text-gray-500 mt-0.5">TOM-relevante Controls aus dem Vendor Register</p>
</div>
<a href="/sdk/vendor-compliance" className="text-sm text-purple-600 hover:text-purple-700 font-medium">
Zum Vendor Register &rarr;
</a>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-200">
<th className="text-left py-2 px-3 text-xs font-medium text-gray-500 uppercase">Vendor</th>
<th className="text-left py-2 px-3 text-xs font-medium text-gray-500 uppercase">Control</th>
<th className="text-left py-2 px-3 text-xs font-medium text-gray-500 uppercase">Status</th>
<th className="text-left py-2 px-3 text-xs font-medium text-gray-500 uppercase">Letzte Pruefung</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{vendorControls.map((vc, i) => (
<tr key={`${vc.vendorId}-${vc.controlId}-${i}`} className="hover:bg-gray-50">
<td className="py-2.5 px-3 font-medium text-gray-900">{vc.vendorName}</td>
<td className="py-2.5 px-3">
<span className="font-mono text-xs text-gray-500">{vc.controlId}</span>
<span className="ml-2 text-gray-700">{vc.controlName !== vc.controlId ? vc.controlName : ''}</span>
</td>
<td className="py-2.5 px-3">
<span className={`px-2 py-0.5 text-xs rounded-full ${
vc.status === 'PASS' ? 'bg-green-100 text-green-700' :
vc.status === 'PARTIAL' ? 'bg-yellow-100 text-yellow-700' :
vc.status === 'FAIL' ? 'bg-red-100 text-red-700' :
'bg-gray-100 text-gray-600'
}`}>
{vc.status === 'PASS' ? 'Bestanden' :
vc.status === 'PARTIAL' ? 'Teilweise' :
vc.status === 'FAIL' ? 'Nicht bestanden' :
vc.status}
</span>
</td>
<td className="py-2.5 px-3 text-gray-500">
{vc.lastTestedAt ? new Date(vc.lastTestedAt).toLocaleDateString('de-DE') : '\u2014'}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,560 @@
'use client'
import { useEffect, useState, useCallback } from 'react'
import {
getAssignments, getContent, getModuleMedia, getQuiz, submitQuiz,
startAssignment, generateCertificate, listCertificates, downloadCertificatePDF,
getMediaStreamURL, getInteractiveManifest, completeAssignment,
} from '@/lib/sdk/training/api'
import type {
TrainingAssignment, ModuleContent, TrainingMedia, QuizSubmitResponse,
InteractiveVideoManifest,
} from '@/lib/sdk/training/types'
import {
STATUS_LABELS, STATUS_COLORS, REGULATION_LABELS,
} from '@/lib/sdk/training/types'
import InteractiveVideoPlayer from '@/components/training/InteractiveVideoPlayer'
type Tab = 'assignments' | 'content' | 'quiz' | 'certificates'
interface QuizQuestionItem {
id: string
question: string
options: string[]
difficulty: string
}
export default function LearnerPage() {
const [activeTab, setActiveTab] = useState<Tab>('assignments')
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Assignments
const [assignments, setAssignments] = useState<TrainingAssignment[]>([])
// Content
const [selectedAssignment, setSelectedAssignment] = useState<TrainingAssignment | null>(null)
const [content, setContent] = useState<ModuleContent | null>(null)
const [media, setMedia] = useState<TrainingMedia[]>([])
// Quiz
const [questions, setQuestions] = useState<QuizQuestionItem[]>([])
const [answers, setAnswers] = useState<Record<string, number>>({})
const [quizResult, setQuizResult] = useState<QuizSubmitResponse | null>(null)
const [quizSubmitting, setQuizSubmitting] = useState(false)
const [quizTimer, setQuizTimer] = useState(0)
const [quizActive, setQuizActive] = useState(false)
// Certificates
const [certificates, setCertificates] = useState<TrainingAssignment[]>([])
const [certGenerating, setCertGenerating] = useState(false)
// Interactive Video
const [interactiveManifest, setInteractiveManifest] = useState<InteractiveVideoManifest | null>(null)
// User simulation
const [userId] = useState('00000000-0000-0000-0000-000000000001')
const loadAssignments = useCallback(async () => {
setLoading(true)
try {
const data = await getAssignments({ user_id: userId, limit: 100 })
setAssignments(data.assignments || [])
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler beim Laden')
} finally {
setLoading(false)
}
}, [userId])
const loadCertificates = useCallback(async () => {
try {
const data = await listCertificates()
setCertificates(data.certificates || [])
} catch {
// Certificates may not exist yet
}
}, [])
useEffect(() => {
loadAssignments()
loadCertificates()
}, [loadAssignments, loadCertificates])
// Quiz timer
useEffect(() => {
if (!quizActive) return
const interval = setInterval(() => setQuizTimer(t => t + 1), 1000)
return () => clearInterval(interval)
}, [quizActive])
async function loadInteractiveManifest(moduleId: string, assignmentId: string) {
try {
const manifest = await getInteractiveManifest(moduleId, assignmentId)
if (manifest && manifest.checkpoints && manifest.checkpoints.length > 0) {
setInteractiveManifest(manifest)
} else {
setInteractiveManifest(null)
}
} catch {
setInteractiveManifest(null)
}
}
async function handleStartAssignment(assignment: TrainingAssignment) {
try {
await startAssignment(assignment.id)
setSelectedAssignment({ ...assignment, status: 'in_progress' })
// Load content
const [contentData, mediaData] = await Promise.all([
getContent(assignment.module_id).catch(() => null),
getModuleMedia(assignment.module_id).catch(() => ({ media: [] })),
])
setContent(contentData)
setMedia(mediaData.media || [])
await loadInteractiveManifest(assignment.module_id, assignment.id)
setActiveTab('content')
loadAssignments()
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler beim Starten')
}
}
async function handleResumeContent(assignment: TrainingAssignment) {
setSelectedAssignment(assignment)
try {
const [contentData, mediaData] = await Promise.all([
getContent(assignment.module_id).catch(() => null),
getModuleMedia(assignment.module_id).catch(() => ({ media: [] })),
])
setContent(contentData)
setMedia(mediaData.media || [])
await loadInteractiveManifest(assignment.module_id, assignment.id)
setActiveTab('content')
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler beim Laden')
}
}
async function handleAllCheckpointsPassed() {
if (!selectedAssignment) return
try {
await completeAssignment(selectedAssignment.id)
setSelectedAssignment({ ...selectedAssignment, status: 'completed' })
loadAssignments()
} catch {
// Assignment completion may already be handled
}
}
async function handleStartQuiz() {
if (!selectedAssignment) return
try {
const data = await getQuiz(selectedAssignment.module_id)
setQuestions(data.questions || [])
setAnswers({})
setQuizResult(null)
setQuizTimer(0)
setQuizActive(true)
setActiveTab('quiz')
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler beim Quiz-Laden')
}
}
async function handleSubmitQuiz() {
if (!selectedAssignment || questions.length === 0) return
setQuizSubmitting(true)
setQuizActive(false)
try {
const answerList = questions.map(q => ({
question_id: q.id,
selected_index: answers[q.id] ?? -1,
}))
const result = await submitQuiz(selectedAssignment.module_id, {
assignment_id: selectedAssignment.id,
answers: answerList,
duration_seconds: quizTimer,
})
setQuizResult(result)
loadAssignments()
} catch (e) {
setError(e instanceof Error ? e.message : 'Quiz-Abgabe fehlgeschlagen')
} finally {
setQuizSubmitting(false)
}
}
async function handleGenerateCertificate(assignmentId: string) {
setCertGenerating(true)
try {
const data = await generateCertificate(assignmentId)
if (data.certificate_id) {
const blob = await downloadCertificatePDF(data.certificate_id)
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `zertifikat-${data.certificate_id.substring(0, 8)}.pdf`
a.click()
URL.revokeObjectURL(url)
}
loadAssignments()
loadCertificates()
} catch (e) {
setError(e instanceof Error ? e.message : 'Zertifikat-Erstellung fehlgeschlagen')
} finally {
setCertGenerating(false)
}
}
async function handleDownloadPDF(certId: string) {
try {
const blob = await downloadCertificatePDF(certId)
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `zertifikat-${certId.substring(0, 8)}.pdf`
a.click()
URL.revokeObjectURL(url)
} catch (e) {
setError(e instanceof Error ? e.message : 'PDF-Download fehlgeschlagen')
}
}
function simpleMarkdownToHtml(md: string): string {
return md
.replace(/^### (.+)$/gm, '<h3 class="text-lg font-semibold mt-4 mb-2">$1</h3>')
.replace(/^## (.+)$/gm, '<h2 class="text-xl font-bold mt-6 mb-3">$1</h2>')
.replace(/^# (.+)$/gm, '<h1 class="text-2xl font-bold mt-6 mb-3">$1</h1>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/^- (.+)$/gm, '<li class="ml-4 list-disc">$1</li>')
.replace(/^(\d+)\. (.+)$/gm, '<li class="ml-4 list-decimal">$2</li>')
.replace(/\n\n/g, '<br/><br/>')
}
function formatTimer(seconds: number): string {
const m = Math.floor(seconds / 60)
const s = seconds % 60
return `${m}:${s.toString().padStart(2, '0')}`
}
const tabs: { key: Tab; label: string }[] = [
{ key: 'assignments', label: 'Meine Schulungen' },
{ key: 'content', label: 'Schulungsinhalt' },
{ key: 'quiz', label: 'Quiz' },
{ key: 'certificates', label: 'Zertifikate' },
]
return (
<div className="max-w-7xl mx-auto p-6">
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900">Learner Portal</h1>
<p className="text-gray-500 mt-1">Absolvieren Sie Ihre Compliance-Schulungen</p>
</div>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
{error}
<button onClick={() => setError(null)} className="ml-2 text-red-500 hover:text-red-700">x</button>
</div>
)}
{/* Tabs */}
<div className="border-b border-gray-200 mb-6">
<div className="flex gap-6">
{tabs.map(tab => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`pb-3 text-sm font-medium border-b-2 transition-colors ${
activeTab === tab.key
? 'border-indigo-500 text-indigo-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
{tab.label}
</button>
))}
</div>
</div>
{/* Tab: Meine Schulungen */}
{activeTab === 'assignments' && (
<div>
{loading ? (
<div className="text-center py-12 text-gray-400">Lade Schulungen...</div>
) : assignments.length === 0 ? (
<div className="text-center py-12 text-gray-400">Keine Schulungen zugewiesen</div>
) : (
<div className="grid gap-4">
{assignments.map(a => (
<div key={a.id} className="bg-white border border-gray-200 rounded-lg p-5 hover:shadow-sm transition-shadow">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3">
<h3 className="font-semibold text-gray-900">{a.module_title || a.module_code}</h3>
<span className={`px-2 py-0.5 text-xs rounded-full ${STATUS_COLORS[a.status]?.bg || 'bg-gray-100'} ${STATUS_COLORS[a.status]?.text || 'text-gray-700'}`}>
{STATUS_LABELS[a.status] || a.status}
</span>
</div>
<p className="text-sm text-gray-500 mt-1">
Code: {a.module_code} | Deadline: {new Date(a.deadline).toLocaleDateString('de-DE')}
{a.quiz_score != null && ` | Quiz: ${Math.round(a.quiz_score)}%`}
</p>
{/* Progress bar */}
<div className="mt-3 w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all ${a.status === 'completed' ? 'bg-green-500' : 'bg-indigo-500'}`}
style={{ width: `${a.progress_percent}%` }}
/>
</div>
<p className="text-xs text-gray-400 mt-1">{a.progress_percent}% abgeschlossen</p>
</div>
<div className="flex gap-2 ml-4">
{a.status === 'pending' && (
<button
onClick={() => handleStartAssignment(a)}
className="px-3 py-1.5 bg-indigo-600 text-white text-sm rounded-lg hover:bg-indigo-700"
>
Starten
</button>
)}
{a.status === 'in_progress' && (
<button
onClick={() => handleResumeContent(a)}
className="px-3 py-1.5 bg-indigo-600 text-white text-sm rounded-lg hover:bg-indigo-700"
>
Fortsetzen
</button>
)}
{a.status === 'completed' && a.quiz_passed && !a.certificate_id && (
<button
onClick={() => handleGenerateCertificate(a.id)}
disabled={certGenerating}
className="px-3 py-1.5 bg-green-600 text-white text-sm rounded-lg hover:bg-green-700 disabled:opacity-50"
>
{certGenerating ? 'Erstelle...' : 'Zertifikat'}
</button>
)}
{a.certificate_id && (
<button
onClick={() => handleDownloadPDF(a.certificate_id!)}
className="px-3 py-1.5 bg-green-100 text-green-700 text-sm rounded-lg hover:bg-green-200"
>
PDF
</button>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
)}
{/* Tab: Schulungsinhalt */}
{activeTab === 'content' && (
<div>
{!selectedAssignment ? (
<div className="text-center py-12 text-gray-400">
Waehlen Sie eine Schulung aus dem Tab &quot;Meine Schulungen&quot;
</div>
) : (
<div>
<div className="mb-4 flex items-center justify-between">
<h2 className="text-lg font-semibold">{selectedAssignment.module_title}</h2>
<button
onClick={handleStartQuiz}
className="px-4 py-2 bg-indigo-600 text-white text-sm rounded-lg hover:bg-indigo-700"
>
Quiz starten
</button>
</div>
{/* Interactive Video Player */}
{interactiveManifest && selectedAssignment && (
<div className="mb-6">
<div className="flex items-center gap-2 mb-3">
<p className="text-sm font-medium text-gray-700">Interaktive Video-Schulung</p>
<span className="px-2 py-0.5 text-xs bg-purple-100 text-purple-700 rounded-full">Interaktiv</span>
</div>
<InteractiveVideoPlayer
manifest={interactiveManifest}
assignmentId={selectedAssignment.id}
onAllCheckpointsPassed={handleAllCheckpointsPassed}
/>
</div>
)}
{/* Media players (standard audio/video) */}
{media.length > 0 && (
<div className="mb-6 grid gap-4 md:grid-cols-2">
{media.filter(m => m.media_type === 'audio' && m.status === 'completed').map(m => (
<div key={m.id} className="bg-gray-50 p-4 rounded-lg">
<p className="text-sm font-medium text-gray-700 mb-2">Audio-Schulung</p>
<audio controls className="w-full" src={getMediaStreamURL(m.id)}>
Ihr Browser unterstuetzt kein Audio.
</audio>
</div>
))}
{media.filter(m => m.media_type === 'video' && m.status === 'completed' && m.generated_by !== 'tts_ffmpeg_interactive').map(m => (
<div key={m.id} className="bg-gray-50 p-4 rounded-lg">
<p className="text-sm font-medium text-gray-700 mb-2">Video-Schulung</p>
<video controls className="w-full rounded" src={getMediaStreamURL(m.id)}>
Ihr Browser unterstuetzt kein Video.
</video>
</div>
))}
</div>
)}
{/* Content body */}
{content ? (
<div className="bg-white border border-gray-200 rounded-lg p-6">
<div
className="prose max-w-none text-gray-800"
dangerouslySetInnerHTML={{ __html: simpleMarkdownToHtml(content.content_body) }}
/>
</div>
) : (
<div className="text-center py-8 text-gray-400">Kein Schulungsinhalt verfuegbar</div>
)}
</div>
)}
</div>
)}
{/* Tab: Quiz */}
{activeTab === 'quiz' && (
<div>
{questions.length === 0 ? (
<div className="text-center py-12 text-gray-400">
Starten Sie ein Quiz aus dem Schulungsinhalt-Tab
</div>
) : quizResult ? (
/* Quiz Results */
<div className="max-w-lg mx-auto">
<div className={`text-center p-8 rounded-lg border-2 ${quizResult.passed ? 'border-green-300 bg-green-50' : 'border-red-300 bg-red-50'}`}>
<div className="text-4xl mb-3">{quizResult.passed ? '\u2705' : '\u274C'}</div>
<h2 className="text-2xl font-bold mb-2">
{quizResult.passed ? 'Bestanden!' : 'Nicht bestanden'}
</h2>
<p className="text-lg text-gray-700">
{quizResult.correct_count} von {quizResult.total_count} richtig ({Math.round(quizResult.score)}%)
</p>
<p className="text-sm text-gray-500 mt-1">
Bestehensgrenze: {quizResult.threshold}% | Zeit: {formatTimer(quizTimer)}
</p>
{quizResult.passed && selectedAssignment && !selectedAssignment.certificate_id && (
<button
onClick={() => handleGenerateCertificate(selectedAssignment.id)}
disabled={certGenerating}
className="mt-4 px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50"
>
{certGenerating ? 'Erstelle Zertifikat...' : 'Zertifikat generieren & herunterladen'}
</button>
)}
{!quizResult.passed && (
<button
onClick={handleStartQuiz}
className="mt-4 px-6 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
Quiz erneut versuchen
</button>
)}
</div>
</div>
) : (
/* Quiz Questions */
<div>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold">Quiz {selectedAssignment?.module_title}</h2>
<span className="text-sm text-gray-500 font-mono bg-gray-100 px-3 py-1 rounded">
{formatTimer(quizTimer)}
</span>
</div>
<div className="space-y-6">
{questions.map((q, idx) => (
<div key={q.id} className="bg-white border border-gray-200 rounded-lg p-5">
<p className="font-medium text-gray-900 mb-3">
<span className="text-indigo-600 mr-2">Frage {idx + 1}.</span>
{q.question}
</p>
<div className="space-y-2">
{q.options.map((opt, oi) => (
<label
key={oi}
className={`flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${
answers[q.id] === oi
? 'border-indigo-500 bg-indigo-50'
: 'border-gray-200 hover:bg-gray-50'
}`}
>
<input
type="radio"
name={q.id}
checked={answers[q.id] === oi}
onChange={() => setAnswers(prev => ({ ...prev, [q.id]: oi }))}
className="text-indigo-600"
/>
<span className="text-sm text-gray-700">{opt}</span>
</label>
))}
</div>
</div>
))}
</div>
<div className="mt-6 flex justify-end">
<button
onClick={handleSubmitQuiz}
disabled={quizSubmitting || Object.keys(answers).length < questions.length}
className="px-6 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50"
>
{quizSubmitting ? 'Wird ausgewertet...' : `Quiz abgeben (${Object.keys(answers).length}/${questions.length})`}
</button>
</div>
</div>
)}
</div>
)}
{/* Tab: Zertifikate */}
{activeTab === 'certificates' && (
<div>
{certificates.length === 0 ? (
<div className="text-center py-12 text-gray-400">
Noch keine Zertifikate vorhanden. Schliessen Sie eine Schulung mit Quiz ab.
</div>
) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{certificates.map(cert => (
<div key={cert.id} className="bg-white border border-gray-200 rounded-lg p-5">
<div className="flex items-start justify-between mb-3">
<h3 className="font-semibold text-gray-900 text-sm">{cert.module_title}</h3>
<span className="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded-full">Bestanden</span>
</div>
<div className="text-xs text-gray-500 space-y-1">
<p>Mitarbeiter: {cert.user_name}</p>
<p>Abschluss: {cert.completed_at ? new Date(cert.completed_at).toLocaleDateString('de-DE') : '-'}</p>
{cert.quiz_score != null && <p>Ergebnis: {Math.round(cert.quiz_score)}%</p>}
<p className="font-mono text-[10px] text-gray-400">ID: {cert.certificate_id?.substring(0, 12)}</p>
</div>
{cert.certificate_id && (
<button
onClick={() => handleDownloadPDF(cert.certificate_id!)}
className="mt-3 w-full px-3 py-1.5 bg-indigo-600 text-white text-sm rounded-lg hover:bg-indigo-700"
>
PDF herunterladen
</button>
)}
</div>
))}
</div>
)}
</div>
)}
</div>
)
}

View File

@@ -9,14 +9,18 @@ import {
createModule, updateModule, deleteModule,
deleteMatrixEntry, setMatrixEntry,
startAssignment, completeAssignment, updateAssignment,
listBlockConfigs, createBlockConfig, deleteBlockConfig,
previewBlock, generateBlock, getCanonicalMeta,
generateInteractiveVideo,
} from '@/lib/sdk/training/api'
import type {
TrainingModule, TrainingAssignment,
MatrixResponse, TrainingStats, DeadlineInfo, AuditLogEntry, ModuleContent, TrainingMedia,
TrainingBlockConfig, CanonicalControlMeta, BlockPreview, BlockGenerateResult,
} from '@/lib/sdk/training/types'
import {
REGULATION_LABELS, REGULATION_COLORS, FREQUENCY_LABELS,
STATUS_LABELS, STATUS_COLORS, ROLE_LABELS, ALL_ROLES,
STATUS_LABELS, STATUS_COLORS, ROLE_LABELS, ALL_ROLES, TARGET_AUDIENCE_LABELS,
} from '@/lib/sdk/training/types'
import AudioPlayer from '@/components/training/AudioPlayer'
import VideoPlayer from '@/components/training/VideoPlayer'
@@ -41,6 +45,7 @@ export default function TrainingPage() {
const [bulkGenerating, setBulkGenerating] = useState(false)
const [bulkResult, setBulkResult] = useState<{ generated: number; skipped: number; errors: string[] } | null>(null)
const [moduleMedia, setModuleMedia] = useState<TrainingMedia[]>([])
const [interactiveGenerating, setInteractiveGenerating] = useState(false)
const [statusFilter, setStatusFilter] = useState<string>('')
const [regulationFilter, setRegulationFilter] = useState<string>('')
@@ -52,6 +57,15 @@ export default function TrainingPage() {
const [selectedAssignment, setSelectedAssignment] = useState<TrainingAssignment | null>(null)
const [escalationResult, setEscalationResult] = useState<{ total_checked: number; escalated: number } | null>(null)
// Block (Controls → Module) state
const [blocks, setBlocks] = useState<TrainingBlockConfig[]>([])
const [canonicalMeta, setCanonicalMeta] = useState<CanonicalControlMeta | null>(null)
const [showBlockCreate, setShowBlockCreate] = useState(false)
const [blockPreview, setBlockPreview] = useState<BlockPreview | null>(null)
const [blockPreviewId, setBlockPreviewId] = useState<string>('')
const [blockGenerating, setBlockGenerating] = useState(false)
const [blockResult, setBlockResult] = useState<BlockGenerateResult | null>(null)
useEffect(() => {
loadData()
}, [])
@@ -66,13 +80,15 @@ export default function TrainingPage() {
setLoading(true)
setError(null)
try {
const [statsRes, modulesRes, matrixRes, assignmentsRes, deadlinesRes, auditRes] = await Promise.allSettled([
const [statsRes, modulesRes, matrixRes, assignmentsRes, deadlinesRes, auditRes, blocksRes, metaRes] = await Promise.allSettled([
getStats(),
getModules(),
getMatrix(),
getAssignments({ limit: 50 }),
getDeadlines(10),
getAuditLog({ limit: 30 }),
listBlockConfigs(),
getCanonicalMeta(),
])
if (statsRes.status === 'fulfilled') setStats(statsRes.value)
@@ -81,6 +97,8 @@ export default function TrainingPage() {
if (assignmentsRes.status === 'fulfilled') setAssignments(assignmentsRes.value.assignments)
if (deadlinesRes.status === 'fulfilled') setDeadlines(deadlinesRes.value.deadlines)
if (auditRes.status === 'fulfilled') setAuditLog(auditRes.value.entries)
if (blocksRes.status === 'fulfilled') setBlocks(blocksRes.value.blocks)
if (metaRes.status === 'fulfilled') setCanonicalMeta(metaRes.value)
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler beim Laden')
} finally {
@@ -114,6 +132,19 @@ export default function TrainingPage() {
}
}
async function handleGenerateInteractiveVideo() {
if (!selectedModuleId) return
setInteractiveGenerating(true)
try {
await generateInteractiveVideo(selectedModuleId)
await loadModuleMedia(selectedModuleId)
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler bei der interaktiven Video-Generierung')
} finally {
setInteractiveGenerating(false)
}
}
async function handlePublishContent(contentId: string) {
try {
await publishContent(contentId)
@@ -190,6 +221,59 @@ export default function TrainingPage() {
}
}
// Block handlers
async function handleCreateBlock(data: {
name: string; description?: string; domain_filter?: string; category_filter?: string;
severity_filter?: string; target_audience_filter?: string; regulation_area: string;
module_code_prefix: string; max_controls_per_module?: number;
}) {
try {
await createBlockConfig(data)
setShowBlockCreate(false)
const res = await listBlockConfigs()
setBlocks(res.blocks)
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler beim Erstellen')
}
}
async function handleDeleteBlock(id: string) {
if (!confirm('Block-Konfiguration wirklich loeschen?')) return
try {
await deleteBlockConfig(id)
const res = await listBlockConfigs()
setBlocks(res.blocks)
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler beim Loeschen')
}
}
async function handlePreviewBlock(id: string) {
setBlockPreviewId(id)
setBlockPreview(null)
setBlockResult(null)
try {
const preview = await previewBlock(id)
setBlockPreview(preview)
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler beim Preview')
}
}
async function handleGenerateBlock(id: string) {
setBlockGenerating(true)
setBlockResult(null)
try {
const result = await generateBlock(id, { language: 'de', auto_matrix: true })
setBlockResult(result)
await loadData()
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler bei der Block-Generierung')
} finally {
setBlockGenerating(false)
}
}
const tabs: { id: Tab; label: string }[] = [
{ id: 'overview', label: 'Uebersicht' },
{ id: 'modules', label: 'Modulkatalog' },
@@ -521,6 +605,228 @@ export default function TrainingPage() {
{activeTab === 'content' && (
<div className="space-y-6">
{/* Training Blocks — Controls → Schulungsmodule */}
<div className="bg-white border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<div>
<h3 className="text-sm font-medium text-gray-700">Schulungsbloecke aus Controls</h3>
<p className="text-xs text-gray-500">
Canonical Controls nach Kriterien filtern und automatisch Schulungsmodule generieren
{canonicalMeta && <span className="ml-2 text-gray-400">({canonicalMeta.total} Controls verfuegbar)</span>}
</p>
</div>
<button
onClick={() => setShowBlockCreate(true)}
className="px-3 py-1.5 text-xs bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
+ Neuen Block erstellen
</button>
</div>
{/* Block list */}
{blocks.length > 0 ? (
<div className="border rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-gray-50">
<tr>
<th className="px-3 py-2 text-left font-medium text-gray-600">Name</th>
<th className="px-3 py-2 text-left font-medium text-gray-600">Domain</th>
<th className="px-3 py-2 text-left font-medium text-gray-600">Zielgruppe</th>
<th className="px-3 py-2 text-left font-medium text-gray-600">Severity</th>
<th className="px-3 py-2 text-left font-medium text-gray-600">Prefix</th>
<th className="px-3 py-2 text-left font-medium text-gray-600">Letzte Generierung</th>
<th className="px-3 py-2 text-right font-medium text-gray-600">Aktionen</th>
</tr>
</thead>
<tbody className="divide-y">
{blocks.map(block => (
<tr key={block.id} className="hover:bg-gray-50">
<td className="px-3 py-2">
<div className="font-medium text-gray-900">{block.name}</div>
{block.description && <div className="text-xs text-gray-500">{block.description}</div>}
</td>
<td className="px-3 py-2 text-gray-600">{block.domain_filter || 'Alle'}</td>
<td className="px-3 py-2 text-gray-600">{block.target_audience_filter ? (TARGET_AUDIENCE_LABELS[block.target_audience_filter] || block.target_audience_filter) : 'Alle'}</td>
<td className="px-3 py-2 text-gray-600">{block.severity_filter || 'Alle'}</td>
<td className="px-3 py-2"><code className="text-xs bg-gray-100 px-1.5 py-0.5 rounded">{block.module_code_prefix}</code></td>
<td className="px-3 py-2 text-gray-500 text-xs">{block.last_generated_at ? new Date(block.last_generated_at).toLocaleString('de-DE') : 'Noch nie'}</td>
<td className="px-3 py-2 text-right">
<div className="flex gap-1 justify-end">
<button
onClick={() => handlePreviewBlock(block.id)}
className="px-2 py-1 text-xs bg-gray-100 text-gray-700 rounded hover:bg-gray-200"
>
Preview
</button>
<button
onClick={() => handleGenerateBlock(block.id)}
disabled={blockGenerating}
className="px-2 py-1 text-xs bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50"
>
{blockGenerating ? 'Generiert...' : 'Generieren'}
</button>
<button
onClick={() => handleDeleteBlock(block.id)}
className="px-2 py-1 text-xs bg-red-100 text-red-700 rounded hover:bg-red-200"
>
Loeschen
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="text-center py-8 text-gray-500 text-sm">
Noch keine Schulungsbloecke konfiguriert. Erstelle einen Block, um Controls automatisch in Module umzuwandeln.
</div>
)}
{/* Preview result */}
{blockPreview && blockPreviewId && (
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h4 className="text-sm font-medium text-blue-800 mb-2">Preview: {blocks.find(b => b.id === blockPreviewId)?.name}</h4>
<div className="flex gap-6 text-sm mb-3">
<span className="text-blue-700">Controls: <strong>{blockPreview.control_count}</strong></span>
<span className="text-blue-700">Module: <strong>{blockPreview.module_count}</strong></span>
<span className="text-blue-700">Rollen: <strong>{blockPreview.proposed_roles.map(r => ROLE_LABELS[r] || r).join(', ')}</strong></span>
</div>
{blockPreview.controls.length > 0 && (
<details className="text-xs">
<summary className="cursor-pointer text-blue-600 hover:text-blue-800">Passende Controls anzeigen ({blockPreview.control_count})</summary>
<div className="mt-2 max-h-48 overflow-y-auto">
{blockPreview.controls.slice(0, 50).map(ctrl => (
<div key={ctrl.control_id} className="flex gap-2 py-1 border-b border-blue-100">
<code className="text-xs bg-blue-100 px-1 rounded shrink-0">{ctrl.control_id}</code>
<span className="text-gray-700 truncate">{ctrl.title}</span>
<span className={`text-xs px-1.5 rounded shrink-0 ${ctrl.severity === 'critical' ? 'bg-red-100 text-red-700' : ctrl.severity === 'high' ? 'bg-orange-100 text-orange-700' : 'bg-gray-100 text-gray-600'}`}>{ctrl.severity}</span>
</div>
))}
{blockPreview.control_count > 50 && <div className="text-gray-500 py-1">... und {blockPreview.control_count - 50} weitere</div>}
</div>
</details>
)}
</div>
)}
{/* Generate result */}
{blockResult && (
<div className="mt-4 p-4 bg-green-50 border border-green-200 rounded-lg">
<h4 className="text-sm font-medium text-green-800 mb-2">Generierung abgeschlossen</h4>
<div className="flex gap-6 text-sm">
<span className="text-green-700">Module erstellt: <strong>{blockResult.modules_created}</strong></span>
<span className="text-green-700">Controls verknuepft: <strong>{blockResult.controls_linked}</strong></span>
<span className="text-green-700">Matrix-Eintraege: <strong>{blockResult.matrix_entries_created}</strong></span>
<span className="text-green-700">Content generiert: <strong>{blockResult.content_generated}</strong></span>
</div>
{blockResult.errors && blockResult.errors.length > 0 && (
<div className="mt-2 text-xs text-red-600">
{blockResult.errors.map((err, i) => <div key={i}>{err}</div>)}
</div>
)}
</div>
)}
</div>
{/* Block Create Modal */}
{showBlockCreate && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg p-6">
<h3 className="text-lg font-semibold mb-4">Neuen Schulungsblock erstellen</h3>
<form onSubmit={e => {
e.preventDefault()
const fd = new FormData(e.currentTarget)
handleCreateBlock({
name: fd.get('name') as string,
description: fd.get('description') as string || undefined,
domain_filter: fd.get('domain_filter') as string || undefined,
category_filter: fd.get('category_filter') as string || undefined,
severity_filter: fd.get('severity_filter') as string || undefined,
target_audience_filter: fd.get('target_audience_filter') as string || undefined,
regulation_area: fd.get('regulation_area') as string,
module_code_prefix: fd.get('module_code_prefix') as string,
max_controls_per_module: parseInt(fd.get('max_controls_per_module') as string) || 20,
})
}} className="space-y-3">
<div>
<label className="text-xs text-gray-600 block mb-1">Name *</label>
<input name="name" required className="w-full px-3 py-2 text-sm border rounded-lg" placeholder="z.B. Authentifizierung fuer Geschaeftsfuehrung" />
</div>
<div>
<label className="text-xs text-gray-600 block mb-1">Beschreibung</label>
<textarea name="description" className="w-full px-3 py-2 text-sm border rounded-lg" rows={2} />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs text-gray-600 block mb-1">Domain-Filter</label>
<select name="domain_filter" className="w-full px-3 py-2 text-sm border rounded-lg bg-white">
<option value="">Alle Domains</option>
{canonicalMeta?.domains.map(d => (
<option key={d.domain} value={d.domain}>{d.domain} ({d.count})</option>
))}
</select>
</div>
<div>
<label className="text-xs text-gray-600 block mb-1">Kategorie-Filter</label>
<select name="category_filter" className="w-full px-3 py-2 text-sm border rounded-lg bg-white">
<option value="">Alle Kategorien</option>
{canonicalMeta?.categories.filter(c => c.category !== 'uncategorized').map(c => (
<option key={c.category} value={c.category}>{c.category} ({c.count})</option>
))}
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs text-gray-600 block mb-1">Zielgruppe</label>
<select name="target_audience_filter" className="w-full px-3 py-2 text-sm border rounded-lg bg-white">
<option value="">Alle Zielgruppen</option>
{canonicalMeta?.audiences.filter(a => a.audience !== 'unset').map(a => (
<option key={a.audience} value={a.audience}>{TARGET_AUDIENCE_LABELS[a.audience] || a.audience} ({a.count})</option>
))}
</select>
</div>
<div>
<label className="text-xs text-gray-600 block mb-1">Severity</label>
<select name="severity_filter" className="w-full px-3 py-2 text-sm border rounded-lg bg-white">
<option value="">Alle</option>
<option value="critical">Critical</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs text-gray-600 block mb-1">Regulierungsbereich *</label>
<select name="regulation_area" required className="w-full px-3 py-2 text-sm border rounded-lg bg-white">
{Object.entries(REGULATION_LABELS).map(([k, v]) => (
<option key={k} value={k}>{v}</option>
))}
</select>
</div>
<div>
<label className="text-xs text-gray-600 block mb-1">Modul-Code-Prefix *</label>
<input name="module_code_prefix" required className="w-full px-3 py-2 text-sm border rounded-lg" placeholder="z.B. CB-AUTH" />
</div>
</div>
<div>
<label className="text-xs text-gray-600 block mb-1">Max. Controls pro Modul</label>
<input name="max_controls_per_module" type="number" defaultValue={20} min={1} max={50} className="w-full px-3 py-2 text-sm border rounded-lg" />
</div>
<div className="flex gap-3 pt-2">
<button type="submit" className="px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700">Erstellen</button>
<button type="button" onClick={() => setShowBlockCreate(false)} className="px-4 py-2 text-sm bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200">Abbrechen</button>
</div>
</form>
</div>
</div>
)}
{/* Bulk Generation */}
<div className="bg-white border rounded-lg p-4">
<h3 className="text-sm font-medium text-gray-700 mb-3">Bulk-Generierung</h3>
@@ -620,6 +926,35 @@ export default function TrainingPage() {
/>
)}
{/* Interactive Video */}
{selectedModuleId && generatedContent?.is_published && (
<div className="bg-white border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<div>
<h3 className="text-sm font-medium text-gray-700">Interaktives Video</h3>
<p className="text-xs text-gray-500">Video mit Narrator-Persona und Checkpoint-Quizzes</p>
</div>
{moduleMedia.some(m => m.media_type === 'interactive_video' && m.status === 'completed') ? (
<span className="px-3 py-1.5 text-xs bg-purple-100 text-purple-700 rounded-full">Interaktiv erstellt</span>
) : (
<button
onClick={handleGenerateInteractiveVideo}
disabled={interactiveGenerating}
className="px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
>
{interactiveGenerating ? 'Generiere interaktives Video...' : 'Interaktives Video generieren'}
</button>
)}
</div>
{moduleMedia.filter(m => m.media_type === 'interactive_video' && m.status === 'completed').map(m => (
<div key={m.id} className="text-xs text-gray-500 space-y-1 bg-gray-50 rounded p-3">
<p>Dauer: {Math.round(m.duration_seconds / 60)} Min | Groesse: {(m.file_size_bytes / 1024 / 1024).toFixed(1)} MB</p>
<p>Generiert: {new Date(m.created_at).toLocaleString('de-DE')}</p>
</div>
))}
</div>
)}
{/* Script Preview */}
{selectedModuleId && generatedContent?.is_published && (
<ScriptPreview moduleId={selectedModuleId} />

View File

@@ -57,6 +57,8 @@ interface FullAssessment {
dsfa_recommended: boolean
art22_risk: boolean
training_allowed: string
betrvg_conflict_score?: number
betrvg_consultation_required?: boolean
triggered_rules?: TriggeredRule[]
required_controls?: RequiredControl[]
recommended_architecture?: PatternRecommendation[]
@@ -167,6 +169,8 @@ export default function AssessmentDetailPage() {
dsfa_recommended: assessment.dsfa_recommended,
art22_risk: assessment.art22_risk,
training_allowed: assessment.training_allowed,
betrvg_conflict_score: assessment.betrvg_conflict_score,
betrvg_consultation_required: assessment.betrvg_consultation_required,
// AssessmentResultCard expects rule_code; backend stores code — map here
triggered_rules: assessment.triggered_rules?.map(r => ({
rule_code: r.code,

View File

@@ -10,6 +10,8 @@ interface Assessment {
feasibility: string
risk_level: string
risk_score: number
betrvg_conflict_score?: number
betrvg_consultation_required?: boolean
domain: string
created_at: string
}
@@ -194,6 +196,16 @@ export default function UseCasesPage() {
<span className={`px-2 py-0.5 text-xs rounded-full ${feasibility.bg} ${feasibility.text}`}>
{feasibility.label}
</span>
{assessment.betrvg_conflict_score != null && assessment.betrvg_conflict_score > 0 && (
<span className={`px-2 py-0.5 text-xs rounded-full ${
assessment.betrvg_conflict_score >= 75 ? 'bg-red-100 text-red-700' :
assessment.betrvg_conflict_score >= 50 ? 'bg-orange-100 text-orange-700' :
assessment.betrvg_conflict_score >= 25 ? 'bg-yellow-100 text-yellow-700' :
'bg-green-100 text-green-700'
}`}>
BR {assessment.betrvg_conflict_score}
</span>
)}
</div>
<div className="flex items-center gap-4 text-sm text-gray-500">
<span>{assessment.domain}</span>

File diff suppressed because it is too large Load Diff

View File

@@ -1128,24 +1128,88 @@ export default function WhistleblowerPage() {
</div>
)}
{/* Info Box about HinSchG Deadlines (Overview Tab) */}
{/* Info Box about HinSchG (Overview Tab) */}
{activeTab === 'overview' && (
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
<div className="flex items-start gap-3">
<svg className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h4 className="font-medium text-blue-800">HinSchG-Fristen</h4>
<p className="text-sm text-blue-600 mt-1">
Nach dem Hinweisgeberschutzgesetz (HinSchG) gelten folgende Fristen:
Die Eingangsbestaetigung muss innerhalb von <strong>7 Tagen</strong> an den
Hinweisgeber versendet werden (ss 17 Abs. 1 S. 2).
Eine Rueckmeldung ueber ergriffene Massnahmen muss innerhalb von <strong>3 Monaten</strong> nach
Eingangsbestaetigung erfolgen (ss 17 Abs. 2).
Der Schutz des Hinweisgebers vor Repressalien ist zwingend sicherzustellen (ss 36).
<div className="space-y-4">
{/* Gesetzliche Grundlage */}
<div className="bg-blue-50 border border-blue-200 rounded-xl p-5">
<div className="flex items-start gap-3">
<svg className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h4 className="font-medium text-blue-800">Gesetzliche Grundlage: Hinweisgeberschutzgesetz (HinSchG)</h4>
<p className="text-sm text-blue-600 mt-1">
Das HinSchG setzt die <strong>EU-Whistleblowing-Richtlinie (2019/1937)</strong> in deutsches Recht um
und ist seit dem <strong>2. Juli 2023</strong> in Kraft. Seit dem <strong>17. Dezember 2023</strong> gilt
die Pflicht zur Einrichtung einer internen Meldestelle auch fuer Unternehmen ab 50 Beschaeftigten (ss 12 HinSchG).
</p>
</div>
</div>
</div>
{/* Fristen & Pflichten */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-white border border-gray-200 rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<svg className="w-4 h-4 text-orange-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h5 className="text-sm font-semibold text-gray-900">7-Tage-Frist</h5>
</div>
<p className="text-xs text-gray-600">
Eingangsbestaetigung an den Hinweisgeber innerhalb von 7 Tagen nach Meldungseingang (ss 17 Abs. 1 S. 2 HinSchG).
</p>
</div>
<div className="bg-white border border-gray-200 rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<svg className="w-4 h-4 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<h5 className="text-sm font-semibold text-gray-900">3-Monate-Frist</h5>
</div>
<p className="text-xs text-gray-600">
Rueckmeldung ueber ergriffene Folgemaßnahmen innerhalb von 3 Monaten nach Eingangsbestaetigung (ss 17 Abs. 2 HinSchG).
</p>
</div>
<div className="bg-white border border-gray-200 rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<svg className="w-4 h-4 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<h5 className="text-sm font-semibold text-gray-900">3 Jahre Aufbewahrung</h5>
</div>
<p className="text-xs text-gray-600">
Dokumentation der Meldungen und Folgemaßnahmen ist 3 Jahre nach Abschluss aufzubewahren (ss 11 Abs. 5 HinSchG).
</p>
</div>
</div>
{/* Sachlicher Anwendungsbereich & Schutz */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
<h5 className="text-sm font-semibold text-amber-800 mb-2">Sachlicher Anwendungsbereich (ss 2 HinSchG)</h5>
<ul className="text-xs text-amber-700 space-y-1">
<li>Verstoesse gegen Strafvorschriften (StGB, Nebenstrafrecht)</li>
<li>Verstoesse gegen Datenschutzrecht (DSGVO, BDSG)</li>
<li>Geldwaesche und Terrorismusfinanzierung (GwG)</li>
<li>Produktsicherheit und Verbraucherschutz</li>
<li>Umweltschutz und Lebensmittelsicherheit</li>
<li>Arbeitsschutz und Arbeitnehmerrechte</li>
<li>Wettbewerbs- und Kartellrecht</li>
<li>Steuer- und Abgabenrecht (bei Unternehmen)</li>
</ul>
</div>
<div className="bg-green-50 border border-green-200 rounded-xl p-4">
<h5 className="text-sm font-semibold text-green-800 mb-2">Schutz des Hinweisgebers (ss 3637 HinSchG)</h5>
<ul className="text-xs text-green-700 space-y-1">
<li><strong>Repressalienverbot:</strong> Jede Benachteiligung ist untersagt (ss 36)</li>
<li><strong>Beweislastumkehr:</strong> Arbeitgeber muss beweisen, dass Maßnahmen nicht mit Meldung zusammenhaengen</li>
<li><strong>Schadensersatz:</strong> Bei Verstoessen gegen Repressalienverbot (ss 37)</li>
<li><strong>Vertraulichkeit:</strong> Identitaet darf nur bei Zustimmung oder gesetzlicher Pflicht offengelegt werden (ss 8)</li>
<li><strong>Bussgelder:</strong> Bis zu 50.000 EUR bei Verstoessen gegen die Einrichtungspflicht (ss 40)</li>
</ul>
</div>
</div>
</div>
)}

View File

@@ -553,6 +553,32 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP
Zusatzmodule
</div>
)}
<AdditionalModuleItem
href="/sdk/training"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
}
label="Schulung (Admin)"
isActive={pathname === '/sdk/training'}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/training/learner"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
}
label="Schulung (Learner)"
isActive={pathname === '/sdk/training/learner'}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/rag"
icon={
@@ -617,6 +643,19 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/assertions"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
</svg>
}
label="Assertions"
isActive={pathname === '/sdk/assertions'}
collapsed={collapsed}
projectId={projectId}
/>
<AdditionalModuleItem
href="/sdk/dsms"
icon={

View File

@@ -545,8 +545,8 @@ export const STEP_EXPLANATIONS = {
},
'tom': {
title: 'Technische und Organisatorische Massnahmen',
description: 'Dokumentieren Sie Ihre TOMs nach Art. 32 DSGVO',
explanation: 'TOMs sind konkrete Sicherheitsmassnahmen zum Schutz personenbezogener Daten. Das Dashboard zeigt den Status aller aus dem TOM Generator abgeleiteten Massnahmen mit SDM-Mapping und Gap-Analyse.',
description: 'TOMs nach Art. 32 DSGVO mit Vendor-Controls-Querverweis',
explanation: 'TOMs sind konkrete Sicherheitsmassnahmen zum Schutz personenbezogener Daten. Das Dashboard zeigt den Status aller aus dem TOM Generator abgeleiteten Massnahmen mit SDM-Mapping und Gap-Analyse. Im Uebersicht-Tab werden zusaetzlich Vendor-TOM-Controls (VND-TOM-01 bis VND-TOM-06) aus dem Vendor-Compliance-Modul als Querverweis angezeigt.',
tips: [
{
icon: 'warning' as const,
@@ -563,12 +563,17 @@ export const STEP_EXPLANATIONS = {
title: 'SDM-Mapping',
description: 'Kontrollen werden den 7 SDM-Gewaehrleistungszielen zugeordnet: Verfuegbarkeit, Integritaet, Vertraulichkeit, Nichtverkettung, Intervenierbarkeit, Transparenz, Datenminimierung.',
},
{
icon: 'success' as const,
title: 'Vendor-Controls',
description: 'Im Uebersicht-Tab werden Vendor-TOM-Controls (VND-TOM-01 bis 06) als Read-Only-Querverweis angezeigt: Verschluesselung, Zugriffskontrolle, Verfuegbarkeit und Ueberpruefungsverfahren Ihrer Auftragsverarbeiter.',
},
],
},
'vvt': {
title: 'Verarbeitungsverzeichnis',
description: 'Erstellen und verwalten Sie Ihr Verzeichnis nach Art. 30 DSGVO',
explanation: 'Das Verarbeitungsverzeichnis (VVT) dokumentiert alle Verarbeitungstaetigkeiten mit personenbezogenen Daten. Der integrierte Generator-Fragebogen befuellt 70-90% der Pflichtfelder automatisch anhand Ihres Unternehmensprofils.',
description: 'Verarbeitungsverzeichnis nach Art. 30 DSGVO mit integriertem Processor-Tab',
explanation: 'Das Verarbeitungsverzeichnis (VVT) dokumentiert alle Verarbeitungstaetigkeiten mit personenbezogenen Daten. Der integrierte Generator-Fragebogen befuellt 70-90% der Pflichtfelder automatisch. Der Tab "Auftragsverarbeiter (Abs. 2)" liest Vendors mit role=PROCESSOR/SUB_PROCESSOR direkt aus der Vendor-Compliance-API — keine doppelte Datenhaltung.',
tips: [
{
icon: 'warning' as const,
@@ -585,6 +590,11 @@ export const STEP_EXPLANATIONS = {
title: 'Kein oeffentliches Dokument',
description: 'Das VVT ist ein internes Dokument. Es muss der Aufsichtsbehoerde nur auf Verlangen vorgelegt werden (Art. 30 Abs. 4).',
},
{
icon: 'success' as const,
title: 'Processor-Tab (Art. 30 Abs. 2)',
description: 'Auftragsverarbeiter werden direkt aus dem Vendor Register gelesen (Read-Only). Neue Vendors werden im Vendor-Compliance-Modul angelegt und erscheinen hier automatisch. PDF-Druck fuer Art. 30 Abs. 2 Dokument.',
},
],
},
'cookie-banner': {
@@ -611,8 +621,8 @@ export const STEP_EXPLANATIONS = {
},
'obligations': {
title: 'Pflichtenuebersicht',
description: 'Alle regulatorischen Pflichten auf einen Blick',
explanation: 'Die Pflichtenuebersicht aggregiert alle Anforderungen aus DSGVO, AI Act, NIS2 und weiteren Regulierungen. Sie sehen auf einen Blick, welche Pflichten fuer Ihr Unternehmen gelten.',
description: 'Regulatorische Pflichten mit 12 Compliance-Checks und Vendor-Verknuepfung',
explanation: 'Die Pflichtenuebersicht aggregiert alle Anforderungen aus DSGVO, AI Act, NIS2 und weiteren Regulierungen. 12 automatische Compliance-Checks pruefen Vollstaendigkeit, Fristen, Nachweise und Vendor-Verknuepfungen. Art.-28-Pflichten koennen mit Auftragsverarbeitern aus dem Vendor Register verknuepft werden. Das Pflichtenregister-Dokument (11 Sektionen) kann als auditfaehiges PDF gedruckt werden.',
tips: [
{
icon: 'info' as const,
@@ -621,15 +631,25 @@ export const STEP_EXPLANATIONS = {
},
{
icon: 'warning' as const,
title: 'Fristen',
description: 'Achten Sie auf die Umsetzungsfristen. Einige Pflichten haben feste Deadlines.',
title: 'Compliance-Checks',
description: '12 automatische Checks: Fehlende Verantwortliche, ueberfaellige Fristen, fehlende Nachweise, keine Rechtsreferenz, stagnierende Regulierungen, nicht gestartete High-Priority-Pflichten, fehlende Vendor-Verknuepfung (Art. 28) u.v.m.',
},
{
icon: 'success' as const,
title: 'Vendor-Verknuepfung',
description: 'Art.-28-Pflichten (Auftragsverarbeitung) koennen direkt mit Vendors aus dem Vendor Register verknuepft werden. Check #12 (MISSING_VENDOR_LINK) warnt bei fehlender Verknuepfung.',
},
{
icon: 'lightbulb' as const,
title: 'Pflichtenregister-Dokument',
description: 'Generieren Sie ein auditfaehiges Pflichtenregister mit 11 Sektionen: Ziel, Geltungsbereich, Methodik, Regulatorische Grundlagen, Pflichtenuebersicht, Details, Verantwortlichkeiten, Fristen, Nachweisverzeichnis, Compliance-Status und Aenderungshistorie.',
},
],
},
'loeschfristen': {
title: 'Loeschfristen',
description: 'Definieren Sie Aufbewahrungsrichtlinien fuer Ihre Daten',
explanation: 'Loeschfristen legen fest, wie lange personenbezogene Daten gespeichert werden duerfen. Die 3-Stufen-Logik (Zweckende, Aufbewahrungspflicht, Legal Hold) stellt sicher, dass alle gesetzlichen Anforderungen beruecksichtigt werden.',
description: 'Aufbewahrungsrichtlinien mit VVT-Verknuepfung und Vendor-Zuordnung',
explanation: 'Loeschfristen legen fest, wie lange personenbezogene Daten gespeichert werden duerfen. Die 3-Stufen-Logik (Zweckende, Aufbewahrungspflicht, Legal Hold) stellt sicher, dass alle gesetzlichen Anforderungen beruecksichtigt werden. Policies koennen mit VVT-Verarbeitungstaetigkeiten und Auftragsverarbeitern aus dem Vendor Register verknuepft werden.',
tips: [
{
icon: 'warning' as const,
@@ -646,6 +666,11 @@ export const STEP_EXPLANATIONS = {
title: 'Backup-Behandlung',
description: 'Auch Backups muessen ins Loeschkonzept einbezogen werden. Daten koennen nach primaerer Loeschung noch in Backup-Systemen existieren.',
},
{
icon: 'success' as const,
title: 'Vendor-Verknuepfung',
description: 'Loeschfrist-Policies koennen mit Auftragsverarbeitern verknuepft werden. So ist dokumentiert, welche Vendors Loeschpflichten fuer bestimmte Datenkategorien haben.',
},
],
},
'consent': {
@@ -716,6 +741,33 @@ export const STEP_EXPLANATIONS = {
},
],
},
'vendor-compliance': {
title: 'Vendor Compliance',
description: 'Auftragsverarbeiter-Management mit Cross-Modul-Integration',
explanation: 'Vendor Compliance verwaltet alle Auftragsverarbeiter (Art. 28 DSGVO) und Drittanbieter. Fuer jeden Vendor werden AVVs, Drittlandtransfers, TOMs und Subunternehmer geprueft. Das Modul ist zentral mit vier weiteren Modulen integriert: VVT-Processor-Tab liest Vendors direkt aus der API, Obligations und Loeschfristen verknuepfen Vendors ueber linked_vendor_ids, TOM zeigt Vendor-Controls als Querverweis.',
tips: [
{
icon: 'warning' as const,
title: 'Art. 28 DSGVO',
description: 'Jede Auftragsverarbeitung erfordert einen schriftlichen Vertrag (AVV). Pruefen Sie: Weisungsgebundenheit, TOMs, Subunternehmer-Genehmigung, Loeschpflicht und Audit-Recht.',
},
{
icon: 'info' as const,
title: 'Cross-Modul-Integration',
description: 'Vendors erscheinen automatisch im VVT-Processor-Tab, koennen in Obligations und Loeschfristen verknuepft werden, und ihre TOM-Controls werden im TOM-Modul als Querverweis angezeigt.',
},
{
icon: 'lightbulb' as const,
title: 'Drittlandtransfer',
description: 'Bei Datenverarbeitung ausserhalb der EU/EWR sind Standardvertragsklauseln (SCCs) oder andere Garantien nach Art. 44-49 DSGVO erforderlich.',
},
{
icon: 'success' as const,
title: 'Controls Library',
description: '6 TOM-Domain Controls (VND-TOM-01 bis VND-TOM-06) pruefen Verschluesselung, Zugriffskontrolle, Verfuegbarkeit und Ueberpruefungsverfahren bei Ihren Auftragsverarbeitern.',
},
],
},
'document-generator': {
title: 'Dokumentengenerator',
description: 'Generieren Sie rechtliche Dokumente aus lizenzkonformen Vorlagen',
@@ -852,18 +904,28 @@ export const STEP_EXPLANATIONS = {
},
'whistleblower': {
title: 'Hinweisgebersystem',
description: 'Meldestelle gemaess Hinweisgeberschutzgesetz (HinSchG)',
explanation: 'Das Hinweisgebersystem bietet eine sichere, anonyme Meldestelle fuer Compliance-Verstoesse gemaess dem Hinweisgeberschutzgesetz (HinSchG). Unternehmen ab 50 Mitarbeitern sind zur Einrichtung verpflichtet.',
description: 'Interne Meldestelle gemaess Hinweisgeberschutzgesetz (HinSchG) — seit 17. Dezember 2023 Pflicht fuer alle Unternehmen ab 50 Beschaeftigten',
explanation: 'Das Hinweisgebersystem implementiert eine HinSchG-konforme interne Meldestelle fuer die sichere, auch anonyme Meldung von Rechtsverstoessen. Es setzt die EU-Whistleblowing-Richtlinie (2019/1937) in deutsches Recht um. Beschaeftigungsgeber mit mindestens 50 Beschaeftigten sind zur Einrichtung verpflichtet (§ 12 HinSchG). Das System unterstuetzt den gesamten Meldeprozess: Einreichung, Eingangsbestaetigung (7-Tage-Frist), Sachverhaltspruefung, Folgemaßnahmen und Rueckmeldung (3-Monate-Frist).',
tips: [
{
icon: 'warning' as const,
title: 'Pflicht ab 50 MA',
description: 'Seit Juli 2023 muessen Unternehmen ab 50 Mitarbeitern eine interne Meldestelle einrichten (HinSchG §12).',
title: 'Pflicht ab 50 Beschaeftigten',
description: 'Seit 17.12.2023 gilt die Pflicht fuer ALLE Unternehmen ab 50 Beschaeftigten (§ 12 HinSchG). Verstoesse koennen mit Bussgeldern bis zu 50.000 EUR geahndet werden (§ 40 HinSchG).',
},
{
icon: 'info' as const,
title: 'Anonymitaet',
description: 'Die Identitaet des Hinweisgebers muss geschuetzt werden. Repressalien gegen Hinweisgeber sind verboten.',
title: 'Anonymitaet & Vertraulichkeit',
description: 'Die Identitaet des Hinweisgebers ist streng vertraulich zu behandeln (§ 8 HinSchG). Anonyme Meldungen sollen bearbeitet werden. Repressalien sind verboten und loesen Schadensersatzpflicht aus (§ 36, § 37 HinSchG).',
},
{
icon: 'lightbulb' as const,
title: 'Gesetzliche Fristen',
description: 'Eingangsbestaetigung innerhalb von 7 Tagen (§ 17 Abs. 1 S. 2). Rueckmeldung ueber ergriffene Folgemaßnahmen innerhalb von 3 Monaten nach Eingangsbestaetigung (§ 17 Abs. 2). Die Dokumentation muss 3 Jahre aufbewahrt werden (§ 11 HinSchG).',
},
{
icon: 'warning' as const,
title: 'Sachlicher Anwendungsbereich',
description: 'Erfasst werden Verstoesse gegen EU-Recht und nationales Recht, u.a. Strafrecht, Datenschutz (DSGVO/BDSG), Arbeitsschutz, Umweltschutz, Geldwaesche, Produktsicherheit und Verbraucherschutz (§ 2 HinSchG).',
},
],
},

View File

@@ -0,0 +1,554 @@
'use client'
import React, { useState, useEffect, useCallback } from 'react'
// =============================================================================
// TYPES
// =============================================================================
interface DecisionTreeQuestion {
id: string
axis: 'high_risk' | 'gpai'
question: string
description: string
article_ref: string
skip_if?: string
}
interface DecisionTreeDefinition {
id: string
name: string
version: string
questions: DecisionTreeQuestion[]
}
interface DecisionTreeAnswer {
question_id: string
value: boolean
note?: string
}
interface GPAIClassification {
is_gpai: boolean
is_systemic_risk: boolean
gpai_category: 'none' | 'standard' | 'systemic'
applicable_articles: string[]
obligations: string[]
}
interface DecisionTreeResult {
id: string
tenant_id: string
system_name: string
system_description?: string
answers: Record<string, DecisionTreeAnswer>
high_risk_result: string
gpai_result: GPAIClassification
combined_obligations: string[]
applicable_articles: string[]
created_at: string
}
// =============================================================================
// CONSTANTS
// =============================================================================
const RISK_LEVEL_CONFIG: Record<string, { label: string; color: string; bg: string; border: string }> = {
unacceptable: { label: 'Unzulässig', color: 'text-red-700', bg: 'bg-red-50', border: 'border-red-200' },
high_risk: { label: 'Hochrisiko', color: 'text-orange-700', bg: 'bg-orange-50', border: 'border-orange-200' },
limited_risk: { label: 'Begrenztes Risiko', color: 'text-yellow-700', bg: 'bg-yellow-50', border: 'border-yellow-200' },
minimal_risk: { label: 'Minimales Risiko', color: 'text-green-700', bg: 'bg-green-50', border: 'border-green-200' },
not_applicable: { label: 'Nicht anwendbar', color: 'text-gray-500', bg: 'bg-gray-50', border: 'border-gray-200' },
}
const GPAI_CONFIG: Record<string, { label: string; color: string; bg: string; border: string }> = {
none: { label: 'Kein GPAI', color: 'text-gray-500', bg: 'bg-gray-50', border: 'border-gray-200' },
standard: { label: 'GPAI Standard', color: 'text-blue-700', bg: 'bg-blue-50', border: 'border-blue-200' },
systemic: { label: 'GPAI Systemisches Risiko', color: 'text-purple-700', bg: 'bg-purple-50', border: 'border-purple-200' },
}
// =============================================================================
// MAIN COMPONENT
// =============================================================================
export default function DecisionTreeWizard() {
const [definition, setDefinition] = useState<DecisionTreeDefinition | null>(null)
const [answers, setAnswers] = useState<Record<string, DecisionTreeAnswer>>({})
const [currentIdx, setCurrentIdx] = useState(0)
const [systemName, setSystemName] = useState('')
const [systemDescription, setSystemDescription] = useState('')
const [result, setResult] = useState<DecisionTreeResult | null>(null)
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [error, setError] = useState<string | null>(null)
const [phase, setPhase] = useState<'intro' | 'questions' | 'result'>('intro')
// Load decision tree definition
useEffect(() => {
const load = async () => {
try {
const res = await fetch('/api/sdk/v1/ucca/decision-tree')
if (res.ok) {
const data = await res.json()
setDefinition(data)
} else {
setError('Entscheidungsbaum konnte nicht geladen werden')
}
} catch {
setError('Verbindung zum Backend fehlgeschlagen')
} finally {
setLoading(false)
}
}
load()
}, [])
// Get visible questions (respecting skip logic)
const getVisibleQuestions = useCallback((): DecisionTreeQuestion[] => {
if (!definition) return []
return definition.questions.filter(q => {
if (!q.skip_if) return true
// Skip this question if the gate question was answered "no"
const gateAnswer = answers[q.skip_if]
if (gateAnswer && !gateAnswer.value) return false
return true
})
}, [definition, answers])
const visibleQuestions = getVisibleQuestions()
const currentQuestion = visibleQuestions[currentIdx]
const totalVisible = visibleQuestions.length
const highRiskQuestions = visibleQuestions.filter(q => q.axis === 'high_risk')
const gpaiQuestions = visibleQuestions.filter(q => q.axis === 'gpai')
const handleAnswer = (value: boolean) => {
if (!currentQuestion) return
setAnswers(prev => ({
...prev,
[currentQuestion.id]: {
question_id: currentQuestion.id,
value,
},
}))
// Auto-advance
if (currentIdx < totalVisible - 1) {
setCurrentIdx(prev => prev + 1)
}
}
const handleBack = () => {
if (currentIdx > 0) {
setCurrentIdx(prev => prev - 1)
}
}
const handleSubmit = async () => {
setSaving(true)
setError(null)
try {
const res = await fetch('/api/sdk/v1/ucca/decision-tree/evaluate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
system_name: systemName,
system_description: systemDescription,
answers,
}),
})
if (res.ok) {
const data = await res.json()
setResult(data)
setPhase('result')
} else {
const err = await res.json().catch(() => ({ error: 'Auswertung fehlgeschlagen' }))
setError(err.error || 'Auswertung fehlgeschlagen')
}
} catch {
setError('Verbindung zum Backend fehlgeschlagen')
} finally {
setSaving(false)
}
}
const handleReset = () => {
setAnswers({})
setCurrentIdx(0)
setSystemName('')
setSystemDescription('')
setResult(null)
setPhase('intro')
setError(null)
}
const allAnswered = visibleQuestions.every(q => answers[q.id] !== undefined)
if (loading) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
<div className="w-10 h-10 border-2 border-purple-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-500">Entscheidungsbaum wird geladen...</p>
</div>
)
}
if (error && !definition) {
return (
<div className="bg-red-50 border border-red-200 rounded-xl p-6 text-center">
<p className="text-red-700">{error}</p>
<p className="text-red-500 text-sm mt-2">Bitte stellen Sie sicher, dass der AI Compliance SDK Service läuft.</p>
</div>
)
}
// =========================================================================
// INTRO PHASE
// =========================================================================
if (phase === 'intro') {
return (
<div className="space-y-6">
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-2">AI Act Entscheidungsbaum</h3>
<p className="text-sm text-gray-500 mb-6">
Klassifizieren Sie Ihr KI-System anhand von 12 Fragen auf zwei Achsen:
<strong> High-Risk</strong> (Anhang III) und <strong>GPAI</strong> (Art. 5156).
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div className="p-4 bg-orange-50 border border-orange-200 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<svg className="w-5 h-5 text-orange-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126z" />
</svg>
<span className="font-medium text-orange-700">Achse 1: High-Risk</span>
</div>
<p className="text-sm text-orange-600">7 Fragen zu Anhang III Kategorien (Biometrie, kritische Infrastruktur, Bildung, Beschäftigung, etc.)</p>
</div>
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<svg className="w-5 h-5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z" />
</svg>
<span className="font-medium text-blue-700">Achse 2: GPAI</span>
</div>
<p className="text-sm text-blue-600">5 Fragen zu General-Purpose AI (Foundation Models, systemisches Risiko, Art. 5156)</p>
</div>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Name des KI-Systems *</label>
<input
type="text"
value={systemName}
onChange={e => setSystemName(e.target.value)}
placeholder="z.B. Dokumenten-Analyse-KI, Chatbot-Service, Code-Assistent"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Beschreibung (optional)</label>
<textarea
value={systemDescription}
onChange={e => setSystemDescription(e.target.value)}
placeholder="Kurze Beschreibung des KI-Systems und seines Einsatzzwecks..."
rows={2}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
</div>
<div className="mt-6 flex justify-end">
<button
onClick={() => setPhase('questions')}
disabled={!systemName.trim()}
className={`px-6 py-2 rounded-lg font-medium transition-colors ${
systemName.trim()
? 'bg-purple-600 text-white hover:bg-purple-700'
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
}`}
>
Klassifizierung starten
</button>
</div>
</div>
</div>
)
}
// =========================================================================
// RESULT PHASE
// =========================================================================
if (phase === 'result' && result) {
const riskConfig = RISK_LEVEL_CONFIG[result.high_risk_result] || RISK_LEVEL_CONFIG.not_applicable
const gpaiConfig = GPAI_CONFIG[result.gpai_result.gpai_category] || GPAI_CONFIG.none
return (
<div className="space-y-6">
{/* Header */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">Klassifizierungsergebnis: {result.system_name}</h3>
<button
onClick={handleReset}
className="px-4 py-2 text-sm text-purple-600 hover:bg-purple-50 rounded-lg transition-colors"
>
Neue Klassifizierung
</button>
</div>
{/* Two-Axis Result Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div className={`p-5 rounded-xl border-2 ${riskConfig.border} ${riskConfig.bg}`}>
<div className="text-sm font-medium text-gray-500 mb-1">Achse 1: High-Risk (Anhang III)</div>
<div className={`text-xl font-bold ${riskConfig.color}`}>{riskConfig.label}</div>
</div>
<div className={`p-5 rounded-xl border-2 ${gpaiConfig.border} ${gpaiConfig.bg}`}>
<div className="text-sm font-medium text-gray-500 mb-1">Achse 2: GPAI (Art. 5156)</div>
<div className={`text-xl font-bold ${gpaiConfig.color}`}>{gpaiConfig.label}</div>
{result.gpai_result.is_systemic_risk && (
<div className="mt-1 text-xs text-purple-600 font-medium">Systemisches Risiko</div>
)}
</div>
</div>
</div>
{/* Applicable Articles */}
{result.applicable_articles && result.applicable_articles.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h4 className="text-sm font-semibold text-gray-900 mb-3">Anwendbare Artikel</h4>
<div className="flex flex-wrap gap-2">
{result.applicable_articles.map(art => (
<span key={art} className="px-3 py-1 text-xs bg-indigo-50 text-indigo-700 rounded-full border border-indigo-200">
{art}
</span>
))}
</div>
</div>
)}
{/* Combined Obligations */}
{result.combined_obligations && result.combined_obligations.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h4 className="text-sm font-semibold text-gray-900 mb-3">
Pflichten ({result.combined_obligations.length})
</h4>
<div className="space-y-2">
{result.combined_obligations.map((obl, i) => (
<div key={i} className="flex items-start gap-2 text-sm">
<svg className="w-4 h-4 text-purple-500 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-gray-700">{obl}</span>
</div>
))}
</div>
</div>
)}
{/* GPAI-specific obligations */}
{result.gpai_result.is_gpai && result.gpai_result.obligations.length > 0 && (
<div className="bg-blue-50 rounded-xl border border-blue-200 p-6">
<h4 className="text-sm font-semibold text-blue-900 mb-3">
GPAI-spezifische Pflichten ({result.gpai_result.obligations.length})
</h4>
<div className="space-y-2">
{result.gpai_result.obligations.map((obl, i) => (
<div key={i} className="flex items-start gap-2 text-sm">
<svg className="w-4 h-4 text-blue-500 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
</svg>
<span className="text-blue-800">{obl}</span>
</div>
))}
</div>
</div>
)}
{/* Answer Summary */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h4 className="text-sm font-semibold text-gray-900 mb-3">Ihre Antworten</h4>
<div className="space-y-2">
{definition?.questions.map(q => {
const answer = result.answers[q.id]
if (!answer) return null
return (
<div key={q.id} className="flex items-center gap-3 text-sm py-1.5 border-b border-gray-100 last:border-0">
<span className="text-xs font-mono text-gray-400 w-8">{q.id}</span>
<span className="flex-1 text-gray-600">{q.question}</span>
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
answer.value ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'
}`}>
{answer.value ? 'Ja' : 'Nein'}
</span>
</div>
)
})}
</div>
</div>
</div>
)
}
// =========================================================================
// QUESTIONS PHASE
// =========================================================================
return (
<div className="space-y-6">
{/* Progress */}
<div className="bg-white rounded-xl border border-gray-200 p-4">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-medium text-gray-700">
{systemName} Frage {currentIdx + 1} von {totalVisible}
</span>
<span className={`px-2 py-1 text-xs rounded-full font-medium ${
currentQuestion?.axis === 'high_risk'
? 'bg-orange-100 text-orange-700'
: 'bg-blue-100 text-blue-700'
}`}>
{currentQuestion?.axis === 'high_risk' ? 'High-Risk' : 'GPAI'}
</span>
</div>
{/* Dual progress bar */}
<div className="flex gap-2">
<div className="flex-1">
<div className="text-[10px] text-orange-600 mb-1 font-medium">
Achse 1: High-Risk ({highRiskQuestions.filter(q => answers[q.id] !== undefined).length}/{highRiskQuestions.length})
</div>
<div className="h-1.5 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full bg-orange-500 rounded-full transition-all"
style={{ width: `${highRiskQuestions.length ? (highRiskQuestions.filter(q => answers[q.id] !== undefined).length / highRiskQuestions.length) * 100 : 0}%` }}
/>
</div>
</div>
<div className="flex-1">
<div className="text-[10px] text-blue-600 mb-1 font-medium">
Achse 2: GPAI ({gpaiQuestions.filter(q => answers[q.id] !== undefined).length}/{gpaiQuestions.length})
</div>
<div className="h-1.5 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full bg-blue-500 rounded-full transition-all"
style={{ width: `${gpaiQuestions.length ? (gpaiQuestions.filter(q => answers[q.id] !== undefined).length / gpaiQuestions.length) * 100 : 0}%` }}
/>
</div>
</div>
</div>
</div>
{/* Error */}
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 flex items-center justify-between">
<span>{error}</span>
<button onClick={() => setError(null)} className="text-red-500 hover:text-red-700">&times;</button>
</div>
)}
{/* Current Question */}
{currentQuestion && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-start gap-3 mb-4">
<span className="px-2 py-1 text-xs font-mono bg-gray-100 text-gray-500 rounded">{currentQuestion.id}</span>
<span className="px-2 py-1 text-xs bg-purple-50 text-purple-700 rounded">{currentQuestion.article_ref}</span>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">{currentQuestion.question}</h3>
<p className="text-sm text-gray-500 mb-6">{currentQuestion.description}</p>
{/* Answer buttons */}
<div className="grid grid-cols-2 gap-4">
<button
onClick={() => handleAnswer(true)}
className={`p-4 rounded-xl border-2 transition-all text-center font-medium ${
answers[currentQuestion.id]?.value === true
? 'border-green-500 bg-green-50 text-green-700'
: 'border-gray-200 hover:border-green-300 hover:bg-green-50/50 text-gray-700'
}`}
>
<svg className="w-8 h-8 mx-auto mb-2 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Ja
</button>
<button
onClick={() => handleAnswer(false)}
className={`p-4 rounded-xl border-2 transition-all text-center font-medium ${
answers[currentQuestion.id]?.value === false
? 'border-gray-500 bg-gray-50 text-gray-700'
: 'border-gray-200 hover:border-gray-300 hover:bg-gray-50/50 text-gray-700'
}`}
>
<svg className="w-8 h-8 mx-auto mb-2 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Nein
</button>
</div>
</div>
)}
{/* Navigation */}
<div className="flex items-center justify-between">
<button
onClick={currentIdx === 0 ? () => setPhase('intro') : handleBack}
className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
>
Zurück
</button>
<div className="flex items-center gap-1">
{visibleQuestions.map((q, i) => (
<button
key={q.id}
onClick={() => setCurrentIdx(i)}
className={`w-2.5 h-2.5 rounded-full transition-colors ${
i === currentIdx
? q.axis === 'high_risk' ? 'bg-orange-500' : 'bg-blue-500'
: answers[q.id] !== undefined
? 'bg-green-400'
: 'bg-gray-200'
}`}
title={`${q.id}: ${q.question}`}
/>
))}
</div>
{allAnswered ? (
<button
onClick={handleSubmit}
disabled={saving}
className={`px-6 py-2 rounded-lg font-medium transition-colors ${
saving
? 'bg-purple-300 text-white cursor-wait'
: 'bg-purple-600 text-white hover:bg-purple-700'
}`}
>
{saving ? (
<span className="flex items-center gap-2">
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Auswertung...
</span>
) : (
'Auswerten'
)}
</button>
) : (
<button
onClick={() => setCurrentIdx(prev => Math.min(prev + 1, totalVisible - 1))}
disabled={currentIdx >= totalVisible - 1}
className="px-4 py-2 text-sm text-purple-600 hover:bg-purple-50 rounded-lg transition-colors disabled:opacity-30"
>
Weiter
</button>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,272 @@
'use client'
import React, { useCallback, useEffect, useRef } from 'react'
import { useEditor, EditorContent, type Editor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import Table from '@tiptap/extension-table'
import TableRow from '@tiptap/extension-table-row'
import TableHeader from '@tiptap/extension-table-header'
import TableCell from '@tiptap/extension-table-cell'
import Image from '@tiptap/extension-image'
interface TechFileEditorProps {
content: string
onSave: (content: string) => void
readOnly?: boolean
}
function normalizeContent(content: string): string {
if (!content) return '<p></p>'
const trimmed = content.trim()
// If it looks like JSON array or has no HTML tags, wrap in <p>
if (trimmed.startsWith('[') || !/<[a-z][\s\S]*>/i.test(trimmed)) {
return `<p>${trimmed.replace(/\n/g, '</p><p>')}</p>`
}
return trimmed
}
interface ToolbarButtonProps {
onClick: () => void
isActive?: boolean
disabled?: boolean
title: string
children: React.ReactNode
}
function ToolbarButton({ onClick, isActive, disabled, title, children }: ToolbarButtonProps) {
return (
<button
type="button"
onClick={onClick}
disabled={disabled}
title={title}
className={`p-1.5 rounded text-sm font-medium transition-colors ${
isActive
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300'
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700'
} disabled:opacity-40 disabled:cursor-not-allowed`}
>
{children}
</button>
)
}
export function TechFileEditor({ content, onSave, readOnly = false }: TechFileEditorProps) {
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
const onSaveRef = useRef(onSave)
onSaveRef.current = onSave
const debouncedSave = useCallback((html: string) => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current)
}
debounceTimer.current = setTimeout(() => {
onSaveRef.current(html)
}, 3000)
}, [])
const editor = useEditor({
extensions: [
StarterKit.configure({
heading: { levels: [2, 3, 4] },
}),
Table.configure({
resizable: true,
HTMLAttributes: { class: 'border-collapse border border-gray-300' },
}),
TableRow,
TableHeader,
TableCell,
Image.configure({
HTMLAttributes: { class: 'max-w-full rounded' },
}),
],
content: normalizeContent(content),
editable: !readOnly,
onUpdate: ({ editor: ed }: { editor: Editor }) => {
if (!readOnly) {
debouncedSave(ed.getHTML())
}
},
editorProps: {
attributes: {
class: 'prose prose-sm max-w-none dark:prose-invert focus:outline-none min-h-[300px] px-4 py-3',
},
},
})
// Update content when parent prop changes
useEffect(() => {
if (editor && content) {
const normalized = normalizeContent(content)
const currentHTML = editor.getHTML()
if (normalized !== currentHTML) {
editor.commands.setContent(normalized)
}
}
}, [content, editor])
// Update editable state when readOnly changes
useEffect(() => {
if (editor) {
editor.setEditable(!readOnly)
}
}, [readOnly, editor])
// Cleanup debounce timer
useEffect(() => {
return () => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current)
}
}
}, [])
if (!editor) {
return (
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 min-h-[300px] flex items-center justify-center">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-purple-600" />
</div>
)
}
return (
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
{/* Toolbar */}
{!readOnly && (
<div className="flex flex-wrap items-center gap-0.5 px-2 py-1.5 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
{/* Text formatting */}
<ToolbarButton
onClick={() => editor.chain().focus().toggleBold().run()}
isActive={editor.isActive('bold')}
title="Fett (Ctrl+B)"
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z" />
</svg>
</ToolbarButton>
<ToolbarButton
onClick={() => editor.chain().focus().toggleItalic().run()}
isActive={editor.isActive('italic')}
title="Kursiv (Ctrl+I)"
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4z" />
</svg>
</ToolbarButton>
<div className="w-px h-5 bg-gray-300 dark:bg-gray-600 mx-1" />
{/* Headings */}
<ToolbarButton
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
isActive={editor.isActive('heading', { level: 2 })}
title="Ueberschrift 2"
>
<span className="text-xs font-bold">H2</span>
</ToolbarButton>
<ToolbarButton
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
isActive={editor.isActive('heading', { level: 3 })}
title="Ueberschrift 3"
>
<span className="text-xs font-bold">H3</span>
</ToolbarButton>
<ToolbarButton
onClick={() => editor.chain().focus().toggleHeading({ level: 4 }).run()}
isActive={editor.isActive('heading', { level: 4 })}
title="Ueberschrift 4"
>
<span className="text-xs font-bold">H4</span>
</ToolbarButton>
<div className="w-px h-5 bg-gray-300 dark:bg-gray-600 mx-1" />
{/* Lists */}
<ToolbarButton
onClick={() => editor.chain().focus().toggleBulletList().run()}
isActive={editor.isActive('bulletList')}
title="Aufzaehlung"
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z" />
</svg>
</ToolbarButton>
<ToolbarButton
onClick={() => editor.chain().focus().toggleOrderedList().run()}
isActive={editor.isActive('orderedList')}
title="Nummerierte Liste"
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 10.9V10H2v1zm5-6v2h14V5H7zm0 14h14v-2H7v2zm0-6h14v-2H7v2z" />
</svg>
</ToolbarButton>
<div className="w-px h-5 bg-gray-300 dark:bg-gray-600 mx-1" />
{/* Table */}
<ToolbarButton
onClick={() => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()}
title="Tabelle einfuegen (3x3)"
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<rect x="3" y="3" width="18" height="18" rx="2" />
<line x1="3" y1="9" x2="21" y2="9" />
<line x1="3" y1="15" x2="21" y2="15" />
<line x1="9" y1="3" x2="9" y2="21" />
<line x1="15" y1="3" x2="15" y2="21" />
</svg>
</ToolbarButton>
{/* Blockquote */}
<ToolbarButton
onClick={() => editor.chain().focus().toggleBlockquote().run()}
isActive={editor.isActive('blockquote')}
title="Zitat"
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z" />
</svg>
</ToolbarButton>
{/* Code Block */}
<ToolbarButton
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
isActive={editor.isActive('codeBlock')}
title="Code-Block"
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="16,18 22,12 16,6" />
<polyline points="8,6 2,12 8,18" />
</svg>
</ToolbarButton>
<div className="w-px h-5 bg-gray-300 dark:bg-gray-600 mx-1" />
{/* Undo / Redo */}
<ToolbarButton
onClick={() => editor.chain().focus().undo().run()}
disabled={!editor.can().undo()}
title="Rueckgaengig (Ctrl+Z)"
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z" />
</svg>
</ToolbarButton>
<ToolbarButton
onClick={() => editor.chain().focus().redo().run()}
disabled={!editor.can().redo()}
title="Wiederholen (Ctrl+Shift+Z)"
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.4 10.6C16.55 8.99 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73.72 5.12 1.88L13 16h9V7l-3.6 3.6z" />
</svg>
</ToolbarButton>
</div>
)}
{/* Editor Content */}
<EditorContent editor={editor} />
</div>
)
}

View File

@@ -0,0 +1,414 @@
'use client'
import { useState, useEffect, useCallback, useMemo } from 'react'
import type { Obligation, ObligationComplianceCheckResult } from '@/lib/sdk/obligations-compliance'
import {
buildObligationDocumentHtml,
createDefaultObligationDocumentOrgHeader,
type ObligationDocumentOrgHeader,
type ObligationDocumentRevision,
} from '@/lib/sdk/obligations-document'
// =============================================================================
// TYPES
// =============================================================================
interface ObligationDocumentTabProps {
obligations: Obligation[]
complianceResult: ObligationComplianceCheckResult | null
}
// =============================================================================
// COMPONENT
// =============================================================================
function ObligationDocumentTab({ obligations, complianceResult }: ObligationDocumentTabProps) {
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
const [orgHeader, setOrgHeader] = useState<ObligationDocumentOrgHeader>(() => {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('bp_obligation_document_orgheader')
if (saved) {
try { return JSON.parse(saved) } catch { /* ignore */ }
}
}
return createDefaultObligationDocumentOrgHeader()
})
const [revisions, setRevisions] = useState<ObligationDocumentRevision[]>(() => {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('bp_obligation_document_revisions')
if (saved) {
try { return JSON.parse(saved) } catch { /* ignore */ }
}
}
return []
})
// ---------------------------------------------------------------------------
// localStorage persistence
// ---------------------------------------------------------------------------
useEffect(() => {
localStorage.setItem('bp_obligation_document_orgheader', JSON.stringify(orgHeader))
}, [orgHeader])
useEffect(() => {
localStorage.setItem('bp_obligation_document_revisions', JSON.stringify(revisions))
}, [revisions])
// ---------------------------------------------------------------------------
// Computed values
// ---------------------------------------------------------------------------
const obligationCount = obligations.length
const completedCount = useMemo(() => {
return obligations.filter(o => o.status === 'completed').length
}, [obligations])
const distinctSources = useMemo(() => {
const sources = new Set(obligations.map(o => o.source || 'Sonstig'))
return sources.size
}, [obligations])
// ---------------------------------------------------------------------------
// Handlers
// ---------------------------------------------------------------------------
const handlePrintDocument = useCallback(() => {
const html = buildObligationDocumentHtml(
obligations,
orgHeader,
complianceResult,
revisions,
)
const printWindow = window.open('', '_blank')
if (printWindow) {
printWindow.document.write(html)
printWindow.document.close()
setTimeout(() => printWindow.print(), 300)
}
}, [obligations, orgHeader, complianceResult, revisions])
const handleDownloadDocumentHtml = useCallback(() => {
const html = buildObligationDocumentHtml(
obligations,
orgHeader,
complianceResult,
revisions,
)
const blob = new Blob([html], { type: 'text/html;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `Pflichtenregister-${orgHeader.organizationName || 'Organisation'}-${new Date().toISOString().split('T')[0]}.html`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, [obligations, orgHeader, complianceResult, revisions])
const handleAddRevision = useCallback(() => {
setRevisions(prev => [...prev, {
version: String(prev.length + 1) + '.0',
date: new Date().toISOString().split('T')[0],
author: '',
changes: '',
}])
}, [])
const handleUpdateRevision = useCallback((index: number, field: keyof ObligationDocumentRevision, value: string) => {
setRevisions(prev => prev.map((r, i) => i === index ? { ...r, [field]: value } : r))
}, [])
const handleRemoveRevision = useCallback((index: number) => {
setRevisions(prev => prev.filter((_, i) => i !== index))
}, [])
const updateOrgHeader = useCallback((field: keyof ObligationDocumentOrgHeader, value: string) => {
setOrgHeader(prev => ({ ...prev, [field]: value }))
}, [])
// ---------------------------------------------------------------------------
// Render
// ---------------------------------------------------------------------------
return (
<div className="space-y-6">
{/* 1. Action Bar */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-gray-900">Pflichtenregister</h3>
<p className="text-sm text-gray-500 mt-1">
Auditfaehiges Dokument mit {obligationCount} Pflichten generieren
</p>
</div>
<div className="flex gap-3">
<button
onClick={handleDownloadDocumentHtml}
disabled={obligationCount === 0}
className="bg-white border border-gray-300 text-gray-700 hover:bg-gray-50 rounded-lg px-4 py-2 text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
HTML herunterladen
</button>
<button
onClick={handlePrintDocument}
disabled={obligationCount === 0}
className="bg-purple-600 text-white hover:bg-purple-700 rounded-lg px-4 py-2 text-sm font-medium transition-colors shadow-sm disabled:opacity-50 disabled:cursor-not-allowed"
>
Als PDF drucken
</button>
</div>
</div>
</div>
{/* 2. Org Header Form */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h4 className="text-base font-semibold text-gray-900 mb-4">Organisationsdaten</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Organisation</label>
<input
type="text"
value={orgHeader.organizationName}
onChange={e => updateOrgHeader('organizationName', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Branche</label>
<input
type="text"
value={orgHeader.industry}
onChange={e => updateOrgHeader('industry', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Datenschutzbeauftragter</label>
<input
type="text"
value={orgHeader.dpoName}
onChange={e => updateOrgHeader('dpoName', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">DSB-Kontakt</label>
<input
type="text"
value={orgHeader.dpoContact}
onChange={e => updateOrgHeader('dpoContact', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Verantwortlicher</label>
<input
type="text"
value={orgHeader.responsiblePerson}
onChange={e => updateOrgHeader('responsiblePerson', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Rechtsabteilung</label>
<input
type="text"
value={orgHeader.legalDepartment}
onChange={e => updateOrgHeader('legalDepartment', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Dokumentversion</label>
<input
type="text"
value={orgHeader.documentVersion}
onChange={e => updateOrgHeader('documentVersion', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Pruefintervall</label>
<input
type="text"
value={orgHeader.reviewInterval}
onChange={e => updateOrgHeader('reviewInterval', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Letzte Pruefung</label>
<input
type="date"
value={orgHeader.lastReviewDate}
onChange={e => updateOrgHeader('lastReviewDate', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Naechste Pruefung</label>
<input
type="date"
value={orgHeader.nextReviewDate}
onChange={e => updateOrgHeader('nextReviewDate', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
</div>
</div>
{/* 3. Revisions Manager */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h4 className="text-base font-semibold text-gray-900">Aenderungshistorie</h4>
<button
onClick={handleAddRevision}
className="text-sm text-purple-600 hover:text-purple-700 font-medium"
>
+ Version hinzufuegen
</button>
</div>
{revisions.length > 0 ? (
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-200">
<th className="text-left py-2 text-gray-600 font-medium">Version</th>
<th className="text-left py-2 text-gray-600 font-medium">Datum</th>
<th className="text-left py-2 text-gray-600 font-medium">Autor</th>
<th className="text-left py-2 text-gray-600 font-medium">Aenderungen</th>
<th className="py-2"></th>
</tr>
</thead>
<tbody>
{revisions.map((revision, index) => (
<tr key={index} className="border-b border-gray-100">
<td className="py-2 pr-2">
<input
type="text"
value={revision.version}
onChange={e => handleUpdateRevision(index, 'version', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-1.5 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</td>
<td className="py-2 pr-2">
<input
type="date"
value={revision.date}
onChange={e => handleUpdateRevision(index, 'date', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-1.5 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</td>
<td className="py-2 pr-2">
<input
type="text"
value={revision.author}
onChange={e => handleUpdateRevision(index, 'author', e.target.value)}
placeholder="Name"
className="w-full rounded-lg border border-gray-300 px-3 py-1.5 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</td>
<td className="py-2 pr-2">
<input
type="text"
value={revision.changes}
onChange={e => handleUpdateRevision(index, 'changes', e.target.value)}
placeholder="Beschreibung der Aenderungen"
className="w-full rounded-lg border border-gray-300 px-3 py-1.5 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</td>
<td className="py-2 text-right">
<button
onClick={() => handleRemoveRevision(index)}
className="text-sm text-red-600 hover:text-red-700 font-medium"
>
Entfernen
</button>
</td>
</tr>
))}
</tbody>
</table>
) : (
<p className="text-sm text-gray-500 italic">
Noch keine Revisionen. Die erste Version wird automatisch im Dokument eingetragen.
</p>
)}
</div>
{/* 4. Document Preview */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h4 className="text-base font-semibold text-gray-900 mb-4">Dokument-Vorschau</h4>
{obligationCount === 0 ? (
<div className="text-center py-8">
<p className="text-gray-500">Erfassen Sie Pflichten, um das Pflichtenregister zu generieren.</p>
</div>
) : (
<div className="space-y-4">
{/* Cover preview */}
<div className="bg-purple-50 rounded-lg p-4 text-center">
<p className="text-purple-700 font-semibold text-lg">Pflichtenregister</p>
<p className="text-purple-600 text-sm">
Regulatorische Pflichten {orgHeader.organizationName || 'Organisation'}
</p>
<p className="text-purple-500 text-xs mt-1">
Version {orgHeader.documentVersion} | Stand: {new Date().toLocaleDateString('de-DE')}
</p>
</div>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
<div className="bg-gray-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-gray-900">{obligationCount}</p>
<p className="text-xs text-gray-500">Pflichten</p>
</div>
<div className="bg-green-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-green-700">{completedCount}</p>
<p className="text-xs text-gray-500">Abgeschlossen</p>
</div>
<div className="bg-purple-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-purple-700">{distinctSources}</p>
<p className="text-xs text-gray-500">Regulierungen</p>
</div>
<div className="bg-gray-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-gray-900">12</p>
<p className="text-xs text-gray-500">Sektionen</p>
</div>
<div className="bg-gray-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-gray-900">
{complianceResult ? complianceResult.score : '—'}
</p>
<p className="text-xs text-gray-500">Compliance-Score</p>
</div>
</div>
{/* 12 Sections list */}
<div>
<p className="text-sm font-medium text-gray-700 mb-2">12 Dokument-Sektionen:</p>
<ol className="list-decimal list-inside text-sm text-gray-600 space-y-1">
<li>Ziel und Zweck</li>
<li>Geltungsbereich</li>
<li>Methodik</li>
<li>Regulatorische Grundlagen</li>
<li>Pflichtenuebersicht</li>
<li>Detaillierte Pflichten</li>
<li>Verantwortlichkeiten</li>
<li>Fristen und Termine</li>
<li>Nachweisverzeichnis</li>
<li>Compliance-Status</li>
<li>Aenderungshistorie</li>
</ol>
</div>
</div>
)}
</div>
</div>
)
}
export { ObligationDocumentTab }

View File

@@ -0,0 +1,449 @@
'use client'
import { useState, useEffect, useCallback, useMemo } from 'react'
import type { TOMGeneratorState } from '@/lib/sdk/tom-generator/types'
import type { TOMComplianceCheckResult } from '@/lib/sdk/tom-compliance'
import {
buildTOMDocumentHtml,
createDefaultTOMDocumentOrgHeader,
type TOMDocumentOrgHeader,
type TOMDocumentRevision,
} from '@/lib/sdk/tom-document'
// =============================================================================
// TYPES
// =============================================================================
interface TOMDocumentTabProps {
state: TOMGeneratorState
complianceResult: TOMComplianceCheckResult | null
}
// =============================================================================
// COMPONENT
// =============================================================================
function TOMDocumentTab({ state, complianceResult }: TOMDocumentTabProps) {
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
const [orgHeader, setOrgHeader] = useState<TOMDocumentOrgHeader>(() => {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('bp_tom_document_orgheader')
if (saved) {
try { return JSON.parse(saved) } catch { /* ignore */ }
}
}
return createDefaultTOMDocumentOrgHeader()
})
const [revisions, setRevisions] = useState<TOMDocumentRevision[]>(() => {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('bp_tom_document_revisions')
if (saved) {
try { return JSON.parse(saved) } catch { /* ignore */ }
}
}
return []
})
// ---------------------------------------------------------------------------
// localStorage persistence
// ---------------------------------------------------------------------------
useEffect(() => {
localStorage.setItem('bp_tom_document_orgheader', JSON.stringify(orgHeader))
}, [orgHeader])
useEffect(() => {
localStorage.setItem('bp_tom_document_revisions', JSON.stringify(revisions))
}, [revisions])
// ---------------------------------------------------------------------------
// Computed values
// ---------------------------------------------------------------------------
const tomCount = useMemo(() => {
if (!state?.derivedTOMs) return 0
return Array.isArray(state.derivedTOMs) ? state.derivedTOMs.length : 0
}, [state?.derivedTOMs])
const applicableTOMs = useMemo(() => {
if (!state?.derivedTOMs || !Array.isArray(state.derivedTOMs)) return []
return state.derivedTOMs.filter(t => t.applicability !== 'NOT_APPLICABLE')
}, [state?.derivedTOMs])
const implementedCount = useMemo(() => {
return applicableTOMs.filter(t => t.implementationStatus === 'IMPLEMENTED').length
}, [applicableTOMs])
const [canonicalCount, setCanonicalCount] = useState(0)
useEffect(() => {
if (tomCount === 0) return
fetch('/api/sdk/v1/compliance/tom-mappings/stats')
.then(r => r.ok ? r.json() : null)
.then(data => { if (data?.sync_state?.canonical_controls_matched) setCanonicalCount(data.sync_state.canonical_controls_matched) })
.catch(() => {})
}, [tomCount])
// ---------------------------------------------------------------------------
// Handlers
// ---------------------------------------------------------------------------
const handlePrintTOMDocument = useCallback(() => {
const html = buildTOMDocumentHtml(
state?.derivedTOMs || [],
orgHeader,
state?.companyProfile || null,
state?.riskProfile || null,
complianceResult,
revisions,
)
const printWindow = window.open('', '_blank')
if (printWindow) {
printWindow.document.write(html)
printWindow.document.close()
setTimeout(() => printWindow.print(), 300)
}
}, [state, orgHeader, complianceResult, revisions])
const handleDownloadTOMDocumentHtml = useCallback(() => {
const html = buildTOMDocumentHtml(
state?.derivedTOMs || [],
orgHeader,
state?.companyProfile || null,
state?.riskProfile || null,
complianceResult,
revisions,
)
const blob = new Blob([html], { type: 'text/html;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `TOM-Dokumentation-${orgHeader.organizationName || 'Organisation'}-${new Date().toISOString().split('T')[0]}.html`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, [state, orgHeader, complianceResult, revisions])
const handleAddRevision = useCallback(() => {
setRevisions(prev => [...prev, {
version: String(prev.length + 1) + '.0',
date: new Date().toISOString().split('T')[0],
author: '',
changes: '',
}])
}, [])
const handleUpdateRevision = useCallback((index: number, field: keyof TOMDocumentRevision, value: string) => {
setRevisions(prev => prev.map((r, i) => i === index ? { ...r, [field]: value } : r))
}, [])
const handleRemoveRevision = useCallback((index: number) => {
setRevisions(prev => prev.filter((_, i) => i !== index))
}, [])
const updateOrgHeader = useCallback((field: keyof TOMDocumentOrgHeader, value: string | string[]) => {
setOrgHeader(prev => ({ ...prev, [field]: value }))
}, [])
// ---------------------------------------------------------------------------
// Render
// ---------------------------------------------------------------------------
return (
<div className="space-y-6">
{/* 1. Action Bar */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-gray-900">TOM-Dokument (Art. 32 DSGVO)</h3>
<p className="text-sm text-gray-500 mt-1">
Auditfaehiges Dokument mit {applicableTOMs.length} Massnahmen generieren
</p>
</div>
<div className="flex gap-3">
<button
onClick={handleDownloadTOMDocumentHtml}
disabled={tomCount === 0}
className="bg-white border border-gray-300 text-gray-700 hover:bg-gray-50 rounded-lg px-4 py-2 text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
HTML herunterladen
</button>
<button
onClick={handlePrintTOMDocument}
disabled={tomCount === 0}
className="bg-purple-600 text-white hover:bg-purple-700 rounded-lg px-4 py-2 text-sm font-medium transition-colors shadow-sm disabled:opacity-50 disabled:cursor-not-allowed"
>
Als PDF drucken
</button>
</div>
</div>
</div>
{/* 2. Org Header Form */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h4 className="text-base font-semibold text-gray-900 mb-4">Organisationsdaten</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Organisation</label>
<input
type="text"
value={orgHeader.organizationName}
onChange={e => updateOrgHeader('organizationName', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Branche</label>
<input
type="text"
value={orgHeader.industry}
onChange={e => updateOrgHeader('industry', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Datenschutzbeauftragter</label>
<input
type="text"
value={orgHeader.dpoName}
onChange={e => updateOrgHeader('dpoName', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">DSB-Kontakt</label>
<input
type="text"
value={orgHeader.dpoContact}
onChange={e => updateOrgHeader('dpoContact', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Verantwortlicher</label>
<input
type="text"
value={orgHeader.responsiblePerson}
onChange={e => updateOrgHeader('responsiblePerson', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">IT-Sicherheitskontakt</label>
<input
type="text"
value={orgHeader.itSecurityContact}
onChange={e => updateOrgHeader('itSecurityContact', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Mitarbeiteranzahl</label>
<input
type="text"
value={orgHeader.employeeCount}
onChange={e => updateOrgHeader('employeeCount', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Standorte</label>
<input
type="text"
value={orgHeader.locations.join(', ')}
onChange={e => updateOrgHeader('locations', e.target.value.split(',').map(s => s.trim()))}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Dokumentversion</label>
<input
type="text"
value={orgHeader.documentVersion}
onChange={e => updateOrgHeader('documentVersion', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Pruefintervall</label>
<input
type="text"
value={orgHeader.reviewInterval}
onChange={e => updateOrgHeader('reviewInterval', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Letzte Pruefung</label>
<input
type="date"
value={orgHeader.lastReviewDate}
onChange={e => updateOrgHeader('lastReviewDate', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Naechste Pruefung</label>
<input
type="date"
value={orgHeader.nextReviewDate}
onChange={e => updateOrgHeader('nextReviewDate', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</div>
</div>
</div>
{/* 3. Revisions Manager */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h4 className="text-base font-semibold text-gray-900">Aenderungshistorie</h4>
<button
onClick={handleAddRevision}
className="text-sm text-purple-600 hover:text-purple-700 font-medium"
>
+ Version hinzufuegen
</button>
</div>
{revisions.length > 0 ? (
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-200">
<th className="text-left py-2 text-gray-600 font-medium">Version</th>
<th className="text-left py-2 text-gray-600 font-medium">Datum</th>
<th className="text-left py-2 text-gray-600 font-medium">Autor</th>
<th className="text-left py-2 text-gray-600 font-medium">Aenderungen</th>
<th className="py-2"></th>
</tr>
</thead>
<tbody>
{revisions.map((revision, index) => (
<tr key={index} className="border-b border-gray-100">
<td className="py-2 pr-2">
<input
type="text"
value={revision.version}
onChange={e => handleUpdateRevision(index, 'version', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-1.5 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</td>
<td className="py-2 pr-2">
<input
type="date"
value={revision.date}
onChange={e => handleUpdateRevision(index, 'date', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-1.5 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</td>
<td className="py-2 pr-2">
<input
type="text"
value={revision.author}
onChange={e => handleUpdateRevision(index, 'author', e.target.value)}
placeholder="Name"
className="w-full rounded-lg border border-gray-300 px-3 py-1.5 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</td>
<td className="py-2 pr-2">
<input
type="text"
value={revision.changes}
onChange={e => handleUpdateRevision(index, 'changes', e.target.value)}
placeholder="Beschreibung der Aenderungen"
className="w-full rounded-lg border border-gray-300 px-3 py-1.5 text-sm focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none"
/>
</td>
<td className="py-2 text-right">
<button
onClick={() => handleRemoveRevision(index)}
className="text-sm text-red-600 hover:text-red-700 font-medium"
>
Entfernen
</button>
</td>
</tr>
))}
</tbody>
</table>
) : (
<p className="text-sm text-gray-500 italic">
Noch keine Revisionen. Die erste Version wird automatisch im Dokument eingetragen.
</p>
)}
</div>
{/* 4. Document Preview */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h4 className="text-base font-semibold text-gray-900 mb-4">Dokument-Vorschau</h4>
{tomCount === 0 ? (
<div className="text-center py-8">
<p className="text-gray-500">Starten Sie den TOM-Generator, um Massnahmen abzuleiten.</p>
</div>
) : (
<div className="space-y-4">
{/* Cover preview */}
<div className="bg-purple-50 rounded-lg p-4 text-center">
<p className="text-purple-700 font-semibold text-lg">TOM-Dokumentation</p>
<p className="text-purple-600 text-sm">
Art. 32 DSGVO {orgHeader.organizationName || 'Organisation'}
</p>
<p className="text-purple-500 text-xs mt-1">
Version {orgHeader.documentVersion} | Stand: {new Date().toLocaleDateString('de-DE')}
</p>
</div>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
<div className="bg-gray-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-gray-900">{applicableTOMs.length}</p>
<p className="text-xs text-gray-500">Massnahmen</p>
</div>
<div className="bg-green-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-green-700">{implementedCount}</p>
<p className="text-xs text-gray-500">Umgesetzt</p>
</div>
<div className="bg-purple-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-purple-700">{canonicalCount || '-'}</p>
<p className="text-xs text-gray-500">Belegende Controls</p>
</div>
<div className="bg-gray-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-gray-900">12</p>
<p className="text-xs text-gray-500">Sektionen</p>
</div>
<div className="bg-gray-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-gray-900">
{complianceResult ? complianceResult.score : '-'}
</p>
<p className="text-xs text-gray-500">Compliance-Score</p>
</div>
</div>
{/* 12 Sections list */}
<div>
<p className="text-sm font-medium text-gray-700 mb-2">12 Dokument-Sektionen:</p>
<ol className="list-decimal list-inside text-sm text-gray-600 space-y-1">
<li>Ziel und Zweck</li>
<li>Geltungsbereich</li>
<li>Grundprinzipien Art. 32</li>
<li>Schutzbedarf und Risikoanalyse</li>
<li>Massnahmen-Uebersicht</li>
<li>Detaillierte Massnahmen</li>
<li>SDM Gewaehrleistungsziele</li>
<li>Verantwortlichkeiten</li>
<li>Pruef- und Revisionszyklus</li>
<li>Compliance-Status</li>
<li>Aenderungshistorie</li>
</ol>
</div>
</div>
)}
</div>
</div>
)
}
export { TOMDocumentTab }

View File

@@ -1,9 +1,18 @@
'use client'
import { useMemo, useState, useEffect } from 'react'
import { useMemo, useState, useEffect, useCallback } from 'react'
import { DerivedTOM, TOMGeneratorState } from '@/lib/sdk/tom-generator/types'
import { getControlById } from '@/lib/sdk/tom-generator/controls/loader'
interface CanonicalMapping {
id: string
canonical_control_code: string
canonical_title: string | null
canonical_severity: string | null
canonical_objective: string | null
mapping_type: string
}
interface TOMEditorTabProps {
state: TOMGeneratorState
selectedTOMId: string | null
@@ -46,6 +55,17 @@ export function TOMEditorTab({ state, selectedTOMId, onUpdateTOM, onBack }: TOME
const [notes, setNotes] = useState('')
const [linkedEvidence, setLinkedEvidence] = useState<string[]>([])
const [selectedEvidenceId, setSelectedEvidenceId] = useState('')
const [canonicalMappings, setCanonicalMappings] = useState<CanonicalMapping[]>([])
const [showCanonical, setShowCanonical] = useState(false)
// Load canonical controls for this TOM's category
useEffect(() => {
if (!control?.category) { setCanonicalMappings([]); return }
fetch(`/api/sdk/v1/compliance/tom-mappings/by-tom/${encodeURIComponent(control.category)}`)
.then(r => r.ok ? r.json() : null)
.then(data => { if (data?.mappings) setCanonicalMappings(data.mappings) })
.catch(() => setCanonicalMappings([]))
}, [control?.category])
useEffect(() => {
if (tom) {
@@ -341,6 +361,62 @@ export function TOMEditorTab({ state, selectedTOMId, onUpdateTOM, onBack }: TOME
</div>
)}
{/* Canonical Controls (Belegende Security-Controls) */}
{canonicalMappings.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-gray-700">
Belegende Security-Controls ({canonicalMappings.length})
</h3>
<button
onClick={() => setShowCanonical(!showCanonical)}
className="text-xs text-purple-600 hover:text-purple-700 font-medium"
>
{showCanonical ? 'Einklappen' : 'Alle anzeigen'}
</button>
</div>
<div className="space-y-2">
{(showCanonical ? canonicalMappings : canonicalMappings.slice(0, 5)).map(m => (
<div key={m.id} className="flex items-start gap-3 bg-gray-50 rounded-lg px-3 py-2">
<div className="flex-shrink-0">
<span className="text-xs font-mono bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded">
{m.canonical_control_code}
</span>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm text-gray-700 font-medium truncate">{m.canonical_title || m.canonical_control_code}</p>
{m.canonical_objective && showCanonical && (
<p className="text-xs text-gray-500 mt-0.5 line-clamp-2">{m.canonical_objective}</p>
)}
</div>
<div className="flex-shrink-0 flex items-center gap-1.5">
{m.canonical_severity && (
<span className={`text-xs px-1.5 py-0.5 rounded-full font-medium ${
m.canonical_severity === 'critical' ? 'bg-red-100 text-red-700' :
m.canonical_severity === 'high' ? 'bg-orange-100 text-orange-700' :
m.canonical_severity === 'medium' ? 'bg-yellow-100 text-yellow-700' :
'bg-gray-100 text-gray-600'
}`}>
{m.canonical_severity}
</span>
)}
<span className={`text-xs px-1.5 py-0.5 rounded-full ${
m.mapping_type === 'manual' ? 'bg-blue-100 text-blue-600' : 'bg-gray-100 text-gray-500'
}`}>
{m.mapping_type === 'manual' ? 'manuell' : 'auto'}
</span>
</div>
</div>
))}
</div>
{!showCanonical && canonicalMappings.length > 5 && (
<p className="text-xs text-gray-400 mt-2">
+ {canonicalMappings.length - 5} weitere Controls
</p>
)}
</div>
)}
{/* Framework Mappings */}
{control?.mappings && control.mappings.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">

View File

@@ -1,6 +1,6 @@
'use client'
import { useMemo, useState } from 'react'
import { useMemo, useState, useEffect, useCallback } from 'react'
import { DerivedTOM, TOMGeneratorState } from '@/lib/sdk/tom-generator/types'
import { getControlById, getControlsByCategory, getAllCategories } from '@/lib/sdk/tom-generator/controls/loader'
import { SDM_GOAL_LABELS, getSDMCoverageStats, SDMGewaehrleistungsziel } from '@/lib/sdk/tom-generator/sdm-mapping'
@@ -11,6 +11,18 @@ interface TOMOverviewTabProps {
onStartGenerator: () => void
}
interface MappingStats {
sync_state: {
profile_hash: string | null
total_mappings: number
canonical_controls_matched: number
tom_controls_covered: number
last_synced_at: string | null
}
category_breakdown: { tom_category: string; total_mappings: number; unique_controls: number }[]
total_canonical_controls_available: number
}
const STATUS_BADGES: Record<string, { label: string; className: string }> = {
IMPLEMENTED: { label: 'Implementiert', className: 'bg-green-100 text-green-700' },
PARTIAL: { label: 'Teilweise', className: 'bg-yellow-100 text-yellow-700' },
@@ -34,9 +46,41 @@ export function TOMOverviewTab({ state, onSelectTOM, onStartGenerator }: TOMOver
const [typeFilter, setTypeFilter] = useState<string>('ALL')
const [statusFilter, setStatusFilter] = useState<string>('ALL')
const [applicabilityFilter, setApplicabilityFilter] = useState<string>('ALL')
const [mappingStats, setMappingStats] = useState<MappingStats | null>(null)
const [syncing, setSyncing] = useState(false)
const categories = useMemo(() => getAllCategories(), [])
// Load mapping stats
useEffect(() => {
if (state.derivedTOMs.length === 0) return
fetch('/api/sdk/v1/compliance/tom-mappings/stats')
.then(r => r.ok ? r.json() : null)
.then(data => { if (data) setMappingStats(data) })
.catch(() => {})
}, [state.derivedTOMs.length])
const handleSyncControls = useCallback(async () => {
setSyncing(true)
try {
const resp = await fetch('/api/sdk/v1/compliance/tom-mappings/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
industry: state.companyProfile?.industry || null,
company_size: state.companyProfile?.size || null,
force: false,
}),
})
if (resp.ok) {
// Reload stats after sync
const statsResp = await fetch('/api/sdk/v1/compliance/tom-mappings/stats')
if (statsResp.ok) setMappingStats(await statsResp.json())
}
} catch { /* ignore */ }
setSyncing(false)
}, [state.companyProfile])
const stats = useMemo(() => {
const toms = state.derivedTOMs
return {
@@ -159,6 +203,59 @@ export function TOMOverviewTab({ state, onSelectTOM, onStartGenerator }: TOMOver
</div>
</div>
{/* Canonical Control Library Coverage */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-3">
<div>
<h3 className="text-sm font-semibold text-gray-700">Canonical Control Library</h3>
<p className="text-xs text-gray-500 mt-0.5">
Belegende Security-Controls aus OWASP, NIST, ENISA
</p>
</div>
<button
onClick={handleSyncControls}
disabled={syncing}
className="bg-purple-600 text-white hover:bg-purple-700 disabled:bg-gray-300 rounded-lg px-4 py-2 text-xs font-medium transition-colors"
>
{syncing ? 'Synchronisiere...' : 'Controls synchronisieren'}
</button>
</div>
{mappingStats ? (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<div className="bg-gray-50 rounded-lg p-3 text-center">
<div className="text-xl font-bold text-gray-900">{mappingStats.sync_state.total_mappings}</div>
<div className="text-xs text-gray-500">Zugeordnete Controls</div>
</div>
<div className="bg-gray-50 rounded-lg p-3 text-center">
<div className="text-xl font-bold text-purple-600">{mappingStats.sync_state.canonical_controls_matched}</div>
<div className="text-xs text-gray-500">Einzigartige Controls</div>
</div>
<div className="bg-gray-50 rounded-lg p-3 text-center">
<div className="text-xl font-bold text-gray-900">{mappingStats.sync_state.tom_controls_covered}/13</div>
<div className="text-xs text-gray-500">Kategorien abgedeckt</div>
</div>
<div className="bg-gray-50 rounded-lg p-3 text-center">
<div className="text-xl font-bold text-gray-900">{mappingStats.total_canonical_controls_available}</div>
<div className="text-xs text-gray-500">Verfuegbare Controls</div>
</div>
</div>
) : (
<div className="text-center py-4">
<p className="text-sm text-gray-400">
Noch keine Controls synchronisiert. Klicken Sie &quot;Controls synchronisieren&quot;, um relevante
Security-Controls aus der Canonical Control Library zuzuordnen.
</p>
</div>
)}
{mappingStats?.sync_state?.last_synced_at && (
<p className="text-xs text-gray-400 mt-2">
Letzte Synchronisation: {new Date(mappingStats.sync_state.last_synced_at).toLocaleDateString('de-DE', {
day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit'
})}
</p>
)}
</div>
{/* Filter Controls */}
<div className="bg-white rounded-xl border border-gray-200 p-4">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">

View File

@@ -1,3 +1,4 @@
export { TOMOverviewTab } from './TOMOverviewTab'
export { TOMEditorTab } from './TOMEditorTab'
export { TOMGapExportTab } from './TOMGapExportTab'
export { TOMDocumentTab } from './TOMDocumentTab'

View File

@@ -10,6 +10,8 @@ interface AssessmentResult {
dsfa_recommended: boolean
art22_risk: boolean
training_allowed: string
betrvg_conflict_score?: number
betrvg_consultation_required?: boolean
summary: string
recommendation: string
alternative_approach?: string
@@ -76,6 +78,21 @@ export function AssessmentResultCard({ result }: AssessmentResultCardProps) {
Art. 22 Risiko
</span>
)}
{result.betrvg_conflict_score != null && result.betrvg_conflict_score > 0 && (
<span className={`px-3 py-1 rounded-full text-sm font-medium ${
result.betrvg_conflict_score >= 75 ? 'bg-red-100 text-red-700' :
result.betrvg_conflict_score >= 50 ? 'bg-orange-100 text-orange-700' :
result.betrvg_conflict_score >= 25 ? 'bg-yellow-100 text-yellow-700' :
'bg-green-100 text-green-700'
}`}>
BR-Konflikt: {result.betrvg_conflict_score}/100
</span>
)}
{result.betrvg_consultation_required && (
<span className="px-3 py-1 rounded-full text-sm bg-purple-100 text-purple-700">
BR-Konsultation erforderlich
</span>
)}
</div>
<p className="text-gray-700">{result.summary}</p>
<p className="text-sm text-gray-500 mt-2">{result.recommendation}</p>

View File

@@ -0,0 +1,321 @@
'use client'
import { useRef, useState, useEffect, useCallback } from 'react'
import type {
InteractiveVideoManifest,
CheckpointEntry,
CheckpointQuizResult,
} from '@/lib/sdk/training/types'
import { submitCheckpointQuiz } from '@/lib/sdk/training/api'
interface Props {
manifest: InteractiveVideoManifest
assignmentId: string
onAllCheckpointsPassed?: () => void
}
export default function InteractiveVideoPlayer({ manifest, assignmentId, onAllCheckpointsPassed }: Props) {
const videoRef = useRef<HTMLVideoElement>(null)
const [currentCheckpoint, setCurrentCheckpoint] = useState<CheckpointEntry | null>(null)
const [showOverlay, setShowOverlay] = useState(false)
const [answers, setAnswers] = useState<Record<number, number>>({})
const [quizResult, setQuizResult] = useState<CheckpointQuizResult | null>(null)
const [submitting, setSubmitting] = useState(false)
const [passedCheckpoints, setPassedCheckpoints] = useState<Set<string>>(new Set())
const [currentTime, setCurrentTime] = useState(0)
const [duration, setDuration] = useState(0)
// Initialize passed checkpoints from manifest progress
useEffect(() => {
const passed = new Set<string>()
for (const cp of manifest.checkpoints) {
if (cp.progress?.passed) {
passed.add(cp.checkpoint_id)
}
}
setPassedCheckpoints(passed)
}, [manifest])
// Find next unpassed checkpoint
const getNextUnpassedCheckpoint = useCallback((): CheckpointEntry | null => {
for (const cp of manifest.checkpoints) {
if (!passedCheckpoints.has(cp.checkpoint_id)) {
return cp
}
}
return null
}, [manifest.checkpoints, passedCheckpoints])
// Time update handler — check for checkpoint triggers
const handleTimeUpdate = useCallback(() => {
if (!videoRef.current || showOverlay) return
const time = videoRef.current.currentTime
setCurrentTime(time)
for (const cp of manifest.checkpoints) {
if (passedCheckpoints.has(cp.checkpoint_id)) continue
// Trigger checkpoint when video reaches its timestamp (within 0.5s)
if (time >= cp.timestamp_seconds && time < cp.timestamp_seconds + 1.0) {
videoRef.current.pause()
setCurrentCheckpoint(cp)
setShowOverlay(true)
setAnswers({})
setQuizResult(null)
break
}
}
}, [manifest.checkpoints, passedCheckpoints, showOverlay])
// Seek protection — prevent skipping past unpassed checkpoints
const handleSeeking = useCallback(() => {
if (!videoRef.current) return
const seekTarget = videoRef.current.currentTime
const nextUnpassed = getNextUnpassedCheckpoint()
if (nextUnpassed && seekTarget > nextUnpassed.timestamp_seconds) {
videoRef.current.currentTime = nextUnpassed.timestamp_seconds - 0.5
}
}, [getNextUnpassedCheckpoint])
// Submit checkpoint quiz
async function handleSubmitQuiz() {
if (!currentCheckpoint) return
setSubmitting(true)
try {
const answerList = currentCheckpoint.questions.map((_, i) => answers[i] ?? -1)
const result = await submitCheckpointQuiz(
currentCheckpoint.checkpoint_id,
assignmentId,
answerList,
)
setQuizResult(result)
if (result.passed) {
setPassedCheckpoints(prev => {
const next = new Set(prev)
next.add(currentCheckpoint.checkpoint_id)
return next
})
}
} catch (e) {
console.error('Checkpoint quiz submission failed:', e)
} finally {
setSubmitting(false)
}
}
// Continue video after passing checkpoint
function handleContinue() {
setShowOverlay(false)
setCurrentCheckpoint(null)
setQuizResult(null)
setAnswers({})
if (videoRef.current) {
videoRef.current.play()
}
// Check if all checkpoints passed
const allPassed = manifest.checkpoints.every(cp => passedCheckpoints.has(cp.checkpoint_id))
if (allPassed && onAllCheckpointsPassed) {
onAllCheckpointsPassed()
}
}
// Retry quiz
function handleRetry() {
setQuizResult(null)
setAnswers({})
}
// Resume to last unpassed checkpoint
useEffect(() => {
if (!videoRef.current || !manifest.checkpoints.length) return
const nextUnpassed = getNextUnpassedCheckpoint()
if (nextUnpassed && nextUnpassed.timestamp_seconds > 0) {
// Start a bit before the checkpoint
const startTime = Math.max(0, nextUnpassed.timestamp_seconds - 2)
videoRef.current.currentTime = startTime
}
}, []) // Only on mount
const handleLoadedMetadata = useCallback(() => {
if (videoRef.current) {
setDuration(videoRef.current.duration)
}
}, [])
// Progress bar percentage
const progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0
return (
<div className="relative bg-black rounded-lg overflow-hidden">
{/* Video element */}
<video
ref={videoRef}
className="w-full"
src={manifest.stream_url}
onTimeUpdate={handleTimeUpdate}
onSeeking={handleSeeking}
onLoadedMetadata={handleLoadedMetadata}
controls={!showOverlay}
/>
{/* Custom progress bar with checkpoint markers */}
<div className="relative h-2 bg-gray-700">
{/* Progress fill */}
<div
className="h-full bg-indigo-500 transition-all"
style={{ width: `${progressPercent}%` }}
/>
{/* Checkpoint markers */}
{manifest.checkpoints.map(cp => {
const pos = duration > 0 ? (cp.timestamp_seconds / duration) * 100 : 0
const isPassed = passedCheckpoints.has(cp.checkpoint_id)
return (
<div
key={cp.checkpoint_id}
className={`absolute top-1/2 -translate-y-1/2 w-3 h-3 rounded-full border-2 border-white ${
isPassed ? 'bg-green-500' : 'bg-red-500'
}`}
style={{ left: `${pos}%` }}
title={`${cp.title} (${isPassed ? 'Bestanden' : 'Ausstehend'})`}
/>
)
})}
</div>
{/* Checkpoint overlay */}
{showOverlay && currentCheckpoint && (
<div className="absolute inset-0 bg-black/80 flex items-center justify-center p-6 overflow-y-auto">
<div className="bg-white rounded-xl p-6 max-w-2xl w-full max-h-[90%] overflow-y-auto">
{/* Header */}
<div className="flex items-center gap-3 mb-4 pb-3 border-b border-gray-200">
<div className="w-8 h-8 bg-red-100 rounded-full flex items-center justify-center">
<span className="text-red-600 font-bold text-sm">
{currentCheckpoint.index + 1}
</span>
</div>
<div>
<h3 className="font-semibold text-gray-900">Checkpoint: {currentCheckpoint.title}</h3>
<p className="text-xs text-gray-500">
Beantworten Sie die Fragen, um fortzufahren
</p>
</div>
</div>
{quizResult ? (
/* Quiz result */
<div>
<div className={`text-center p-6 rounded-lg mb-4 ${
quizResult.passed ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'
}`}>
<div className="text-3xl mb-2">{quizResult.passed ? '\u2705' : '\u274C'}</div>
<h4 className="text-lg font-bold mb-1">
{quizResult.passed ? 'Checkpoint bestanden!' : 'Nicht bestanden'}
</h4>
<p className="text-sm text-gray-600">
Ergebnis: {Math.round(quizResult.score)}%
</p>
</div>
{/* Feedback */}
<div className="space-y-3 mb-4">
{quizResult.feedback.map((fb, i) => (
<div key={i} className={`p-3 rounded-lg text-sm ${
fb.correct ? 'bg-green-50 border-l-4 border-green-400' : 'bg-red-50 border-l-4 border-red-400'
}`}>
<p className="font-medium">{fb.question}</p>
{!fb.correct && (
<p className="text-gray-600 mt-1">{fb.explanation}</p>
)}
</div>
))}
</div>
{quizResult.passed ? (
<button
onClick={handleContinue}
className="w-full px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700"
>
Video fortsetzen
</button>
) : (
<button
onClick={handleRetry}
className="w-full px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
Erneut versuchen
</button>
)}
</div>
) : (
/* Quiz questions */
<div>
<div className="space-y-4">
{currentCheckpoint.questions.map((q, qIdx) => (
<div key={qIdx} className="bg-gray-50 rounded-lg p-4">
<p className="font-medium text-gray-900 mb-3 text-sm">
<span className="text-indigo-600 mr-1">Frage {qIdx + 1}.</span>
{q.question}
</p>
<div className="space-y-2">
{q.options.map((opt, oIdx) => (
<label
key={oIdx}
className={`flex items-center gap-3 p-2.5 rounded-lg border cursor-pointer transition-colors text-sm ${
answers[qIdx] === oIdx
? 'border-indigo-500 bg-indigo-50'
: 'border-gray-200 hover:bg-white'
}`}
>
<input
type="radio"
name={`checkpoint-q-${qIdx}`}
checked={answers[qIdx] === oIdx}
onChange={() => setAnswers(prev => ({ ...prev, [qIdx]: oIdx }))}
className="text-indigo-600"
/>
<span className="text-gray-700">{opt}</span>
</label>
))}
</div>
</div>
))}
</div>
<button
onClick={handleSubmitQuiz}
disabled={submitting || Object.keys(answers).length < currentCheckpoint.questions.length}
className="w-full mt-4 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50"
>
{submitting ? 'Wird ausgewertet...' : `Antworten absenden (${Object.keys(answers).length}/${currentCheckpoint.questions.length})`}
</button>
</div>
)}
</div>
</div>
)}
{/* Checkpoint status bar */}
<div className="bg-gray-800 px-4 py-2 flex items-center gap-2 text-xs text-gray-300">
<span>Checkpoints:</span>
{manifest.checkpoints.map(cp => (
<span
key={cp.checkpoint_id}
className={`px-2 py-0.5 rounded-full ${
passedCheckpoints.has(cp.checkpoint_id)
? 'bg-green-700 text-green-100'
: 'bg-gray-600 text-gray-300'
}`}
>
{cp.title}
</span>
))}
{manifest.checkpoints.length > 0 && (
<span className="ml-auto">
{passedCheckpoints.size}/{manifest.checkpoints.length} bestanden
</span>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,510 @@
/**
* Integration Tests: Company Profile → Compliance Scope → VVT Generator
*
* Tests the complete data pipeline from Company Profile master data
* through the Compliance Scope Engine to VVT activity generation.
*/
import { describe, it, expect } from 'vitest'
import {
prefillFromCompanyProfile,
exportToVVTAnswers,
getAutoFilledScoringAnswers,
SCOPE_QUESTION_BLOCKS,
} from '../compliance-scope-profiling'
import {
generateActivities,
PROFILING_QUESTIONS,
DEPARTMENT_DATA_CATEGORIES,
SCOPE_PREFILLED_VVT_QUESTIONS,
} from '../vvt-profiling'
import type { ScopeProfilingAnswer } from '../compliance-scope-types'
// Helper
function ans(questionId: string, value: unknown): ScopeProfilingAnswer {
return { questionId, value } as ScopeProfilingAnswer
}
// =============================================================================
// 1. Company Profile → Scope Prefill
// =============================================================================
describe('CompanyProfile → Scope prefill', () => {
it('prefills org_has_dsb when dpoName is set', () => {
const profile = { dpoName: 'Max Mustermann' } as any
const answers = prefillFromCompanyProfile(profile)
expect(answers.find((a) => a.questionId === 'org_has_dsb')?.value).toBe(true)
})
it('does NOT prefill org_has_dsb when dpoName is empty', () => {
const profile = { dpoName: '' } as any
const answers = prefillFromCompanyProfile(profile)
expect(answers.find((a) => a.questionId === 'org_has_dsb')).toBeUndefined()
})
it('maps offerings to prod_type correctly', () => {
const profile = { offerings: ['WebApp', 'SaaS', 'API'] } as any
const answers = prefillFromCompanyProfile(profile)
const prodType = answers.find((a) => a.questionId === 'prod_type')
expect(prodType?.value).toEqual(expect.arrayContaining(['webapp', 'saas', 'api']))
})
it('detects webshop in offerings', () => {
const profile = { offerings: ['Webshop'] } as any
const answers = prefillFromCompanyProfile(profile)
expect(answers.find((a) => a.questionId === 'prod_webshop')?.value).toBe(true)
})
it('returns empty array when profile has no relevant data', () => {
const profile = {} as any
const answers = prefillFromCompanyProfile(profile)
expect(answers).toEqual([])
})
})
describe('CompanyProfile → Scope scoring answers', () => {
it('maps employeeCount to org_employee_count', () => {
const profile = { employeeCount: '50-249' } as any
const answers = getAutoFilledScoringAnswers(profile)
expect(answers.find((a) => a.questionId === 'org_employee_count')?.value).toBe('50-249')
})
it('maps industry to org_industry', () => {
const profile = { industry: ['IT & Software', 'Finanzdienstleistungen'] } as any
const answers = getAutoFilledScoringAnswers(profile)
expect(answers.find((a) => a.questionId === 'org_industry')?.value).toBe(
'IT & Software, Finanzdienstleistungen'
)
})
it('maps annualRevenue to org_annual_revenue', () => {
const profile = { annualRevenue: '1-10M' } as any
const answers = getAutoFilledScoringAnswers(profile)
expect(answers.find((a) => a.questionId === 'org_annual_revenue')?.value).toBe('1-10M')
})
it('maps businessModel to org_business_model', () => {
const profile = { businessModel: 'B2B' } as any
const answers = getAutoFilledScoringAnswers(profile)
expect(answers.find((a) => a.questionId === 'org_business_model')?.value).toBe('B2B')
})
})
// =============================================================================
// 2. Scope → VVT Answer Mapping (exportToVVTAnswers)
// =============================================================================
describe('Scope → VVT answer export', () => {
it('maps scope questions with mapsToVVTQuestion property', () => {
// Block 9: dk_dept_hr maps to dept_hr_categories
const scopeAnswers: ScopeProfilingAnswer[] = [
ans('dk_dept_hr', ['NAME', 'SALARY_DATA', 'HEALTH_DATA']),
]
const vvtAnswers = exportToVVTAnswers(scopeAnswers)
expect(vvtAnswers.dept_hr_categories).toEqual(['NAME', 'SALARY_DATA', 'HEALTH_DATA'])
})
it('maps multiple department data categories', () => {
const scopeAnswers: ScopeProfilingAnswer[] = [
ans('dk_dept_hr', ['NAME', 'BANK_ACCOUNT']),
ans('dk_dept_finance', ['INVOICE_DATA', 'TAX_ID']),
ans('dk_dept_marketing', ['EMAIL', 'TRACKING_DATA']),
]
const vvtAnswers = exportToVVTAnswers(scopeAnswers)
expect(vvtAnswers.dept_hr_categories).toEqual(['NAME', 'BANK_ACCOUNT'])
expect(vvtAnswers.dept_finance_categories).toEqual(['INVOICE_DATA', 'TAX_ID'])
expect(vvtAnswers.dept_marketing_categories).toEqual(['EMAIL', 'TRACKING_DATA'])
})
it('ignores scope questions without mapsToVVTQuestion', () => {
const scopeAnswers: ScopeProfilingAnswer[] = [
ans('vvt_has_vvt', true), // No mapsToVVTQuestion property
]
const vvtAnswers = exportToVVTAnswers(scopeAnswers)
expect(Object.keys(vvtAnswers)).toHaveLength(0)
})
it('handles empty scope answers', () => {
const vvtAnswers = exportToVVTAnswers([])
expect(vvtAnswers).toEqual({})
})
})
// =============================================================================
// 3. Scope → VVT Profiling Prefill
// Note: prefillFromScopeAnswers() uses dynamic require('./compliance-scope-profiling')
// which doesn't resolve in vitest. We test the same pipeline by calling
// exportToVVTAnswers() directly (which is what prefillFromScopeAnswers wraps).
// =============================================================================
describe('Scope → VVT Profiling Prefill (via exportToVVTAnswers)', () => {
it('converts scope answers to VVT ProfilingAnswers format', () => {
const scopeAnswers: ScopeProfilingAnswer[] = [
ans('dk_dept_hr', ['NAME', 'HEALTH_DATA']),
ans('dk_dept_finance', ['BANK_ACCOUNT']),
]
const exported = exportToVVTAnswers(scopeAnswers)
// Same transformation as prefillFromScopeAnswers
const profiling: Record<string, unknown> = {}
for (const [key, value] of Object.entries(exported)) {
if (value !== undefined && value !== null) profiling[key] = value
}
expect(profiling.dept_hr_categories).toEqual(['NAME', 'HEALTH_DATA'])
expect(profiling.dept_finance_categories).toEqual(['BANK_ACCOUNT'])
})
it('filters out null/undefined values', () => {
const scopeAnswers: ScopeProfilingAnswer[] = [ans('dk_dept_hr', null)]
const exported = exportToVVTAnswers(scopeAnswers)
const profiling: Record<string, unknown> = {}
for (const [key, value] of Object.entries(exported)) {
if (value !== undefined && value !== null) profiling[key] = value
}
expect(profiling.dept_hr_categories).toBeUndefined()
})
})
// =============================================================================
// 4. VVT Generator — generateActivities
// =============================================================================
describe('generateActivities', () => {
it('always generates 4 IT baseline activities', () => {
const result = generateActivities({})
const names = result.generatedActivities.map((a) => a.name)
expect(result.generatedActivities.length).toBeGreaterThanOrEqual(4)
// IT baselines are always added
const itTemplates = result.generatedActivities.filter(
(a) => a.businessFunction === 'it_operations'
)
expect(itTemplates.length).toBeGreaterThanOrEqual(4)
})
it('triggers HR templates when dept_hr=true', () => {
const result = generateActivities({ dept_hr: true })
const hrActivities = result.generatedActivities.filter(
(a) => a.businessFunction === 'hr'
)
expect(hrActivities.length).toBeGreaterThanOrEqual(3) // mitarbeiter, gehalt, zeiterfassung
})
it('triggers finance templates when dept_finance=true', () => {
const result = generateActivities({ dept_finance: true })
const financeActivities = result.generatedActivities.filter(
(a) => a.businessFunction === 'finance'
)
expect(financeActivities.length).toBeGreaterThanOrEqual(2) // buchhaltung, zahlungsverkehr
})
it('enriches activities with US cloud third-country transfer', () => {
const result = generateActivities({ dept_hr: true, transfer_cloud_us: true })
// Every activity should have a US third-country transfer
for (const activity of result.generatedActivities) {
expect(activity.thirdCountryTransfers.some((t) => t.country === 'US')).toBe(true)
}
})
it('adds HEALTH_DATA to HR activities when data_health=true', () => {
const result = generateActivities({ dept_hr: true, data_health: true })
const hrActivities = result.generatedActivities.filter(
(a) => a.businessFunction === 'hr'
)
expect(hrActivities.length).toBeGreaterThan(0)
for (const hr of hrActivities) {
expect(hr.personalDataCategories).toContain('HEALTH_DATA')
}
})
it('calculates Art. 30 Abs. 5 exemption correctly', () => {
// < 250 employees, no special categories → exempt
const result1 = generateActivities({ org_employees: 50 })
expect(result1.art30Abs5Exempt).toBe(true)
// >= 250 employees → not exempt
const result2 = generateActivities({ org_employees: 500 })
expect(result2.art30Abs5Exempt).toBe(false)
// < 250 but with special categories → not exempt
const result3 = generateActivities({ org_employees: 50, data_health: true })
expect(result3.art30Abs5Exempt).toBe(false)
})
it('generates unique VVT IDs for all activities', () => {
const result = generateActivities({
dept_hr: true,
dept_finance: true,
dept_sales: true,
dept_marketing: true,
})
const ids = result.generatedActivities.map((a) => a.vvtId)
const uniqueIds = new Set(ids)
expect(uniqueIds.size).toBe(ids.length)
})
it('calculates coverage score > 0 for template-generated activities', () => {
const result = generateActivities({ dept_hr: true })
expect(result.coverageScore).toBeGreaterThan(0)
})
})
// =============================================================================
// 5. Full Pipeline: Company Profile → Scope → VVT
// =============================================================================
describe('Full Pipeline: CompanyProfile → Scope → VVT Generation', () => {
// Helper: replicate what prefillFromScopeAnswers does (avoiding dynamic require)
function scopeToProfilingAnswers(
scopeAnswers: ScopeProfilingAnswer[]
): Record<string, string | string[] | number | boolean> {
const exported = exportToVVTAnswers(scopeAnswers)
const profiling: Record<string, string | string[] | number | boolean> = {}
for (const [key, value] of Object.entries(exported)) {
if (value !== undefined && value !== null) {
profiling[key] = value as string | string[] | number | boolean
}
}
return profiling
}
it('complete flow: profile with DSB → scope prefill → VVT generation', () => {
// Step 1: Company Profile
const profile = {
dpoName: 'Dr. Datenschutz',
employeeCount: '50-249',
industry: ['IT & Software'],
offerings: ['WebApp', 'SaaS'],
} as any
// Step 2: Prefill scope from profile
const profileAnswers = prefillFromCompanyProfile(profile)
const scoringAnswers = getAutoFilledScoringAnswers(profile)
// Simulate user answering scope questions + auto-prefilled from profile
const userAnswers: ScopeProfilingAnswer[] = [
// Block 8: departments
ans('vvt_departments', ['personal', 'finanzen', 'it']),
// Block 9: data categories per department
ans('dk_dept_hr', ['NAME', 'ADDRESS', 'SALARY_DATA', 'HEALTH_DATA']),
ans('dk_dept_finance', ['NAME', 'BANK_ACCOUNT', 'INVOICE_DATA', 'TAX_ID']),
ans('dk_dept_it', ['USER_ACCOUNTS', 'LOG_DATA', 'DEVICE_DATA']),
// Block 2: data types
ans('data_art9', true),
ans('data_minors', false),
]
const allScopeAnswers = [...profileAnswers, ...scoringAnswers, ...userAnswers]
// Step 3: Export to VVT format
const vvtAnswers = exportToVVTAnswers(allScopeAnswers)
expect(vvtAnswers.dept_hr_categories).toEqual([
'NAME',
'ADDRESS',
'SALARY_DATA',
'HEALTH_DATA',
])
expect(vvtAnswers.dept_finance_categories).toEqual([
'NAME',
'BANK_ACCOUNT',
'INVOICE_DATA',
'TAX_ID',
])
// Step 4: Prefill VVT profiling from scope (via direct export)
const profilingAnswers = scopeToProfilingAnswers(allScopeAnswers)
// Verify data survived the transformation
expect(profilingAnswers.dept_hr_categories).toEqual([
'NAME',
'ADDRESS',
'SALARY_DATA',
'HEALTH_DATA',
])
// Step 5: Generate VVT activities
// Add department triggers that match Block 8 selections
profilingAnswers.dept_hr = true
profilingAnswers.dept_finance = true
const result = generateActivities(profilingAnswers)
// Verify activities were generated
expect(result.generatedActivities.length).toBeGreaterThan(4) // 4 IT baseline + HR + Finance
// Verify HR activities exist
const hrActivities = result.generatedActivities.filter(
(a) => a.businessFunction === 'hr'
)
expect(hrActivities.length).toBeGreaterThanOrEqual(3)
// Verify finance activities exist
const financeActivities = result.generatedActivities.filter(
(a) => a.businessFunction === 'finance'
)
expect(financeActivities.length).toBeGreaterThanOrEqual(2)
})
it('end-to-end: departments selected in scope generate correct VVT activities', () => {
// Simulate a complete scope session with department selections
const scopeAnswers: ScopeProfilingAnswer[] = [
// Block 2: data_art9 maps to data_health in VVT
ans('data_art9', true),
// Block 4: tech_third_country maps to transfer_cloud_us
ans('tech_third_country', true),
// Block 8: departments
ans('vvt_departments', ['personal', 'marketing', 'kundenservice']),
// Block 9: per-department data categories
ans('dk_dept_hr', ['NAME', 'HEALTH_DATA', 'RELIGIOUS_BELIEFS']),
ans('dk_dept_marketing', ['EMAIL', 'TRACKING_DATA', 'CONSENT_DATA']),
ans('dk_dept_support', ['NAME', 'TICKET_DATA', 'COMMUNICATION_DATA']),
]
// Transform to VVT answers
const vvtAnswers = exportToVVTAnswers(scopeAnswers)
// Verify Block 9 data categories are mapped correctly
expect(vvtAnswers.dept_hr_categories).toEqual(['NAME', 'HEALTH_DATA', 'RELIGIOUS_BELIEFS'])
expect(vvtAnswers.dept_marketing_categories).toEqual([
'EMAIL',
'TRACKING_DATA',
'CONSENT_DATA',
])
expect(vvtAnswers.dept_support_categories).toEqual([
'NAME',
'TICKET_DATA',
'COMMUNICATION_DATA',
])
// Verify the full pipeline using direct export
const profilingAnswers = scopeToProfilingAnswers(scopeAnswers)
expect(profilingAnswers.dept_hr_categories).toBeDefined()
expect(profilingAnswers.dept_marketing_categories).toBeDefined()
expect(profilingAnswers.dept_support_categories).toBeDefined()
})
})
// =============================================================================
// 6. DEPARTMENT_DATA_CATEGORIES Integrity
// =============================================================================
describe('DEPARTMENT_DATA_CATEGORIES consistency', () => {
it('all 12 departments are defined', () => {
const expected = [
'dept_hr',
'dept_recruiting',
'dept_finance',
'dept_sales',
'dept_marketing',
'dept_support',
'dept_it',
'dept_recht',
'dept_produktion',
'dept_logistik',
'dept_einkauf',
'dept_facility',
]
for (const dept of expected) {
expect(DEPARTMENT_DATA_CATEGORIES[dept]).toBeDefined()
expect(DEPARTMENT_DATA_CATEGORIES[dept].categories.length).toBeGreaterThan(0)
}
})
it('every department has a label and icon', () => {
for (const [, dept] of Object.entries(DEPARTMENT_DATA_CATEGORIES)) {
expect(dept.label).toBeTruthy()
expect(dept.icon).toBeTruthy()
}
})
it('every category has id and label', () => {
for (const [, dept] of Object.entries(DEPARTMENT_DATA_CATEGORIES)) {
for (const cat of dept.categories) {
expect(cat.id).toBeTruthy()
expect(cat.label).toBeTruthy()
expect(cat.info).toBeTruthy()
}
}
})
it('Art. 9 categories are correctly flagged', () => {
const art9Categories = [
{ dept: 'dept_hr', id: 'HEALTH_DATA' },
{ dept: 'dept_hr', id: 'RELIGIOUS_BELIEFS' },
{ dept: 'dept_recruiting', id: 'HEALTH_DATA' },
{ dept: 'dept_recht', id: 'CRIMINAL_DATA' },
{ dept: 'dept_produktion', id: 'HEALTH_DATA' },
{ dept: 'dept_facility', id: 'HEALTH_DATA' },
]
for (const { dept, id } of art9Categories) {
const cat = DEPARTMENT_DATA_CATEGORIES[dept].categories.find((c) => c.id === id)
expect(cat?.isArt9).toBe(true)
}
})
})
// =============================================================================
// 7. Block 9 ↔ VVT Mapping Integrity
// =============================================================================
describe('Block 9 Scope ↔ VVT question mapping', () => {
it('every Block 9 question has mapsToVVTQuestion', () => {
const block9 = SCOPE_QUESTION_BLOCKS.find((b) => b.id === 'datenkategorien_detail')
expect(block9).toBeDefined()
for (const q of block9!.questions) {
expect(q.mapsToVVTQuestion).toBeTruthy()
expect(q.mapsToVVTQuestion).toMatch(/^dept_\w+_categories$/)
}
})
it('Block 9 question options match DEPARTMENT_DATA_CATEGORIES', () => {
const block9 = SCOPE_QUESTION_BLOCKS.find((b) => b.id === 'datenkategorien_detail')
expect(block9).toBeDefined()
// dk_dept_hr should have same options as DEPARTMENT_DATA_CATEGORIES.dept_hr
const hrQuestion = block9!.questions.find((q) => q.id === 'dk_dept_hr')
expect(hrQuestion).toBeDefined()
const expectedIds = DEPARTMENT_DATA_CATEGORIES.dept_hr.categories.map((c) => c.id)
const actualIds = hrQuestion!.options!.map((o) => o.value)
expect(actualIds).toEqual(expectedIds)
})
it('SCOPE_PREFILLED_VVT_QUESTIONS lists all cross-module questions', () => {
expect(SCOPE_PREFILLED_VVT_QUESTIONS).toContain('org_industry')
expect(SCOPE_PREFILLED_VVT_QUESTIONS).toContain('dept_hr')
expect(SCOPE_PREFILLED_VVT_QUESTIONS).toContain('data_health')
expect(SCOPE_PREFILLED_VVT_QUESTIONS).toContain('transfer_cloud_us')
expect(SCOPE_PREFILLED_VVT_QUESTIONS.length).toBeGreaterThanOrEqual(15)
})
})
// =============================================================================
// 8. Edge Cases
// =============================================================================
describe('Edge cases', () => {
it('generateActivities with no answers still produces IT baselines', () => {
const result = generateActivities({})
expect(result.generatedActivities.length).toBe(4) // 4 IT baselines
expect(result.art30Abs5Exempt).toBe(true) // 0 employees, no special categories
})
it('same template triggered by multiple questions is only generated once', () => {
const result = generateActivities({
dept_sales: true, // triggers sales-kundenverwaltung
sys_crm: true, // also triggers sales-kundenverwaltung
})
const salesKunden = result.generatedActivities.filter((a) =>
a.name.toLowerCase().includes('kundenverwaltung')
)
// Should be deduplicated (Set-based triggeredIds)
expect(salesKunden.length).toBe(1)
})
it('empty department category selections produce valid but empty mappings', () => {
const scopeAnswers: ScopeProfilingAnswer[] = [ans('dk_dept_hr', [])]
const vvtAnswers = exportToVVTAnswers(scopeAnswers)
expect(vvtAnswers.dept_hr_categories).toEqual([])
})
})

View File

@@ -1,9 +1,9 @@
/**
* Loeschfristen Baseline-Katalog
*
* 18 vordefinierte Aufbewahrungsfristen-Templates fuer gaengige
* 25 vordefinierte Aufbewahrungsfristen-Templates fuer gaengige
* Datenobjekte in deutschen Unternehmen. Basierend auf AO, HGB,
* UStG, BGB, ArbZG, AGG, BDSG und BSIG.
* UStG, BGB, ArbZG, AGG, BDSG, BSIG und ArbMedVV.
*
* Werden genutzt, um neue Loeschfrist-Policies schnell aus
* bewaehrten Vorlagen zu erstellen.
@@ -48,7 +48,7 @@ export interface BaselineTemplate {
}
// =============================================================================
// BASELINE TEMPLATES (18 Vorlagen)
// BASELINE TEMPLATES (25 Vorlagen)
// =============================================================================
export const BASELINE_TEMPLATES: BaselineTemplate[] = [
@@ -519,6 +519,188 @@ export const BASELINE_TEMPLATES: BaselineTemplate[] = [
reviewInterval: 'ANNUAL',
tags: ['datenschutz', 'consent'],
},
// ==================== 19. E-Mail-Archivierung ====================
{
templateId: 'email-archivierung',
dataObjectName: 'E-Mail-Archivierung',
description:
'Archivierte geschaeftliche E-Mails inkl. Anhaenge, die als Handelsbriefe oder steuerrelevante Korrespondenz einzustufen sind.',
affectedGroups: ['Mitarbeiter', 'Kunden', 'Lieferanten'],
dataCategories: ['E-Mail-Korrespondenz', 'Anhaenge', 'Metadaten'],
primaryPurpose:
'Erfuellung der handelsrechtlichen Aufbewahrungspflicht fuer geschaeftliche Korrespondenz, die als Handelsbrief einzuordnen ist.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'HGB_257',
retentionDriverDetail:
'Aufbewahrungspflicht gemaess 257 HGB fuer empfangene und versandte Handelsbriefe (6 Jahre) bzw. buchhalterisch relevante E-Mails (10 Jahre).',
retentionDuration: 6,
retentionUnit: 'YEARS',
retentionDescription: '6 Jahre nach Versand/Empfang der E-Mail',
startEvent: 'Versand- bzw. Empfangsdatum der E-Mail',
deletionMethod: 'AUTO_DELETE',
deletionMethodDetail:
'Automatische Loeschung durch das E-Mail-Archivierungssystem nach Ablauf der konfigurierten Aufbewahrungsfrist. Vor Loeschung wird geprueft, ob die E-Mail in laufenden Verfahren benoetigt wird.',
responsibleRole: 'IT-Abteilung',
reviewInterval: 'ANNUAL',
tags: ['kommunikation', 'hgb'],
},
// ==================== 20. Zutrittsprotokolle ====================
{
templateId: 'zutrittsprotokolle',
dataObjectName: 'Zutrittsprotokolle',
description:
'Protokolle des Zutrittskontrollsystems inkl. Zeitstempel, Kartennummer, Zutrittsort und Zugangsentscheidung (gewaehrt/verweigert).',
affectedGroups: ['Mitarbeiter', 'Besucher'],
dataCategories: ['Zutrittsdaten', 'Zeitstempel', 'Kartennummern', 'Standortdaten'],
primaryPurpose:
'Sicherstellung der physischen Sicherheit, Nachvollziehbarkeit von Zutritten und Unterstuetzung bei der Aufklaerung von Sicherheitsvorfaellen.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'BSIG',
retentionDriverDetail:
'Aufbewahrung gemaess BSI-Grundschutz-Empfehlung fuer Zutrittsprotokolle zur Analyse von Sicherheitsvorfaellen (90 Tage).',
retentionDuration: 90,
retentionUnit: 'DAYS',
retentionDescription: '90 Tage nach Zeitpunkt des Zutritts',
startEvent: 'Zeitpunkt des protokollierten Zutrittsereignisses',
deletionMethod: 'AUTO_DELETE',
deletionMethodDetail:
'Automatische Rotation und Loeschung der Zutrittsprotokolle durch das Zutrittskontrollsystem nach Ablauf der 90-Tage-Frist.',
responsibleRole: 'Facility Management',
reviewInterval: 'QUARTERLY',
tags: ['sicherheit', 'zutritt'],
},
// ==================== 21. Schulungsnachweise ====================
{
templateId: 'schulungsnachweise',
dataObjectName: 'Schulungsnachweise',
description:
'Teilnahmebestaetigungen, Zertifikate und Protokolle von Mitarbeiterschulungen (Datenschutz, Arbeitssicherheit, Compliance).',
affectedGroups: ['Mitarbeiter'],
dataCategories: ['Schulungsdaten', 'Zertifikate', 'Teilnahmelisten'],
primaryPurpose:
'Nachweis der Durchfuehrung gesetzlich vorgeschriebener Schulungen und Dokumentation der Mitarbeiterqualifikation.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'CUSTOM',
retentionDriverDetail:
'Aufbewahrung fuer 3 Jahre nach Ende des Beschaeftigungsverhaeltnisses als Nachweis der ordnungsgemaessen Schulungsdurchfuehrung.',
retentionDuration: 3,
retentionUnit: 'YEARS',
retentionDescription: '3 Jahre nach Ende des Beschaeftigungsverhaeltnisses',
startEvent: 'Ende des Beschaeftigungsverhaeltnisses des geschulten Mitarbeiters',
deletionMethod: 'MANUAL_REVIEW_DELETE',
deletionMethodDetail:
'Manuelle Pruefung durch die HR-Abteilung vor Loeschung, da Schulungsnachweise als Compliance-Nachweis in Audits relevant sein koennen.',
responsibleRole: 'HR-Abteilung',
reviewInterval: 'ANNUAL',
tags: ['hr', 'schulung'],
},
// ==================== 22. Betriebsarzt-Dokumentation ====================
{
templateId: 'betriebsarzt-doku',
dataObjectName: 'Betriebsarzt-Dokumentation',
description:
'Ergebnisse arbeitsmedizinischer Vorsorgeuntersuchungen, Eignungsuntersuchungen und arbeitsmedizinische Empfehlungen.',
affectedGroups: ['Mitarbeiter'],
dataCategories: ['Gesundheitsdaten', 'Vorsorgeuntersuchungen', 'Eignungsbefunde'],
primaryPurpose:
'Erfuellung der Dokumentationspflicht fuer arbeitsmedizinische Vorsorge gemaess ArbMedVV und Nachweisfuehrung gegenueber Berufsgenossenschaften.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'CUSTOM',
retentionDriverDetail:
'Aufbewahrungspflicht gemaess ArbMedVV (Verordnung zur arbeitsmedizinischen Vorsorge) und Berufsgenossenschaftliche Grundsaetze: bis zu 40 Jahre bei Exposition gegenueber krebserzeugenden Gefahrstoffen.',
retentionDuration: 40,
retentionUnit: 'YEARS',
retentionDescription: '40 Jahre nach letzter Exposition (bei Gefahrstoffen), sonst 10 Jahre nach Ende der Taetigkeit',
startEvent: 'Ende der expositionsrelevanten Taetigkeit bzw. Ende des Beschaeftigungsverhaeltnisses',
deletionMethod: 'PHYSICAL_DESTROY',
deletionMethodDetail:
'Physische Vernichtung der Papierunterlagen durch zertifizierten Aktenvernichtungsdienstleister (DIN 66399, Sicherheitsstufe P-5). Digitale Daten werden kryptographisch geloescht.',
responsibleRole: 'Betriebsarzt / Arbeitsmedizinischer Dienst',
reviewInterval: 'ANNUAL',
tags: ['hr', 'gesundheit'],
},
// ==================== 23. Kundenreklamationen ====================
{
templateId: 'kundenreklamationen',
dataObjectName: 'Kundenreklamationen',
description:
'Reklamationsvorgaenge inkl. Beschwerdeinhalt, Kommunikationsverlauf, Massnahmen und Ergebnis der Reklamationsbearbeitung.',
affectedGroups: ['Kunden'],
dataCategories: ['Reklamationsdaten', 'Kommunikation', 'Massnahmenprotokolle'],
primaryPurpose:
'Dokumentation und Bearbeitung von Kundenreklamationen, Qualitaetssicherung und Absicherung gegen Gewaehrleistungsansprueche.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'BGB_195',
retentionDriverDetail:
'Aufbewahrung fuer die Dauer der regelmaessigen Verjaehrungsfrist gemaess 195 BGB (3 Jahre) zur Absicherung gegen Gewaehrleistungs- und Schadensersatzansprueche.',
retentionDuration: 3,
retentionUnit: 'YEARS',
retentionDescription: '3 Jahre nach Abschluss des Reklamationsvorgangs',
startEvent: 'Abschluss des Reklamationsvorgangs (letzte Massnahme)',
deletionMethod: 'ANONYMIZATION',
deletionMethodDetail:
'Anonymisierung der personenbezogenen Daten nach Ablauf der Frist. Anonymisierte Reklamationsstatistiken bleiben fuer die Qualitaetssicherung erhalten.',
responsibleRole: 'Qualitaetsmanagement',
reviewInterval: 'ANNUAL',
tags: ['kunden', 'qualitaet'],
},
// ==================== 24. Lieferantenbewertungen ====================
{
templateId: 'lieferantenbewertungen',
dataObjectName: 'Lieferantenbewertungen',
description:
'Bewertungen und Auditergebnisse von Lieferanten und Auftragsverarbeitern inkl. Qualitaets-, Compliance- und Datenschutz-Bewertungen.',
affectedGroups: ['Lieferanten', 'Auftragsverarbeiter'],
dataCategories: ['Bewertungsdaten', 'Auditberichte', 'Vertragsdaten'],
primaryPurpose:
'Dokumentation der Sorgfaltspflicht bei der Auswahl und Ueberwachung von Auftragsverarbeitern gemaess Art. 28 DSGVO und Qualitaetssicherung in der Lieferkette.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'HGB_257',
retentionDriverDetail:
'Aufbewahrung gemaess 257 HGB als handelsrelevante Unterlagen sowie zur Nachweisfuehrung der Sorgfaltspflicht bei der Auftragsverarbeitung.',
retentionDuration: 6,
retentionUnit: 'YEARS',
retentionDescription: '6 Jahre nach Ende der Geschaeftsbeziehung',
startEvent: 'Ende der Geschaeftsbeziehung mit dem Lieferanten/Auftragsverarbeiter',
deletionMethod: 'MANUAL_REVIEW_DELETE',
deletionMethodDetail:
'Manuelle Pruefung durch den Einkauf/Compliance-Abteilung vor Loeschung, um sicherzustellen, dass keine Nachweispflichten aus laufenden Vertraegen oder Audits bestehen.',
responsibleRole: 'Einkauf / Compliance',
reviewInterval: 'ANNUAL',
tags: ['lieferanten', 'einkauf'],
},
// ==================== 25. Social-Media-Marketingdaten ====================
{
templateId: 'social-media-daten',
dataObjectName: 'Social-Media-Marketingdaten',
description:
'Personenbezogene Daten aus Social-Media-Kampagnen inkl. Nutzerinteraktionen, Custom Audiences, Retargeting-Listen und Kampagnen-Analytics.',
affectedGroups: ['Kunden', 'Interessenten', 'Website-Besucher'],
dataCategories: ['Interaktionsdaten', 'Zielgruppendaten', 'Tracking-Daten', 'Profilmerkmale'],
primaryPurpose:
'Durchfuehrung zielgerichteter Marketing-Kampagnen auf Social-Media-Plattformen und Analyse der Kampagneneffektivitaet.',
deletionTrigger: 'PURPOSE_END',
retentionDriver: null,
retentionDriverDetail:
'Keine gesetzliche Aufbewahrungspflicht. Daten werden bis zum Widerruf der Einwilligung bzw. bis zum Ende der Kampagne gespeichert (Art. 6 Abs. 1 lit. a DSGVO).',
retentionDuration: null,
retentionUnit: null,
retentionDescription: 'Bis zum Widerruf der Einwilligung oder Ende des Kampagnenzwecks',
startEvent: 'Widerruf der Einwilligung oder Ende der Marketing-Kampagne',
deletionMethod: 'AUTO_DELETE',
deletionMethodDetail:
'Automatische Loeschung der personenbezogenen Daten in den Social-Media-Werbekonten und internen Systemen nach Zweckwegfall. Custom Audiences werden bei Plattformanbietern geloescht.',
responsibleRole: 'Marketing',
reviewInterval: 'SEMI_ANNUAL',
tags: ['marketing', 'social'],
},
]
// =============================================================================

View File

@@ -6,8 +6,10 @@
import {
LoeschfristPolicy,
PolicyStatus,
RetentionDriverType,
isPolicyOverdue,
getActiveLegalHolds,
RETENTION_DRIVER_META,
} from './loeschfristen-types'
// =============================================================================
@@ -22,6 +24,10 @@ export type ComplianceIssueType =
| 'LEGAL_HOLD_CONFLICT'
| 'STALE_DRAFT'
| 'UNCOVERED_VVT_CATEGORY'
| 'MISSING_DELETION_METHOD'
| 'MISSING_STORAGE_LOCATIONS'
| 'EXCESSIVE_RETENTION'
| 'MISSING_DATA_CATEGORIES'
export type ComplianceIssueSeverity = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'
@@ -219,6 +225,108 @@ function checkStaleDraft(policy: LoeschfristPolicy): ComplianceIssue | null {
return null
}
/**
* Check 8: MISSING_DELETION_METHOD (MEDIUM)
* Active policy without a deletion method detail description.
*/
function checkMissingDeletionMethod(policy: LoeschfristPolicy): ComplianceIssue | null {
if (policy.status === 'ACTIVE' && !policy.deletionMethodDetail.trim()) {
return createIssue(
policy,
'MISSING_DELETION_METHOD',
'MEDIUM',
'Keine Loeschmethode beschrieben',
`Die aktive Policy "${policy.dataObjectName}" hat keine detaillierte Beschreibung der Loeschmethode. Fuer ein auditfaehiges Loeschkonzept muss dokumentiert sein, wie die Loeschung technisch durchgefuehrt wird.`,
'Ergaenzen Sie eine detaillierte Beschreibung der Loeschmethode (z.B. automatisches Loeschen durch Datenbank-Job, manuelle Pruefung durch Fachabteilung, kryptographische Loeschung).'
)
}
return null
}
/**
* Check 9: MISSING_STORAGE_LOCATIONS (MEDIUM)
* Active policy without any documented storage locations.
*/
function checkMissingStorageLocations(policy: LoeschfristPolicy): ComplianceIssue | null {
if (policy.status === 'ACTIVE' && policy.storageLocations.length === 0) {
return createIssue(
policy,
'MISSING_STORAGE_LOCATIONS',
'MEDIUM',
'Keine Speicherorte dokumentiert',
`Die aktive Policy "${policy.dataObjectName}" hat keine Speicherorte hinterlegt. Ohne Speicherort-Dokumentation ist unklar, wo die Daten gespeichert sind und wo die Loeschung durchgefuehrt werden muss.`,
'Dokumentieren Sie mindestens einen Speicherort (z.B. Datenbank, Cloud-Speicher, E-Mail-System, Papierarchiv).'
)
}
return null
}
/**
* Check 10: EXCESSIVE_RETENTION (HIGH)
* Retention duration exceeds 2x the legal default for the driver.
*/
function checkExcessiveRetention(policy: LoeschfristPolicy): ComplianceIssue | null {
if (
policy.retentionDriver &&
policy.retentionDriver !== 'CUSTOM' &&
policy.retentionDuration !== null &&
policy.retentionUnit !== null
) {
const meta = RETENTION_DRIVER_META[policy.retentionDriver]
if (meta.defaultDuration !== null && meta.defaultUnit !== null) {
// Normalize both to days for comparison
const policyDays = toDays(policy.retentionDuration, policy.retentionUnit)
const legalDays = toDays(meta.defaultDuration, meta.defaultUnit)
if (legalDays > 0 && policyDays > legalDays * 2) {
return createIssue(
policy,
'EXCESSIVE_RETENTION',
'HIGH',
'Ueberschreitung der gesetzlichen Aufbewahrungsfrist',
`Die Policy "${policy.dataObjectName}" hat eine Aufbewahrungsdauer von ${policy.retentionDuration} ${policy.retentionUnit === 'YEARS' ? 'Jahren' : policy.retentionUnit === 'MONTHS' ? 'Monaten' : 'Tagen'}, die mehr als das Doppelte der gesetzlichen Frist (${meta.defaultDuration} ${meta.defaultUnit === 'YEARS' ? 'Jahre' : meta.defaultUnit === 'MONTHS' ? 'Monate' : 'Tage'} nach ${meta.statute}) betraegt. Ueberlange Speicherung widerspricht dem Grundsatz der Speicherbegrenzung (Art. 5 Abs. 1 lit. e DSGVO).`,
'Pruefen Sie, ob die verlaengerte Aufbewahrungsdauer gerechtfertigt ist. Falls nicht, reduzieren Sie sie auf die gesetzliche Mindestfrist.'
)
}
}
}
return null
}
/**
* Check 11: MISSING_DATA_CATEGORIES (LOW)
* Non-draft policy without any data categories assigned.
*/
function checkMissingDataCategories(policy: LoeschfristPolicy): ComplianceIssue | null {
if (policy.status !== 'DRAFT' && policy.dataCategories.length === 0) {
return createIssue(
policy,
'MISSING_DATA_CATEGORIES',
'LOW',
'Keine Datenkategorien zugeordnet',
`Die Policy "${policy.dataObjectName}" (Status: ${policy.status}) hat keine Datenkategorien zugeordnet. Ohne Datenkategorien ist unklar, welche personenbezogenen Daten von dieser Loeschregel betroffen sind.`,
'Ordnen Sie mindestens eine Datenkategorie zu (z.B. Stammdaten, Kontaktdaten, Finanzdaten, Gesundheitsdaten).'
)
}
return null
}
/**
* Helper: convert retention duration to days for comparison.
*/
function toDays(duration: number, unit: string): number {
switch (unit) {
case 'DAYS': return duration
case 'MONTHS': return duration * 30
case 'YEARS': return duration * 365
default: return duration
}
}
// =============================================================================
// MAIN COMPLIANCE CHECK
// =============================================================================
@@ -248,6 +356,10 @@ export function runComplianceCheck(
checkNoResponsible(policy),
checkLegalHoldConflict(policy),
checkStaleDraft(policy),
checkMissingDeletionMethod(policy),
checkMissingStorageLocations(policy),
checkExcessiveRetention(policy),
checkMissingDataCategories(policy),
]
for (const issue of checks) {

View File

@@ -0,0 +1,879 @@
// =============================================================================
// Loeschfristen Module - Loeschkonzept Document Generator
// Generates a printable, audit-ready HTML document according to DSGVO Art. 5/17/30
// =============================================================================
import type {
LoeschfristPolicy,
RetentionDriverType,
} from './loeschfristen-types'
import {
RETENTION_DRIVER_META,
DELETION_METHOD_LABELS,
STATUS_LABELS,
TRIGGER_LABELS,
REVIEW_INTERVAL_LABELS,
formatRetentionDuration,
getEffectiveDeletionTrigger,
getActiveLegalHolds,
} from './loeschfristen-types'
import type { ComplianceCheckResult, ComplianceIssueSeverity } from './loeschfristen-compliance'
// =============================================================================
// TYPES
// =============================================================================
export interface LoeschkonzeptOrgHeader {
organizationName: string
industry: string
dpoName: string
dpoContact: string
responsiblePerson: string
locations: string[]
employeeCount: string
loeschkonzeptVersion: string
lastReviewDate: string
nextReviewDate: string
reviewInterval: string
}
export interface LoeschkonzeptRevision {
version: string
date: string
author: string
changes: string
}
// =============================================================================
// DEFAULTS
// =============================================================================
export function createDefaultLoeschkonzeptOrgHeader(): LoeschkonzeptOrgHeader {
const now = new Date()
const nextYear = new Date()
nextYear.setFullYear(nextYear.getFullYear() + 1)
return {
organizationName: '',
industry: '',
dpoName: '',
dpoContact: '',
responsiblePerson: '',
locations: [],
employeeCount: '',
loeschkonzeptVersion: '1.0',
lastReviewDate: now.toISOString().split('T')[0],
nextReviewDate: nextYear.toISOString().split('T')[0],
reviewInterval: 'Jaehrlich',
}
}
// =============================================================================
// SEVERITY LABELS (for Compliance Status section)
// =============================================================================
const SEVERITY_LABELS_DE: Record<ComplianceIssueSeverity, string> = {
CRITICAL: 'Kritisch',
HIGH: 'Hoch',
MEDIUM: 'Mittel',
LOW: 'Niedrig',
}
const SEVERITY_COLORS: Record<ComplianceIssueSeverity, string> = {
CRITICAL: '#dc2626',
HIGH: '#ea580c',
MEDIUM: '#d97706',
LOW: '#6b7280',
}
// =============================================================================
// HTML DOCUMENT BUILDER
// =============================================================================
export function buildLoeschkonzeptHtml(
policies: LoeschfristPolicy[],
orgHeader: LoeschkonzeptOrgHeader,
vvtActivities: Array<{ id: string; vvt_id?: string; vvtId?: string; name?: string; activity_name?: string }>,
complianceResult: ComplianceCheckResult | null,
revisions: LoeschkonzeptRevision[]
): string {
const today = new Date().toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
const activePolicies = policies.filter(p => p.status !== 'ARCHIVED')
const orgName = orgHeader.organizationName || 'Organisation'
// Collect unique storage locations across all policies
const allStorageLocations = new Set<string>()
for (const p of activePolicies) {
for (const loc of p.storageLocations) {
allStorageLocations.add(loc.name || loc.type)
}
}
// Collect unique responsible roles
const roleMap = new Map<string, string[]>()
for (const p of activePolicies) {
const role = p.responsibleRole || p.responsiblePerson || 'Nicht zugewiesen'
if (!roleMap.has(role)) roleMap.set(role, [])
roleMap.get(role)!.push(p.dataObjectName || p.policyId)
}
// Collect active legal holds
const allActiveLegalHolds: Array<{ policy: string; hold: LoeschfristPolicy['legalHolds'][0] }> = []
for (const p of activePolicies) {
for (const h of getActiveLegalHolds(p)) {
allActiveLegalHolds.push({ policy: p.dataObjectName || p.policyId, hold: h })
}
}
// Build VVT cross-reference data
const vvtRefs: Array<{ policyName: string; policyId: string; vvtId: string; vvtName: string }> = []
for (const p of activePolicies) {
for (const linkedId of p.linkedVVTActivityIds) {
const activity = vvtActivities.find(a => a.id === linkedId)
if (activity) {
vvtRefs.push({
policyName: p.dataObjectName || p.policyId,
policyId: p.policyId,
vvtId: activity.vvt_id || activity.vvtId || linkedId.substring(0, 8),
vvtName: activity.activity_name || activity.name || 'Unbenannte Verarbeitungstaetigkeit',
})
}
}
}
// Build vendor cross-reference data
const vendorRefs: Array<{ policyName: string; policyId: string; vendorId: string; duration: string }> = []
for (const p of activePolicies) {
if (p.linkedVendorIds && p.linkedVendorIds.length > 0) {
for (const vendorId of p.linkedVendorIds) {
vendorRefs.push({
policyName: p.dataObjectName || p.policyId,
policyId: p.policyId,
vendorId,
duration: formatRetentionDuration(p.retentionDuration, p.retentionUnit),
})
}
}
}
// =========================================================================
// HTML Template
// =========================================================================
let html = `<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Loeschkonzept — ${escHtml(orgName)}</title>
<style>
@page { size: A4; margin: 20mm 18mm 22mm 18mm; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 10pt;
line-height: 1.5;
color: #1e293b;
}
/* Cover */
.cover {
min-height: 90vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
page-break-after: always;
}
.cover h1 {
font-size: 28pt;
color: #5b21b6;
margin-bottom: 8px;
font-weight: 700;
}
.cover .subtitle {
font-size: 14pt;
color: #7c3aed;
margin-bottom: 40px;
}
.cover .org-info {
background: #f5f3ff;
border: 1px solid #ddd6fe;
border-radius: 8px;
padding: 24px 40px;
text-align: left;
width: 400px;
margin-bottom: 24px;
}
.cover .org-info div { margin-bottom: 6px; }
.cover .org-info .label { font-weight: 600; color: #5b21b6; display: inline-block; min-width: 160px; }
.cover .legal-ref {
font-size: 9pt;
color: #64748b;
margin-top: 20px;
}
/* TOC */
.toc {
page-break-after: always;
padding-top: 40px;
}
.toc h2 {
font-size: 18pt;
color: #5b21b6;
margin-bottom: 20px;
border-bottom: 2px solid #5b21b6;
padding-bottom: 8px;
}
.toc-entry {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px dotted #cbd5e1;
font-size: 10pt;
}
.toc-entry .toc-num { font-weight: 600; color: #5b21b6; min-width: 40px; }
/* Sections */
.section {
page-break-inside: avoid;
margin-bottom: 24px;
}
.section-header {
font-size: 14pt;
color: #5b21b6;
font-weight: 700;
margin: 30px 0 12px 0;
border-bottom: 2px solid #ddd6fe;
padding-bottom: 6px;
}
.section-body { margin-bottom: 16px; }
/* Tables */
table {
width: 100%;
border-collapse: collapse;
margin: 10px 0 16px 0;
font-size: 9pt;
}
th, td {
border: 1px solid #e2e8f0;
padding: 6px 8px;
text-align: left;
vertical-align: top;
}
th {
background: #f5f3ff;
color: #5b21b6;
font-weight: 600;
font-size: 8.5pt;
text-transform: uppercase;
letter-spacing: 0.3px;
}
tr:nth-child(even) td { background: #faf5ff; }
/* Detail cards */
.policy-detail {
page-break-inside: avoid;
border: 1px solid #e2e8f0;
border-radius: 6px;
margin-bottom: 16px;
overflow: hidden;
}
.policy-detail-header {
background: #f5f3ff;
padding: 8px 12px;
font-weight: 700;
color: #5b21b6;
border-bottom: 1px solid #ddd6fe;
display: flex;
justify-content: space-between;
}
.policy-detail-body { padding: 0; }
.policy-detail-body table { margin: 0; }
.policy-detail-body th { width: 200px; }
/* Badges */
.badge {
display: inline-block;
padding: 1px 8px;
border-radius: 9999px;
font-size: 8pt;
font-weight: 600;
}
.badge-active { background: #dcfce7; color: #166534; }
.badge-draft { background: #f3f4f6; color: #374151; }
.badge-review { background: #fef9c3; color: #854d0e; }
.badge-critical { background: #fecaca; color: #991b1b; }
.badge-high { background: #fed7aa; color: #9a3412; }
.badge-medium { background: #fef3c7; color: #92400e; }
.badge-low { background: #f3f4f6; color: #4b5563; }
/* Principles */
.principle {
margin-bottom: 10px;
padding-left: 20px;
position: relative;
}
.principle::before {
content: '';
position: absolute;
left: 0;
top: 6px;
width: 10px;
height: 10px;
background: #7c3aed;
border-radius: 50%;
}
.principle strong { color: #5b21b6; }
/* Score */
.score-box {
display: inline-block;
padding: 4px 16px;
border-radius: 8px;
font-size: 18pt;
font-weight: 700;
margin-right: 12px;
}
.score-excellent { background: #dcfce7; color: #166534; }
.score-good { background: #dbeafe; color: #1e40af; }
.score-needs-work { background: #fef3c7; color: #92400e; }
.score-poor { background: #fecaca; color: #991b1b; }
/* Footer */
.page-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 8px 18mm;
font-size: 7.5pt;
color: #94a3b8;
display: flex;
justify-content: space-between;
border-top: 1px solid #e2e8f0;
}
/* Print */
@media print {
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.no-print { display: none !important; }
.page-break { page-break-after: always; }
}
</style>
</head>
<body>
`
// =========================================================================
// Section 0: Cover Page
// =========================================================================
html += `
<div class="cover">
<h1>Loeschkonzept</h1>
<div class="subtitle">gemaess Art. 5 Abs. 1 lit. e, Art. 17, Art. 30 DSGVO</div>
<div class="org-info">
<div><span class="label">Organisation:</span> ${escHtml(orgName)}</div>
${orgHeader.industry ? `<div><span class="label">Branche:</span> ${escHtml(orgHeader.industry)}</div>` : ''}
${orgHeader.dpoName ? `<div><span class="label">Datenschutzbeauftragter:</span> ${escHtml(orgHeader.dpoName)}</div>` : ''}
${orgHeader.dpoContact ? `<div><span class="label">DSB-Kontakt:</span> ${escHtml(orgHeader.dpoContact)}</div>` : ''}
${orgHeader.responsiblePerson ? `<div><span class="label">Verantwortlicher:</span> ${escHtml(orgHeader.responsiblePerson)}</div>` : ''}
${orgHeader.employeeCount ? `<div><span class="label">Mitarbeiter:</span> ${escHtml(orgHeader.employeeCount)}</div>` : ''}
${orgHeader.locations.length > 0 ? `<div><span class="label">Standorte:</span> ${escHtml(orgHeader.locations.join(', '))}</div>` : ''}
</div>
<div class="legal-ref">
Version ${escHtml(orgHeader.loeschkonzeptVersion)} | Stand: ${today}<br/>
Letzte Pruefung: ${formatDateDE(orgHeader.lastReviewDate)} | Naechste Pruefung: ${formatDateDE(orgHeader.nextReviewDate)}<br/>
Pruefintervall: ${escHtml(orgHeader.reviewInterval)}
</div>
</div>
`
// =========================================================================
// Table of Contents
// =========================================================================
const sections = [
'Ziel und Zweck',
'Geltungsbereich',
'Grundprinzipien der Datenspeicherung',
'Loeschregeln-Uebersicht',
'Detaillierte Loeschregeln',
'VVT-Verknuepfung',
'Auftragsverarbeiter mit Loeschpflichten',
'Legal Hold Verfahren',
'Verantwortlichkeiten',
'Pruef- und Revisionszyklus',
'Compliance-Status',
'Aenderungshistorie',
]
html += `
<div class="toc">
<h2>Inhaltsverzeichnis</h2>
${sections.map((s, i) => `<div class="toc-entry"><span><span class="toc-num">${i + 1}.</span> ${escHtml(s)}</span></div>`).join('\n ')}
</div>
`
// =========================================================================
// Section 1: Ziel und Zweck
// =========================================================================
html += `
<div class="section">
<div class="section-header">1. Ziel und Zweck</div>
<div class="section-body">
<p>Dieses Loeschkonzept definiert die systematischen Regeln und Verfahren fuer die Loeschung
personenbezogener Daten bei <strong>${escHtml(orgName)}</strong>. Es dient der Umsetzung
folgender DSGVO-Anforderungen:</p>
<table>
<tr><th>Rechtsgrundlage</th><th>Inhalt</th></tr>
<tr><td><strong>Art. 5 Abs. 1 lit. e DSGVO</strong></td><td>Grundsatz der Speicherbegrenzung — personenbezogene Daten duerfen nur so lange gespeichert werden, wie es fuer die Zwecke der Verarbeitung erforderlich ist.</td></tr>
<tr><td><strong>Art. 17 DSGVO</strong></td><td>Recht auf Loeschung (&bdquo;Recht auf Vergessenwerden&ldquo;) — Betroffene haben das Recht, die Loeschung ihrer Daten zu verlangen.</td></tr>
<tr><td><strong>Art. 30 DSGVO</strong></td><td>Verzeichnis von Verarbeitungstaetigkeiten — vorgesehene Fristen fuer die Loeschung der verschiedenen Datenkategorien muessen dokumentiert werden.</td></tr>
</table>
<p>Das Loeschkonzept ist fester Bestandteil des Datenschutz-Managementsystems und wird
regelmaessig ueberprueft und aktualisiert.</p>
</div>
</div>
`
// =========================================================================
// Section 2: Geltungsbereich
// =========================================================================
const storageListHtml = allStorageLocations.size > 0
? Array.from(allStorageLocations).map(s => `<li>${escHtml(s)}</li>`).join('')
: '<li><em>Keine Speicherorte dokumentiert</em></li>'
html += `
<div class="section">
<div class="section-header">2. Geltungsbereich</div>
<div class="section-body">
<p>Dieses Loeschkonzept gilt fuer alle personenbezogenen Daten, die von <strong>${escHtml(orgName)}</strong>
verarbeitet werden. Es umfasst <strong>${activePolicies.length}</strong> Loeschregeln fuer folgende Systeme und Speicherorte:</p>
<ul style="margin: 8px 0 8px 24px;">
${storageListHtml}
</ul>
<p>Saemtliche Verarbeitungstaetigkeiten, die im Verzeichnis von Verarbeitungstaetigkeiten (VVT)
erfasst sind, werden durch dieses Loeschkonzept abgedeckt.</p>
</div>
</div>
`
// =========================================================================
// Section 3: Grundprinzipien
// =========================================================================
html += `
<div class="section">
<div class="section-header">3. Grundprinzipien der Datenspeicherung</div>
<div class="section-body">
<div class="principle"><strong>Speicherbegrenzung:</strong> Personenbezogene Daten werden nur so lange gespeichert, wie es fuer den jeweiligen Verarbeitungszweck erforderlich ist (Art. 5 Abs. 1 lit. e DSGVO).</div>
<div class="principle"><strong>3-Level-Loeschlogik:</strong> Die Loeschung folgt einer dreistufigen Priorisierung: (1) Zweckende, (2) gesetzliche Aufbewahrungspflichten, (3) Legal Hold — jeweils mit der laengsten Frist als massgeblich.</div>
<div class="principle"><strong>Dokumentationspflicht:</strong> Jede Loeschregel ist dokumentiert mit Rechtsgrundlage, Frist, Loeschmethode und Verantwortlichkeit.</div>
<div class="principle"><strong>Regelmaessige Ueberpruefung:</strong> Alle Loeschregeln werden im definierten Intervall ueberprueft und bei Bedarf angepasst.</div>
<div class="principle"><strong>Datenschutz durch Technikgestaltung:</strong> Loeschmechanismen werden moeglichst automatisiert, um menschliche Fehler zu minimieren (Art. 25 DSGVO).</div>
</div>
</div>
`
// =========================================================================
// Section 4: Loeschregeln-Uebersicht
// =========================================================================
html += `
<div class="section page-break">
<div class="section-header">4. Loeschregeln-Uebersicht</div>
<div class="section-body">
<p>Die folgende Tabelle zeigt eine Uebersicht aller ${activePolicies.length} aktiven Loeschregeln:</p>
<table>
<tr>
<th>LF-Nr.</th>
<th>Datenobjekt</th>
<th>Loeschtrigger</th>
<th>Aufbewahrungsfrist</th>
<th>Loeschmethode</th>
<th>Status</th>
</tr>
`
for (const p of activePolicies) {
const trigger = TRIGGER_LABELS[getEffectiveDeletionTrigger(p)]
const duration = formatRetentionDuration(p.retentionDuration, p.retentionUnit)
const method = DELETION_METHOD_LABELS[p.deletionMethod]
const statusLabel = STATUS_LABELS[p.status]
const statusClass = p.status === 'ACTIVE' ? 'badge-active' : p.status === 'REVIEW_NEEDED' ? 'badge-review' : 'badge-draft'
html += ` <tr>
<td>${escHtml(p.policyId)}</td>
<td>${escHtml(p.dataObjectName)}</td>
<td>${escHtml(trigger)}</td>
<td>${escHtml(duration)}</td>
<td>${escHtml(method)}</td>
<td><span class="badge ${statusClass}">${escHtml(statusLabel)}</span></td>
</tr>
`
}
html += ` </table>
</div>
</div>
`
// =========================================================================
// Section 5: Detaillierte Loeschregeln
// =========================================================================
html += `
<div class="section">
<div class="section-header">5. Detaillierte Loeschregeln</div>
<div class="section-body">
`
for (const p of activePolicies) {
const trigger = TRIGGER_LABELS[getEffectiveDeletionTrigger(p)]
const duration = formatRetentionDuration(p.retentionDuration, p.retentionUnit)
const method = DELETION_METHOD_LABELS[p.deletionMethod]
const statusLabel = STATUS_LABELS[p.status]
const driverLabel = p.retentionDriver ? RETENTION_DRIVER_META[p.retentionDriver]?.label || p.retentionDriver : '-'
const driverStatute = p.retentionDriver ? RETENTION_DRIVER_META[p.retentionDriver]?.statute || '' : ''
const locations = p.storageLocations.map(l => l.name || l.type).join(', ') || '-'
const responsible = [p.responsiblePerson, p.responsibleRole].filter(s => s.trim()).join(' / ') || '-'
const activeHolds = getActiveLegalHolds(p)
html += `
<div class="policy-detail">
<div class="policy-detail-header">
<span>${escHtml(p.policyId)}${escHtml(p.dataObjectName)}</span>
<span class="badge ${p.status === 'ACTIVE' ? 'badge-active' : 'badge-draft'}">${escHtml(statusLabel)}</span>
</div>
<div class="policy-detail-body">
<table>
<tr><th>Beschreibung</th><td>${escHtml(p.description || '-')}</td></tr>
<tr><th>Betroffenengruppen</th><td>${escHtml(p.affectedGroups.join(', ') || '-')}</td></tr>
<tr><th>Datenkategorien</th><td>${escHtml(p.dataCategories.join(', ') || '-')}</td></tr>
<tr><th>Verarbeitungszweck</th><td>${escHtml(p.primaryPurpose || '-')}</td></tr>
<tr><th>Loeschtrigger</th><td>${escHtml(trigger)}</td></tr>
<tr><th>Aufbewahrungstreiber</th><td>${escHtml(driverLabel)}${driverStatute ? ` (${escHtml(driverStatute)})` : ''}</td></tr>
<tr><th>Aufbewahrungsfrist</th><td>${escHtml(duration)}</td></tr>
<tr><th>Startereignis</th><td>${escHtml(p.startEvent || '-')}</td></tr>
<tr><th>Loeschmethode</th><td>${escHtml(method)}</td></tr>
<tr><th>Loeschmethode (Detail)</th><td>${escHtml(p.deletionMethodDetail || '-')}</td></tr>
<tr><th>Speicherorte</th><td>${escHtml(locations)}</td></tr>
<tr><th>Verantwortlich</th><td>${escHtml(responsible)}</td></tr>
<tr><th>Pruefintervall</th><td>${escHtml(REVIEW_INTERVAL_LABELS[p.reviewInterval] || p.reviewInterval)}</td></tr>
${activeHolds.length > 0 ? `<tr><th>Aktive Legal Holds</th><td>${activeHolds.map(h => `${escHtml(h.reason)} (seit ${formatDateDE(h.startDate)})`).join('<br/>')}</td></tr>` : ''}
</table>
</div>
</div>
`
}
html += ` </div>
</div>
`
// =========================================================================
// Section 6: VVT-Verknuepfung
// =========================================================================
html += `
<div class="section page-break">
<div class="section-header">6. VVT-Verknuepfung</div>
<div class="section-body">
<p>Die folgende Tabelle zeigt die Verknuepfung zwischen Loeschregeln und Verarbeitungstaetigkeiten
im VVT (Art. 30 DSGVO):</p>
`
if (vvtRefs.length > 0) {
html += ` <table>
<tr><th>Loeschregel</th><th>LF-Nr.</th><th>VVT-Nr.</th><th>Verarbeitungstaetigkeit</th></tr>
`
for (const ref of vvtRefs) {
html += ` <tr>
<td>${escHtml(ref.policyName)}</td>
<td>${escHtml(ref.policyId)}</td>
<td>${escHtml(ref.vvtId)}</td>
<td>${escHtml(ref.vvtName)}</td>
</tr>
`
}
html += ` </table>
`
} else {
html += ` <p><em>Noch keine VVT-Verknuepfungen dokumentiert. Verknuepfen Sie Ihre Loeschregeln
mit den entsprechenden Verarbeitungstaetigkeiten im Editor-Tab.</em></p>
`
}
html += ` </div>
</div>
`
// =========================================================================
// Section 7: Auftragsverarbeiter mit Loeschpflichten
// =========================================================================
html += `
<div class="section">
<div class="section-header">7. Auftragsverarbeiter mit Loeschpflichten</div>
<div class="section-body">
<p>Die folgende Tabelle zeigt Loeschregeln, die mit Auftragsverarbeitern verknuepft sind.
Diese Verknuepfungen stellen sicher, dass auch bei extern verarbeiteten Daten die Loeschpflichten
eingehalten werden (Art. 28 DSGVO).</p>
`
if (vendorRefs.length > 0) {
html += ` <table>
<tr><th>Loeschregel</th><th>LF-Nr.</th><th>Auftragsverarbeiter (ID)</th><th>Aufbewahrungsfrist</th></tr>
`
for (const ref of vendorRefs) {
html += ` <tr>
<td>${escHtml(ref.policyName)}</td>
<td>${escHtml(ref.policyId)}</td>
<td>${escHtml(ref.vendorId)}</td>
<td>${escHtml(ref.duration)}</td>
</tr>
`
}
html += ` </table>
`
} else {
html += ` <p><em>Noch keine Auftragsverarbeiter mit Loeschregeln verknuepft. Verknuepfen Sie Ihre
Loeschregeln mit den entsprechenden Auftragsverarbeitern im Editor-Tab.</em></p>
`
}
html += ` </div>
</div>
`
// =========================================================================
// Section 8: Legal Hold Verfahren
// =========================================================================
html += `
<div class="section">
<div class="section-header">8. Legal Hold Verfahren</div>
<div class="section-body">
<p>Ein Legal Hold (Aufbewahrungspflicht aufgrund rechtlicher Verfahren) setzt die regulaere
Loeschung aus. Betroffene Daten duerfen trotz abgelaufener Loeschfrist nicht geloescht werden,
bis der Legal Hold aufgehoben wird.</p>
<p><strong>Verfahrensschritte:</strong></p>
<ol style="margin: 8px 0 8px 24px;">
<li>Rechtsabteilung/DSB identifiziert betroffene Datenkategorien</li>
<li>Legal Hold wird im System aktiviert (Status: Aktiv)</li>
<li>Automatische Loeschung wird fuer betroffene Policies ausgesetzt</li>
<li>Regelmaessige Pruefung, ob der Legal Hold noch erforderlich ist</li>
<li>Nach Aufhebung: Regulaere Loeschfristen greifen wieder</li>
</ol>
`
if (allActiveLegalHolds.length > 0) {
html += ` <p><strong>Aktuell aktive Legal Holds (${allActiveLegalHolds.length}):</strong></p>
<table>
<tr><th>Datenobjekt</th><th>Grund</th><th>Rechtsgrundlage</th><th>Seit</th><th>Voraussichtlich bis</th></tr>
`
for (const { policy, hold } of allActiveLegalHolds) {
html += ` <tr>
<td>${escHtml(policy)}</td>
<td>${escHtml(hold.reason)}</td>
<td>${escHtml(hold.legalBasis)}</td>
<td>${formatDateDE(hold.startDate)}</td>
<td>${hold.expectedEndDate ? formatDateDE(hold.expectedEndDate) : 'Unbefristet'}</td>
</tr>
`
}
html += ` </table>
`
} else {
html += ` <p><em>Derzeit sind keine aktiven Legal Holds vorhanden.</em></p>
`
}
html += ` </div>
</div>
`
// =========================================================================
// Section 9: Verantwortlichkeiten
// =========================================================================
html += `
<div class="section">
<div class="section-header">9. Verantwortlichkeiten</div>
<div class="section-body">
<p>Die folgende Rollenmatrix zeigt, welche Organisationseinheiten fuer welche Datenobjekte
die Loeschverantwortung tragen:</p>
<table>
<tr><th>Rolle / Verantwortlich</th><th>Datenobjekte</th><th>Anzahl</th></tr>
`
for (const [role, objects] of roleMap.entries()) {
html += ` <tr>
<td>${escHtml(role)}</td>
<td>${objects.map(o => escHtml(o)).join(', ')}</td>
<td>${objects.length}</td>
</tr>
`
}
html += ` </table>
</div>
</div>
`
// =========================================================================
// Section 10: Pruef- und Revisionszyklus
// =========================================================================
html += `
<div class="section">
<div class="section-header">10. Pruef- und Revisionszyklus</div>
<div class="section-body">
<table>
<tr><th>Eigenschaft</th><th>Wert</th></tr>
<tr><td>Aktuelles Pruefintervall</td><td>${escHtml(orgHeader.reviewInterval)}</td></tr>
<tr><td>Letzte Pruefung</td><td>${formatDateDE(orgHeader.lastReviewDate)}</td></tr>
<tr><td>Naechste Pruefung</td><td>${formatDateDE(orgHeader.nextReviewDate)}</td></tr>
<tr><td>Aktuelle Version</td><td>${escHtml(orgHeader.loeschkonzeptVersion)}</td></tr>
</table>
<p style="margin-top: 8px;">Bei jeder Pruefung wird das Loeschkonzept auf folgende Punkte ueberprueft:</p>
<ul style="margin: 8px 0 8px 24px;">
<li>Vollstaendigkeit aller Loeschregeln (neue Verarbeitungen erfasst?)</li>
<li>Aktualitaet der gesetzlichen Aufbewahrungsfristen</li>
<li>Wirksamkeit der technischen Loeschmechanismen</li>
<li>Einhaltung der definierten Loeschfristen</li>
<li>Angemessenheit der Verantwortlichkeiten</li>
</ul>
</div>
</div>
`
// =========================================================================
// Section 11: Compliance-Status
// =========================================================================
html += `
<div class="section page-break">
<div class="section-header">11. Compliance-Status</div>
<div class="section-body">
`
if (complianceResult) {
const scoreClass = complianceResult.score >= 90 ? 'score-excellent'
: complianceResult.score >= 75 ? 'score-good'
: complianceResult.score >= 50 ? 'score-needs-work'
: 'score-poor'
const scoreLabel = complianceResult.score >= 90 ? 'Ausgezeichnet'
: complianceResult.score >= 75 ? 'Gut'
: complianceResult.score >= 50 ? 'Verbesserungswuerdig'
: 'Mangelhaft'
html += ` <p><span class="score-box ${scoreClass}">${complianceResult.score}/100</span> ${escHtml(scoreLabel)}</p>
<table style="margin-top: 12px;">
<tr><th>Kennzahl</th><th>Wert</th></tr>
<tr><td>Gepruefte Policies</td><td>${complianceResult.stats.total}</td></tr>
<tr><td>Bestanden</td><td>${complianceResult.stats.passed}</td></tr>
<tr><td>Beanstandungen</td><td>${complianceResult.stats.failed}</td></tr>
</table>
`
if (complianceResult.issues.length > 0) {
html += ` <p style="margin-top: 12px;"><strong>Befunde nach Schweregrad:</strong></p>
<table>
<tr><th>Schweregrad</th><th>Anzahl</th><th>Befunde</th></tr>
`
const severityOrder: ComplianceIssueSeverity[] = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']
for (const sev of severityOrder) {
const count = complianceResult.stats.bySeverity[sev]
if (count === 0) continue
const issuesForSev = complianceResult.issues.filter(i => i.severity === sev)
html += ` <tr>
<td><span class="badge badge-${sev.toLowerCase()}" style="color: ${SEVERITY_COLORS[sev]}">${SEVERITY_LABELS_DE[sev]}</span></td>
<td>${count}</td>
<td>${issuesForSev.map(i => escHtml(i.title)).join('; ')}</td>
</tr>
`
}
html += ` </table>
`
} else {
html += ` <p style="margin-top: 8px;"><em>Keine Beanstandungen. Alle Policies sind konform.</em></p>
`
}
} else {
html += ` <p><em>Compliance-Check wurde noch nicht ausgefuehrt. Fuehren Sie den Check im
Export-Tab durch, um den Status in das Dokument aufzunehmen.</em></p>
`
}
html += ` </div>
</div>
`
// =========================================================================
// Section 12: Aenderungshistorie
// =========================================================================
html += `
<div class="section">
<div class="section-header">12. Aenderungshistorie</div>
<div class="section-body">
<table>
<tr><th>Version</th><th>Datum</th><th>Autor</th><th>Aenderungen</th></tr>
`
if (revisions.length > 0) {
for (const rev of revisions) {
html += ` <tr>
<td>${escHtml(rev.version)}</td>
<td>${formatDateDE(rev.date)}</td>
<td>${escHtml(rev.author)}</td>
<td>${escHtml(rev.changes)}</td>
</tr>
`
}
} else {
html += ` <tr>
<td>${escHtml(orgHeader.loeschkonzeptVersion)}</td>
<td>${today}</td>
<td>${escHtml(orgHeader.dpoName || orgHeader.responsiblePerson || '-')}</td>
<td>Erstversion des Loeschkonzepts</td>
</tr>
`
}
html += ` </table>
</div>
</div>
`
// =========================================================================
// Footer
// =========================================================================
html += `
<div class="page-footer">
<span>Loeschkonzept — ${escHtml(orgName)}</span>
<span>Stand: ${today} | Version ${escHtml(orgHeader.loeschkonzeptVersion)}</span>
</div>
</body>
</html>`
return html
}
// =============================================================================
// INTERNAL HELPERS
// =============================================================================
function escHtml(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
function formatDateDE(dateStr: string | null | undefined): string {
if (!dateStr) return '-'
try {
const date = new Date(dateStr)
if (isNaN(date.getTime())) return '-'
return date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
} catch {
return '-'
}
}

View File

@@ -1,6 +1,6 @@
// =============================================================================
// Loeschfristen Module - Profiling Wizard
// 4-Step Profiling (15 Fragen) zur Generierung von Baseline-Loeschrichtlinien
// 4-Step Profiling (16 Fragen) zur Generierung von Baseline-Loeschrichtlinien
// =============================================================================
import type { LoeschfristPolicy, StorageLocation } from './loeschfristen-types'
@@ -42,7 +42,7 @@ export interface ProfilingResult {
}
// =============================================================================
// PROFILING STEPS (4 Steps, 15 Questions)
// PROFILING STEPS (4 Steps, 16 Questions)
// =============================================================================
export const PROFILING_STEPS: ProfilingStep[] = [
@@ -163,7 +163,7 @@ export const PROFILING_STEPS: ProfilingStep[] = [
},
// =========================================================================
// Step 3: Systeme (3 Fragen)
// Step 3: Systeme (4 Fragen)
// =========================================================================
{
id: 'systems',
@@ -194,6 +194,14 @@ export const PROFILING_STEPS: ProfilingStep[] = [
type: 'boolean',
required: true,
},
{
id: 'sys-zutritt',
step: 'systems',
question: 'Nutzen Sie ein Zutrittskontrollsystem?',
helpText: 'Zutrittskontrollsysteme erzeugen Protokolle, die personenbezogene Daten enthalten und einer Loeschfrist unterliegen.',
type: 'boolean',
required: true,
},
],
},
@@ -340,6 +348,7 @@ export function generatePoliciesFromProfile(answers: ProfilingAnswer[]): Profili
matchedTemplateIds.add('zeiterfassung')
matchedTemplateIds.add('bewerbungsunterlagen')
matchedTemplateIds.add('krankmeldungen')
matchedTemplateIds.add('schulungsnachweise')
}
// -------------------------------------------------------------------------
@@ -358,6 +367,8 @@ export function generatePoliciesFromProfile(answers: ProfilingAnswer[]): Profili
matchedTemplateIds.add('vertraege')
matchedTemplateIds.add('geschaeftsbriefe')
matchedTemplateIds.add('kundenstammdaten')
matchedTemplateIds.add('kundenreklamationen')
matchedTemplateIds.add('lieferantenbewertungen')
}
// -------------------------------------------------------------------------
@@ -367,6 +378,7 @@ export function generatePoliciesFromProfile(answers: ProfilingAnswer[]): Profili
matchedTemplateIds.add('newsletter-einwilligungen')
matchedTemplateIds.add('crm-kontakthistorie')
matchedTemplateIds.add('cookie-consent-logs')
matchedTemplateIds.add('social-media-daten')
}
// -------------------------------------------------------------------------
@@ -384,6 +396,20 @@ export function generatePoliciesFromProfile(answers: ProfilingAnswer[]): Profili
matchedTemplateIds.add('cookie-consent-logs')
}
// -------------------------------------------------------------------------
// Cloud (sys-cloud = true) → E-Mail-Archivierung
// -------------------------------------------------------------------------
if (getBool('sys-cloud')) {
matchedTemplateIds.add('email-archivierung')
}
// -------------------------------------------------------------------------
// Zutritt (sys-zutritt = true)
// -------------------------------------------------------------------------
if (getBool('sys-zutritt')) {
matchedTemplateIds.add('zutrittsprotokolle')
}
// -------------------------------------------------------------------------
// ERP/CRM (sys-erp = true)
// -------------------------------------------------------------------------
@@ -405,6 +431,7 @@ export function generatePoliciesFromProfile(answers: ProfilingAnswer[]): Profili
if (getBool('special-gesundheit')) {
// Ensure krankmeldungen is included even without full HR data
matchedTemplateIds.add('krankmeldungen')
matchedTemplateIds.add('betriebsarzt-doku')
}
// -------------------------------------------------------------------------

View File

@@ -91,6 +91,7 @@ export interface LoeschfristPolicy {
responsiblePerson: string
releaseProcess: string
linkedVVTActivityIds: string[]
linkedVendorIds: string[]
// Status & Review
status: PolicyStatus
lastReviewDate: string
@@ -272,6 +273,7 @@ export function createEmptyPolicy(): LoeschfristPolicy {
responsiblePerson: '',
releaseProcess: '',
linkedVVTActivityIds: [],
linkedVendorIds: [],
status: 'DRAFT',
lastReviewDate: now,
nextReviewDate: nextYear.toISOString(),

View File

@@ -0,0 +1,395 @@
// =============================================================================
// Obligations Module - Compliance Check Engine
// Prueft Pflichten auf Vollstaendigkeit, Konsistenz und Auditfaehigkeit
// =============================================================================
// =============================================================================
// TYPES
// =============================================================================
export interface Obligation {
id: string
title: string
description: string
source: string
source_article: string
deadline: string | null
status: 'pending' | 'in-progress' | 'completed' | 'overdue'
priority: 'critical' | 'high' | 'medium' | 'low'
responsible: string
linked_systems: string[]
linked_vendor_ids?: string[]
assessment_id?: string
rule_code?: string
notes?: string
created_at?: string
updated_at?: string
evidence?: string[]
review_date?: string
category?: string
}
export type ObligationComplianceIssueType =
| 'MISSING_RESPONSIBLE'
| 'OVERDUE_DEADLINE'
| 'MISSING_EVIDENCE'
| 'MISSING_DESCRIPTION'
| 'NO_LEGAL_REFERENCE'
| 'INCOMPLETE_REGULATION'
| 'HIGH_PRIORITY_NOT_STARTED'
| 'STALE_PENDING'
| 'MISSING_LINKED_SYSTEMS'
| 'NO_REVIEW_PROCESS'
| 'CRITICAL_WITHOUT_EVIDENCE'
| 'MISSING_VENDOR_LINK'
export type ObligationComplianceIssueSeverity = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'
export interface ObligationComplianceIssue {
type: ObligationComplianceIssueType
severity: ObligationComplianceIssueSeverity
message: string
affectedObligations: string[]
recommendation: string
}
export interface ObligationComplianceCheckResult {
score: number
issues: ObligationComplianceIssue[]
summary: { total: number; critical: number; high: number; medium: number; low: number }
checkedAt: string
}
// =============================================================================
// CONSTANTS
// =============================================================================
export const OBLIGATION_SEVERITY_LABELS_DE: Record<ObligationComplianceIssueSeverity, string> = {
CRITICAL: 'Kritisch',
HIGH: 'Hoch',
MEDIUM: 'Mittel',
LOW: 'Niedrig',
}
export const OBLIGATION_SEVERITY_COLORS: Record<ObligationComplianceIssueSeverity, string> = {
CRITICAL: '#dc2626',
HIGH: '#ea580c',
MEDIUM: '#d97706',
LOW: '#6b7280',
}
// =============================================================================
// HELPERS
// =============================================================================
function daysBetween(date: Date, now: Date): number {
const diffMs = now.getTime() - date.getTime()
return Math.floor(diffMs / (1000 * 60 * 60 * 24))
}
// =============================================================================
// PER-OBLIGATION CHECKS (1-5, 9, 11)
// =============================================================================
/**
* Check 1: MISSING_RESPONSIBLE (MEDIUM)
* Pflicht ohne verantwortliche Person/Abteilung.
*/
function checkMissingResponsible(obligations: Obligation[]): ObligationComplianceIssue | null {
const affected = obligations.filter(o => !o.responsible || o.responsible.trim() === '')
if (affected.length === 0) return null
return {
type: 'MISSING_RESPONSIBLE',
severity: 'MEDIUM',
message: `${affected.length} Pflicht(en) ohne verantwortliche Person oder Abteilung. Ohne klare Zustaendigkeit koennen Pflichten nicht zuverlaessig umgesetzt werden.`,
affectedObligations: affected.map(o => o.id),
recommendation: 'Weisen Sie jeder Pflicht eine verantwortliche Person oder Abteilung zu.',
}
}
/**
* Check 2: OVERDUE_DEADLINE (HIGH)
* Pflicht mit Deadline in der Vergangenheit + Status != completed.
*/
function checkOverdueDeadline(obligations: Obligation[]): ObligationComplianceIssue | null {
const now = new Date()
const affected = obligations.filter(o => {
if (!o.deadline || o.status === 'completed') return false
return new Date(o.deadline) < now
})
if (affected.length === 0) return null
return {
type: 'OVERDUE_DEADLINE',
severity: 'HIGH',
message: `${affected.length} Pflicht(en) mit ueberschrittener Frist. Ueberfaellige Pflichten stellen ein Compliance-Risiko dar und koennen zu Bussgeldern fuehren.`,
affectedObligations: affected.map(o => o.id),
recommendation: 'Bearbeiten Sie ueberfaellige Pflichten umgehend oder passen Sie die Fristen an.',
}
}
/**
* Check 3: MISSING_EVIDENCE (HIGH)
* Completed-Pflicht ohne Evidence.
*/
function checkMissingEvidence(obligations: Obligation[]): ObligationComplianceIssue | null {
const affected = obligations.filter(o =>
o.status === 'completed' && (!o.evidence || o.evidence.length === 0)
)
if (affected.length === 0) return null
return {
type: 'MISSING_EVIDENCE',
severity: 'HIGH',
message: `${affected.length} abgeschlossene Pflicht(en) ohne Nachweis. Ohne Nachweise ist die Erfuellung im Audit nicht belegbar.`,
affectedObligations: affected.map(o => o.id),
recommendation: 'Hinterlegen Sie Nachweisdokumente fuer alle abgeschlossenen Pflichten.',
}
}
/**
* Check 4: MISSING_DESCRIPTION (MEDIUM)
* Pflicht ohne Beschreibung.
*/
function checkMissingDescription(obligations: Obligation[]): ObligationComplianceIssue | null {
const affected = obligations.filter(o => !o.description || o.description.trim() === '')
if (affected.length === 0) return null
return {
type: 'MISSING_DESCRIPTION',
severity: 'MEDIUM',
message: `${affected.length} Pflicht(en) ohne Beschreibung. Eine fehlende Beschreibung erschwert die Nachvollziehbarkeit und Umsetzung.`,
affectedObligations: affected.map(o => o.id),
recommendation: 'Ergaenzen Sie eine Beschreibung fuer jede Pflicht, die den Inhalt und die Anforderungen erlaeutert.',
}
}
/**
* Check 5: NO_LEGAL_REFERENCE (HIGH)
* Pflicht ohne source_article (kein Artikel-Bezug).
*/
function checkNoLegalReference(obligations: Obligation[]): ObligationComplianceIssue | null {
const affected = obligations.filter(o => !o.source_article || o.source_article.trim() === '')
if (affected.length === 0) return null
return {
type: 'NO_LEGAL_REFERENCE',
severity: 'HIGH',
message: `${affected.length} Pflicht(en) ohne Artikel-/Paragraphen-Referenz. Ohne Rechtsbezug ist die Pflicht im Audit nicht nachvollziehbar.`,
affectedObligations: affected.map(o => o.id),
recommendation: 'Ergaenzen Sie die Rechtsgrundlage (z.B. Art. 32 DSGVO) fuer jede Pflicht.',
}
}
/**
* Check 9: MISSING_LINKED_SYSTEMS (MEDIUM)
* Pflicht ohne verknuepfte Systeme/Verarbeitungen.
*/
function checkMissingLinkedSystems(obligations: Obligation[]): ObligationComplianceIssue | null {
const affected = obligations.filter(o => !o.linked_systems || o.linked_systems.length === 0)
if (affected.length === 0) return null
return {
type: 'MISSING_LINKED_SYSTEMS',
severity: 'MEDIUM',
message: `${affected.length} Pflicht(en) ohne verknuepfte Systeme oder Verarbeitungstaetigkeiten. Ohne Systemzuordnung fehlt der operative Bezug.`,
affectedObligations: affected.map(o => o.id),
recommendation: 'Ordnen Sie jeder Pflicht die betroffenen IT-Systeme oder Verarbeitungstaetigkeiten zu.',
}
}
/**
* Check 11: CRITICAL_WITHOUT_EVIDENCE (CRITICAL)
* Critical-Pflicht ohne Evidence.
*/
function checkCriticalWithoutEvidence(obligations: Obligation[]): ObligationComplianceIssue | null {
const affected = obligations.filter(o =>
o.priority === 'critical' && (!o.evidence || o.evidence.length === 0)
)
if (affected.length === 0) return null
return {
type: 'CRITICAL_WITHOUT_EVIDENCE',
severity: 'CRITICAL',
message: `${affected.length} kritische Pflicht(en) ohne Nachweis. Kritische Pflichten erfordern zwingend eine Dokumentation der Erfuellung.`,
affectedObligations: affected.map(o => o.id),
recommendation: 'Hinterlegen Sie umgehend Nachweise fuer alle kritischen Pflichten.',
}
}
/**
* Check 12: MISSING_VENDOR_LINK (MEDIUM)
* Art.-28-Pflicht ohne verknuepften Auftragsverarbeiter.
*/
function checkMissingVendorLink(obligations: Obligation[]): ObligationComplianceIssue | null {
const affected = obligations.filter(o =>
o.source_article?.includes('Art. 28') &&
(!o.linked_vendor_ids || o.linked_vendor_ids.length === 0)
)
if (affected.length === 0) return null
return {
type: 'MISSING_VENDOR_LINK',
severity: 'MEDIUM',
message: `${affected.length} Art.-28-Pflicht(en) ohne verknuepften Auftragsverarbeiter.`,
affectedObligations: affected.map(o => o.id),
recommendation: 'Verknuepfen Sie Art.-28-Pflichten mit den betroffenen Auftragsverarbeitern im Vendor Register.',
}
}
// =============================================================================
// AGGREGATE CHECKS (6-8, 10)
// =============================================================================
/**
* Check 6: INCOMPLETE_REGULATION (HIGH)
* Regulierung, bei der alle Pflichten pending/overdue sind.
*/
function checkIncompleteRegulation(obligations: Obligation[]): ObligationComplianceIssue | null {
const bySource = new Map<string, Obligation[]>()
for (const o of obligations) {
const src = o.source || 'Unbekannt'
if (!bySource.has(src)) bySource.set(src, [])
bySource.get(src)!.push(o)
}
const incompleteRegs: string[] = []
const affectedIds: string[] = []
for (const [source, obls] of bySource.entries()) {
if (obls.length < 2) continue // Skip single-obligation regulations
const allStalled = obls.every(o => o.status === 'pending' || o.status === 'overdue')
if (allStalled) {
incompleteRegs.push(source)
affectedIds.push(...obls.map(o => o.id))
}
}
if (incompleteRegs.length === 0) return null
return {
type: 'INCOMPLETE_REGULATION',
severity: 'HIGH',
message: `${incompleteRegs.length} Regulierung(en) vollstaendig ohne Umsetzung: ${incompleteRegs.join(', ')}. Alle Pflichten sind ausstehend oder ueberfaellig.`,
affectedObligations: affectedIds,
recommendation: 'Beginnen Sie mit der Umsetzung der wichtigsten Pflichten in den betroffenen Regulierungen.',
}
}
/**
* Check 7: HIGH_PRIORITY_NOT_STARTED (CRITICAL)
* Critical/High-Pflicht seit > 30 Tagen pending.
*/
function checkHighPriorityNotStarted(obligations: Obligation[]): ObligationComplianceIssue | null {
const now = new Date()
const affected = obligations.filter(o => {
if (o.status !== 'pending') return false
if (o.priority !== 'critical' && o.priority !== 'high') return false
if (!o.created_at) return false
return daysBetween(new Date(o.created_at), now) > 30
})
if (affected.length === 0) return null
return {
type: 'HIGH_PRIORITY_NOT_STARTED',
severity: 'CRITICAL',
message: `${affected.length} hochprioritaere Pflicht(en) seit ueber 30 Tagen nicht begonnen. Dies deutet auf organisatorische Blockaden oder fehlende Priorisierung hin.`,
affectedObligations: affected.map(o => o.id),
recommendation: 'Starten Sie umgehend mit der Bearbeitung dieser kritischen/hohen Pflichten und erstellen Sie einen Umsetzungsplan.',
}
}
/**
* Check 8: STALE_PENDING (LOW)
* Pflicht seit > 90 Tagen pending.
*/
function checkStalePending(obligations: Obligation[]): ObligationComplianceIssue | null {
const now = new Date()
const affected = obligations.filter(o => {
if (o.status !== 'pending') return false
if (!o.created_at) return false
return daysBetween(new Date(o.created_at), now) > 90
})
if (affected.length === 0) return null
return {
type: 'STALE_PENDING',
severity: 'LOW',
message: `${affected.length} Pflicht(en) seit ueber 90 Tagen ausstehend. Langfristig unbearbeitete Pflichten sollten priorisiert oder als nicht relevant markiert werden.`,
affectedObligations: affected.map(o => o.id),
recommendation: 'Pruefen Sie, ob die Pflichten weiterhin relevant sind, und setzen Sie Prioritaeten fuer die Umsetzung.',
}
}
/**
* Check 10: NO_REVIEW_PROCESS (MEDIUM)
* Keine einzige Pflicht hat review_date.
*/
function checkNoReviewProcess(obligations: Obligation[]): ObligationComplianceIssue | null {
if (obligations.length === 0) return null
const hasAnyReview = obligations.some(o => o.review_date)
if (hasAnyReview) return null
return {
type: 'NO_REVIEW_PROCESS',
severity: 'MEDIUM',
message: 'Keine Pflicht hat ein Pruefungsdatum (review_date). Ohne regelmaessige Ueberpruefung ist die Aktualitaet des Pflichtenregisters nicht gewaehrleistet.',
affectedObligations: [],
recommendation: 'Fuehren Sie ein Pruefintervall ein und setzen Sie review_date fuer alle Pflichten.',
}
}
// =============================================================================
// MAIN COMPLIANCE CHECK
// =============================================================================
/**
* Fuehrt einen vollstaendigen Compliance-Check ueber alle Pflichten durch.
*/
export function runObligationComplianceCheck(obligations: Obligation[]): ObligationComplianceCheckResult {
const issues: ObligationComplianceIssue[] = []
const checks = [
checkMissingResponsible(obligations),
checkOverdueDeadline(obligations),
checkMissingEvidence(obligations),
checkMissingDescription(obligations),
checkNoLegalReference(obligations),
checkIncompleteRegulation(obligations),
checkHighPriorityNotStarted(obligations),
checkStalePending(obligations),
checkMissingLinkedSystems(obligations),
checkNoReviewProcess(obligations),
checkCriticalWithoutEvidence(obligations),
checkMissingVendorLink(obligations),
]
for (const issue of checks) {
if (issue !== null) {
issues.push(issue)
}
}
// Calculate score
const summary = { total: issues.length, critical: 0, high: 0, medium: 0, low: 0 }
for (const issue of issues) {
switch (issue.severity) {
case 'CRITICAL': summary.critical++; break
case 'HIGH': summary.high++; break
case 'MEDIUM': summary.medium++; break
case 'LOW': summary.low++; break
}
}
const rawScore = 100 - (summary.critical * 15 + summary.high * 10 + summary.medium * 5 + summary.low * 2)
const score = Math.max(0, rawScore)
return {
score,
issues,
summary,
checkedAt: new Date().toISOString(),
}
}

View File

@@ -0,0 +1,915 @@
// =============================================================================
// Obligations Module - Pflichtenregister Document Generator
// Generates a printable, audit-ready HTML document for the obligation register
// =============================================================================
import type { Obligation, ObligationComplianceCheckResult, ObligationComplianceIssueSeverity } from './obligations-compliance'
import { OBLIGATION_SEVERITY_LABELS_DE, OBLIGATION_SEVERITY_COLORS } from './obligations-compliance'
// =============================================================================
// TYPES
// =============================================================================
export interface ObligationDocumentOrgHeader {
organizationName: string
industry: string
dpoName: string
dpoContact: string
responsiblePerson: string
legalDepartment: string
documentVersion: string
lastReviewDate: string
nextReviewDate: string
reviewInterval: string
}
export interface ObligationDocumentRevision {
version: string
date: string
author: string
changes: string
}
// =============================================================================
// DEFAULTS
// =============================================================================
export function createDefaultObligationDocumentOrgHeader(): ObligationDocumentOrgHeader {
const now = new Date()
const nextYear = new Date()
nextYear.setFullYear(nextYear.getFullYear() + 1)
return {
organizationName: '',
industry: '',
dpoName: '',
dpoContact: '',
responsiblePerson: '',
legalDepartment: '',
documentVersion: '1.0',
lastReviewDate: now.toISOString().split('T')[0],
nextReviewDate: nextYear.toISOString().split('T')[0],
reviewInterval: 'Jaehrlich',
}
}
// =============================================================================
// STATUS & PRIORITY LABELS
// =============================================================================
const STATUS_LABELS_DE: Record<string, string> = {
'pending': 'Ausstehend',
'in-progress': 'In Bearbeitung',
'completed': 'Abgeschlossen',
'overdue': 'Ueberfaellig',
}
const STATUS_BADGE_CLASSES: Record<string, string> = {
'pending': 'badge-draft',
'in-progress': 'badge-review',
'completed': 'badge-active',
'overdue': 'badge-critical',
}
const PRIORITY_LABELS_DE: Record<string, string> = {
critical: 'Kritisch',
high: 'Hoch',
medium: 'Mittel',
low: 'Niedrig',
}
const PRIORITY_BADGE_CLASSES: Record<string, string> = {
critical: 'badge-critical',
high: 'badge-high',
medium: 'badge-medium',
low: 'badge-low',
}
// =============================================================================
// HTML DOCUMENT BUILDER
// =============================================================================
export function buildObligationDocumentHtml(
obligations: Obligation[],
orgHeader: ObligationDocumentOrgHeader,
complianceResult: ObligationComplianceCheckResult | null,
revisions: ObligationDocumentRevision[]
): string {
const today = new Date().toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
const orgName = orgHeader.organizationName || 'Organisation'
// Group obligations by source (regulation)
const bySource = new Map<string, Obligation[]>()
for (const o of obligations) {
const src = o.source || 'Sonstig'
if (!bySource.has(src)) bySource.set(src, [])
bySource.get(src)!.push(o)
}
// Build role map
const roleMap = new Map<string, Obligation[]>()
for (const o of obligations) {
const role = o.responsible || 'Nicht zugewiesen'
if (!roleMap.has(role)) roleMap.set(role, [])
roleMap.get(role)!.push(o)
}
// Distinct sources
const distinctSources = Array.from(bySource.keys()).sort()
// =========================================================================
// HTML Template
// =========================================================================
let html = `<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Pflichtenregister — ${escHtml(orgName)}</title>
<style>
@page { size: A4; margin: 20mm 18mm 22mm 18mm; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 10pt;
line-height: 1.5;
color: #1e293b;
}
/* Cover */
.cover {
min-height: 90vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
page-break-after: always;
}
.cover h1 {
font-size: 28pt;
color: #5b21b6;
margin-bottom: 8px;
font-weight: 700;
}
.cover .subtitle {
font-size: 14pt;
color: #7c3aed;
margin-bottom: 40px;
}
.cover .org-info {
background: #f5f3ff;
border: 1px solid #ddd6fe;
border-radius: 8px;
padding: 24px 40px;
text-align: left;
width: 400px;
margin-bottom: 24px;
}
.cover .org-info div { margin-bottom: 6px; }
.cover .org-info .label { font-weight: 600; color: #5b21b6; display: inline-block; min-width: 160px; }
.cover .legal-ref {
font-size: 9pt;
color: #64748b;
margin-top: 20px;
}
/* TOC */
.toc {
page-break-after: always;
padding-top: 40px;
}
.toc h2 {
font-size: 18pt;
color: #5b21b6;
margin-bottom: 20px;
border-bottom: 2px solid #5b21b6;
padding-bottom: 8px;
}
.toc-entry {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px dotted #cbd5e1;
font-size: 10pt;
}
.toc-entry .toc-num { font-weight: 600; color: #5b21b6; min-width: 40px; }
/* Sections */
.section {
page-break-inside: avoid;
margin-bottom: 24px;
}
.section-header {
font-size: 14pt;
color: #5b21b6;
font-weight: 700;
margin: 30px 0 12px 0;
border-bottom: 2px solid #ddd6fe;
padding-bottom: 6px;
}
.section-body { margin-bottom: 16px; }
/* Tables */
table {
width: 100%;
border-collapse: collapse;
margin: 10px 0 16px 0;
font-size: 9pt;
}
th, td {
border: 1px solid #e2e8f0;
padding: 6px 8px;
text-align: left;
vertical-align: top;
}
th {
background: #f5f3ff;
color: #5b21b6;
font-weight: 600;
font-size: 8.5pt;
text-transform: uppercase;
letter-spacing: 0.3px;
}
tr:nth-child(even) td { background: #faf5ff; }
/* Detail cards */
.policy-detail {
page-break-inside: avoid;
border: 1px solid #e2e8f0;
border-radius: 6px;
margin-bottom: 16px;
overflow: hidden;
}
.policy-detail-header {
background: #f5f3ff;
padding: 8px 12px;
font-weight: 700;
color: #5b21b6;
border-bottom: 1px solid #ddd6fe;
display: flex;
justify-content: space-between;
}
.policy-detail-body { padding: 0; }
.policy-detail-body table { margin: 0; }
.policy-detail-body th { width: 200px; }
/* Badges */
.badge {
display: inline-block;
padding: 1px 8px;
border-radius: 9999px;
font-size: 8pt;
font-weight: 600;
}
.badge-active { background: #dcfce7; color: #166534; }
.badge-draft { background: #f3f4f6; color: #374151; }
.badge-review { background: #fef9c3; color: #854d0e; }
.badge-critical { background: #fecaca; color: #991b1b; }
.badge-high { background: #fed7aa; color: #9a3412; }
.badge-medium { background: #fef3c7; color: #92400e; }
.badge-low { background: #f3f4f6; color: #4b5563; }
/* Principles */
.principle {
margin-bottom: 10px;
padding-left: 20px;
position: relative;
}
.principle::before {
content: '';
position: absolute;
left: 0;
top: 6px;
width: 10px;
height: 10px;
background: #7c3aed;
border-radius: 50%;
}
.principle strong { color: #5b21b6; }
/* Score */
.score-box {
display: inline-block;
padding: 4px 16px;
border-radius: 8px;
font-size: 18pt;
font-weight: 700;
margin-right: 12px;
}
.score-excellent { background: #dcfce7; color: #166534; }
.score-good { background: #dbeafe; color: #1e40af; }
.score-needs-work { background: #fef3c7; color: #92400e; }
.score-poor { background: #fecaca; color: #991b1b; }
/* Footer */
.page-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 8px 18mm;
font-size: 7.5pt;
color: #94a3b8;
display: flex;
justify-content: space-between;
border-top: 1px solid #e2e8f0;
}
/* Print */
@media print {
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.no-print { display: none !important; }
.page-break { page-break-after: always; }
}
</style>
</head>
<body>
`
// =========================================================================
// Section 0: Cover Page
// =========================================================================
html += `
<div class="cover">
<h1>Pflichtenregister</h1>
<div class="subtitle">Regulatorische Pflichten — DSGVO, AI Act, NIS2 und weitere</div>
<div class="org-info">
<div><span class="label">Organisation:</span> ${escHtml(orgName)}</div>
${orgHeader.industry ? `<div><span class="label">Branche:</span> ${escHtml(orgHeader.industry)}</div>` : ''}
${orgHeader.dpoName ? `<div><span class="label">DSB:</span> ${escHtml(orgHeader.dpoName)}</div>` : ''}
${orgHeader.dpoContact ? `<div><span class="label">DSB-Kontakt:</span> ${escHtml(orgHeader.dpoContact)}</div>` : ''}
${orgHeader.responsiblePerson ? `<div><span class="label">Verantwortlicher:</span> ${escHtml(orgHeader.responsiblePerson)}</div>` : ''}
${orgHeader.legalDepartment ? `<div><span class="label">Rechtsabteilung:</span> ${escHtml(orgHeader.legalDepartment)}</div>` : ''}
</div>
<div class="legal-ref">
Version ${escHtml(orgHeader.documentVersion)} | Stand: ${today}<br/>
Letzte Pruefung: ${formatDateDE(orgHeader.lastReviewDate)} | Naechste Pruefung: ${formatDateDE(orgHeader.nextReviewDate)}<br/>
Pruefintervall: ${escHtml(orgHeader.reviewInterval)}
</div>
</div>
`
// =========================================================================
// Table of Contents
// =========================================================================
const sections = [
'Ziel und Zweck',
'Geltungsbereich',
'Methodik',
'Regulatorische Grundlagen',
'Pflichtenuebersicht',
'Detaillierte Pflichten',
'Verantwortlichkeiten',
'Fristen und Termine',
'Nachweisverzeichnis',
'Compliance-Status',
'Aenderungshistorie',
]
html += `
<div class="toc">
<h2>Inhaltsverzeichnis</h2>
${sections.map((s, i) => `<div class="toc-entry"><span><span class="toc-num">${i + 1}.</span> ${escHtml(s)}</span></div>`).join('\n ')}
</div>
`
// =========================================================================
// Section 1: Ziel und Zweck
// =========================================================================
html += `
<div class="section">
<div class="section-header">1. Ziel und Zweck</div>
<div class="section-body">
<p>Dieses Pflichtenregister dokumentiert alle regulatorischen Pflichten, denen
<strong>${escHtml(orgName)}</strong> unterliegt. Es dient der systematischen Erfassung,
Ueberwachung und Nachverfolgung aller Compliance-Anforderungen aus den anwendbaren
Regulierungen.</p>
<p style="margin-top: 8px;">Das Register erfuellt folgende Zwecke:</p>
<ul style="margin: 8px 0 8px 24px;">
<li>Vollstaendige Erfassung aller anwendbaren regulatorischen Pflichten</li>
<li>Zuordnung von Verantwortlichkeiten und Fristen</li>
<li>Nachverfolgung des Umsetzungsstatus</li>
<li>Dokumentation von Nachweisen fuer Audits</li>
<li>Identifikation von Compliance-Luecken und Handlungsbedarf</li>
</ul>
<table>
<tr><th>Rechtsrahmen</th><th>Relevanz</th></tr>
<tr><td><strong>DSGVO (EU) 2016/679</strong></td><td>Datenschutz-Grundverordnung — Kernregulierung fuer personenbezogene Daten</td></tr>
<tr><td><strong>AI Act (EU) 2024/1689</strong></td><td>KI-Verordnung — Anforderungen an KI-Systeme nach Risikoklasse</td></tr>
<tr><td><strong>NIS2 (EU) 2022/2555</strong></td><td>Netzwerk- und Informationssicherheit — Cybersicherheitspflichten</td></tr>
<tr><td><strong>BDSG</strong></td><td>Bundesdatenschutzgesetz — Nationale Ergaenzung zur DSGVO</td></tr>
</table>
</div>
</div>
`
// =========================================================================
// Section 2: Geltungsbereich
// =========================================================================
html += `
<div class="section">
<div class="section-header">2. Geltungsbereich</div>
<div class="section-body">
<p>Dieses Pflichtenregister gilt fuer alle Geschaeftsprozesse und IT-Systeme von
<strong>${escHtml(orgName)}</strong>${orgHeader.industry ? ` (Branche: ${escHtml(orgHeader.industry)})` : ''}.</p>
<p style="margin-top: 8px;">Anwendbare Regulierungen:</p>
<table>
<tr><th>Regulierung</th><th>Anzahl Pflichten</th><th>Status</th></tr>
`
for (const [source, obls] of bySource.entries()) {
const completed = obls.filter(o => o.status === 'completed').length
const pct = obls.length > 0 ? Math.round((completed / obls.length) * 100) : 0
html += ` <tr>
<td>${escHtml(source)}</td>
<td>${obls.length}</td>
<td>${completed}/${obls.length} abgeschlossen (${pct}%)</td>
</tr>
`
}
html += ` </table>
<p>Insgesamt umfasst dieses Register <strong>${obligations.length}</strong> Pflichten aus
<strong>${distinctSources.length}</strong> Regulierungen.</p>
</div>
</div>
`
// =========================================================================
// Section 3: Methodik
// =========================================================================
html += `
<div class="section">
<div class="section-header">3. Methodik</div>
<div class="section-body">
<p>Die Identifikation und Bewertung der Pflichten erfolgt in drei Schritten:</p>
<div class="principle"><strong>Pflicht-Identifikation:</strong> Systematische Analyse aller anwendbaren Regulierungen und Extraktion der einzelnen Pflichten mit Artikel-Referenz, Beschreibung und Zielgruppe.</div>
<div class="principle"><strong>Bewertung und Priorisierung:</strong> Jede Pflicht wird nach Prioritaet (kritisch, hoch, mittel, niedrig) und Dringlichkeit (Frist) bewertet. Die Bewertung basiert auf dem Risikopotenzial bei Nichterfuellung.</div>
<div class="principle"><strong>Ueberwachung und Nachverfolgung:</strong> Regelmaessige Pruefung des Umsetzungsstatus, Aktualisierung der Fristen und Dokumentation von Nachweisen.</div>
<p style="margin-top: 12px;">Die Pflichten werden ueber einen automatisierten Compliance-Check geprueft, der
11 Kriterien umfasst (siehe Abschnitt 10: Compliance-Status).</p>
</div>
</div>
`
// =========================================================================
// Section 4: Regulatorische Grundlagen
// =========================================================================
html += `
<div class="section page-break">
<div class="section-header">4. Regulatorische Grundlagen</div>
<div class="section-body">
<p>Die folgende Tabelle zeigt die regulatorischen Grundlagen mit Artikelzahl und Umsetzungsstatus:</p>
<table>
<tr>
<th>Regulierung</th>
<th>Pflichten</th>
<th>Kritisch</th>
<th>Hoch</th>
<th>Mittel</th>
<th>Niedrig</th>
<th>Abgeschlossen</th>
</tr>
`
for (const [source, obls] of bySource.entries()) {
const critical = obls.filter(o => o.priority === 'critical').length
const high = obls.filter(o => o.priority === 'high').length
const medium = obls.filter(o => o.priority === 'medium').length
const low = obls.filter(o => o.priority === 'low').length
const completed = obls.filter(o => o.status === 'completed').length
html += ` <tr>
<td><strong>${escHtml(source)}</strong></td>
<td>${obls.length}</td>
<td>${critical}</td>
<td>${high}</td>
<td>${medium}</td>
<td>${low}</td>
<td>${completed}</td>
</tr>
`
}
// Totals row
const totalCritical = obligations.filter(o => o.priority === 'critical').length
const totalHigh = obligations.filter(o => o.priority === 'high').length
const totalMedium = obligations.filter(o => o.priority === 'medium').length
const totalLow = obligations.filter(o => o.priority === 'low').length
const totalCompleted = obligations.filter(o => o.status === 'completed').length
html += ` <tr style="font-weight: 700; background: #f5f3ff;">
<td>Gesamt</td>
<td>${obligations.length}</td>
<td>${totalCritical}</td>
<td>${totalHigh}</td>
<td>${totalMedium}</td>
<td>${totalLow}</td>
<td>${totalCompleted}</td>
</tr>
`
html += ` </table>
</div>
</div>
`
// =========================================================================
// Section 5: Pflichtenuebersicht
// =========================================================================
html += `
<div class="section">
<div class="section-header">5. Pflichtenuebersicht</div>
<div class="section-body">
<p>Uebersicht aller ${obligations.length} Pflichten nach Regulierung und Status:</p>
<table>
<tr>
<th>Regulierung</th>
<th>Gesamt</th>
<th>Ausstehend</th>
<th>In Bearbeitung</th>
<th>Abgeschlossen</th>
<th>Ueberfaellig</th>
</tr>
`
for (const [source, obls] of bySource.entries()) {
const pending = obls.filter(o => o.status === 'pending').length
const inProgress = obls.filter(o => o.status === 'in-progress').length
const completed = obls.filter(o => o.status === 'completed').length
const overdue = obls.filter(o => o.status === 'overdue').length
html += ` <tr>
<td>${escHtml(source)}</td>
<td>${obls.length}</td>
<td>${pending}</td>
<td>${inProgress}</td>
<td>${completed}</td>
<td>${overdue > 0 ? `<span class="badge badge-critical">${overdue}</span>` : '0'}</td>
</tr>
`
}
html += ` </table>
</div>
</div>
`
// =========================================================================
// Section 6: Detaillierte Pflichten
// =========================================================================
html += `
<div class="section">
<div class="section-header">6. Detaillierte Pflichten</div>
<div class="section-body">
`
for (const [source, obls] of bySource.entries()) {
// Sort by priority (critical first) then by title
const priorityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 }
const sorted = [...obls].sort((a, b) => {
const pa = priorityOrder[a.priority] ?? 2
const pb = priorityOrder[b.priority] ?? 2
if (pa !== pb) return pa - pb
return a.title.localeCompare(b.title)
})
html += ` <h3 style="color: #5b21b6; margin: 20px 0 10px 0; font-size: 11pt;">${escHtml(source)} <span style="font-weight: 400; font-size: 9pt; color: #64748b;">(${sorted.length} Pflichten)</span></h3>
`
for (const o of sorted) {
const statusLabel = STATUS_LABELS_DE[o.status] || o.status
const statusBadge = STATUS_BADGE_CLASSES[o.status] || 'badge-draft'
const priorityLabel = PRIORITY_LABELS_DE[o.priority] || o.priority
const priorityBadge = PRIORITY_BADGE_CLASSES[o.priority] || 'badge-draft'
const deadlineStr = o.deadline ? formatDateDE(o.deadline) : '—'
const evidenceStr = o.evidence && o.evidence.length > 0
? o.evidence.map(e => escHtml(e)).join(', ')
: '<em style="color: #d97706;">Kein Nachweis</em>'
const systemsStr = o.linked_systems && o.linked_systems.length > 0
? o.linked_systems.map(s => escHtml(s)).join(', ')
: '—'
html += `
<div class="policy-detail">
<div class="policy-detail-header">
<span>${escHtml(o.title)}</span>
<span class="badge ${statusBadge}">${escHtml(statusLabel)}</span>
</div>
<div class="policy-detail-body">
<table>
<tr><th>Rechtsquelle</th><td>${escHtml(o.source)} ${escHtml(o.source_article || '')}</td></tr>
<tr><th>Beschreibung</th><td>${escHtml(o.description || '—')}</td></tr>
<tr><th>Prioritaet</th><td><span class="badge ${priorityBadge}">${escHtml(priorityLabel)}</span></td></tr>
<tr><th>Status</th><td><span class="badge ${statusBadge}">${escHtml(statusLabel)}</span></td></tr>
<tr><th>Verantwortlich</th><td>${escHtml(o.responsible || '—')}</td></tr>
<tr><th>Frist</th><td>${deadlineStr}</td></tr>
<tr><th>Nachweise</th><td>${evidenceStr}</td></tr>
<tr><th>Betroffene Systeme</th><td>${systemsStr}</td></tr>
${o.linked_vendor_ids && o.linked_vendor_ids.length > 0 ? `<tr><th>Auftragsverarbeiter</th><td>${o.linked_vendor_ids.map(id => escHtml(id)).join(', ')}</td></tr>` : ''}
${o.notes ? `<tr><th>Notizen</th><td>${escHtml(o.notes)}</td></tr>` : ''}
</table>
</div>
</div>
`
}
}
html += ` </div>
</div>
`
// =========================================================================
// Section 7: Verantwortlichkeiten
// =========================================================================
html += `
<div class="section page-break">
<div class="section-header">7. Verantwortlichkeiten</div>
<div class="section-body">
<p>Die folgende Rollenmatrix zeigt, welche Personen oder Abteilungen fuer welche Pflichten
die Umsetzungsverantwortung tragen:</p>
<table>
<tr><th>Verantwortlich</th><th>Pflichten</th><th>Anzahl</th><th>Davon offen</th></tr>
`
for (const [role, obls] of roleMap.entries()) {
const openCount = obls.filter(o => o.status !== 'completed').length
const titles = obls.slice(0, 5).map(o => escHtml(o.title))
const suffix = obls.length > 5 ? `, ... (+${obls.length - 5})` : ''
html += ` <tr>
<td>${escHtml(role)}</td>
<td>${titles.join('; ')}${suffix}</td>
<td>${obls.length}</td>
<td>${openCount}</td>
</tr>
`
}
html += ` </table>
</div>
</div>
`
// =========================================================================
// Section 8: Fristen und Termine
// =========================================================================
const now = new Date()
const withDeadline = obligations
.filter(o => o.deadline && o.status !== 'completed')
.sort((a, b) => new Date(a.deadline!).getTime() - new Date(b.deadline!).getTime())
const overdue = withDeadline.filter(o => new Date(o.deadline!) < now)
const upcoming = withDeadline.filter(o => new Date(o.deadline!) >= now)
html += `
<div class="section">
<div class="section-header">8. Fristen und Termine</div>
<div class="section-body">
`
if (overdue.length > 0) {
html += ` <h4 style="color: #dc2626; margin-bottom: 8px;">Ueberfaellige Pflichten (${overdue.length})</h4>
<table>
<tr><th>Pflicht</th><th>Regulierung</th><th>Frist</th><th>Tage ueberfaellig</th><th>Prioritaet</th></tr>
`
for (const o of overdue) {
const days = daysBetween(new Date(o.deadline!), now)
html += ` <tr>
<td>${escHtml(o.title)}</td>
<td>${escHtml(o.source)}</td>
<td>${formatDateDE(o.deadline)}</td>
<td><span class="badge badge-critical">${days} Tage</span></td>
<td><span class="badge ${PRIORITY_BADGE_CLASSES[o.priority] || 'badge-draft'}">${escHtml(PRIORITY_LABELS_DE[o.priority] || o.priority)}</span></td>
</tr>
`
}
html += ` </table>
`
}
if (upcoming.length > 0) {
html += ` <h4 style="color: #5b21b6; margin: 16px 0 8px 0;">Anstehende Fristen (${upcoming.length})</h4>
<table>
<tr><th>Pflicht</th><th>Regulierung</th><th>Frist</th><th>Verbleibend</th><th>Verantwortlich</th></tr>
`
for (const o of upcoming.slice(0, 20)) {
const days = daysBetween(now, new Date(o.deadline!))
html += ` <tr>
<td>${escHtml(o.title)}</td>
<td>${escHtml(o.source)}</td>
<td>${formatDateDE(o.deadline)}</td>
<td>${days} Tage</td>
<td>${escHtml(o.responsible || '—')}</td>
</tr>
`
}
if (upcoming.length > 20) {
html += ` <tr><td colspan="5" style="text-align: center; color: #64748b;">... und ${upcoming.length - 20} weitere</td></tr>
`
}
html += ` </table>
`
}
if (withDeadline.length === 0) {
html += ` <p><em>Keine offenen Pflichten mit Fristen vorhanden.</em></p>
`
}
html += ` </div>
</div>
`
// =========================================================================
// Section 9: Nachweisverzeichnis
// =========================================================================
const withEvidence = obligations.filter(o => o.evidence && o.evidence.length > 0)
const withoutEvidence = obligations.filter(o => !o.evidence || o.evidence.length === 0)
html += `
<div class="section page-break">
<div class="section-header">9. Nachweisverzeichnis</div>
<div class="section-body">
<p>${withEvidence.length} von ${obligations.length} Pflichten haben Nachweise hinterlegt.</p>
`
if (withEvidence.length > 0) {
html += ` <table>
<tr><th>Pflicht</th><th>Regulierung</th><th>Nachweise</th><th>Status</th></tr>
`
for (const o of withEvidence) {
html += ` <tr>
<td>${escHtml(o.title)}</td>
<td>${escHtml(o.source)}</td>
<td>${o.evidence!.map(e => escHtml(e)).join(', ')}</td>
<td><span class="badge ${STATUS_BADGE_CLASSES[o.status] || 'badge-draft'}">${escHtml(STATUS_LABELS_DE[o.status] || o.status)}</span></td>
</tr>
`
}
html += ` </table>
`
}
if (withoutEvidence.length > 0) {
html += ` <p style="margin-top: 12px;"><strong>Pflichten ohne Nachweise (${withoutEvidence.length}):</strong></p>
<ul style="margin: 4px 0 8px 24px; font-size: 9pt; color: #d97706;">
`
for (const o of withoutEvidence.slice(0, 15)) {
html += ` <li>${escHtml(o.title)} (${escHtml(o.source)})</li>
`
}
if (withoutEvidence.length > 15) {
html += ` <li>... und ${withoutEvidence.length - 15} weitere</li>
`
}
html += ` </ul>
`
}
html += ` </div>
</div>
`
// =========================================================================
// Section 10: Compliance-Status
// =========================================================================
html += `
<div class="section">
<div class="section-header">10. Compliance-Status</div>
<div class="section-body">
`
if (complianceResult) {
const scoreClass = complianceResult.score >= 90 ? 'score-excellent'
: complianceResult.score >= 75 ? 'score-good'
: complianceResult.score >= 50 ? 'score-needs-work'
: 'score-poor'
const scoreLabel = complianceResult.score >= 90 ? 'Ausgezeichnet'
: complianceResult.score >= 75 ? 'Gut'
: complianceResult.score >= 50 ? 'Verbesserungswuerdig'
: 'Mangelhaft'
html += ` <p><span class="score-box ${scoreClass}">${complianceResult.score}/100</span> ${escHtml(scoreLabel)}</p>
<table style="margin-top: 12px;">
<tr><th>Kennzahl</th><th>Wert</th></tr>
<tr><td>Geprueft am</td><td>${formatDateDE(complianceResult.checkedAt)}</td></tr>
<tr><td>Befunde gesamt</td><td>${complianceResult.summary.total}</td></tr>
<tr><td>Kritisch</td><td>${complianceResult.summary.critical}</td></tr>
<tr><td>Hoch</td><td>${complianceResult.summary.high}</td></tr>
<tr><td>Mittel</td><td>${complianceResult.summary.medium}</td></tr>
<tr><td>Niedrig</td><td>${complianceResult.summary.low}</td></tr>
</table>
`
if (complianceResult.issues.length > 0) {
html += ` <p style="margin-top: 12px;"><strong>Befunde nach Schweregrad:</strong></p>
<table>
<tr><th>Schweregrad</th><th>Befund</th><th>Betroffene Pflichten</th><th>Empfehlung</th></tr>
`
const severityOrder: ObligationComplianceIssueSeverity[] = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']
for (const sev of severityOrder) {
const issuesForSev = complianceResult.issues.filter(i => i.severity === sev)
for (const issue of issuesForSev) {
html += ` <tr>
<td><span class="badge badge-${sev.toLowerCase()}" style="color: ${OBLIGATION_SEVERITY_COLORS[sev]}">${OBLIGATION_SEVERITY_LABELS_DE[sev]}</span></td>
<td>${escHtml(issue.message)}</td>
<td>${issue.affectedObligations.length > 0 ? issue.affectedObligations.length + ' Pflicht(en)' : '—'}</td>
<td>${escHtml(issue.recommendation)}</td>
</tr>
`
}
}
html += ` </table>
`
} else {
html += ` <p style="margin-top: 8px;"><em>Keine Beanstandungen. Alle Pflichten sind konform.</em></p>
`
}
} else {
html += ` <p><em>Compliance-Check wurde noch nicht ausgefuehrt. Fuehren Sie den Check im
Pflichtenregister-Tab durch, um den Status in das Dokument aufzunehmen.</em></p>
`
}
html += ` </div>
</div>
`
// =========================================================================
// Section 11: Aenderungshistorie
// =========================================================================
html += `
<div class="section">
<div class="section-header">11. Aenderungshistorie</div>
<div class="section-body">
<table>
<tr><th>Version</th><th>Datum</th><th>Autor</th><th>Aenderungen</th></tr>
`
if (revisions.length > 0) {
for (const rev of revisions) {
html += ` <tr>
<td>${escHtml(rev.version)}</td>
<td>${formatDateDE(rev.date)}</td>
<td>${escHtml(rev.author)}</td>
<td>${escHtml(rev.changes)}</td>
</tr>
`
}
} else {
html += ` <tr>
<td>${escHtml(orgHeader.documentVersion)}</td>
<td>${today}</td>
<td>${escHtml(orgHeader.dpoName || orgHeader.responsiblePerson || '—')}</td>
<td>Erstversion des Pflichtenregisters</td>
</tr>
`
}
html += ` </table>
</div>
</div>
`
// =========================================================================
// Footer
// =========================================================================
html += `
<div class="page-footer">
<span>Pflichtenregister — ${escHtml(orgName)}</span>
<span>Stand: ${today} | Version ${escHtml(orgHeader.documentVersion)}</span>
</div>
</body>
</html>`
return html
}
// =============================================================================
// INTERNAL HELPERS
// =============================================================================
function escHtml(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
function formatDateDE(dateStr: string | null | undefined): string {
if (!dateStr) return '—'
try {
const date = new Date(dateStr)
if (isNaN(date.getTime())) return '—'
return date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
} catch {
return '—'
}
}
function daysBetween(earlier: Date, later: Date): number {
const diffMs = later.getTime() - earlier.getTime()
return Math.floor(diffMs / (1000 * 60 * 60 * 24))
}

View File

@@ -0,0 +1,553 @@
// =============================================================================
// TOM Module - Compliance Check Engine
// Prueft Technische und Organisatorische Massnahmen auf Vollstaendigkeit,
// Konsistenz und DSGVO-Konformitaet (Art. 32 DSGVO)
// =============================================================================
import type {
TOMGeneratorState,
DerivedTOM,
RiskProfile,
DataProfile,
ControlCategory,
ImplementationStatus,
} from './tom-generator/types'
import { getControlById, getControlsByCategory, getAllCategories } from './tom-generator/controls/loader'
import { SDM_CATEGORY_MAPPING } from './tom-generator/types'
// =============================================================================
// TYPES
// =============================================================================
export type TOMComplianceIssueSeverity = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'
export type TOMComplianceIssueType =
| 'MISSING_RESPONSIBLE'
| 'OVERDUE_REVIEW'
| 'MISSING_EVIDENCE'
| 'INCOMPLETE_CATEGORY'
| 'NO_ENCRYPTION_MEASURES'
| 'NO_PSEUDONYMIZATION'
| 'MISSING_AVAILABILITY'
| 'NO_REVIEW_PROCESS'
| 'UNCOVERED_SDM_GOAL'
| 'HIGH_RISK_WITHOUT_MEASURES'
| 'STALE_NOT_IMPLEMENTED'
export interface TOMComplianceIssue {
id: string
controlId: string
controlName: string
type: TOMComplianceIssueType
severity: TOMComplianceIssueSeverity
title: string
description: string
recommendation: string
}
export interface TOMComplianceCheckResult {
issues: TOMComplianceIssue[]
score: number // 0-100
stats: {
total: number
passed: number
failed: number
bySeverity: Record<TOMComplianceIssueSeverity, number>
}
}
// =============================================================================
// CONSTANTS
// =============================================================================
export const TOM_SEVERITY_LABELS_DE: Record<TOMComplianceIssueSeverity, string> = {
CRITICAL: 'Kritisch',
HIGH: 'Hoch',
MEDIUM: 'Mittel',
LOW: 'Niedrig',
}
export const TOM_SEVERITY_COLORS: Record<TOMComplianceIssueSeverity, string> = {
CRITICAL: '#dc2626',
HIGH: '#ea580c',
MEDIUM: '#d97706',
LOW: '#6b7280',
}
// =============================================================================
// HELPERS
// =============================================================================
let issueCounter = 0
function createIssueId(): string {
issueCounter++
return `TCI-${Date.now()}-${String(issueCounter).padStart(4, '0')}`
}
function createIssue(
controlId: string,
controlName: string,
type: TOMComplianceIssueType,
severity: TOMComplianceIssueSeverity,
title: string,
description: string,
recommendation: string
): TOMComplianceIssue {
return { id: createIssueId(), controlId, controlName, type, severity, title, description, recommendation }
}
function daysBetween(date: Date, now: Date): number {
const diffMs = now.getTime() - date.getTime()
return Math.floor(diffMs / (1000 * 60 * 60 * 24))
}
// =============================================================================
// PER-TOM CHECKS (1-3, 11)
// =============================================================================
/**
* Check 1: MISSING_RESPONSIBLE (MEDIUM)
* REQUIRED TOM without responsiblePerson AND responsibleDepartment.
*/
function checkMissingResponsible(tom: DerivedTOM): TOMComplianceIssue | null {
if (tom.applicability !== 'REQUIRED') return null
if (!tom.responsiblePerson && !tom.responsibleDepartment) {
return createIssue(
tom.controlId,
tom.name,
'MISSING_RESPONSIBLE',
'MEDIUM',
'Keine verantwortliche Person/Abteilung',
`Die TOM "${tom.name}" ist als REQUIRED eingestuft, hat aber weder eine verantwortliche Person noch eine verantwortliche Abteilung zugewiesen. Ohne klare Verantwortlichkeit kann die Massnahme nicht zuverlaessig umgesetzt und gepflegt werden.`,
'Weisen Sie eine verantwortliche Person oder Abteilung zu, die fuer die Umsetzung und regelmaessige Pruefung dieser Massnahme zustaendig ist.'
)
}
return null
}
/**
* Check 2: OVERDUE_REVIEW (MEDIUM)
* TOM with reviewDate in the past.
*/
function checkOverdueReview(tom: DerivedTOM): TOMComplianceIssue | null {
if (!tom.reviewDate) return null
const reviewDate = new Date(tom.reviewDate)
const now = new Date()
if (reviewDate < now) {
const overdueDays = daysBetween(reviewDate, now)
return createIssue(
tom.controlId,
tom.name,
'OVERDUE_REVIEW',
'MEDIUM',
'Ueberfaellige Pruefung',
`Die TOM "${tom.name}" haette am ${reviewDate.toLocaleDateString('de-DE')} geprueft werden muessen. Die Pruefung ist ${overdueDays} Tag(e) ueberfaellig. Gemaess Art. 32 Abs. 1 lit. d DSGVO ist eine regelmaessige Ueberpruefung der Wirksamkeit von TOMs erforderlich.`,
'Fuehren Sie umgehend eine Wirksamkeitspruefung dieser Massnahme durch und aktualisieren Sie das naechste Pruefungsdatum.'
)
}
return null
}
/**
* Check 3: MISSING_EVIDENCE (HIGH)
* IMPLEMENTED TOM where linkedEvidence is empty but the control has evidenceRequirements.
*/
function checkMissingEvidence(tom: DerivedTOM): TOMComplianceIssue | null {
if (tom.implementationStatus !== 'IMPLEMENTED') return null
if (tom.linkedEvidence.length > 0) return null
const control = getControlById(tom.controlId)
if (!control || control.evidenceRequirements.length === 0) return null
return createIssue(
tom.controlId,
tom.name,
'MISSING_EVIDENCE',
'HIGH',
'Kein Nachweis hinterlegt',
`Die TOM "${tom.name}" ist als IMPLEMENTED markiert, hat aber keine verknuepften Nachweisdokumente. Der Control erfordert ${control.evidenceRequirements.length} Nachweis(e): ${control.evidenceRequirements.join(', ')}. Ohne Nachweise ist die Umsetzung nicht auditfaehig.`,
'Laden Sie die erforderlichen Nachweisdokumente hoch und verknuepfen Sie sie mit dieser Massnahme.'
)
}
/**
* Check 11: STALE_NOT_IMPLEMENTED (LOW)
* REQUIRED TOM that has been NOT_IMPLEMENTED for >90 days.
* Uses implementationDate === null and state.createdAt / state.updatedAt as reference.
*/
function checkStaleNotImplemented(tom: DerivedTOM, state: TOMGeneratorState): TOMComplianceIssue | null {
if (tom.applicability !== 'REQUIRED') return null
if (tom.implementationStatus !== 'NOT_IMPLEMENTED') return null
if (tom.implementationDate !== null) return null
const referenceDate = state.createdAt ? new Date(state.createdAt) : (state.updatedAt ? new Date(state.updatedAt) : null)
if (!referenceDate) return null
const ageInDays = daysBetween(referenceDate, new Date())
if (ageInDays <= 90) return null
return createIssue(
tom.controlId,
tom.name,
'STALE_NOT_IMPLEMENTED',
'LOW',
'Langfristig nicht umgesetzte Pflichtmassnahme',
`Die TOM "${tom.name}" ist als REQUIRED eingestuft, aber seit ${ageInDays} Tagen nicht umgesetzt. Pflichtmassnahmen, die laenger als 90 Tage nicht implementiert werden, deuten auf organisatorische Blockaden oder unzureichende Priorisierung hin.`,
'Pruefen Sie, ob die Massnahme weiterhin erforderlich ist, und erstellen Sie einen konkreten Umsetzungsplan mit Verantwortlichkeiten und Fristen.'
)
}
// =============================================================================
// AGGREGATE CHECKS (4-10)
// =============================================================================
/**
* Check 4: INCOMPLETE_CATEGORY (HIGH)
* Category where ALL applicable (REQUIRED) controls are NOT_IMPLEMENTED.
*/
function checkIncompleteCategory(toms: DerivedTOM[]): TOMComplianceIssue[] {
const issues: TOMComplianceIssue[] = []
// Group applicable TOMs by category
const categoryMap = new Map<ControlCategory, DerivedTOM[]>()
for (const tom of toms) {
const control = getControlById(tom.controlId)
if (!control) continue
const category = control.category
if (!categoryMap.has(category)) {
categoryMap.set(category, [])
}
categoryMap.get(category)!.push(tom)
}
for (const [category, categoryToms] of Array.from(categoryMap.entries())) {
// Only check categories that have at least one REQUIRED control
const requiredToms = categoryToms.filter((t: DerivedTOM) => t.applicability === 'REQUIRED')
if (requiredToms.length === 0) continue
const allNotImplemented = requiredToms.every((t: DerivedTOM) => t.implementationStatus === 'NOT_IMPLEMENTED')
if (allNotImplemented) {
issues.push(
createIssue(
category,
category,
'INCOMPLETE_CATEGORY',
'HIGH',
`Kategorie "${category}" vollstaendig ohne Umsetzung`,
`Alle ${requiredToms.length} Pflichtmassnahme(n) in der Kategorie "${category}" sind nicht umgesetzt. Eine vollstaendig unabgedeckte Kategorie stellt eine erhebliche Luecke im TOM-Konzept dar.`,
`Setzen Sie mindestens die wichtigsten Massnahmen in der Kategorie "${category}" um, um eine Grundabdeckung sicherzustellen.`
)
)
}
}
return issues
}
/**
* Check 5: NO_ENCRYPTION_MEASURES (CRITICAL)
* No ENCRYPTION control with status IMPLEMENTED.
*/
function checkNoEncryption(toms: DerivedTOM[]): TOMComplianceIssue | null {
const hasImplementedEncryption = toms.some((tom) => {
const control = getControlById(tom.controlId)
return control?.category === 'ENCRYPTION' && tom.implementationStatus === 'IMPLEMENTED'
})
if (!hasImplementedEncryption) {
return createIssue(
'ENCRYPTION',
'Verschluesselung',
'NO_ENCRYPTION_MEASURES',
'CRITICAL',
'Keine Verschluesselungsmassnahmen umgesetzt',
'Es ist keine einzige Verschluesselungsmassnahme als IMPLEMENTED markiert. Art. 32 Abs. 1 lit. a DSGVO nennt Verschluesselung explizit als geeignete technische Massnahme. Ohne Verschluesselung sind personenbezogene Daten bei Zugriff oder Verlust ungeschuetzt.',
'Implementieren Sie umgehend Verschluesselungsmassnahmen fuer Daten im Ruhezustand (Encryption at Rest) und waehrend der Uebertragung (Encryption in Transit).'
)
}
return null
}
/**
* Check 6: NO_PSEUDONYMIZATION (MEDIUM)
* DataProfile has special categories (Art. 9) but no PSEUDONYMIZATION control implemented.
*/
function checkNoPseudonymization(toms: DerivedTOM[], dataProfile: DataProfile | null): TOMComplianceIssue | null {
if (!dataProfile || !dataProfile.hasSpecialCategories) return null
const hasImplementedPseudonymization = toms.some((tom) => {
const control = getControlById(tom.controlId)
return control?.category === 'PSEUDONYMIZATION' && tom.implementationStatus === 'IMPLEMENTED'
})
if (!hasImplementedPseudonymization) {
return createIssue(
'PSEUDONYMIZATION',
'Pseudonymisierung',
'NO_PSEUDONYMIZATION',
'MEDIUM',
'Keine Pseudonymisierung bei besonderen Datenkategorien',
'Das Datenprofil enthaelt besondere Kategorien personenbezogener Daten (Art. 9 DSGVO), aber keine Pseudonymisierungsmassnahme ist umgesetzt. Art. 32 Abs. 1 lit. a DSGVO empfiehlt Pseudonymisierung ausdruecklich als Schutzmassnahme.',
'Implementieren Sie Pseudonymisierungsmassnahmen fuer die Verarbeitung besonderer Datenkategorien, um das Risiko fuer betroffene Personen zu minimieren.'
)
}
return null
}
/**
* Check 7: MISSING_AVAILABILITY (HIGH)
* No AVAILABILITY or RECOVERY control implemented AND no DR plan in securityProfile.
*/
function checkMissingAvailability(toms: DerivedTOM[], state: TOMGeneratorState): TOMComplianceIssue | null {
const hasAvailabilityOrRecovery = toms.some((tom) => {
const control = getControlById(tom.controlId)
return (
(control?.category === 'AVAILABILITY' || control?.category === 'RECOVERY') &&
tom.implementationStatus === 'IMPLEMENTED'
)
})
const hasDRPlan = state.securityProfile?.hasDRPlan ?? false
if (!hasAvailabilityOrRecovery && !hasDRPlan) {
return createIssue(
'AVAILABILITY',
'Verfuegbarkeit / Wiederherstellbarkeit',
'MISSING_AVAILABILITY',
'HIGH',
'Keine Verfuegbarkeits- oder Wiederherstellungsmassnahmen',
'Weder Verfuegbarkeits- noch Wiederherstellungsmassnahmen sind umgesetzt, und es existiert kein Disaster-Recovery-Plan im Security-Profil. Art. 32 Abs. 1 lit. b und c DSGVO verlangen die Faehigkeit zur raschen Wiederherstellung der Verfuegbarkeit personenbezogener Daten.',
'Implementieren Sie Backup-Konzepte, Redundanzloesungen und einen Disaster-Recovery-Plan, um die Verfuegbarkeit und Wiederherstellbarkeit sicherzustellen.'
)
}
return null
}
/**
* Check 8: NO_REVIEW_PROCESS (MEDIUM)
* No REVIEW control implemented (Art. 32 Abs. 1 lit. d requires periodic review).
*/
function checkNoReviewProcess(toms: DerivedTOM[]): TOMComplianceIssue | null {
const hasImplementedReview = toms.some((tom) => {
const control = getControlById(tom.controlId)
return control?.category === 'REVIEW' && tom.implementationStatus === 'IMPLEMENTED'
})
if (!hasImplementedReview) {
return createIssue(
'REVIEW',
'Ueberpruefung & Bewertung',
'NO_REVIEW_PROCESS',
'MEDIUM',
'Kein Verfahren zur regelmaessigen Ueberpruefung',
'Es ist keine Ueberpruefungsmassnahme als IMPLEMENTED markiert. Art. 32 Abs. 1 lit. d DSGVO verlangt ein Verfahren zur regelmaessigen Ueberpruefung, Bewertung und Evaluierung der Wirksamkeit der technischen und organisatorischen Massnahmen.',
'Implementieren Sie einen regelmaessigen Review-Prozess (z.B. quartalsweise TOM-Audits, jaehrliche Wirksamkeitspruefung) und dokumentieren Sie die Ergebnisse.'
)
}
return null
}
/**
* Check 9: UNCOVERED_SDM_GOAL (HIGH)
* SDM goal with 0% coverage — no implemented control maps to it via SDM_CATEGORY_MAPPING.
*/
function checkUncoveredSDMGoal(toms: DerivedTOM[]): TOMComplianceIssue[] {
const issues: TOMComplianceIssue[] = []
// Build reverse mapping: SDM goal -> ControlCategories that cover it
const sdmGoals = [
'Verfuegbarkeit',
'Integritaet',
'Vertraulichkeit',
'Nichtverkettung',
'Intervenierbarkeit',
'Transparenz',
'Datenminimierung',
] as const
const goalToCategoriesMap = new Map<string, ControlCategory[]>()
for (const goal of sdmGoals) {
goalToCategoriesMap.set(goal, [])
}
// Build reverse lookup from SDM_CATEGORY_MAPPING
for (const [category, goals] of Object.entries(SDM_CATEGORY_MAPPING)) {
for (const goal of goals) {
const existing = goalToCategoriesMap.get(goal)
if (existing) {
existing.push(category as ControlCategory)
}
}
}
// Collect implemented categories
const implementedCategories = new Set<ControlCategory>()
for (const tom of toms) {
if (tom.implementationStatus !== 'IMPLEMENTED') continue
const control = getControlById(tom.controlId)
if (control) {
implementedCategories.add(control.category)
}
}
// Check each SDM goal
for (const goal of sdmGoals) {
const coveringCategories = goalToCategoriesMap.get(goal) ?? []
const hasCoverage = coveringCategories.some((cat) => implementedCategories.has(cat))
if (!hasCoverage) {
issues.push(
createIssue(
`SDM-${goal}`,
goal,
'UNCOVERED_SDM_GOAL',
'HIGH',
`SDM-Gewaehrleistungsziel "${goal}" nicht abgedeckt`,
`Das Gewaehrleistungsziel "${goal}" des Standard-Datenschutzmodells (SDM) ist durch keine umgesetzte Massnahme abgedeckt. Zugehoerige Kategorien (${coveringCategories.join(', ')}) haben keine IMPLEMENTED Controls. Das SDM ist die anerkannte Methodik zur Umsetzung der DSGVO-Anforderungen.`,
`Setzen Sie mindestens eine Massnahme aus den Kategorien ${coveringCategories.join(', ')} um, um das SDM-Ziel "${goal}" abzudecken.`
)
)
}
}
return issues
}
/**
* Check 10: HIGH_RISK_WITHOUT_MEASURES (CRITICAL)
* Protection level VERY_HIGH but < 50% of REQUIRED controls implemented.
*/
function checkHighRiskWithoutMeasures(toms: DerivedTOM[], riskProfile: RiskProfile | null): TOMComplianceIssue | null {
if (!riskProfile || riskProfile.protectionLevel !== 'VERY_HIGH') return null
const requiredToms = toms.filter((t) => t.applicability === 'REQUIRED')
if (requiredToms.length === 0) return null
const implementedCount = requiredToms.filter((t) => t.implementationStatus === 'IMPLEMENTED').length
const implementationRate = implementedCount / requiredToms.length
if (implementationRate < 0.5) {
const percentage = Math.round(implementationRate * 100)
return createIssue(
'RISK-PROFILE',
'Risikoprofil VERY_HIGH',
'HIGH_RISK_WITHOUT_MEASURES',
'CRITICAL',
'Sehr hoher Schutzbedarf bei niedriger Umsetzungsrate',
`Der Schutzbedarf ist als VERY_HIGH eingestuft, aber nur ${implementedCount} von ${requiredToms.length} Pflichtmassnahmen (${percentage}%) sind umgesetzt. Bei sehr hohem Schutzbedarf muessen mindestens 50% der Pflichtmassnahmen implementiert sein, um ein angemessenes Schutzniveau gemaess Art. 32 DSGVO zu gewaehrleisten.`,
'Priorisieren Sie die Umsetzung der verbleibenden Pflichtmassnahmen. Beginnen Sie mit CRITICAL- und HIGH-Priority Controls. Erwaeegen Sie einen Umsetzungsplan mit klaren Meilensteinen.'
)
}
return null
}
// =============================================================================
// MAIN COMPLIANCE CHECK
// =============================================================================
/**
* Fuehrt einen vollstaendigen Compliance-Check ueber alle TOMs durch.
*
* @param state - Der vollstaendige TOMGeneratorState
* @returns TOMComplianceCheckResult mit Issues, Score und Statistiken
*/
export function runTOMComplianceCheck(state: TOMGeneratorState): TOMComplianceCheckResult {
// Reset counter for deterministic IDs within a single check run
issueCounter = 0
const issues: TOMComplianceIssue[] = []
// Filter to applicable TOMs only (REQUIRED or RECOMMENDED, exclude NOT_APPLICABLE)
const applicableTOMs = state.derivedTOMs.filter(
(tom) => tom.applicability === 'REQUIRED' || tom.applicability === 'RECOMMENDED'
)
// Run per-TOM checks (1-3, 11) on each applicable TOM
for (const tom of applicableTOMs) {
const perTomChecks = [
checkMissingResponsible(tom),
checkOverdueReview(tom),
checkMissingEvidence(tom),
checkStaleNotImplemented(tom, state),
]
for (const issue of perTomChecks) {
if (issue !== null) {
issues.push(issue)
}
}
}
// Run aggregate checks (4-10)
issues.push(...checkIncompleteCategory(applicableTOMs))
const aggregateChecks = [
checkNoEncryption(applicableTOMs),
checkNoPseudonymization(applicableTOMs, state.dataProfile),
checkMissingAvailability(applicableTOMs, state),
checkNoReviewProcess(applicableTOMs),
checkHighRiskWithoutMeasures(applicableTOMs, state.riskProfile),
]
for (const issue of aggregateChecks) {
if (issue !== null) {
issues.push(issue)
}
}
issues.push(...checkUncoveredSDMGoal(applicableTOMs))
// Calculate score
const bySeverity: Record<TOMComplianceIssueSeverity, number> = {
LOW: 0,
MEDIUM: 0,
HIGH: 0,
CRITICAL: 0,
}
for (const issue of issues) {
bySeverity[issue.severity]++
}
const rawScore =
100 -
(bySeverity.CRITICAL * 15 +
bySeverity.HIGH * 10 +
bySeverity.MEDIUM * 5 +
bySeverity.LOW * 2)
const score = Math.max(0, rawScore)
// Calculate pass/fail per TOM
const failedControlIds = new Set(
issues.filter((i) => !i.controlId.startsWith('SDM-') && i.controlId !== 'RISK-PROFILE').map((i) => i.controlId)
)
const totalTOMs = applicableTOMs.length
const failedCount = failedControlIds.size
const passedCount = Math.max(0, totalTOMs - failedCount)
return {
issues,
score,
stats: {
total: totalTOMs,
passed: passedCount,
failed: failedCount,
bySeverity,
},
}
}

View File

@@ -0,0 +1,906 @@
// =============================================================================
// TOM Module - TOM-Dokumentation Document Generator
// Generates a printable, audit-ready HTML document according to DSGVO Art. 32
// =============================================================================
import type {
TOMGeneratorState,
DerivedTOM,
CompanyProfile,
RiskProfile,
ControlCategory,
} from './tom-generator/types'
import { SDM_CATEGORY_MAPPING } from './tom-generator/types'
import {
getControlById,
getControlsByCategory,
getAllCategories,
getCategoryMetadata,
} from './tom-generator/controls/loader'
import type { TOMComplianceCheckResult, TOMComplianceIssueSeverity } from './tom-compliance'
// =============================================================================
// TYPES
// =============================================================================
export interface TOMDocumentOrgHeader {
organizationName: string
industry: string
dpoName: string
dpoContact: string
responsiblePerson: string
itSecurityContact: string
locations: string[]
employeeCount: string
documentVersion: string
lastReviewDate: string
nextReviewDate: string
reviewInterval: string
}
export interface TOMDocumentRevision {
version: string
date: string
author: string
changes: string
}
// =============================================================================
// DEFAULTS
// =============================================================================
export function createDefaultTOMDocumentOrgHeader(): TOMDocumentOrgHeader {
const now = new Date()
const nextYear = new Date()
nextYear.setFullYear(nextYear.getFullYear() + 1)
return {
organizationName: '',
industry: '',
dpoName: '',
dpoContact: '',
responsiblePerson: '',
itSecurityContact: '',
locations: [],
employeeCount: '',
documentVersion: '1.0',
lastReviewDate: now.toISOString().split('T')[0],
nextReviewDate: nextYear.toISOString().split('T')[0],
reviewInterval: 'Jaehrlich',
}
}
// =============================================================================
// SEVERITY LABELS (for Compliance Status section)
// =============================================================================
const SEVERITY_LABELS_DE: Record<TOMComplianceIssueSeverity, string> = {
CRITICAL: 'Kritisch',
HIGH: 'Hoch',
MEDIUM: 'Mittel',
LOW: 'Niedrig',
}
const SEVERITY_COLORS: Record<TOMComplianceIssueSeverity, string> = {
CRITICAL: '#dc2626',
HIGH: '#ea580c',
MEDIUM: '#d97706',
LOW: '#6b7280',
}
// =============================================================================
// CATEGORY LABELS (German)
// =============================================================================
const CATEGORY_LABELS_DE: Record<ControlCategory, string> = {
ACCESS_CONTROL: 'Zutrittskontrolle',
ADMISSION_CONTROL: 'Zugangskontrolle',
ACCESS_AUTHORIZATION: 'Zugriffskontrolle',
TRANSFER_CONTROL: 'Weitergabekontrolle',
INPUT_CONTROL: 'Eingabekontrolle',
ORDER_CONTROL: 'Auftragskontrolle',
AVAILABILITY: 'Verfuegbarkeit',
SEPARATION: 'Trennbarkeit',
ENCRYPTION: 'Verschluesselung',
PSEUDONYMIZATION: 'Pseudonymisierung',
RESILIENCE: 'Belastbarkeit',
RECOVERY: 'Wiederherstellbarkeit',
REVIEW: 'Ueberpruefung & Bewertung',
}
// =============================================================================
// STATUS & APPLICABILITY LABELS
// =============================================================================
const STATUS_LABELS_DE: Record<string, string> = {
IMPLEMENTED: 'Umgesetzt',
PARTIAL: 'Teilweise umgesetzt',
NOT_IMPLEMENTED: 'Nicht umgesetzt',
}
const STATUS_BADGE_CLASSES: Record<string, string> = {
IMPLEMENTED: 'badge-active',
PARTIAL: 'badge-review',
NOT_IMPLEMENTED: 'badge-critical',
}
const APPLICABILITY_LABELS_DE: Record<string, string> = {
REQUIRED: 'Erforderlich',
RECOMMENDED: 'Empfohlen',
OPTIONAL: 'Optional',
NOT_APPLICABLE: 'Nicht anwendbar',
}
// =============================================================================
// HTML DOCUMENT BUILDER
// =============================================================================
export function buildTOMDocumentHtml(
derivedTOMs: DerivedTOM[],
orgHeader: TOMDocumentOrgHeader,
companyProfile: CompanyProfile | null,
riskProfile: RiskProfile | null,
complianceResult: TOMComplianceCheckResult | null,
revisions: TOMDocumentRevision[]
): string {
const today = new Date().toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
const orgName = orgHeader.organizationName || 'Organisation'
// Filter out NOT_APPLICABLE TOMs for display
const applicableTOMs = derivedTOMs.filter(t => t.applicability !== 'NOT_APPLICABLE')
// Group TOMs by category via control library lookup
const tomsByCategory = new Map<ControlCategory, DerivedTOM[]>()
for (const tom of applicableTOMs) {
const control = getControlById(tom.controlId)
const cat = control?.category || 'REVIEW'
if (!tomsByCategory.has(cat)) tomsByCategory.set(cat, [])
tomsByCategory.get(cat)!.push(tom)
}
// Build role map: role/department → list of control codes
const roleMap = new Map<string, string[]>()
for (const tom of applicableTOMs) {
const role = tom.responsiblePerson || tom.responsibleDepartment || 'Nicht zugewiesen'
if (!roleMap.has(role)) roleMap.set(role, [])
const control = getControlById(tom.controlId)
roleMap.get(role)!.push(control?.code || tom.controlId)
}
// =========================================================================
// HTML Template
// =========================================================================
let html = `<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>TOM-Dokumentation — ${escHtml(orgName)}</title>
<style>
@page { size: A4; margin: 20mm 18mm 22mm 18mm; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 10pt;
line-height: 1.5;
color: #1e293b;
}
/* Cover */
.cover {
min-height: 90vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
page-break-after: always;
}
.cover h1 {
font-size: 28pt;
color: #5b21b6;
margin-bottom: 8px;
font-weight: 700;
}
.cover .subtitle {
font-size: 14pt;
color: #7c3aed;
margin-bottom: 40px;
}
.cover .org-info {
background: #f5f3ff;
border: 1px solid #ddd6fe;
border-radius: 8px;
padding: 24px 40px;
text-align: left;
width: 400px;
margin-bottom: 24px;
}
.cover .org-info div { margin-bottom: 6px; }
.cover .org-info .label { font-weight: 600; color: #5b21b6; display: inline-block; min-width: 160px; }
.cover .legal-ref {
font-size: 9pt;
color: #64748b;
margin-top: 20px;
}
/* TOC */
.toc {
page-break-after: always;
padding-top: 40px;
}
.toc h2 {
font-size: 18pt;
color: #5b21b6;
margin-bottom: 20px;
border-bottom: 2px solid #5b21b6;
padding-bottom: 8px;
}
.toc-entry {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px dotted #cbd5e1;
font-size: 10pt;
}
.toc-entry .toc-num { font-weight: 600; color: #5b21b6; min-width: 40px; }
/* Sections */
.section {
page-break-inside: avoid;
margin-bottom: 24px;
}
.section-header {
font-size: 14pt;
color: #5b21b6;
font-weight: 700;
margin: 30px 0 12px 0;
border-bottom: 2px solid #ddd6fe;
padding-bottom: 6px;
}
.section-body { margin-bottom: 16px; }
/* Tables */
table {
width: 100%;
border-collapse: collapse;
margin: 10px 0 16px 0;
font-size: 9pt;
}
th, td {
border: 1px solid #e2e8f0;
padding: 6px 8px;
text-align: left;
vertical-align: top;
}
th {
background: #f5f3ff;
color: #5b21b6;
font-weight: 600;
font-size: 8.5pt;
text-transform: uppercase;
letter-spacing: 0.3px;
}
tr:nth-child(even) td { background: #faf5ff; }
/* Detail cards */
.policy-detail {
page-break-inside: avoid;
border: 1px solid #e2e8f0;
border-radius: 6px;
margin-bottom: 16px;
overflow: hidden;
}
.policy-detail-header {
background: #f5f3ff;
padding: 8px 12px;
font-weight: 700;
color: #5b21b6;
border-bottom: 1px solid #ddd6fe;
display: flex;
justify-content: space-between;
}
.policy-detail-body { padding: 0; }
.policy-detail-body table { margin: 0; }
.policy-detail-body th { width: 200px; }
/* Badges */
.badge {
display: inline-block;
padding: 1px 8px;
border-radius: 9999px;
font-size: 8pt;
font-weight: 600;
}
.badge-active { background: #dcfce7; color: #166534; }
.badge-draft { background: #f3f4f6; color: #374151; }
.badge-review { background: #fef9c3; color: #854d0e; }
.badge-critical { background: #fecaca; color: #991b1b; }
.badge-high { background: #fed7aa; color: #9a3412; }
.badge-medium { background: #fef3c7; color: #92400e; }
.badge-low { background: #f3f4f6; color: #4b5563; }
/* Principles */
.principle {
margin-bottom: 10px;
padding-left: 20px;
position: relative;
}
.principle::before {
content: '';
position: absolute;
left: 0;
top: 6px;
width: 10px;
height: 10px;
background: #7c3aed;
border-radius: 50%;
}
.principle strong { color: #5b21b6; }
/* Score */
.score-box {
display: inline-block;
padding: 4px 16px;
border-radius: 8px;
font-size: 18pt;
font-weight: 700;
margin-right: 12px;
}
.score-excellent { background: #dcfce7; color: #166534; }
.score-good { background: #dbeafe; color: #1e40af; }
.score-needs-work { background: #fef3c7; color: #92400e; }
.score-poor { background: #fecaca; color: #991b1b; }
/* Footer */
.page-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 8px 18mm;
font-size: 7.5pt;
color: #94a3b8;
display: flex;
justify-content: space-between;
border-top: 1px solid #e2e8f0;
}
/* Print */
@media print {
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.no-print { display: none !important; }
.page-break { page-break-after: always; }
}
</style>
</head>
<body>
`
// =========================================================================
// Section 0: Cover Page
// =========================================================================
html += `
<div class="cover">
<h1>TOM-Dokumentation</h1>
<div class="subtitle">Technische und Organisatorische Massnahmen gemaess Art. 32 DSGVO</div>
<div class="org-info">
<div><span class="label">Organisation:</span> ${escHtml(orgName)}</div>
${orgHeader.industry ? `<div><span class="label">Branche:</span> ${escHtml(orgHeader.industry)}</div>` : ''}
${orgHeader.dpoName ? `<div><span class="label">DSB:</span> ${escHtml(orgHeader.dpoName)}</div>` : ''}
${orgHeader.dpoContact ? `<div><span class="label">DSB-Kontakt:</span> ${escHtml(orgHeader.dpoContact)}</div>` : ''}
${orgHeader.responsiblePerson ? `<div><span class="label">Verantwortlicher:</span> ${escHtml(orgHeader.responsiblePerson)}</div>` : ''}
${orgHeader.itSecurityContact ? `<div><span class="label">IT-Sicherheit:</span> ${escHtml(orgHeader.itSecurityContact)}</div>` : ''}
${orgHeader.employeeCount ? `<div><span class="label">Mitarbeiter:</span> ${escHtml(orgHeader.employeeCount)}</div>` : ''}
${orgHeader.locations.length > 0 ? `<div><span class="label">Standorte:</span> ${escHtml(orgHeader.locations.join(', '))}</div>` : ''}
</div>
<div class="legal-ref">
Version ${escHtml(orgHeader.documentVersion)} | Stand: ${today}<br/>
Letzte Pruefung: ${formatDateDE(orgHeader.lastReviewDate)} | Naechste Pruefung: ${formatDateDE(orgHeader.nextReviewDate)}<br/>
Pruefintervall: ${escHtml(orgHeader.reviewInterval)}
</div>
</div>
`
// =========================================================================
// Table of Contents
// =========================================================================
const sections = [
'Ziel und Zweck',
'Geltungsbereich',
'Grundprinzipien Art. 32',
'Schutzbedarf und Risikoanalyse',
'Massnahmen-Uebersicht',
'Detaillierte Massnahmen',
'SDM Gewaehrleistungsziele',
'Verantwortlichkeiten',
'Pruef- und Revisionszyklus',
'Compliance-Status',
'Aenderungshistorie',
]
html += `
<div class="toc">
<h2>Inhaltsverzeichnis</h2>
${sections.map((s, i) => `<div class="toc-entry"><span><span class="toc-num">${i + 1}.</span> ${escHtml(s)}</span></div>`).join('\n ')}
</div>
`
// =========================================================================
// Section 1: Ziel und Zweck
// =========================================================================
html += `
<div class="section">
<div class="section-header">1. Ziel und Zweck</div>
<div class="section-body">
<p>Diese TOM-Dokumentation beschreibt die technischen und organisatorischen Massnahmen
zum Schutz personenbezogener Daten bei <strong>${escHtml(orgName)}</strong>. Sie dient
der Umsetzung folgender DSGVO-Anforderungen:</p>
<table>
<tr><th>Rechtsgrundlage</th><th>Inhalt</th></tr>
<tr><td><strong>Art. 32 Abs. 1 lit. a DSGVO</strong></td><td>Pseudonymisierung und Verschluesselung personenbezogener Daten</td></tr>
<tr><td><strong>Art. 32 Abs. 1 lit. b DSGVO</strong></td><td>Faehigkeit, die Vertraulichkeit, Integritaet, Verfuegbarkeit und Belastbarkeit der Systeme und Dienste im Zusammenhang mit der Verarbeitung auf Dauer sicherzustellen</td></tr>
<tr><td><strong>Art. 32 Abs. 1 lit. c DSGVO</strong></td><td>Faehigkeit, die Verfuegbarkeit der personenbezogenen Daten und den Zugang zu ihnen bei einem physischen oder technischen Zwischenfall rasch wiederherzustellen</td></tr>
<tr><td><strong>Art. 32 Abs. 1 lit. d DSGVO</strong></td><td>Verfahren zur regelmaessigen Ueberpruefung, Bewertung und Evaluierung der Wirksamkeit der technischen und organisatorischen Massnahmen</td></tr>
</table>
<p>Die TOM-Dokumentation ist fester Bestandteil des Datenschutz-Managementsystems und wird
regelmaessig ueberprueft und aktualisiert.</p>
</div>
</div>
`
// =========================================================================
// Section 2: Geltungsbereich
// =========================================================================
const industryInfo = companyProfile?.industry || orgHeader.industry || ''
const hostingInfo = companyProfile ? `Unternehmen: ${escHtml(companyProfile.name || orgName)}, Groesse: ${escHtml(companyProfile.size || '-')}` : ''
html += `
<div class="section">
<div class="section-header">2. Geltungsbereich</div>
<div class="section-body">
<p>Diese TOM-Dokumentation gilt fuer alle IT-Systeme, Anwendungen und Verarbeitungsprozesse
von <strong>${escHtml(orgName)}</strong>${industryInfo ? ` (Branche: ${escHtml(industryInfo)})` : ''}.</p>
${hostingInfo ? `<p>${hostingInfo}</p>` : ''}
${orgHeader.locations.length > 0 ? `<p>Standorte: ${escHtml(orgHeader.locations.join(', '))}</p>` : ''}
<p>Die dokumentierten Massnahmen stammen aus zwei Quellen:</p>
<ul style="margin: 8px 0 8px 24px;">
<li><strong>Embedded Library (TOM-xxx):</strong> Integrierte Kontrollbibliothek mit spezifischen Massnahmen fuer Art. 32 DSGVO</li>
<li><strong>Canonical Control Library (CP-CLIB):</strong> Uebergreifende Kontrollbibliothek mit framework-uebergreifenden Massnahmen</li>
</ul>
<p>Insgesamt umfasst dieses Dokument <strong>${applicableTOMs.length}</strong> anwendbare Massnahmen
in <strong>${tomsByCategory.size}</strong> Kategorien.</p>
</div>
</div>
`
// =========================================================================
// Section 3: Grundprinzipien Art. 32
// =========================================================================
html += `
<div class="section">
<div class="section-header">3. Grundprinzipien Art. 32</div>
<div class="section-body">
<div class="principle"><strong>Vertraulichkeit:</strong> Schutz personenbezogener Daten vor unbefugter Kenntnisnahme durch Zutrittskontrolle, Zugangskontrolle, Zugriffskontrolle und Verschluesselung (Art. 32 Abs. 1 lit. b DSGVO).</div>
<div class="principle"><strong>Integritaet:</strong> Sicherstellung, dass personenbezogene Daten nicht unbefugt oder unbeabsichtigt veraendert werden koennen, durch Eingabekontrolle, Weitergabekontrolle und Protokollierung (Art. 32 Abs. 1 lit. b DSGVO).</div>
<div class="principle"><strong>Verfuegbarkeit und Belastbarkeit:</strong> Gewaehrleistung, dass Systeme und Dienste bei Lastspitzen und Stoerungen zuverlaessig funktionieren, durch Backup, Redundanz und Disaster Recovery (Art. 32 Abs. 1 lit. b DSGVO).</div>
<div class="principle"><strong>Rasche Wiederherstellbarkeit:</strong> Faehigkeit, nach einem physischen oder technischen Zwischenfall Daten und Systeme schnell wiederherzustellen, durch getestete Recovery-Prozesse (Art. 32 Abs. 1 lit. c DSGVO).</div>
<div class="principle"><strong>Regelmaessige Wirksamkeitspruefung:</strong> Verfahren zur regelmaessigen Ueberpruefung, Bewertung und Evaluierung der Wirksamkeit aller technischen und organisatorischen Massnahmen (Art. 32 Abs. 1 lit. d DSGVO).</div>
</div>
</div>
`
// =========================================================================
// Section 4: Schutzbedarf und Risikoanalyse
// =========================================================================
html += `
<div class="section">
<div class="section-header">4. Schutzbedarf und Risikoanalyse</div>
<div class="section-body">
`
if (riskProfile) {
html += ` <p>Die folgende Schutzbedarfsanalyse bildet die Grundlage fuer die Auswahl und Priorisierung
der technischen und organisatorischen Massnahmen:</p>
<table>
<tr><th>Kriterium</th><th>Bewertung</th></tr>
<tr><td>Vertraulichkeit</td><td>${riskProfile.ciaAssessment.confidentiality}/5</td></tr>
<tr><td>Integritaet</td><td>${riskProfile.ciaAssessment.integrity}/5</td></tr>
<tr><td>Verfuegbarkeit</td><td>${riskProfile.ciaAssessment.availability}/5</td></tr>
<tr><td>Schutzniveau</td><td><strong>${escHtml(riskProfile.protectionLevel)}</strong></td></tr>
<tr><td>DSFA-Pflicht</td><td>${riskProfile.dsfaRequired ? 'Ja' : 'Nein'}</td></tr>
${riskProfile.specialRisks.length > 0 ? `<tr><td>Spezialrisiken</td><td>${escHtml(riskProfile.specialRisks.join(', '))}</td></tr>` : ''}
${riskProfile.regulatoryRequirements.length > 0 ? `<tr><td>Regulatorische Anforderungen</td><td>${escHtml(riskProfile.regulatoryRequirements.join(', '))}</td></tr>` : ''}
</table>
`
} else {
html += ` <p><em>Die Schutzbedarfsanalyse wurde noch nicht durchgefuehrt. Fuehren Sie den
Risiko-Wizard im TOM-Generator durch, um den Schutzbedarf zu ermitteln.</em></p>
`
}
html += ` </div>
</div>
`
// =========================================================================
// Section 5: Massnahmen-Uebersicht
// =========================================================================
html += `
<div class="section page-break">
<div class="section-header">5. Massnahmen-Uebersicht</div>
<div class="section-body">
<p>Die folgende Tabelle zeigt eine Uebersicht aller ${applicableTOMs.length} anwendbaren Massnahmen
nach Kategorie:</p>
<table>
<tr>
<th>Kategorie</th>
<th>Gesamt</th>
<th>Umgesetzt</th>
<th>Teilweise</th>
<th>Offen</th>
</tr>
`
const allCategories = getAllCategories()
for (const cat of allCategories) {
const tomsInCat = tomsByCategory.get(cat)
if (!tomsInCat || tomsInCat.length === 0) continue
const implemented = tomsInCat.filter(t => t.implementationStatus === 'IMPLEMENTED').length
const partial = tomsInCat.filter(t => t.implementationStatus === 'PARTIAL').length
const notImpl = tomsInCat.filter(t => t.implementationStatus === 'NOT_IMPLEMENTED').length
const catLabel = CATEGORY_LABELS_DE[cat] || cat
html += ` <tr>
<td>${escHtml(catLabel)}</td>
<td>${tomsInCat.length}</td>
<td>${implemented}</td>
<td>${partial}</td>
<td>${notImpl}</td>
</tr>
`
}
html += ` </table>
</div>
</div>
`
// =========================================================================
// Section 6: Detaillierte Massnahmen
// =========================================================================
html += `
<div class="section">
<div class="section-header">6. Detaillierte Massnahmen</div>
<div class="section-body">
`
for (const cat of allCategories) {
const tomsInCat = tomsByCategory.get(cat)
if (!tomsInCat || tomsInCat.length === 0) continue
const catLabel = CATEGORY_LABELS_DE[cat] || cat
const catMeta = getCategoryMetadata(cat)
const gdprRef = catMeta?.gdprReference || ''
html += ` <h3 style="color: #5b21b6; margin: 20px 0 10px 0; font-size: 11pt;">${escHtml(catLabel)}${gdprRef ? ` <span style="font-weight: 400; font-size: 9pt; color: #64748b;">(${escHtml(gdprRef)})</span>` : ''}</h3>
`
// Sort TOMs by control code
const sortedTOMs = [...tomsInCat].sort((a, b) => {
const codeA = getControlById(a.controlId)?.code || a.controlId
const codeB = getControlById(b.controlId)?.code || b.controlId
return codeA.localeCompare(codeB)
})
for (const tom of sortedTOMs) {
const control = getControlById(tom.controlId)
const code = control?.code || tom.controlId
const nameDE = control?.name?.de || tom.name
const descDE = control?.description?.de || tom.description
const typeLabel = control?.type === 'TECHNICAL' ? 'Technisch' : control?.type === 'ORGANIZATIONAL' ? 'Organisatorisch' : '-'
const statusLabel = STATUS_LABELS_DE[tom.implementationStatus] || tom.implementationStatus
const statusBadge = STATUS_BADGE_CLASSES[tom.implementationStatus] || 'badge-draft'
const applicabilityLabel = APPLICABILITY_LABELS_DE[tom.applicability] || tom.applicability
const responsible = [tom.responsiblePerson, tom.responsibleDepartment].filter(s => s && s.trim()).join(' / ') || '-'
const implDate = tom.implementationDate ? formatDateDE(typeof tom.implementationDate === 'string' ? tom.implementationDate : tom.implementationDate.toISOString()) : '-'
const reviewDate = tom.reviewDate ? formatDateDE(typeof tom.reviewDate === 'string' ? tom.reviewDate : tom.reviewDate.toISOString()) : '-'
// Evidence
const evidenceInfo = tom.linkedEvidence.length > 0
? tom.linkedEvidence.join(', ')
: tom.evidenceGaps.length > 0
? `<em style="color: #d97706;">Fehlend: ${escHtml(tom.evidenceGaps.join(', '))}</em>`
: '-'
// Framework mappings
let mappingsHtml = '-'
if (control?.mappings && control.mappings.length > 0) {
mappingsHtml = control.mappings.map(m => `${escHtml(m.framework)}: ${escHtml(m.reference)}`).join('<br/>')
}
html += `
<div class="policy-detail">
<div class="policy-detail-header">
<span>${escHtml(code)}${escHtml(nameDE)}</span>
<span class="badge ${statusBadge}">${escHtml(statusLabel)}</span>
</div>
<div class="policy-detail-body">
<table>
<tr><th>Beschreibung</th><td>${escHtml(descDE)}</td></tr>
<tr><th>Massnahmentyp</th><td>${escHtml(typeLabel)}</td></tr>
<tr><th>Anwendbarkeit</th><td>${escHtml(applicabilityLabel)}${tom.applicabilityReason ? `${escHtml(tom.applicabilityReason)}` : ''}</td></tr>
<tr><th>Umsetzungsstatus</th><td><span class="badge ${statusBadge}">${escHtml(statusLabel)}</span></td></tr>
<tr><th>Verantwortlich</th><td>${escHtml(responsible)}</td></tr>
<tr><th>Umsetzungsdatum</th><td>${implDate}</td></tr>
<tr><th>Naechste Pruefung</th><td>${reviewDate}</td></tr>
<tr><th>Evidence</th><td>${evidenceInfo}</td></tr>
<tr><th>Framework-Mappings</th><td>${mappingsHtml}</td></tr>
</table>
</div>
</div>
`
}
}
html += ` </div>
</div>
`
// =========================================================================
// Section 7: SDM Gewaehrleistungsziele
// =========================================================================
const sdmGoals: Array<{ goal: string; categories: ControlCategory[] }> = []
const allSDMGoals = [
'Verfuegbarkeit',
'Integritaet',
'Vertraulichkeit',
'Nichtverkettung',
'Intervenierbarkeit',
'Transparenz',
'Datenminimierung',
] as const
for (const goal of allSDMGoals) {
const cats: ControlCategory[] = []
for (const [cat, goals] of Object.entries(SDM_CATEGORY_MAPPING)) {
if (goals.includes(goal)) {
cats.push(cat as ControlCategory)
}
}
sdmGoals.push({ goal, categories: cats })
}
html += `
<div class="section page-break">
<div class="section-header">7. SDM Gewaehrleistungsziele</div>
<div class="section-body">
<p>Die folgende Tabelle zeigt die Abdeckung der sieben Gewaehrleistungsziele des
Standard-Datenschutzmodells (SDM) durch die implementierten Massnahmen:</p>
<table>
<tr>
<th>Gewaehrleistungsziel</th>
<th>Abgedeckt</th>
<th>Gesamt</th>
<th>Abdeckung (%)</th>
</tr>
`
for (const { goal, categories } of sdmGoals) {
let totalInGoal = 0
let implementedInGoal = 0
for (const cat of categories) {
const tomsInCat = tomsByCategory.get(cat) || []
totalInGoal += tomsInCat.length
implementedInGoal += tomsInCat.filter(t => t.implementationStatus === 'IMPLEMENTED').length
}
const percentage = totalInGoal > 0 ? Math.round((implementedInGoal / totalInGoal) * 100) : 0
html += ` <tr>
<td>${escHtml(goal)}</td>
<td>${implementedInGoal}</td>
<td>${totalInGoal}</td>
<td>${percentage}%</td>
</tr>
`
}
html += ` </table>
</div>
</div>
`
// =========================================================================
// Section 8: Verantwortlichkeiten
// =========================================================================
html += `
<div class="section">
<div class="section-header">8. Verantwortlichkeiten</div>
<div class="section-body">
<p>Die folgende Rollenmatrix zeigt, welche Personen oder Abteilungen fuer welche Massnahmen
die Umsetzungsverantwortung tragen:</p>
<table>
<tr><th>Rolle / Verantwortlich</th><th>Massnahmen</th><th>Anzahl</th></tr>
`
for (const [role, controls] of roleMap.entries()) {
html += ` <tr>
<td>${escHtml(role)}</td>
<td>${controls.map(c => escHtml(c)).join(', ')}</td>
<td>${controls.length}</td>
</tr>
`
}
html += ` </table>
</div>
</div>
`
// =========================================================================
// Section 9: Pruef- und Revisionszyklus
// =========================================================================
html += `
<div class="section">
<div class="section-header">9. Pruef- und Revisionszyklus</div>
<div class="section-body">
<table>
<tr><th>Eigenschaft</th><th>Wert</th></tr>
<tr><td>Aktuelles Pruefintervall</td><td>${escHtml(orgHeader.reviewInterval)}</td></tr>
<tr><td>Letzte Pruefung</td><td>${formatDateDE(orgHeader.lastReviewDate)}</td></tr>
<tr><td>Naechste Pruefung</td><td>${formatDateDE(orgHeader.nextReviewDate)}</td></tr>
<tr><td>Aktuelle Version</td><td>${escHtml(orgHeader.documentVersion)}</td></tr>
</table>
<p style="margin-top: 8px;">Bei jeder Pruefung wird die TOM-Dokumentation auf folgende Punkte ueberprueft:</p>
<ul style="margin: 8px 0 8px 24px;">
<li>Vollstaendigkeit aller Massnahmen (neue Systeme oder Verarbeitungen erfasst?)</li>
<li>Aktualitaet des Umsetzungsstatus (Aenderungen seit letzter Pruefung?)</li>
<li>Wirksamkeit der technischen Massnahmen (Penetration-Tests, Audit-Ergebnisse)</li>
<li>Angemessenheit der organisatorischen Massnahmen (Schulungen, Richtlinien aktuell?)</li>
<li>Abdeckung aller SDM-Gewaehrleistungsziele</li>
<li>Zuordnung von Verantwortlichkeiten zu allen Massnahmen</li>
</ul>
</div>
</div>
`
// =========================================================================
// Section 10: Compliance-Status
// =========================================================================
html += `
<div class="section page-break">
<div class="section-header">10. Compliance-Status</div>
<div class="section-body">
`
if (complianceResult) {
const scoreClass = complianceResult.score >= 90 ? 'score-excellent'
: complianceResult.score >= 75 ? 'score-good'
: complianceResult.score >= 50 ? 'score-needs-work'
: 'score-poor'
const scoreLabel = complianceResult.score >= 90 ? 'Ausgezeichnet'
: complianceResult.score >= 75 ? 'Gut'
: complianceResult.score >= 50 ? 'Verbesserungswuerdig'
: 'Mangelhaft'
html += ` <p><span class="score-box ${scoreClass}">${complianceResult.score}/100</span> ${escHtml(scoreLabel)}</p>
<table style="margin-top: 12px;">
<tr><th>Kennzahl</th><th>Wert</th></tr>
<tr><td>Gepruefte Massnahmen</td><td>${complianceResult.stats.total}</td></tr>
<tr><td>Bestanden</td><td>${complianceResult.stats.passed}</td></tr>
<tr><td>Beanstandungen</td><td>${complianceResult.stats.failed}</td></tr>
</table>
`
if (complianceResult.issues.length > 0) {
html += ` <p style="margin-top: 12px;"><strong>Befunde nach Schweregrad:</strong></p>
<table>
<tr><th>Schweregrad</th><th>Anzahl</th><th>Befunde</th></tr>
`
const severityOrder: TOMComplianceIssueSeverity[] = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']
for (const sev of severityOrder) {
const count = complianceResult.stats.bySeverity[sev]
if (count === 0) continue
const issuesForSev = complianceResult.issues.filter(i => i.severity === sev)
html += ` <tr>
<td><span class="badge badge-${sev.toLowerCase()}" style="color: ${SEVERITY_COLORS[sev]}">${SEVERITY_LABELS_DE[sev]}</span></td>
<td>${count}</td>
<td>${issuesForSev.map(i => escHtml(i.title)).join('; ')}</td>
</tr>
`
}
html += ` </table>
`
} else {
html += ` <p style="margin-top: 8px;"><em>Keine Beanstandungen. Alle Massnahmen sind konform.</em></p>
`
}
} else {
html += ` <p><em>Compliance-Check wurde noch nicht ausgefuehrt. Fuehren Sie den Check im
Export-Tab durch, um den Status in das Dokument aufzunehmen.</em></p>
`
}
html += ` </div>
</div>
`
// =========================================================================
// Section 11: Aenderungshistorie
// =========================================================================
html += `
<div class="section">
<div class="section-header">11. Aenderungshistorie</div>
<div class="section-body">
<table>
<tr><th>Version</th><th>Datum</th><th>Autor</th><th>Aenderungen</th></tr>
`
if (revisions.length > 0) {
for (const rev of revisions) {
html += ` <tr>
<td>${escHtml(rev.version)}</td>
<td>${formatDateDE(rev.date)}</td>
<td>${escHtml(rev.author)}</td>
<td>${escHtml(rev.changes)}</td>
</tr>
`
}
} else {
html += ` <tr>
<td>${escHtml(orgHeader.documentVersion)}</td>
<td>${today}</td>
<td>${escHtml(orgHeader.dpoName || orgHeader.responsiblePerson || '-')}</td>
<td>Erstversion der TOM-Dokumentation</td>
</tr>
`
}
html += ` </table>
</div>
</div>
`
// =========================================================================
// Footer
// =========================================================================
html += `
<div class="page-footer">
<span>TOM-Dokumentation — ${escHtml(orgName)}</span>
<span>Stand: ${today} | Version ${escHtml(orgHeader.documentVersion)}</span>
</div>
</body>
</html>`
return html
}
// =============================================================================
// INTERNAL HELPERS
// =============================================================================
function escHtml(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
function formatDateDE(dateStr: string | null | undefined): string {
if (!dateStr) return '-'
try {
const date = new Date(dateStr)
if (isNaN(date.getTime())) return '-'
return date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
} catch {
return '-'
}
}

View File

@@ -89,9 +89,9 @@ export interface ControlLibrary {
const CONTROL_LIBRARY_DATA: ControlLibrary = {
metadata: {
version: '1.0.0',
lastUpdated: '2026-02-04',
totalControls: 60,
version: '1.1.0',
lastUpdated: '2026-03-19',
totalControls: 88,
},
categories: new Map([
[
@@ -2353,6 +2353,648 @@ const CONTROL_LIBRARY_DATA: ControlLibrary = {
complexity: 'MEDIUM',
tags: ['training', 'security-awareness', 'phishing', 'social-engineering'],
},
// =========================================================================
// NEW CONTROLS (v1.1.0) — 25 additional measures
// =========================================================================
// ENCRYPTION — 2 new
{
id: 'TOM-ENC-04',
code: 'TOM-ENC-04',
category: 'ENCRYPTION',
type: 'TECHNICAL',
name: { de: 'Zertifikatsmanagement (TLS/SSL)', en: 'Certificate Management (TLS/SSL)' },
description: {
de: 'Systematische Verwaltung, Ueberwachung und rechtzeitige Erneuerung aller TLS/SSL-Zertifikate zur Vermeidung von Sicherheitsluecken durch abgelaufene Zertifikate.',
en: 'Systematic management, monitoring and timely renewal of all TLS/SSL certificates to prevent security gaps from expired certificates.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. a' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.10.1.2' },
{ framework: 'BSI_IT_GRUNDSCHUTZ', reference: 'CON.1' },
],
applicabilityConditions: [
{ field: 'architectureProfile.encryptionInTransit', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['Zertifikatsinventar', 'Monitoring-Konfiguration', 'Erneuerungsprotokolle'],
reviewFrequency: 'QUARTERLY',
priority: 'HIGH',
complexity: 'MEDIUM',
tags: ['encryption', 'certificates', 'tls'],
},
{
id: 'TOM-ENC-05',
code: 'TOM-ENC-05',
category: 'ENCRYPTION',
type: 'ORGANIZATIONAL',
name: { de: 'Schluesselmanagement-Policy', en: 'Key Management Policy' },
description: {
de: 'Dokumentierte Richtlinie fuer den gesamten Lebenszyklus kryptografischer Schluessel inkl. Erzeugung, Verteilung, Speicherung, Rotation und Vernichtung.',
en: 'Documented policy for the full lifecycle of cryptographic keys including generation, distribution, storage, rotation and destruction.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. a' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.10.1.2' },
],
applicabilityConditions: [
{ field: 'architectureProfile.encryptionAtRest', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
{ field: 'dataProfile.hasSpecialCategories', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 30 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['Schluesselmanagement-Richtlinie', 'Schluesselrotationsplan'],
reviewFrequency: 'ANNUAL',
priority: 'HIGH',
complexity: 'LOW',
tags: ['encryption', 'key-management', 'policy'],
},
// PSEUDONYMIZATION — 2 new
{
id: 'TOM-PS-03',
code: 'TOM-PS-03',
category: 'PSEUDONYMIZATION',
type: 'TECHNICAL',
name: { de: 'Anonymisierung fuer Analysezwecke', en: 'Anonymization for Analytics' },
description: {
de: 'Technische Verfahren zur irreversiblen Anonymisierung personenbezogener Daten fuer statistische Auswertungen und Analysen.',
en: 'Technical procedures for irreversible anonymization of personal data for statistical evaluations and analyses.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. a' },
{ framework: 'GDPR_ART25', reference: 'Art. 25 Abs. 1' },
],
applicabilityConditions: [
{ field: 'dataProfile.dataVolume', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'RECOMMENDED', priority: 15 },
],
defaultApplicability: 'OPTIONAL',
evidenceRequirements: ['Anonymisierungsverfahren-Dokumentation', 'Re-Identifizierungs-Risikoanalyse'],
reviewFrequency: 'ANNUAL',
priority: 'MEDIUM',
complexity: 'HIGH',
tags: ['pseudonymization', 'anonymization', 'analytics'],
},
{
id: 'TOM-PS-04',
code: 'TOM-PS-04',
category: 'PSEUDONYMIZATION',
type: 'ORGANIZATIONAL',
name: { de: 'Pseudonymisierungskonzept', en: 'Pseudonymization Concept' },
description: {
de: 'Dokumentiertes Konzept fuer die Pseudonymisierung personenbezogener Daten mit Definition der Verfahren, Zustaendigkeiten und Zuordnungsregeln.',
en: 'Documented concept for pseudonymization of personal data with definition of procedures, responsibilities and mapping rules.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. a' },
{ framework: 'GDPR_ART25', reference: 'Art. 25 Abs. 1' },
{ framework: 'BSI_IT_GRUNDSCHUTZ', reference: 'CON.2' },
],
applicabilityConditions: [
{ field: 'dataProfile.hasSpecialCategories', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['Pseudonymisierungskonzept', 'Verfahrensdokumentation'],
reviewFrequency: 'ANNUAL',
priority: 'HIGH',
complexity: 'MEDIUM',
tags: ['pseudonymization', 'concept', 'documentation'],
},
// INPUT_CONTROL — 1 new
{
id: 'TOM-IN-05',
code: 'TOM-IN-05',
category: 'INPUT_CONTROL',
type: 'TECHNICAL',
name: { de: 'Automatisierte Eingabevalidierung', en: 'Automated Input Validation' },
description: {
de: 'Technische Validierung aller Benutzereingaben zur Verhinderung von Injection-Angriffen und Sicherstellung der Datenintegritaet.',
en: 'Technical validation of all user inputs to prevent injection attacks and ensure data integrity.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.14.2.5' },
],
applicabilityConditions: [],
defaultApplicability: 'REQUIRED',
evidenceRequirements: ['Validierungsregeln-Dokumentation', 'Penetrationstest-Berichte'],
reviewFrequency: 'QUARTERLY',
priority: 'HIGH',
complexity: 'MEDIUM',
tags: ['input-validation', 'security', 'injection-prevention'],
},
// ORDER_CONTROL — 2 new
{
id: 'TOM-OR-05',
code: 'TOM-OR-05',
category: 'ORDER_CONTROL',
type: 'ORGANIZATIONAL',
name: { de: 'Auftragsverarbeiter-Monitoring', en: 'Processor Monitoring' },
description: {
de: 'Regelmaessige Ueberpruefung und Bewertung der Datenschutz-Massnahmen bei Auftragsverarbeitern gemaess Art. 28 Abs. 3 lit. h DSGVO.',
en: 'Regular review and assessment of data protection measures at processors according to Art. 28(3)(h) GDPR.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 28 Abs. 3 lit. h' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.15.2.1' },
],
applicabilityConditions: [
{ field: 'architectureProfile.hasSubprocessors', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['Audit-Berichte der Auftragsverarbeiter', 'Monitoring-Checklisten'],
reviewFrequency: 'ANNUAL',
priority: 'HIGH',
complexity: 'MEDIUM',
tags: ['order-control', 'processor', 'monitoring'],
},
{
id: 'TOM-OR-06',
code: 'TOM-OR-06',
category: 'ORDER_CONTROL',
type: 'ORGANIZATIONAL',
name: { de: 'Sub-Processor Management', en: 'Sub-Processor Management' },
description: {
de: 'Dokumentiertes Verfahren zur Genehmigung, Ueberwachung und Dokumentation von Unterauftragsverarbeitern.',
en: 'Documented process for approval, monitoring and documentation of sub-processors.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 28 Abs. 2, 4' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.15.1.2' },
],
applicabilityConditions: [
{ field: 'architectureProfile.hasSubprocessors', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
{ field: 'architectureProfile.subprocessorCount', operator: 'GREATER_THAN', value: 3, result: 'REQUIRED', priority: 20 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['Sub-Processor-Register', 'Genehmigungsverfahren', 'Vertragsdokumentation'],
reviewFrequency: 'SEMI_ANNUAL',
priority: 'HIGH',
complexity: 'MEDIUM',
tags: ['order-control', 'sub-processor'],
},
// RESILIENCE — 2 new
{
id: 'TOM-RE-04',
code: 'TOM-RE-04',
category: 'RESILIENCE',
type: 'TECHNICAL',
name: { de: 'DDoS-Abwehr (erweitert)', en: 'DDoS Mitigation (Advanced)' },
description: {
de: 'Erweiterte DDoS-Schutzmassnahmen inkl. Traffic-Analyse, automatischer Mitigation und Incident-Response-Integration.',
en: 'Advanced DDoS protection measures including traffic analysis, automatic mitigation and incident response integration.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.13.1.1' },
],
applicabilityConditions: [
{ field: 'riskProfile.protectionLevel', operator: 'EQUALS', value: 'VERY_HIGH', result: 'REQUIRED', priority: 25 },
{ field: 'architectureProfile.hostingModel', operator: 'IN', value: ['PUBLIC_CLOUD', 'HYBRID'], result: 'RECOMMENDED', priority: 15 },
],
defaultApplicability: 'OPTIONAL',
evidenceRequirements: ['DDoS-Schutzkonzept (erweitert)', 'Mitigation-Berichte', 'Incident-Playbooks'],
reviewFrequency: 'QUARTERLY',
priority: 'HIGH',
complexity: 'HIGH',
tags: ['resilience', 'ddos', 'advanced'],
},
{
id: 'TOM-RE-05',
code: 'TOM-RE-05',
category: 'RESILIENCE',
type: 'ORGANIZATIONAL',
name: { de: 'Kapazitaetsplanung', en: 'Capacity Planning' },
description: {
de: 'Systematische Planung und Ueberwachung von IT-Kapazitaeten zur Sicherstellung der Systemverfuegbarkeit bei wachsender Nutzung.',
en: 'Systematic planning and monitoring of IT capacities to ensure system availability with growing usage.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.12.1.3' },
],
applicabilityConditions: [
{ field: 'dataProfile.dataVolume', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'REQUIRED', priority: 20 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['Kapazitaetsplan', 'Trend-Analysen', 'Skalierungskonzept'],
reviewFrequency: 'QUARTERLY',
priority: 'MEDIUM',
complexity: 'MEDIUM',
tags: ['resilience', 'capacity', 'planning'],
},
// RECOVERY — 2 new
{
id: 'TOM-RC-04',
code: 'TOM-RC-04',
category: 'RECOVERY',
type: 'TECHNICAL',
name: { de: 'Georedundantes Backup', en: 'Geo-Redundant Backup' },
description: {
de: 'Speicherung von Backup-Kopien an geografisch getrennten Standorten zum Schutz vor standortbezogenen Katastrophen.',
en: 'Storage of backup copies at geographically separated locations to protect against site-specific disasters.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. c' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.12.3.1' },
{ framework: 'BSI_IT_GRUNDSCHUTZ', reference: 'CON.3' },
],
applicabilityConditions: [
{ field: 'riskProfile.protectionLevel', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'REQUIRED', priority: 25 },
{ field: 'riskProfile.ciaAssessment.availability', operator: 'GREATER_THAN', value: 3, result: 'REQUIRED', priority: 20 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['Georedundanz-Konzept', 'Backup-Standort-Dokumentation', 'Wiederherstellungstests'],
reviewFrequency: 'SEMI_ANNUAL',
priority: 'HIGH',
complexity: 'HIGH',
tags: ['recovery', 'backup', 'geo-redundancy'],
},
{
id: 'TOM-RC-05',
code: 'TOM-RC-05',
category: 'RECOVERY',
type: 'ORGANIZATIONAL',
name: { de: 'Notfallwiederherstellungs-Tests', en: 'Disaster Recovery Testing' },
description: {
de: 'Regelmaessige Durchfuehrung und Dokumentation von Notfallwiederherstellungstests zur Validierung der RTO/RPO-Ziele.',
en: 'Regular execution and documentation of disaster recovery tests to validate RTO/RPO targets.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. c, d' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.17.1.3' },
],
applicabilityConditions: [
{ field: 'securityProfile.hasDRPlan', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['DR-Testberichte', 'RTO/RPO-Messungen', 'Verbesserungsmassnahmen'],
reviewFrequency: 'SEMI_ANNUAL',
priority: 'HIGH',
complexity: 'MEDIUM',
tags: ['recovery', 'dr-testing', 'rto', 'rpo'],
},
// SEPARATION — 2 new
{
id: 'TOM-SE-05',
code: 'TOM-SE-05',
category: 'SEPARATION',
type: 'TECHNICAL',
name: { de: 'Netzwerksegmentierung', en: 'Network Segmentation' },
description: {
de: 'Aufteilung des Netzwerks in separate Sicherheitszonen mit kontrollierten Uebergaengen zur Begrenzung der Ausbreitung von Sicherheitsvorfaellen.',
en: 'Division of the network into separate security zones with controlled transitions to limit the spread of security incidents.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.13.1.3' },
{ framework: 'BSI_IT_GRUNDSCHUTZ', reference: 'NET.1.1' },
],
applicabilityConditions: [
{ field: 'architectureProfile.hostingModel', operator: 'IN', value: ['ON_PREMISE', 'PRIVATE_CLOUD', 'HYBRID'], result: 'REQUIRED', priority: 20 },
{ field: 'riskProfile.protectionLevel', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'REQUIRED', priority: 25 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['Netzwerkplan', 'Firewall-Regeln', 'Segmentierungskonzept'],
reviewFrequency: 'SEMI_ANNUAL',
priority: 'HIGH',
complexity: 'HIGH',
tags: ['separation', 'network', 'segmentation'],
},
{
id: 'TOM-SE-06',
code: 'TOM-SE-06',
category: 'SEPARATION',
type: 'TECHNICAL',
name: { de: 'Mandantenisolierung in Cloud', en: 'Tenant Isolation in Cloud' },
description: {
de: 'Technische Sicherstellung der vollstaendigen Datentrennung zwischen verschiedenen Mandanten in Multi-Tenant-Cloud-Umgebungen.',
en: 'Technical assurance of complete data separation between different tenants in multi-tenant cloud environments.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.13.1.3' },
],
applicabilityConditions: [
{ field: 'architectureProfile.multiTenancy', operator: 'EQUALS', value: 'MULTI_TENANT', result: 'REQUIRED', priority: 30 },
{ field: 'architectureProfile.hostingModel', operator: 'IN', value: ['PUBLIC_CLOUD', 'HYBRID'], result: 'RECOMMENDED', priority: 15 },
],
defaultApplicability: 'OPTIONAL',
evidenceRequirements: ['Mandantentrennungskonzept', 'Isolierungstests', 'Cloud-Security-Assessment'],
reviewFrequency: 'SEMI_ANNUAL',
priority: 'CRITICAL',
complexity: 'HIGH',
tags: ['separation', 'multi-tenant', 'cloud'],
},
// ACCESS_CONTROL — 1 new
{
id: 'TOM-AC-06',
code: 'TOM-AC-06',
category: 'ACCESS_CONTROL',
type: 'ORGANIZATIONAL',
name: { de: 'Besuchermanagement (erweitert)', en: 'Visitor Management (Extended)' },
description: {
de: 'Erweitertes Besuchermanagement mit Voranmeldung, Identitaetspruefung, Begleitpflicht und zeitlich begrenztem Zugang zu sicherheitsrelevanten Bereichen.',
en: 'Extended visitor management with pre-registration, identity verification, escort requirement and time-limited access to security-relevant areas.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.7.2' },
],
applicabilityConditions: [
{ field: 'riskProfile.protectionLevel', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'REQUIRED', priority: 20 },
{ field: 'dataProfile.hasSpecialCategories', operator: 'EQUALS', value: true, result: 'RECOMMENDED', priority: 15 },
],
defaultApplicability: 'OPTIONAL',
evidenceRequirements: ['Besuchermanagement-Richtlinie', 'Besucherprotokolle', 'Zonenkonzept'],
reviewFrequency: 'ANNUAL',
priority: 'MEDIUM',
complexity: 'LOW',
tags: ['physical-security', 'visitors', 'extended'],
},
// ADMISSION_CONTROL — 1 new
{
id: 'TOM-ADM-06',
code: 'TOM-ADM-06',
category: 'ADMISSION_CONTROL',
type: 'TECHNICAL',
name: { de: 'Endpoint Detection & Response (EDR)', en: 'Endpoint Detection & Response (EDR)' },
description: {
de: 'Einsatz von EDR-Loesungen zur Erkennung und Abwehr von Bedrohungen auf Endgeraeten in Echtzeit.',
en: 'Deployment of EDR solutions for real-time threat detection and response on endpoints.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.12.2.1' },
{ framework: 'BSI_IT_GRUNDSCHUTZ', reference: 'OPS.1.1.4' },
],
applicabilityConditions: [
{ field: 'riskProfile.protectionLevel', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'REQUIRED', priority: 25 },
{ field: 'companyProfile.size', operator: 'IN', value: ['LARGE', 'ENTERPRISE'], result: 'RECOMMENDED', priority: 10 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['EDR-Konfiguration', 'Bedrohungsberichte', 'Incident-Response-Statistiken'],
reviewFrequency: 'QUARTERLY',
priority: 'HIGH',
complexity: 'HIGH',
tags: ['endpoint', 'edr', 'threat-detection'],
},
// ACCESS_AUTHORIZATION — 2 new
{
id: 'TOM-AZ-06',
code: 'TOM-AZ-06',
category: 'ACCESS_AUTHORIZATION',
type: 'TECHNICAL',
name: { de: 'API-Zugriffskontrolle', en: 'API Access Control' },
description: {
de: 'Implementierung von Authentifizierungs- und Autorisierungsmechanismen fuer APIs (OAuth 2.0, API-Keys, Rate Limiting).',
en: 'Implementation of authentication and authorization mechanisms for APIs (OAuth 2.0, API keys, rate limiting).',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.9.4.1' },
],
applicabilityConditions: [
{ field: 'architectureProfile.hostingModel', operator: 'IN', value: ['PUBLIC_CLOUD', 'HYBRID'], result: 'REQUIRED', priority: 20 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['API-Security-Konzept', 'OAuth-Konfiguration', 'Rate-Limiting-Regeln'],
reviewFrequency: 'QUARTERLY',
priority: 'HIGH',
complexity: 'MEDIUM',
tags: ['authorization', 'api', 'oauth'],
},
{
id: 'TOM-AZ-07',
code: 'TOM-AZ-07',
category: 'ACCESS_AUTHORIZATION',
type: 'ORGANIZATIONAL',
name: { de: 'Regelmaessiger Berechtigungsreview', en: 'Regular Permission Review' },
description: {
de: 'Systematische Ueberpruefung und Bereinigung von Zugriffsberechtigungen in regelmaessigen Abstaenden durch die jeweiligen Fachverantwortlichen.',
en: 'Systematic review and cleanup of access permissions at regular intervals by the respective department heads.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. d' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.9.2.5' },
],
applicabilityConditions: [],
defaultApplicability: 'REQUIRED',
evidenceRequirements: ['Review-Protokolle', 'Berechtigungsaenderungslog', 'Freigabedokumentation'],
reviewFrequency: 'SEMI_ANNUAL',
priority: 'HIGH',
complexity: 'LOW',
tags: ['authorization', 'review', 'permissions'],
},
// TRANSFER_CONTROL — 2 new
{
id: 'TOM-TR-06',
code: 'TOM-TR-06',
category: 'TRANSFER_CONTROL',
type: 'TECHNICAL',
name: { de: 'E-Mail-Verschluesselung (erweitert)', en: 'Email Encryption (Extended)' },
description: {
de: 'Erweiterte E-Mail-Verschluesselung mit automatischer Erkennung sensibler Inhalte und erzwungener Gateway-Verschluesselung.',
en: 'Extended email encryption with automatic detection of sensitive content and enforced gateway encryption.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. a' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.13.2.3' },
],
applicabilityConditions: [
{ field: 'dataProfile.hasSpecialCategories', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
{ field: 'riskProfile.protectionLevel', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'RECOMMENDED', priority: 15 },
],
defaultApplicability: 'OPTIONAL',
evidenceRequirements: ['E-Mail-Verschluesselungs-Policy', 'Gateway-Konfiguration', 'DLP-Regeln'],
reviewFrequency: 'SEMI_ANNUAL',
priority: 'MEDIUM',
complexity: 'MEDIUM',
tags: ['transfer', 'email', 'encryption'],
},
{
id: 'TOM-TR-07',
code: 'TOM-TR-07',
category: 'TRANSFER_CONTROL',
type: 'ORGANIZATIONAL',
name: { de: 'Drittstaat-Transferbewertung', en: 'Third Country Transfer Assessment' },
description: {
de: 'Dokumentierte Bewertung und Absicherung von Datenuebermittlungen in Drittstaaten gemaess Art. 44-49 DSGVO (Standardvertragsklauseln, TIA).',
en: 'Documented assessment and safeguarding of data transfers to third countries according to Art. 44-49 GDPR (Standard Contractual Clauses, TIA).',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 44-49' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.15.1.2' },
],
applicabilityConditions: [
{ field: 'dataProfile.thirdCountryTransfers', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 30 },
{ field: 'architectureProfile.hostingLocation', operator: 'IN', value: ['THIRD_COUNTRY_ADEQUATE', 'THIRD_COUNTRY'], result: 'REQUIRED', priority: 25 },
],
defaultApplicability: 'OPTIONAL',
evidenceRequirements: ['Transfer Impact Assessment', 'Standardvertragsklauseln', 'Angemessenheitsbeschluss-Pruefung'],
reviewFrequency: 'ANNUAL',
priority: 'CRITICAL',
complexity: 'MEDIUM',
tags: ['transfer', 'third-country', 'schrems-ii'],
},
// AVAILABILITY — 2 new
{
id: 'TOM-AV-06',
code: 'TOM-AV-06',
category: 'AVAILABILITY',
type: 'TECHNICAL',
name: { de: 'Monitoring und Alerting', en: 'Monitoring and Alerting' },
description: {
de: 'Implementierung einer umfassenden Ueberwachung aller IT-Systeme mit automatischen Benachrichtigungen bei Stoerungen oder Schwellenwert-Ueberschreitungen.',
en: 'Implementation of comprehensive monitoring of all IT systems with automatic notifications for disruptions or threshold violations.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.12.4.1' },
{ framework: 'BSI_IT_GRUNDSCHUTZ', reference: 'OPS.1.1.2' },
],
applicabilityConditions: [],
defaultApplicability: 'REQUIRED',
evidenceRequirements: ['Monitoring-Konzept', 'Alerting-Konfiguration', 'Eskalationsmatrix'],
reviewFrequency: 'QUARTERLY',
priority: 'HIGH',
complexity: 'MEDIUM',
tags: ['availability', 'monitoring', 'alerting'],
},
{
id: 'TOM-AV-07',
code: 'TOM-AV-07',
category: 'AVAILABILITY',
type: 'ORGANIZATIONAL',
name: { de: 'Service Level Management', en: 'Service Level Management' },
description: {
de: 'Definition und Ueberwachung von Service Level Agreements (SLAs) fuer alle kritischen IT-Services mit klaren Verfuegbarkeitszielen.',
en: 'Definition and monitoring of Service Level Agreements (SLAs) for all critical IT services with clear availability targets.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.15.2.1' },
],
applicabilityConditions: [
{ field: 'companyProfile.size', operator: 'IN', value: ['MEDIUM', 'LARGE', 'ENTERPRISE'], result: 'RECOMMENDED', priority: 10 },
{ field: 'architectureProfile.hasSubprocessors', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 20 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['SLA-Dokumentation', 'Verfuegbarkeitsberichte', 'Eskalationsverfahren'],
reviewFrequency: 'QUARTERLY',
priority: 'MEDIUM',
complexity: 'LOW',
tags: ['availability', 'sla', 'service-management'],
},
// SEPARATION — 1 more new (TOM-DL-05)
{
id: 'TOM-DL-05',
code: 'TOM-DL-05',
category: 'SEPARATION',
type: 'ORGANIZATIONAL',
name: { de: 'Datenloesch-Audit', en: 'Data Deletion Audit' },
description: {
de: 'Regelmaessige Ueberpruefung der Wirksamkeit und Vollstaendigkeit von Datenloeschvorgaengen durch unabhaengige Stellen.',
en: 'Regular review of the effectiveness and completeness of data deletion processes by independent parties.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 5 Abs. 1 lit. e' },
{ framework: 'GDPR_ART32', reference: 'Art. 17' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.8.3.2' },
],
applicabilityConditions: [
{ field: 'dataProfile.hasSpecialCategories', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
],
defaultApplicability: 'RECOMMENDED',
evidenceRequirements: ['Audit-Berichte', 'Loeschprotokolle', 'Stichproben-Ergebnisse'],
reviewFrequency: 'ANNUAL',
priority: 'MEDIUM',
complexity: 'MEDIUM',
tags: ['separation', 'deletion', 'audit'],
},
// REVIEW — 3 new
{
id: 'TOM-RV-09',
code: 'TOM-RV-09',
category: 'REVIEW',
type: 'ORGANIZATIONAL',
name: { de: 'Datenschutz-Audit-Programm', en: 'Data Protection Audit Program' },
description: {
de: 'Systematisches Programm zur regelmaessigen internen Ueberpruefung aller Datenschutzmassnahmen mit dokumentierten Ergebnissen und Massnahmenverfolgung.',
en: 'Systematic program for regular internal review of all data protection measures with documented results and action tracking.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. d' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.18.2.1' },
{ framework: 'BSI_IT_GRUNDSCHUTZ', reference: 'DER.3.1' },
],
applicabilityConditions: [],
defaultApplicability: 'REQUIRED',
evidenceRequirements: ['Audit-Programm', 'Audit-Berichte', 'Massnahmenplan'],
reviewFrequency: 'ANNUAL',
priority: 'HIGH',
complexity: 'MEDIUM',
tags: ['review', 'audit', 'data-protection'],
},
{
id: 'TOM-RV-10',
code: 'TOM-RV-10',
category: 'REVIEW',
type: 'TECHNICAL',
name: { de: 'Automatisierte Compliance-Pruefung', en: 'Automated Compliance Checking' },
description: {
de: 'Einsatz automatisierter Tools zur kontinuierlichen Ueberpruefung der Einhaltung von Sicherheits- und Datenschutzrichtlinien.',
en: 'Use of automated tools for continuous monitoring of compliance with security and data protection policies.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. d' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.18.2.2' },
],
applicabilityConditions: [
{ field: 'companyProfile.size', operator: 'IN', value: ['MEDIUM', 'LARGE', 'ENTERPRISE'], result: 'RECOMMENDED', priority: 10 },
{ field: 'riskProfile.protectionLevel', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'RECOMMENDED', priority: 15 },
],
defaultApplicability: 'OPTIONAL',
evidenceRequirements: ['Tool-Konfiguration', 'Compliance-Dashboard', 'Automatisierte Berichte'],
reviewFrequency: 'QUARTERLY',
priority: 'MEDIUM',
complexity: 'HIGH',
tags: ['review', 'automation', 'compliance'],
},
{
id: 'TOM-RV-11',
code: 'TOM-RV-11',
category: 'REVIEW',
type: 'ORGANIZATIONAL',
name: { de: 'Management Review (Art. 32 Abs. 1 lit. d)', en: 'Management Review (Art. 32(1)(d))' },
description: {
de: 'Regelmaessige Ueberpruefung der Wirksamkeit aller technischen und organisatorischen Massnahmen durch die Geschaeftsfuehrung mit dokumentierten Ergebnissen.',
en: 'Regular review of the effectiveness of all technical and organizational measures by management with documented results.',
},
mappings: [
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. d' },
{ framework: 'ISO27001_ANNEX_A', reference: 'A.18.2.1' },
],
applicabilityConditions: [],
defaultApplicability: 'REQUIRED',
evidenceRequirements: ['Management-Review-Protokolle', 'Massnahmenplan', 'Wirksamkeitsbewertung'],
reviewFrequency: 'ANNUAL',
priority: 'HIGH',
complexity: 'LOW',
tags: ['review', 'management', 'effectiveness'],
},
],
}

View File

@@ -320,3 +320,158 @@ export async function generateVideo(moduleId: string): Promise<TrainingMedia> {
export async function previewVideoScript(moduleId: string): Promise<{ title: string; sections: Array<{ heading: string; text: string; bullet_points: string[] }> }> {
return apiFetch(`/content/${moduleId}/preview-script`, { method: 'POST' })
}
// =============================================================================
// TRAINING BLOCKS (Controls → Schulungsmodule)
// =============================================================================
import type {
TrainingBlockConfig,
CanonicalControlSummary,
CanonicalControlMeta,
BlockPreview,
BlockGenerateResult,
TrainingBlockControlLink,
} from './types'
export async function listBlockConfigs(): Promise<{ blocks: TrainingBlockConfig[]; total: number }> {
return apiFetch('/blocks')
}
export async function createBlockConfig(data: {
name: string
description?: string
domain_filter?: string
category_filter?: string
severity_filter?: string
target_audience_filter?: string
regulation_area: string
module_code_prefix: string
frequency_type?: string
duration_minutes?: number
pass_threshold?: number
max_controls_per_module?: number
}): Promise<TrainingBlockConfig> {
return apiFetch<TrainingBlockConfig>('/blocks', {
method: 'POST',
body: JSON.stringify(data),
})
}
export async function getBlockConfig(id: string): Promise<TrainingBlockConfig> {
return apiFetch<TrainingBlockConfig>(`/blocks/${id}`)
}
export async function updateBlockConfig(id: string, data: Record<string, unknown>): Promise<TrainingBlockConfig> {
return apiFetch<TrainingBlockConfig>(`/blocks/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
})
}
export async function deleteBlockConfig(id: string): Promise<void> {
return apiFetch(`/blocks/${id}`, { method: 'DELETE' })
}
export async function previewBlock(id: string): Promise<BlockPreview> {
return apiFetch<BlockPreview>(`/blocks/${id}/preview`, { method: 'POST' })
}
export async function generateBlock(id: string, data?: {
language?: string
auto_matrix?: boolean
}): Promise<BlockGenerateResult> {
return apiFetch<BlockGenerateResult>(`/blocks/${id}/generate`, {
method: 'POST',
body: JSON.stringify(data || { language: 'de', auto_matrix: true }),
})
}
export async function getBlockControls(id: string): Promise<{ controls: TrainingBlockControlLink[]; total: number }> {
return apiFetch(`/blocks/${id}/controls`)
}
export async function listCanonicalControls(filters?: {
domain?: string
category?: string
severity?: string
target_audience?: string
}): Promise<{ controls: CanonicalControlSummary[]; total: number }> {
const params = new URLSearchParams()
if (filters?.domain) params.set('domain', filters.domain)
if (filters?.category) params.set('category', filters.category)
if (filters?.severity) params.set('severity', filters.severity)
if (filters?.target_audience) params.set('target_audience', filters.target_audience)
const qs = params.toString()
return apiFetch(`/canonical/controls${qs ? `?${qs}` : ''}`)
}
export async function getCanonicalMeta(): Promise<CanonicalControlMeta> {
return apiFetch<CanonicalControlMeta>('/canonical/meta')
}
// =============================================================================
// CERTIFICATES
// =============================================================================
export async function generateCertificate(assignmentId: string): Promise<{ certificate_id: string; assignment: TrainingAssignment }> {
return apiFetch(`/certificates/generate/${assignmentId}`, { method: 'POST' })
}
export async function listCertificates(): Promise<{ certificates: TrainingAssignment[]; total: number }> {
return apiFetch('/certificates')
}
export async function downloadCertificatePDF(certificateId: string): Promise<Blob> {
const res = await fetch(`${BASE_URL}/certificates/${certificateId}/pdf`, {
headers: {
'X-Tenant-ID': typeof window !== 'undefined'
? (localStorage.getItem('bp-tenant-id') || 'default')
: 'default',
},
})
if (!res.ok) throw new Error(`PDF download failed: ${res.status}`)
return res.blob()
}
export async function verifyCertificate(certificateId: string): Promise<{ valid: boolean; assignment: TrainingAssignment }> {
return apiFetch(`/certificates/${certificateId}/verify`)
}
// =============================================================================
// MEDIA STREAMING
// =============================================================================
export function getMediaStreamURL(mediaId: string): string {
return `${BASE_URL}/media/${mediaId}/stream`
}
// =============================================================================
// INTERACTIVE VIDEO
// =============================================================================
import type {
InteractiveVideoManifest,
CheckpointQuizResult,
CheckpointProgress,
} from './types'
export async function generateInteractiveVideo(moduleId: string): Promise<TrainingMedia> {
return apiFetch<TrainingMedia>(`/content/${moduleId}/generate-interactive`, { method: 'POST' })
}
export async function getInteractiveManifest(moduleId: string, assignmentId?: string): Promise<InteractiveVideoManifest> {
const qs = assignmentId ? `?assignment_id=${assignmentId}` : ''
return apiFetch<InteractiveVideoManifest>(`/content/${moduleId}/interactive-manifest${qs}`)
}
export async function submitCheckpointQuiz(checkpointId: string, assignmentId: string, answers: number[]): Promise<CheckpointQuizResult> {
return apiFetch<CheckpointQuizResult>(`/checkpoints/${checkpointId}/submit`, {
method: 'POST',
body: JSON.stringify({ assignment_id: assignmentId, answers }),
})
}
export async function getCheckpointProgress(assignmentId: string): Promise<{ progress: CheckpointProgress[]; total: number }> {
return apiFetch(`/checkpoints/progress/${assignmentId}`)
}

View File

@@ -65,9 +65,17 @@ export const ROLE_LABELS: Record<string, string> = {
R7: 'Fachabteilung',
R8: 'IT-Administration',
R9: 'Alle Mitarbeiter',
R10: 'Behoerden / Oeffentlicher Dienst',
}
export const ALL_ROLES = ['R1', 'R2', 'R3', 'R4', 'R5', 'R6', 'R7', 'R8', 'R9'] as const
export const ALL_ROLES = ['R1', 'R2', 'R3', 'R4', 'R5', 'R6', 'R7', 'R8', 'R9', 'R10'] as const
export const TARGET_AUDIENCE_LABELS: Record<string, string> = {
enterprise: 'Unternehmen',
authority: 'Behoerden',
provider: 'IT-Dienstleister',
all: 'Alle',
}
// =============================================================================
// MAIN ENTITIES
@@ -273,7 +281,7 @@ export interface QuizSubmitResponse {
// MEDIA (Audio/Video)
// =============================================================================
export type MediaType = 'audio' | 'video'
export type MediaType = 'audio' | 'video' | 'interactive_video'
export type MediaStatus = 'processing' | 'completed' | 'failed'
export interface TrainingMedia {
@@ -307,3 +315,121 @@ export interface VideoScriptSection {
text: string
bullet_points: string[]
}
// =============================================================================
// TRAINING BLOCKS (Controls → Schulungsmodule)
// =============================================================================
export interface TrainingBlockConfig {
id: string
tenant_id: string
name: string
description?: string
domain_filter?: string
category_filter?: string
severity_filter?: string
target_audience_filter?: string
regulation_area: RegulationArea
module_code_prefix: string
frequency_type: FrequencyType
duration_minutes: number
pass_threshold: number
max_controls_per_module: number
is_active: boolean
last_generated_at?: string
created_at: string
updated_at: string
}
export interface CanonicalControlSummary {
control_id: string
title: string
objective: string
rationale: string
requirements: string[]
severity: string
category: string
target_audience: string
tags: string[]
}
export interface CanonicalControlMeta {
domains: { domain: string; count: number }[]
categories: { category: string; count: number }[]
audiences: { audience: string; count: number }[]
total: number
}
export interface BlockPreview {
control_count: number
module_count: number
controls: CanonicalControlSummary[]
proposed_roles: string[]
}
export interface BlockGenerateResult {
modules_created: number
controls_linked: number
matrix_entries_created: number
content_generated: number
errors?: string[]
}
export interface TrainingBlockControlLink {
id: string
block_config_id: string
module_id: string
control_id: string
control_title: string
control_objective: string
control_requirements: string[]
sort_order: number
created_at: string
}
// =============================================================================
// INTERACTIVE VIDEO / CHECKPOINTS
// =============================================================================
export interface InteractiveVideoManifest {
media_id: string
stream_url: string
checkpoints: CheckpointEntry[]
}
export interface CheckpointEntry {
checkpoint_id: string
index: number
title: string
timestamp_seconds: number
questions: CheckpointQuestion[]
progress?: CheckpointProgress
}
export interface CheckpointQuestion {
question: string
options: string[]
correct_index: number
explanation: string
}
export interface CheckpointProgress {
id: string
assignment_id: string
checkpoint_id: string
passed: boolean
attempts: number
last_attempt_at?: string
}
export interface CheckpointQuizResult {
passed: boolean
score: number
feedback: CheckpointQuizFeedback[]
}
export interface CheckpointQuizFeedback {
question: string
correct: boolean
explanation: string
}

View File

@@ -796,16 +796,16 @@ export const SDK_STEPS: SDKStep[] = [
},
{
id: 'vendor-compliance',
seq: 4200,
seq: 2500,
phase: 2,
package: 'betrieb',
order: 3,
package: 'dokumentation',
order: 6,
name: 'Vendor Compliance',
nameShort: 'Vendor',
description: 'Dienstleister-Management',
url: '/sdk/vendor-compliance',
checkpointId: 'CP-VEND',
prerequisiteSteps: ['escalations'],
prerequisiteSteps: ['vvt'],
isOptional: false,
},
{
@@ -920,6 +920,20 @@ export const SDK_STEPS: SDKStep[] = [
prerequisiteSteps: [],
isOptional: true,
},
{
id: 'atomic-controls',
seq: 4925,
phase: 2,
package: 'betrieb',
order: 11.5,
name: 'Atomare Controls',
nameShort: 'Atomar',
description: 'Deduplizierte atomare Controls mit Herkunftsnachweis',
url: '/sdk/atomic-controls',
checkpointId: 'CP-ATOM',
prerequisiteSteps: [],
isOptional: true,
},
{
id: 'control-provenance',
seq: 4950,
@@ -1939,6 +1953,7 @@ export type LicenseType =
* Template types available for document generation
*/
export type TemplateType =
// Legal / Vertragsvorlagen
| 'privacy_policy'
| 'terms_of_service'
| 'agb'
@@ -1956,6 +1971,55 @@ export type TemplateType =
| 'copyright_policy'
| 'clause'
| 'dsfa'
// Sicherheitskonzepte (Migration 051)
| 'it_security_concept'
| 'data_protection_concept'
| 'backup_recovery_concept'
| 'logging_concept'
| 'incident_response_plan'
| 'access_control_concept'
| 'risk_management_concept'
// CRA Cybersecurity (Migration 056)
| 'cybersecurity_policy'
// IT-Sicherheit Policies (Migration 071)
| 'information_security_policy'
| 'access_control_policy'
| 'password_policy'
| 'encryption_policy'
| 'logging_policy'
| 'backup_policy'
| 'incident_response_policy'
| 'change_management_policy'
| 'patch_management_policy'
| 'asset_management_policy'
| 'cloud_security_policy'
| 'devsecops_policy'
| 'secrets_management_policy'
| 'vulnerability_management_policy'
// Daten-Policies (Migration 072)
| 'data_protection_policy'
| 'data_classification_policy'
| 'data_retention_policy'
| 'data_transfer_policy'
| 'privacy_incident_policy'
// Personal-Policies (Migration 072)
| 'employee_security_policy'
| 'security_awareness_policy'
| 'remote_work_policy'
| 'offboarding_policy'
// Lieferanten-Policies (Migration 072)
| 'vendor_risk_management_policy'
| 'third_party_security_policy'
| 'supplier_security_policy'
// BCM/Notfall (Migration 072)
| 'business_continuity_policy'
| 'disaster_recovery_policy'
| 'crisis_management_policy'
// Modul-Dokumente (Migration 073)
| 'vvt_register'
| 'tom_documentation'
| 'loeschkonzept'
| 'pflichtenregister'
/**
* Jurisdiction codes for legal documents
@@ -2190,6 +2254,7 @@ export const DEFAULT_PLACEHOLDERS: Record<string, string> = {
* Template type labels for display
*/
export const TEMPLATE_TYPE_LABELS: Record<TemplateType, string> = {
// Legal / Vertragsvorlagen
privacy_policy: 'Datenschutzerklärung',
terms_of_service: 'Nutzungsbedingungen',
agb: 'Allgemeine Geschäftsbedingungen',
@@ -2207,6 +2272,54 @@ export const TEMPLATE_TYPE_LABELS: Record<TemplateType, string> = {
copyright_policy: 'Urheberrechtsrichtlinie',
clause: 'Vertragsklausel',
dsfa: 'Datenschutz-Folgenabschätzung',
// Sicherheitskonzepte
it_security_concept: 'IT-Sicherheitskonzept',
data_protection_concept: 'Datenschutzkonzept',
backup_recovery_concept: 'Backup- und Recovery-Konzept',
logging_concept: 'Logging-Konzept',
incident_response_plan: 'Incident-Response-Plan',
access_control_concept: 'Zugriffskonzept',
risk_management_concept: 'Risikomanagement-Konzept',
cybersecurity_policy: 'Cybersecurity-Richtlinie (CRA)',
// IT-Sicherheit Policies
information_security_policy: 'Informationssicherheitsrichtlinie',
access_control_policy: 'Zugriffskontrollrichtlinie',
password_policy: 'Passwortrichtlinie',
encryption_policy: 'Verschlüsselungsrichtlinie',
logging_policy: 'Protokollierungsrichtlinie',
backup_policy: 'Datensicherungsrichtlinie',
incident_response_policy: 'Incident-Response-Richtlinie',
change_management_policy: 'Change-Management-Richtlinie',
patch_management_policy: 'Patch-Management-Richtlinie',
asset_management_policy: 'Asset-Management-Richtlinie',
cloud_security_policy: 'Cloud-Security-Richtlinie',
devsecops_policy: 'DevSecOps-Richtlinie',
secrets_management_policy: 'Secrets-Management-Richtlinie',
vulnerability_management_policy: 'Schwachstellenmanagement-Richtlinie',
// Daten-Policies
data_protection_policy: 'Datenschutzrichtlinie',
data_classification_policy: 'Datenklassifizierungsrichtlinie',
data_retention_policy: 'Aufbewahrungsrichtlinie',
data_transfer_policy: 'Datenübermittlungsrichtlinie',
privacy_incident_policy: 'Datenschutzvorfall-Richtlinie',
// Personal-Policies
employee_security_policy: 'Mitarbeiter-Sicherheitsrichtlinie',
security_awareness_policy: 'Security-Awareness-Richtlinie',
remote_work_policy: 'Remote-Work-Richtlinie',
offboarding_policy: 'Offboarding-Richtlinie',
// Lieferanten-Policies
vendor_risk_management_policy: 'Lieferanten-Risikomanagement',
third_party_security_policy: 'Drittanbieter-Sicherheitsrichtlinie',
supplier_security_policy: 'Lieferanten-Sicherheitsanforderungen',
// BCM/Notfall
business_continuity_policy: 'Business-Continuity-Richtlinie',
disaster_recovery_policy: 'Disaster-Recovery-Richtlinie',
crisis_management_policy: 'Krisenmanagement-Richtlinie',
// Modul-Dokumente
vvt_register: 'Verarbeitungsverzeichnis (Art. 30)',
tom_documentation: 'TOM-Dokumentation (Art. 32)',
loeschkonzept: 'Löschkonzept (Art. 5/17)',
pflichtenregister: 'Pflichtenregister',
}
/**

View File

@@ -15,16 +15,9 @@ import {
VendorComplianceAction,
VendorComplianceContextValue,
ProcessingActivity,
Vendor,
ContractDocument,
Finding,
Control,
ControlInstance,
RiskAssessment,
VendorStatistics,
ComplianceStatistics,
RiskOverview,
ExportFormat,
VendorStatus,
VendorRole,
RiskLevel,
@@ -475,24 +468,6 @@ export function VendorComplianceProvider({
[apiBase]
)
const updateProcessingActivity = useCallback(
async (id: string, data: Partial<ProcessingActivity>): Promise<void> => {
const response = await fetch(`${apiBase}/processing-activities/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Fehler beim Aktualisieren der Verarbeitungstätigkeit')
}
dispatch({ type: 'UPDATE_PROCESSING_ACTIVITY', payload: { id, data } })
},
[apiBase]
)
const deleteProcessingActivity = useCallback(
async (id: string): Promise<void> => {
const response = await fetch(`${apiBase}/processing-activities/${id}`, {
@@ -537,49 +512,6 @@ export function VendorComplianceProvider({
// VENDOR ACTIONS
// ==========================================
const createVendor = useCallback(
async (
data: Omit<Vendor, 'id' | 'tenantId' | 'createdAt' | 'updatedAt'>
): Promise<Vendor> => {
const response = await fetch(`${apiBase}/vendors`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Fehler beim Erstellen des Vendors')
}
const result = await response.json()
const vendor = result.data
dispatch({ type: 'ADD_VENDOR', payload: vendor })
return vendor
},
[apiBase]
)
const updateVendor = useCallback(
async (id: string, data: Partial<Vendor>): Promise<void> => {
const response = await fetch(`${apiBase}/vendors/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Fehler beim Aktualisieren des Vendors')
}
dispatch({ type: 'UPDATE_VENDOR', payload: { id, data } })
},
[apiBase]
)
const deleteVendor = useCallback(
async (id: string): Promise<void> => {
const response = await fetch(`${apiBase}/vendors/${id}`, {
@@ -600,67 +532,6 @@ export function VendorComplianceProvider({
// CONTRACT ACTIONS
// ==========================================
const uploadContract = useCallback(
async (
vendorId: string,
file: File,
metadata: Partial<ContractDocument>
): Promise<ContractDocument> => {
const formData = new FormData()
formData.append('file', file)
formData.append('vendorId', vendorId)
formData.append('metadata', JSON.stringify(metadata))
const response = await fetch(`${apiBase}/contracts`, {
method: 'POST',
body: formData,
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Fehler beim Hochladen des Vertrags')
}
const result = await response.json()
const contract = result.data
dispatch({ type: 'ADD_CONTRACT', payload: contract })
// Update vendor's contracts list
const vendor = state.vendors.find((v) => v.id === vendorId)
if (vendor) {
dispatch({
type: 'UPDATE_VENDOR',
payload: {
id: vendorId,
data: { contracts: [...vendor.contracts, contract.id] },
},
})
}
return contract
},
[apiBase, state.vendors]
)
const updateContract = useCallback(
async (id: string, data: Partial<ContractDocument>): Promise<void> => {
const response = await fetch(`${apiBase}/contracts/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Fehler beim Aktualisieren des Vertrags')
}
dispatch({ type: 'UPDATE_CONTRACT', payload: { id, data } })
},
[apiBase]
)
const deleteContract = useCallback(
async (id: string): Promise<void> => {
const contract = state.contracts.find((c) => c.id === id)
@@ -736,125 +607,6 @@ export function VendorComplianceProvider({
[apiBase]
)
// ==========================================
// FINDINGS ACTIONS
// ==========================================
const updateFinding = useCallback(
async (id: string, data: Partial<Finding>): Promise<void> => {
const response = await fetch(`${apiBase}/findings/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Fehler beim Aktualisieren des Findings')
}
dispatch({ type: 'UPDATE_FINDING', payload: { id, data } })
},
[apiBase]
)
const resolveFinding = useCallback(
async (id: string, resolution: string): Promise<void> => {
await updateFinding(id, {
status: 'RESOLVED',
resolution,
resolvedAt: new Date(),
})
},
[updateFinding]
)
// ==========================================
// CONTROL ACTIONS
// ==========================================
const updateControlInstance = useCallback(
async (id: string, data: Partial<ControlInstance>): Promise<void> => {
const response = await fetch(`${apiBase}/control-instances/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Fehler beim Aktualisieren des Control-Status')
}
dispatch({ type: 'UPDATE_CONTROL_INSTANCE', payload: { id, data } })
},
[apiBase]
)
// ==========================================
// EXPORT ACTIONS
// ==========================================
const exportVVT = useCallback(
async (format: ExportFormat, activityIds?: string[]): Promise<string> => {
const params = new URLSearchParams({ format })
if (activityIds && activityIds.length > 0) {
params.append('activityIds', activityIds.join(','))
}
const response = await fetch(`${apiBase}/export/vvt?${params}`)
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Fehler beim Exportieren des VVT')
}
const blob = await response.blob()
const url = URL.createObjectURL(blob)
return url
},
[apiBase]
)
const exportVendorAuditPack = useCallback(
async (vendorId: string, format: ExportFormat): Promise<string> => {
const params = new URLSearchParams({ format, vendorId })
const response = await fetch(`${apiBase}/export/vendor-audit?${params}`)
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Fehler beim Exportieren des Vendor Audit Packs')
}
const blob = await response.blob()
const url = URL.createObjectURL(blob)
return url
},
[apiBase]
)
const exportRoPA = useCallback(
async (format: ExportFormat): Promise<string> => {
const params = new URLSearchParams({ format })
const response = await fetch(`${apiBase}/export/ropa?${params}`)
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Fehler beim Exportieren des RoPA')
}
const blob = await response.blob()
const url = URL.createObjectURL(blob)
return url
},
[apiBase]
)
// ==========================================
// INITIALIZATION
// ==========================================
@@ -877,23 +629,11 @@ export function VendorComplianceProvider({
vendorStats,
complianceStats,
riskOverview,
createProcessingActivity,
updateProcessingActivity,
deleteProcessingActivity,
duplicateProcessingActivity,
createVendor,
updateVendor,
deleteVendor,
uploadContract,
updateContract,
deleteContract,
startContractReview,
updateFinding,
resolveFinding,
updateControlInstance,
exportVVT,
exportVendorAuditPack,
exportRoPA,
loadData,
refresh,
}),
@@ -902,23 +642,11 @@ export function VendorComplianceProvider({
vendorStats,
complianceStats,
riskOverview,
createProcessingActivity,
updateProcessingActivity,
deleteProcessingActivity,
duplicateProcessingActivity,
createVendor,
updateVendor,
deleteVendor,
uploadContract,
updateContract,
deleteContract,
startContractReview,
updateFinding,
resolveFinding,
updateControlInstance,
exportVVT,
exportVendorAuditPack,
exportRoPA,
loadData,
refresh,
]
@@ -947,64 +675,3 @@ export function useVendorCompliance(): VendorComplianceContextValue {
return context
}
// ==========================================
// SELECTORS
// ==========================================
export function useVendor(vendorId: string | null) {
const { vendors } = useVendorCompliance()
return useMemo(
() => vendors.find((v) => v.id === vendorId) ?? null,
[vendors, vendorId]
)
}
export function useProcessingActivity(activityId: string | null) {
const { processingActivities } = useVendorCompliance()
return useMemo(
() => processingActivities.find((a) => a.id === activityId) ?? null,
[processingActivities, activityId]
)
}
export function useVendorContracts(vendorId: string | null) {
const { contracts } = useVendorCompliance()
return useMemo(
() => contracts.filter((c) => c.vendorId === vendorId),
[contracts, vendorId]
)
}
export function useVendorFindings(vendorId: string | null) {
const { findings } = useVendorCompliance()
return useMemo(
() => findings.filter((f) => f.vendorId === vendorId),
[findings, vendorId]
)
}
export function useContractFindings(contractId: string | null) {
const { findings } = useVendorCompliance()
return useMemo(
() => findings.filter((f) => f.contractId === contractId),
[findings, contractId]
)
}
export function useControlInstancesForEntity(
entityType: 'VENDOR' | 'PROCESSING_ACTIVITY',
entityId: string | null
) {
const { controlInstances, controls } = useVendorCompliance()
return useMemo(() => {
if (!entityId) return []
return controlInstances
.filter((ci) => ci.entityType === entityType && ci.entityId === entityId)
.map((ci) => ({
...ci,
control: controls.find((c) => c.id === ci.controlId),
}))
}, [controlInstances, controls, entityType, entityId])
}

View File

@@ -21,12 +21,6 @@ export * from './types'
export {
VendorComplianceProvider,
useVendorCompliance,
useVendor,
useProcessingActivity,
useVendorContracts,
useVendorFindings,
useContractFindings,
useControlInstancesForEntity,
} from './context'
// ==========================================

View File

@@ -828,34 +828,16 @@ export interface VendorComplianceContextValue extends VendorComplianceState {
riskOverview: RiskOverview
// Actions - Processing Activities
createProcessingActivity: (data: Omit<ProcessingActivity, 'id' | 'tenantId' | 'createdAt' | 'updatedAt'>) => Promise<ProcessingActivity>
updateProcessingActivity: (id: string, data: Partial<ProcessingActivity>) => Promise<void>
deleteProcessingActivity: (id: string) => Promise<void>
duplicateProcessingActivity: (id: string) => Promise<ProcessingActivity>
// Actions - Vendors
createVendor: (data: Omit<Vendor, 'id' | 'tenantId' | 'createdAt' | 'updatedAt'>) => Promise<Vendor>
updateVendor: (id: string, data: Partial<Vendor>) => Promise<void>
deleteVendor: (id: string) => Promise<void>
// Actions - Contracts
uploadContract: (vendorId: string, file: File, metadata: Partial<ContractDocument>) => Promise<ContractDocument>
updateContract: (id: string, data: Partial<ContractDocument>) => Promise<void>
deleteContract: (id: string) => Promise<void>
startContractReview: (contractId: string) => Promise<void>
// Actions - Findings
updateFinding: (id: string, data: Partial<Finding>) => Promise<void>
resolveFinding: (id: string, resolution: string) => Promise<void>
// Actions - Controls
updateControlInstance: (id: string, data: Partial<ControlInstance>) => Promise<void>
// Actions - Export
exportVVT: (format: ExportFormat, activityIds?: string[]) => Promise<string>
exportVendorAuditPack: (vendorId: string, format: ExportFormat) => Promise<string>
exportRoPA: (format: ExportFormat) => Promise<string>
// Data Loading
loadData: () => Promise<void>
refresh: () => Promise<void>
@@ -959,15 +941,6 @@ export interface VendorFormData {
notes?: string
}
export interface ContractUploadData {
vendorId: string
documentType: DocumentType
version: string
effectiveDate?: Date
expirationDate?: Date
autoRenewal?: boolean
}
// ==========================================
// HELPER FUNCTIONS
// ==========================================

View File

@@ -103,25 +103,21 @@ export interface VVTActivity {
owner: string
createdAt: string
updatedAt: string
}
// Processor-Record (Art. 30 Abs. 2)
export interface VVTProcessorActivity {
id: string
vvtId: string
controllerReference: string
processingCategories: string[]
subProcessorChain: SubProcessor[]
thirdCountryTransfers: { country: string; recipient: string; transferMechanism: string }[]
tomDescription: string
status: 'DRAFT' | 'REVIEW' | 'APPROVED' | 'ARCHIVED'
}
export interface SubProcessor {
name: string
purpose: string
country: string
isThirdCountry: boolean
// Library refs (optional, parallel to freetext)
purposeRefs?: string[]
legalBasisRefs?: string[]
dataSubjectRefs?: string[]
dataCategoryRefs?: string[]
recipientRefs?: string[]
retentionRuleRef?: string
transferMechanismRefs?: string[]
tomRefs?: string[]
linkedLoeschfristenIds?: string[]
linkedTomMeasureIds?: string[]
sourceTemplateId?: string
riskScore?: number
art30Completeness?: VVTCompleteness
}
// =============================================================================
@@ -186,6 +182,14 @@ export const ART9_CATEGORIES: string[] = [
'CRIMINAL_DATA',
]
export interface VVTCompleteness {
score: number
missing: string[]
warnings: string[]
passed: number
total: number
}
// =============================================================================
// HELPER: Create empty activity
// =============================================================================

View File

@@ -0,0 +1,53 @@
-- Wiki Article: BetrVG & KI — Mitbestimmung bei IT-Systemen
-- Kategorie: arbeitsrecht (existiert bereits)
-- Ausfuehren auf Production-DB nach Compliance-Refactoring
INSERT INTO compliance.compliance_wiki_articles (id, category_id, title, summary, content, legal_refs, tags, relevance, source_urls, version)
VALUES
('betrvg-ki-mitbestimmung', 'arbeitsrecht',
'BetrVG & KI — Mitbestimmung bei IT-Systemen',
'Uebersicht der Mitbestimmungsrechte des Betriebsrats bei Einfuehrung von KI- und IT-Systemen gemaess §87 Abs.1 Nr.6 BetrVG. Inkl. BAG-Rechtsprechung und Konflikt-Score.',
'# BetrVG & KI — Mitbestimmung bei IT-Systemen
## Kernregel: §87 Abs.1 Nr.6 BetrVG
Die **Einfuehrung und Anwendung** von technischen Einrichtungen, die dazu **geeignet** sind, das Verhalten oder die Leistung der Arbeitnehmer zu ueberwachen, beduerfen der **Zustimmung des Betriebsrats**.
### Wichtig: Eignung genuegt!
Das BAG hat klargestellt: Bereits die **objektive Eignung** zur Ueberwachung genuegt — eine tatsaechliche Nutzung zu diesem Zweck ist nicht erforderlich.
---
## Leitentscheidungen des BAG
### Microsoft Office 365 (BAG 1 ABR 20/21, 08.03.2022)
Das BAG hat ausdruecklich entschieden, dass Microsoft Office 365 der Mitbestimmung unterliegt.
### Standardsoftware (BAG 1 ABN 36/18, 23.10.2018)
Auch alltaegliche Standardsoftware wie Excel ist mitbestimmungsrelevant. Keine Geringfuegigkeitsschwelle.
### SAP ERP (BAG 1 ABR 45/11, 25.09.2012)
HR-/ERP-Systeme erheben und verknuepfen individualisierbare Verhaltens- und Leistungsdaten.
### SaaS/Cloud (BAG 1 ABR 68/13, 21.07.2015)
Auch bei Ueberwachung ueber Dritt-Systeme bleibt der Betriebsrat zu beteiligen.
### Belastungsstatistik (BAG 1 ABR 46/15, 25.04.2017)
Dauerhafte Kennzahlenueberwachung ist ein schwerwiegender Eingriff in das Persoenlichkeitsrecht.
---
## Betriebsrats-Konflikt-Score (SDK)
Das SDK berechnet automatisch einen Konflikt-Score (0-100):
- Beschaeftigtendaten (+10), Ueberwachungseignung (+20), HR-Bezug (+20)
- Individualisierbare Logs (+15), Kommunikationsanalyse (+10)
- Scoring/Ranking (+10), Vollautomatisiert (+10), Keine BR-Konsultation (+5)
Eskalation: Score >= 50 ohne BR → E2, Score >= 75 → E3.',
'["§87 Abs.1 Nr.6 BetrVG", "§90 BetrVG", "§94 BetrVG", "§95 BetrVG", "Art. 88 DSGVO", "§26 BDSG"]',
ARRAY['BetrVG', 'Mitbestimmung', 'Betriebsrat', 'KI', 'Ueberwachung', 'Microsoft 365'],
'critical',
'["https://www.bundesarbeitsgericht.de/entscheidung/1-abr-20-21/", "https://www.bundesarbeitsgericht.de/entscheidung/1-abn-36-18/"]',
1)
ON CONFLICT (id) DO UPDATE SET content = EXCLUDED.content, summary = EXCLUDED.summary, updated_at = NOW();

View File

@@ -0,0 +1,157 @@
-- Wiki Articles: Domain-spezifische KI-Compliance
-- 4 Artikel fuer die wichtigsten Hochrisiko-Domains
-- 1. KI im Recruiting
INSERT INTO compliance.compliance_wiki_articles (id, category_id, title, summary, content, legal_refs, tags, relevance, source_urls, version)
VALUES ('ki-recruiting-compliance', 'arbeitsrecht',
'KI im Recruiting — AGG, DSGVO Art. 22, AI Act Hochrisiko',
'Compliance-Anforderungen bei KI-gestuetzter Personalauswahl: Automatisierte Absagen, Bias-Risiken, Beweislastumkehr.',
'# KI im Recruiting — Compliance-Anforderungen
## AI Act Einstufung
KI im Recruiting faellt unter **Annex III Nr. 4 (Employment)** = **High-Risk**.
## Kritische Punkte
### Art. 22 DSGVO — Automatisierte Entscheidungen
Vollautomatische Absagen ohne menschliche Pruefung sind **grundsaetzlich unzulaessig**.
Erlaubt: KI erstellt Vorschlag → Mensch prueft → Mensch entscheidet → Mensch gibt Absage frei.
### AGG — Diskriminierungsverbot
- § 1 AGG: Keine Benachteiligung nach Geschlecht, Alter, Herkunft, Religion, Behinderung
- § 22 AGG: **Beweislastumkehr** — Arbeitgeber muss beweisen, dass KEINE Diskriminierung vorliegt
- § 15 AGG: Schadensersatz bis 3 Monatsgehaelter pro Fall
- Proxy-Merkmale vermeiden: Name→Herkunft, Foto→Alter
### BetrVG — Mitbestimmung
- § 87 Abs. 1 Nr. 6: Betriebsrat muss zustimmen
- § 95: Auswahlrichtlinien mitbestimmungspflichtig
- BAG 1 ABR 20/21: Gilt auch fuer Standardsoftware
## Pflichtmassnahmen
1. Human-in-the-Loop (echt, kein Rubber Stamping)
2. Regelmaessige Bias-Audits
3. DSFA durchfuehren
4. Betriebsvereinbarung abschliessen
5. Bewerber ueber KI-Nutzung informieren',
'["Art. 22 DSGVO", "§ 1 AGG", "§ 22 AGG", "§ 15 AGG", "§ 87 BetrVG", "§ 95 BetrVG", "Annex III Nr. 4 AI Act"]',
ARRAY['Recruiting', 'HR', 'AGG', 'Bias', 'Art. 22', 'Beweislastumkehr', 'Betriebsrat'],
'critical',
'["https://www.bundesarbeitsgericht.de/entscheidung/1-abr-20-21/"]',
1)
ON CONFLICT (id) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
-- 2. KI in der Bildung
INSERT INTO compliance.compliance_wiki_articles (id, category_id, title, summary, content, legal_refs, tags, relevance, source_urls, version)
VALUES ('ki-bildung-compliance', 'branchenspezifisch',
'KI in der Bildung — Notenvergabe, Pruefungsbewertung, Minderjaehrige',
'AI Act Annex III Nr. 3: Hochrisiko bei KI-gestuetzter Bewertung in Bildung und Ausbildung.',
'# KI in der Bildung — Compliance-Anforderungen
## AI Act Einstufung
KI in Bildung/Ausbildung faellt unter **Annex III Nr. 3 (Education)** = **High-Risk**.
## Kritische Szenarien
- KI beeinflusst Noten → High-Risk
- KI bewertet Pruefungen → High-Risk
- KI steuert Zugang zu Bildungsangeboten → High-Risk
- Minderjaehrige betroffen → Besonderer Schutz (Art. 24 EU-Grundrechtecharta)
## BLOCK-Regel
**Minderjaehrige betroffen + keine Lehrkraft-Pruefung = UNZULAESSIG**
## Pflichtmassnahmen
1. Lehrkraft prueft JEDES KI-Ergebnis vor Mitteilung an Schueler
2. Chancengleichheit unabhaengig von sozioekonomischem Hintergrund
3. Keine Benachteiligung durch Sprache oder Behinderung
4. FRIA durchfuehren (Grundrechte-Folgenabschaetzung)
5. DSFA bei Verarbeitung von Schuelerdaten
## Grundrechte
- Recht auf Bildung (Art. 14 EU-Charta)
- Rechte des Kindes (Art. 24 EU-Charta)
- Nicht-Diskriminierung (Art. 21 EU-Charta)',
'["Annex III Nr. 3 AI Act", "Art. 14 EU-Grundrechtecharta", "Art. 24 EU-Grundrechtecharta", "Art. 35 DSGVO"]',
ARRAY['Bildung', 'Education', 'Noten', 'Pruefung', 'Minderjaehrige', 'Schule'],
'critical',
'[]',
1)
ON CONFLICT (id) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
-- 3. KI im Gesundheitswesen
INSERT INTO compliance.compliance_wiki_articles (id, category_id, title, summary, content, legal_refs, tags, relevance, source_urls, version)
VALUES ('ki-gesundheit-compliance', 'branchenspezifisch',
'KI im Gesundheitswesen — MDR, Diagnose, Triage',
'AI Act Annex III Nr. 5 + MDR: Hochrisiko bei KI in Diagnose, Behandlung und Triage.',
'# KI im Gesundheitswesen — Compliance-Anforderungen
## Regulatorischer Rahmen
- **AI Act Annex III Nr. 5** — Zugang zu wesentlichen Diensten (Gesundheit)
- **MDR (EU) 2017/745** — Medizinprodukteverordnung
- **DSGVO Art. 9** — Gesundheitsdaten = besondere Kategorie
## Kritische Szenarien
- KI unterstuetzt Diagnosen → High-Risk + DSFA Pflicht
- KI priorisiert Patienten (Triage) → Lebenskritisch, hoechste Anforderungen
- KI empfiehlt Behandlungen → High-Risk
- System ist Medizinprodukt → MDR-Zertifizierung erforderlich
## BLOCK-Regeln
- **Medizinprodukt ohne klinische Validierung = UNZULAESSIG**
- MDR Art. 61: Klinische Bewertung ist Pflicht
## Grundrechte
- Menschenwuerde (Art. 1 EU-Charta)
- Schutz personenbezogener Daten (Art. 8 EU-Charta)
- Patientenautonomie
## Pflichtmassnahmen
1. Klinische Validierung vor Einsatz
2. Human Oversight durch qualifiziertes Fachpersonal
3. DSFA fuer Gesundheitsdatenverarbeitung
4. Genauigkeitsmetriken definieren und messen
5. Incident Reporting bei Fehlfunktionen',
'["Annex III Nr. 5 AI Act", "MDR (EU) 2017/745", "Art. 9 DSGVO", "Art. 35 DSGVO"]',
ARRAY['Gesundheit', 'Healthcare', 'MDR', 'Diagnose', 'Triage', 'Medizinprodukt'],
'critical',
'[]',
1)
ON CONFLICT (id) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
-- 4. KI in Finanzdienstleistungen
INSERT INTO compliance.compliance_wiki_articles (id, category_id, title, summary, content, legal_refs, tags, relevance, source_urls, version)
VALUES ('ki-finance-compliance', 'branchenspezifisch',
'KI in Finanzdienstleistungen — Scoring, DORA, Versicherung',
'AI Act Annex III Nr. 5 + DORA + MaRisk: Compliance bei Kredit-Scoring, Algo-Trading, Versicherungspraemien.',
'# KI in Finanzdienstleistungen — Compliance-Anforderungen
## Regulatorischer Rahmen
- **AI Act Annex III Nr. 5** — Zugang zu wesentlichen Diensten
- **DORA** — Digital Operational Resilience Act
- **MaRisk/BAIT** — Bankaufsichtliche Anforderungen
- **MiFID II** — Algorithmischer Handel
## Kritische Szenarien
- Kredit-Scoring → High-Risk (Art. 22 DSGVO + Annex III)
- Automatisierte Schadenbearbeitung → Art. 22 Risiko
- Individuelle Praemienberechnung → Diskriminierungsrisiko
- Algo-Trading → MiFID II Anforderungen
- Robo Advisor → WpHG-Pflichten
## Pflichtmassnahmen
1. Transparenz bei Scoring-Entscheidungen
2. Bias-Audits bei Kreditvergabe
3. Human Oversight bei Ablehnungen
4. DORA-konforme IT-Resilienz
5. Incident Reporting
## Besondere Risiken
- Diskriminierendes Kredit-Scoring (AGG + AI Act)
- Ungerechtfertigte Verweigerung von Finanzdienstleistungen
- Mangelnde Erklaerbarkeit bei Scoring-Algorithmen',
'["Annex III Nr. 5 AI Act", "DORA", "MaRisk", "MiFID II", "Art. 22 DSGVO", "§ 1 AGG"]',
ARRAY['Finance', 'Banking', 'Versicherung', 'Scoring', 'DORA', 'Kredit', 'Algo-Trading'],
'critical',
'[]',
1)
ON CONFLICT (id) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,14 @@
"test:all": "vitest run && playwright test --project=chromium"
},
"dependencies": {
"@tiptap/extension-image": "^3.20.2",
"@tiptap/extension-table": "^3.20.2",
"@tiptap/extension-table-cell": "^3.20.2",
"@tiptap/extension-table-header": "^3.20.2",
"@tiptap/extension-table-row": "^3.20.2",
"@tiptap/pm": "^3.20.2",
"@tiptap/react": "^3.20.2",
"@tiptap/starter-kit": "^3.20.2",
"bpmn-js": "^18.0.1",
"jspdf": "^4.1.0",
"jszip": "^3.10.1",

View File

@@ -104,13 +104,16 @@ func main() {
auditHandlers := handlers.NewAuditHandlers(auditStore, exporter)
uccaHandlers := handlers.NewUCCAHandlers(uccaStore, escalationStore, providerRegistry)
escalationHandlers := handlers.NewEscalationHandlers(escalationStore, uccaStore)
registrationStore := ucca.NewRegistrationStore(pool)
registrationHandlers := handlers.NewRegistrationHandlers(registrationStore, uccaStore)
roadmapHandlers := handlers.NewRoadmapHandlers(roadmapStore)
workshopHandlers := handlers.NewWorkshopHandlers(workshopStore)
portfolioHandlers := handlers.NewPortfolioHandlers(portfolioStore)
academyHandlers := handlers.NewAcademyHandlers(academyStore, trainingStore)
whistleblowerHandlers := handlers.NewWhistleblowerHandlers(whistleblowerStore)
iaceHandler := handlers.NewIACEHandler(iaceStore)
trainingHandlers := handlers.NewTrainingHandlers(trainingStore, contentGenerator)
iaceHandler := handlers.NewIACEHandler(iaceStore, providerRegistry)
blockGenerator := training.NewBlockGenerator(trainingStore, contentGenerator)
trainingHandlers := handlers.NewTrainingHandlers(trainingStore, contentGenerator, blockGenerator, ttsClient)
ragHandlers := handlers.NewRAGHandlers(corpusVersionStore)
// Initialize obligations framework (v2 with TOM mapping)
@@ -269,10 +272,32 @@ func main() {
uccaRoutes.POST("/escalations/:id/review", escalationHandlers.StartReview)
uccaRoutes.POST("/escalations/:id/decide", escalationHandlers.DecideEscalation)
// AI Act Decision Tree
dtRoutes := uccaRoutes.Group("/decision-tree")
{
dtRoutes.GET("", uccaHandlers.GetDecisionTree)
dtRoutes.POST("/evaluate", uccaHandlers.EvaluateDecisionTree)
dtRoutes.GET("/results", uccaHandlers.ListDecisionTreeResults)
dtRoutes.GET("/results/:id", uccaHandlers.GetDecisionTreeResult)
dtRoutes.DELETE("/results/:id", uccaHandlers.DeleteDecisionTreeResult)
}
// Obligations framework (v2 with TOM mapping)
obligationsHandlers.RegisterRoutes(uccaRoutes)
}
// AI Registration routes - EU AI Database (Art. 49)
regRoutes := v1.Group("/ai-registration")
{
regRoutes.POST("", registrationHandlers.Create)
regRoutes.GET("", registrationHandlers.List)
regRoutes.GET("/:id", registrationHandlers.Get)
regRoutes.PUT("/:id", registrationHandlers.Update)
regRoutes.PATCH("/:id/status", registrationHandlers.UpdateStatus)
regRoutes.POST("/prefill/:assessment_id", registrationHandlers.Prefill)
regRoutes.GET("/:id/export", registrationHandlers.Export)
}
// RAG routes - Legal Corpus Search & Versioning
ragRoutes := v1.Group("/rag")
{
@@ -280,6 +305,7 @@ func main() {
ragRoutes.GET("/regulations", ragHandlers.ListRegulations)
ragRoutes.GET("/corpus-status", ragHandlers.CorpusStatus)
ragRoutes.GET("/corpus-versions/:collection", ragHandlers.CorpusVersionHistory)
ragRoutes.GET("/scroll", ragHandlers.HandleScrollChunks)
}
// Roadmap routes - Compliance Implementation Roadmaps
@@ -432,6 +458,7 @@ func main() {
trainingRoutes.GET("/modules/:id", trainingHandlers.GetModule)
trainingRoutes.POST("/modules", trainingHandlers.CreateModule)
trainingRoutes.PUT("/modules/:id", trainingHandlers.UpdateModule)
trainingRoutes.DELETE("/modules/:id", trainingHandlers.DeleteModule)
// Compliance Training Matrix (CTM)
trainingRoutes.GET("/matrix", trainingHandlers.GetMatrix)
@@ -446,6 +473,7 @@ func main() {
trainingRoutes.POST("/assignments/:id/start", trainingHandlers.StartAssignment)
trainingRoutes.POST("/assignments/:id/progress", trainingHandlers.UpdateAssignmentProgress)
trainingRoutes.POST("/assignments/:id/complete", trainingHandlers.CompleteAssignment)
trainingRoutes.PUT("/assignments/:id", trainingHandlers.UpdateAssignment)
// Quiz
trainingRoutes.GET("/quiz/:moduleId", trainingHandlers.GetQuiz)
@@ -478,6 +506,10 @@ func main() {
c.Params = append(c.Params, gin.Param{Key: "id", Value: c.Param("mediaId")})
trainingHandlers.PublishMedia(c)
})
trainingRoutes.GET("/media/:mediaId/stream", func(c *gin.Context) {
c.Params = append(c.Params, gin.Param{Key: "id", Value: c.Param("mediaId")})
trainingHandlers.StreamMedia(c)
})
// Deadlines & Escalation
trainingRoutes.GET("/deadlines", trainingHandlers.GetDeadlines)
@@ -489,7 +521,30 @@ func main() {
trainingRoutes.GET("/stats", trainingHandlers.GetStats)
// Certificates
trainingRoutes.POST("/certificates/generate/:assignmentId", trainingHandlers.GenerateCertificate)
trainingRoutes.GET("/certificates", trainingHandlers.ListCertificates)
trainingRoutes.GET("/certificates/:id/verify", trainingHandlers.VerifyCertificate)
trainingRoutes.GET("/certificates/:id/pdf", trainingHandlers.DownloadCertificatePDF)
// Training Blocks — Controls → Schulungsmodule Pipeline
trainingRoutes.GET("/blocks", trainingHandlers.ListBlockConfigs)
trainingRoutes.POST("/blocks", trainingHandlers.CreateBlockConfig)
trainingRoutes.GET("/blocks/:id", trainingHandlers.GetBlockConfig)
trainingRoutes.PUT("/blocks/:id", trainingHandlers.UpdateBlockConfig)
trainingRoutes.DELETE("/blocks/:id", trainingHandlers.DeleteBlockConfig)
trainingRoutes.POST("/blocks/:id/preview", trainingHandlers.PreviewBlock)
trainingRoutes.POST("/blocks/:id/generate", trainingHandlers.GenerateBlock)
trainingRoutes.GET("/blocks/:id/controls", trainingHandlers.GetBlockControls)
// Canonical Controls Browsing
trainingRoutes.GET("/canonical/controls", trainingHandlers.ListCanonicalControls)
trainingRoutes.GET("/canonical/meta", trainingHandlers.GetCanonicalMeta)
// Interactive Video (Narrator + Checkpoints)
trainingRoutes.POST("/content/:moduleId/generate-interactive", trainingHandlers.GenerateInteractiveVideo)
trainingRoutes.GET("/content/:moduleId/interactive-manifest", trainingHandlers.GetInteractiveManifest)
trainingRoutes.POST("/checkpoints/:checkpointId/submit", trainingHandlers.SubmitCheckpointQuiz)
trainingRoutes.GET("/checkpoints/progress/:assignmentId", trainingHandlers.GetCheckpointProgress)
}
// Whistleblower routes - Hinweisgebersystem (HinSchG)
@@ -523,6 +578,18 @@ func main() {
iaceRoutes.GET("/hazard-library", iaceHandler.ListHazardLibrary)
// Controls Library (project-independent)
iaceRoutes.GET("/controls-library", iaceHandler.ListControlsLibrary)
// ISO 12100 reference data (project-independent)
iaceRoutes.GET("/lifecycle-phases", iaceHandler.ListLifecyclePhases)
iaceRoutes.GET("/roles", iaceHandler.ListRoles)
iaceRoutes.GET("/evidence-types", iaceHandler.ListEvidenceTypes)
iaceRoutes.GET("/protective-measures-library", iaceHandler.ListProtectiveMeasures)
// Component Library & Energy Sources (Hazard Matching Engine)
iaceRoutes.GET("/component-library", iaceHandler.ListComponentLibrary)
iaceRoutes.GET("/energy-sources", iaceHandler.ListEnergySources)
// Tag Taxonomy
iaceRoutes.GET("/tags", iaceHandler.ListTags)
// Hazard Patterns
iaceRoutes.GET("/hazard-patterns", iaceHandler.ListHazardPatterns)
// Project Management
iaceRoutes.POST("/projects", iaceHandler.CreateProject)
@@ -552,6 +619,12 @@ func main() {
iaceRoutes.PUT("/projects/:id/hazards/:hid", iaceHandler.UpdateHazard)
iaceRoutes.POST("/projects/:id/hazards/suggest", iaceHandler.SuggestHazards)
// Pattern Matching Engine
iaceRoutes.POST("/projects/:id/match-patterns", iaceHandler.MatchPatterns)
iaceRoutes.POST("/projects/:id/apply-patterns", iaceHandler.ApplyPatternResults)
iaceRoutes.POST("/projects/:id/hazards/:hid/suggest-measures", iaceHandler.SuggestMeasuresForHazard)
iaceRoutes.POST("/projects/:id/mitigations/:mid/suggest-evidence", iaceHandler.SuggestEvidenceForMitigation)
// Risk Assessment
iaceRoutes.POST("/projects/:id/hazards/:hid/assess", iaceHandler.AssessRisk)
iaceRoutes.GET("/projects/:id/risk-summary", iaceHandler.GetRiskSummary)
@@ -561,6 +634,7 @@ func main() {
iaceRoutes.POST("/projects/:id/hazards/:hid/mitigations", iaceHandler.CreateMitigation)
iaceRoutes.PUT("/mitigations/:mid", iaceHandler.UpdateMitigation)
iaceRoutes.POST("/mitigations/:mid/verify", iaceHandler.VerifyMitigation)
iaceRoutes.POST("/projects/:id/validate-mitigation-hierarchy", iaceHandler.ValidateMitigationHierarchy)
// Evidence
iaceRoutes.POST("/projects/:id/evidence", iaceHandler.UploadEvidence)
@@ -576,6 +650,7 @@ func main() {
iaceRoutes.GET("/projects/:id/tech-file", iaceHandler.ListTechFileSections)
iaceRoutes.PUT("/projects/:id/tech-file/:section", iaceHandler.UpdateTechFileSection)
iaceRoutes.POST("/projects/:id/tech-file/:section/approve", iaceHandler.ApproveTechFileSection)
iaceRoutes.POST("/projects/:id/tech-file/:section/generate", iaceHandler.GenerateSingleSection)
iaceRoutes.GET("/projects/:id/tech-file/export", iaceHandler.ExportTechFile)
// Monitoring
@@ -585,6 +660,10 @@ func main() {
// Audit Trail
iaceRoutes.GET("/projects/:id/audit-trail", iaceHandler.GetAuditTrail)
// RAG Library Search (Phase 6)
iaceRoutes.POST("/library-search", iaceHandler.SearchLibrary)
iaceRoutes.POST("/projects/:id/tech-file/:section/enrich", iaceHandler.EnrichTechFileSection)
}
}

View File

@@ -0,0 +1,270 @@
Evidence Library — 50 Nachweisarten (vollständig beschrieben)
Jeder Nachweis dient dazu, die Wirksamkeit einer Schutzmaßnahme oder Sicherheitsanforderung nachzuweisen. Die Struktur ist so gestaltet, dass sie direkt in eine Compliance oder CEDokumentationsEngine integriert werden kann.
Struktur eines Nachweises
evidence_id
title
purpose
verification_method
typical_steps
expected_result
generated_document
### E01 Konstruktionsreview
**Purpose:** Überprüfung der sicherheitsrelevanten Konstruktion. Verification Method: Engineering Review. Typical Steps: Zeichnungen prüfen, Gefahrenstellen identifizieren, Schutzmaßnahmen bewerten. Expected Result: Konstruktion erfüllt Sicherheitsanforderungen. Generated Document: ReviewProtokoll.
### E02 Sicherheitskonzept
**Purpose:** Dokumentation der Sicherheitsarchitektur. Verification Method: Architekturprüfung. Typical Steps: Systemgrenzen definieren, Schutzkonzept beschreiben. Expected Result: vollständiges Sicherheitskonzept. Generated Document: Sicherheitsdokument.
### E03 Gefährdungsanalyse
**Purpose:** Identifikation aller relevanten Gefährdungen. Verification Method: strukturierte Analyse. Typical Steps: Gefährdungsliste erstellen, Risikobewertung durchführen. Expected Result: vollständige Hazard List. Generated Document: Risikoanalysebericht.
### E04 Sicherheitsabstandsberechnung
**Purpose:** Nachweis sicherer Mindestabstände. Verification Method: mathematische Berechnung. Typical Steps: Bewegungsenergie bestimmen, Distanz berechnen. Expected Result: Mindestabstand erfüllt Anforderungen. Generated Document: Berechnungsprotokoll.
### E05 Festigkeitsnachweis
**Purpose:** strukturelle Stabilität sicherstellen. Verification Method: statische Berechnung oder Simulation. Typical Steps: Belastungen definieren, Struktur analysieren. Expected Result: Bauteil hält Belastungen stand. Generated Document: Festigkeitsbericht.
### E06 Risikoanalysebericht
**Purpose:** Dokumentation der Risikobeurteilung. Verification Method: Risikomodell. Typical Steps: Gefährdungen bewerten, Maßnahmen definieren. Expected Result: akzeptables Restrisiko. Generated Document: Risikobeurteilung.
### E07 Architekturdiagramm
**Purpose:** Darstellung der Systemarchitektur. Verification Method: Systemmodellierung. Typical Steps: Komponenten und Schnittstellen beschreiben. Expected Result: nachvollziehbare Systemstruktur. Generated Document: Architekturdiagramm.
### E08 SoftwareDesignreview
**Purpose:** Bewertung des Softwaredesigns. Verification Method: Entwicklerreview. Typical Steps: Architektur analysieren, Sicherheitslogik prüfen. Expected Result: robustes Design. Generated Document: Reviewbericht.
### E09 Code Review
**Purpose:** Fehler und Sicherheitsprobleme erkennen. Verification Method: Peer Review. Typical Steps: Quellcode analysieren. Expected Result: sicherer und wartbarer Code. Generated Document: CodeReviewProtokoll.
### E10 Sicherheitsanforderungsdokument
**Purpose:** Definition der Sicherheitsanforderungen. Verification Method: Dokumentationsprüfung. Typical Steps: Anforderungen sammeln und validieren. Expected Result: vollständige Security Requirements. Generated Document: Requirements Dokument.
### E11 Funktionstest
**Purpose:** Überprüfung der Systemfunktion. Verification Method: Testfallausführung. Typical Steps: Testfälle definieren, Ergebnisse dokumentieren. Expected Result: Funktionen arbeiten korrekt. Generated Document: Testprotokoll.
### E12 Integrationstest
**Purpose:** Zusammenspiel von Komponenten prüfen. Verification Method: Systemtests. Typical Steps: Schnittstellen testen. Expected Result: korrekte Interaktion. Generated Document: Integrationsbericht.
### E13 Systemtest
**Purpose:** Gesamtfunktion der Maschine prüfen. Verification Method: EndtoEnd Test. Typical Steps: reale Betriebsbedingungen simulieren. Expected Result: System arbeitet stabil. Generated Document: Systemtestbericht.
### E14 Sicherheitsfunktionstest
**Purpose:** Wirksamkeit der Sicherheitsfunktion prüfen. Verification Method: gezielte Auslösung. Typical Steps: Sicherheitsfunktion aktivieren. Expected Result: sichere Reaktion. Generated Document: Sicherheitsprotokoll.
### E15 NotHalt Test
**Purpose:** Funktion des NotHalts sicherstellen. Verification Method: manuelle Betätigung. Typical Steps: NotHalt drücken, Stopzeit messen. Expected Result: Maschine stoppt sofort. Generated Document: Testbericht.
### E16 Verriegelungstest
**Purpose:** Schutzsystem prüfen. Verification Method: mechanischer Test. Typical Steps: Tür öffnen während Betrieb. Expected Result: Maschine stoppt. Generated Document: Prüfprotokoll.
### E17 Fault Injection Test
**Purpose:** Fehlerreaktionen prüfen. Verification Method: simulierte Fehler. Typical Steps: Sensorfehler auslösen. Expected Result: sichere Reaktion. Generated Document: Testreport.
### E18 Simulationstest
**Purpose:** Verhalten im Modell prüfen. Verification Method: Simulation. Typical Steps: Szenarien simulieren. Expected Result: korrektes Verhalten. Generated Document: Simulationsbericht.
### E19 Lasttest
**Purpose:** Verhalten unter Last prüfen. Verification Method: Belastungstest. Typical Steps: maximale Last anwenden. Expected Result: System bleibt stabil. Generated Document: Lasttestbericht.
### E20 Stresstest
**Purpose:** Extrembedingungen prüfen. Verification Method: Überlastsimulation. Typical Steps: Grenzwerte testen. Expected Result: System bleibt kontrollierbar. Generated Document: Stresstestbericht.
### E21 Schutzleiterprüfung
**Purpose:** Erdung überprüfen. Verification Method: elektrische Messung. Expected Result: ausreichende Leitfähigkeit. Generated Document: Messprotokoll.
### E22 Isolationsmessung
**Purpose:** elektrische Isolation prüfen. Verification Method: Hochspannungsmessung. Expected Result: Isolation ausreichend. Generated Document: Prüfbericht.
### E23 Hochspannungsprüfung
**Purpose:** elektrische Sicherheit testen. Verification Method: HVTest. Expected Result: keine Durchschläge. Generated Document: Testprotokoll.
### E24 Kurzschlussprüfung
**Purpose:** Verhalten bei Kurzschluss prüfen. Verification Method: Simulation. Expected Result: sichere Abschaltung. Generated Document: Testbericht.
### E25 Erdungsmessung
**Purpose:** Erdungssystem validieren. Verification Method: Widerstandsmessung. Expected Result: zulässiger Erdungswert. Generated Document: Messprotokoll.
### E26 Penetration Test
**Purpose:** ITSicherheit prüfen. Verification Method: Angriffssimulation. Expected Result: keine kritischen Schwachstellen. Generated Document: PentestReport.
### E27 Vulnerability Scan
**Purpose:** bekannte Schwachstellen erkennen. Verification Method: automatisierter Scan. Expected Result: Schwachstellenliste. Generated Document: Scanbericht.
### E28 SBOM Prüfung
**Purpose:** Softwareabhängigkeiten prüfen. Verification Method: Komponentenliste analysieren. Expected Result: bekannte Risiken erkannt. Generated Document: SBOMReport.
### E29 Dependency Scan
**Purpose:** Bibliotheken prüfen. Verification Method: CVEAbgleich. Expected Result: keine kritischen Abhängigkeiten. Generated Document: Scanreport.
### E30 UpdateSignaturprüfung
**Purpose:** Authentizität von Updates prüfen. Verification Method: kryptographische Validierung. Expected Result: gültige Signatur. Generated Document: Verifikationsprotokoll.
### E31 Betriebsanleitung
**Purpose:** sichere Nutzung dokumentieren. Verification Method: Dokumentationsprüfung. Expected Result: vollständige Anleitung. Generated Document: Handbuch.
### E32 Wartungsanleitung
**Purpose:** sichere Wartung ermöglichen. Verification Method: Review. Expected Result: klare Wartungsprozesse. Generated Document: Wartungsdokument.
### E33 Sicherheitsanweisung
**Purpose:** Sicherheitsregeln festlegen. Verification Method: Freigabeprozess. Expected Result: verbindliche Richtlinie. Generated Document: Sicherheitsdokument.
### E34 Schulungsnachweis
**Purpose:** Kompetenz nachweisen. Verification Method: Teilnahmeprotokoll. Expected Result: Mitarbeiter geschult. Generated Document: Trainingszertifikat.
### E35 Risikoabnahmeprotokoll
**Purpose:** Freigabe der Risikobeurteilung. Verification Method: Managementreview. Expected Result: Risiko akzeptiert. Generated Document: Freigabedokument.
### E36 Freigabedokument
**Purpose:** formale Systemfreigabe. Verification Method: Genehmigungsprozess. Expected Result: System genehmigt. Generated Document: Freigabeprotokoll.
### E37 Änderungsprotokoll
**Purpose:** Änderungen nachvollziehen. Verification Method: Change Management. Expected Result: Änderungsverlauf dokumentiert. Generated Document: Change Log.
### E38 Auditbericht
**Purpose:** Compliance prüfen. Verification Method: Audit. Expected Result: Audit ohne kritische Abweichungen. Generated Document: Auditreport.
### E39 Abnahmeprotokoll
**Purpose:** Endabnahme dokumentieren. Verification Method: Abnahmetest. Expected Result: System akzeptiert. Generated Document: Abnahmebericht.
### E40 Prüfprotokoll
**Purpose:** Prüfergebnisse festhalten. Verification Method: standardisierte Tests. Expected Result: erfolgreiche Prüfung. Generated Document: Prüfprotokoll.
### E41 MonitoringLogs
**Purpose:** Betriebsüberwachung. Verification Method: Loganalyse. Expected Result: keine Sicherheitsereignisse. Generated Document: Logreport.
### E42 Ereignisprotokolle
**Purpose:** sicherheitsrelevante Ereignisse dokumentieren. Verification Method: Ereignisaufzeichnung. Expected Result: vollständige Historie. Generated Document: Ereignisbericht.
### E43 Alarmberichte
**Purpose:** Systemalarme dokumentieren. Verification Method: Alarmanalyse. Expected Result: nachvollziehbare Alarmhistorie. Generated Document: Alarmreport.
### E44 IncidentReport
**Purpose:** Sicherheitsvorfall dokumentieren. Verification Method: Incident Management. Expected Result: Ursachenanalyse abgeschlossen. Generated Document: Incidentbericht.
### E45 Wartungsbericht
**Purpose:** Wartungsarbeiten dokumentieren. Verification Method: Servicebericht. Expected Result: Wartung durchgeführt. Generated Document: Wartungsprotokoll.
### E46 Redundanzprüfung
**Purpose:** Redundante Systeme testen. Verification Method: FailoverTest. Expected Result: System bleibt funktionsfähig. Generated Document: Redundanzbericht.
### E47 Sicherheitsvalidierung
**Purpose:** Gesamtvalidierung der Sicherheitsfunktionen. Verification Method: kombinierte Tests. Expected Result: Sicherheitsanforderungen erfüllt. Generated Document: Validierungsbericht.
### E48 CyberSecurityAudit
**Purpose:** ITSicherheitsprüfung. Verification Method: Auditverfahren. Expected Result: Sicherheitsniveau bestätigt. Generated Document: Auditbericht.
### E49 Konfigurationsprüfung
**Purpose:** Systemkonfiguration prüfen. Verification Method: Konfigurationsreview. Expected Result: sichere Einstellungen. Generated Document: Konfigurationsbericht.
### E50 Endabnahmebericht
**Purpose:** finale Systemfreigabe. Verification Method: Abschlussprüfung. Expected Result: Maschine freigegeben. Generated Document: Endabnahmebericht.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,145 @@
Lifecycle Phases Library — 25 Lebensphasen vollständig beschrieben
Diese Bibliothek beschreibt die typischen Lebensphasen einer Maschine oder Anlage über ihren gesamten Lebenszyklus. Die Struktur ist so ausgelegt, dass sie direkt in eine CERisikobeurteilungs oder ComplianceEngine integriert werden kann.
Struktur pro Lebensphase
phase_id
title
description
typical_activities
typical_hazards
involved_roles
safety_focus
### LP01 Transport
**Description:** Bewegung der Maschine oder einzelner Komponenten vom Hersteller zum Installationsort. Typical Activities: - Verladen - Transport per LKW / Kran - Entladen Typical Hazards: - Absturz von Lasten - Quetschungen - Kollisionen Involved Roles: - Logistikpersonal - Kranführer Safety Focus: - Sichere Lastaufnahme - Transportverriegelungen
### LP02 Lagerung
**Description:** Zwischenlagerung der Maschine oder Baugruppen vor Installation. Typical Activities: - Lagerhaltung - Schutz vor Umwelteinflüssen Typical Hazards: - Instabilität - Korrosion Involved Roles: - Logistikpersonal Safety Focus: - sichere Lagerposition - Schutzabdeckung
### LP03 Montage
**Description:** Zusammenbau einzelner Komponenten zur vollständigen Maschine. Typical Activities: - mechanische Montage - Verschraubungen Typical Hazards: - Quetschungen - Absturz von Bauteilen Involved Roles: - Monteure Safety Focus: - sichere Montageverfahren
### LP04 Installation
**Description:** Aufstellung und Anschluss der Maschine am Einsatzort. Typical Activities: - Positionierung - Anschluss von Energiequellen Typical Hazards: - elektrische Gefahren - mechanische Belastungen Involved Roles: - Installationspersonal - Elektriker Safety Focus: - korrekte Installation
### LP05 Inbetriebnahme
**Description:** Erstmaliges Starten der Maschine nach Installation. Typical Activities: - Funktionstests - Parametrierung Typical Hazards: - unerwartete Bewegungen Involved Roles: - Servicetechniker - Einrichter Safety Focus: - sichere Testbedingungen
### LP06 Parametrierung
**Description:** Konfiguration von Maschinenparametern und Steuerungswerten. Typical Activities: - Softwareeinstellungen - Prozessparameter definieren Typical Hazards: - Fehlkonfiguration Involved Roles: - Einrichter - Softwareingenieur Safety Focus: - sichere Parameter
### LP07 Einrichten
**Description:** Vorbereitung der Maschine für einen Produktionsauftrag. Typical Activities: - Werkzeugwechsel - Prozessanpassung Typical Hazards: - Quetschungen - unerwartete Bewegungen Involved Roles: - Einrichter Safety Focus: - sichere Einrichtverfahren
### LP08 Normalbetrieb
**Description:** regulärer Produktionsbetrieb. Typical Activities: - Maschinenbedienung - Prozessüberwachung Typical Hazards: - mechanische Gefahren Involved Roles: - Maschinenbediener Safety Focus: - sichere Bedienung
### LP09 Automatikbetrieb
**Description:** vollautomatisierter Betrieb ohne direkte Bedienerinteraktion. Typical Activities: - automatisierte Produktionszyklen Typical Hazards: - Kollisionen Involved Roles: - Anlagenfahrer Safety Focus: - sichere Steuerungslogik
### LP10 Handbetrieb
**Description:** Betrieb der Maschine im manuellen Modus. Typical Activities: - manuelle Bewegungssteuerung Typical Hazards: - direkte Nähe zu beweglichen Teilen Involved Roles: - Einrichter Safety Focus: - reduzierte Geschwindigkeit
### LP11 Teach-Modus
**Description:** Programmierung von Bewegungsabläufen. Typical Activities: - Roboterprogrammierung Typical Hazards: - unerwartete Bewegungen Involved Roles: - Programmierer Safety Focus: - Zustimmschalter
### LP12 Produktionsstart
**Description:** Übergang vom Stillstand zur Produktion. Typical Activities: - Start des Produktionszyklus Typical Hazards: - unerwarteter Start Involved Roles: - Maschinenbediener Safety Focus: - Startfreigabe
### LP13 Produktionsstopp
**Description:** planmäßiges Stoppen der Produktion. Typical Activities: - Maschinenabschaltung Typical Hazards: - Restbewegungen Involved Roles: - Maschinenbediener Safety Focus: - kontrolliertes Stoppen
### LP14 Prozessüberwachung
**Description:** Überwachung des laufenden Produktionsprozesses. Typical Activities: - Anzeigenkontrolle Typical Hazards: - Fehlinterpretation Involved Roles: - Anlagenfahrer Safety Focus: - Alarmüberwachung
### LP15 Reinigung
**Description:** Entfernen von Produktionsrückständen. Typical Activities: - manuelle Reinigung Typical Hazards: - Kontakt mit gefährlichen Bereichen Involved Roles: - Reinigungspersonal Safety Focus: - Energieabschaltung
### LP16 Wartung
**Description:** planmäßige Wartung der Maschine. Typical Activities: - Austausch von Verschleißteilen Typical Hazards: - unerwarteter Wiederanlauf Involved Roles: - Wartungstechniker Safety Focus: - Lockout Tagout
### LP17 Inspektion
**Description:** Überprüfung des Maschinenzustands. Typical Activities: - Sichtprüfung Typical Hazards: - Zugang zu Gefahrenbereichen Involved Roles: - Wartungspersonal Safety Focus: - sichere Zugänge
### LP18 Kalibrierung
**Description:** Justierung von Sensoren oder Messsystemen. Typical Activities: - Kalibrierprozesse Typical Hazards: - Fehlmessungen Involved Roles: - Techniker Safety Focus: - präzise Einstellung
### LP19 Störungsbeseitigung
**Description:** Diagnose und Behebung von Störungen. Typical Activities: - Fehleranalyse Typical Hazards: - unerwartete Bewegungen Involved Roles: - Servicetechniker Safety Focus: - sichere Diagnoseverfahren
### LP20 Reparatur
**Description:** Austausch oder Reparatur beschädigter Komponenten. Typical Activities: - Komponentenwechsel Typical Hazards: - mechanische Gefahren Involved Roles: - Wartungstechniker Safety Focus: - sichere Reparaturverfahren
### LP21 Umrüstung
**Description:** Anpassung der Maschine für neue Produkte. Typical Activities: - Werkzeugwechsel Typical Hazards: - Quetschungen Involved Roles: - Einrichter Safety Focus: - sichere Umrüstprozesse
### LP22 Software-Update
**Description:** Aktualisierung der Steuerungssoftware. Typical Activities: - Firmwareupdate Typical Hazards: - Fehlkonfiguration Involved Roles: - Softwareingenieur Safety Focus: - sichere Updateprozesse
### LP23 Fernwartung
**Description:** Wartung über Remotezugriff. Typical Activities: - Diagnose Typical Hazards: - unautorisierter Zugriff Involved Roles: - Fernwartungsdienst Safety Focus: - sichere Authentifizierung
### LP24 Außerbetriebnahme
**Description:** dauerhafte Stilllegung der Maschine. Typical Activities: - Abschaltung Typical Hazards: - Restenergie Involved Roles: - Betreiber Safety Focus: - sichere Abschaltung
### LP25 Demontage / Entsorgung
**Description:** Zerlegung und Entsorgung der Maschine. Typical Activities: - Demontage Typical Hazards: - Absturz von Bauteilen Involved Roles: - Demontagepersonal Safety Focus: - sichere Demontageverfahren

View File

@@ -0,0 +1,155 @@
Maschinenkomponenten- und Energiequellenbibliothek
Dieses Dokument enthält zwei zentrale Bibliotheken für eine CE-Risikobeurteilungs- oder Compliance-Engine:
Maschinenkomponentenbibliothek (≈120 typische Komponenten)
Energiequellenbibliothek (≈20 Energiearten)
Diese Bibliotheken ermöglichen eine automatische Zuordnung von Gefährdungen, sobald ein Benutzer eine Maschine, Anlage oder Produktionslinie beschreibt.
1. Maschinenkomponentenbibliothek (120 Komponenten)
Struktur pro Komponente
component_id
name
category
description
typical_hazards
typical_energy_sources
Mechanische Komponenten
### C001 Roboterarm C002 Roboter-Greifer C003 Förderband C004 Rollenförderer C005 Kettenförderer C006 Hubtisch C007 Linearachse C008 Rotationsachse C009 Pressmechanismus C010 Stanzwerkzeug C011 Schneidwerkzeug C012 Fräswerkzeug C013 Bohrspindel C014 Schleifaggregat C015 Werkzeugwechsler C016 Werkstückhalter C017 Spannvorrichtung C018 Greifersystem C019 Manipulator C020 Pick-and-Place-Einheit
Strukturelle Komponenten
### C021 Maschinenrahmen C022 Schutzgehäuse C023 Schutzgitter C024 Schutzhaube C025 Zugangstür C026 Wartungsklappe C027 Podest C028 Leiter C029 Plattform C030 Fundament
Antriebskomponenten
### C031 Elektromotor C032 Servomotor C033 Schrittmotor C034 Getriebe C035 Kupplung C036 Bremse C037 Riemenantrieb C038 Zahnradantrieb C039 Kettenantrieb C040 Spindelantrieb
Hydraulische Komponenten
### C041 Hydraulikpumpe C042 Hydraulikzylinder C043 Hydraulikventil C044 Hydraulikleitung C045 Hydraulikaggregat C046 Hydraulikspeicher C047 Hydraulikfilter C048 Druckregler C049 Drucksensor C050 Hydraulikverteiler
Pneumatische Komponenten
### C051 Pneumatikzylinder C052 Pneumatikventil C053 Druckluftleitung C054 Druckregler C055 Luftfilter C056 Kompressor C057 Pneumatikverteiler C058 Vakuumerzeuger C059 Sauggreifer C060 Drucksensor Pneumatik
Elektrische Komponenten
### C061 Schaltschrank C062 Stromversorgung C063 Netzteil C064 Sicherung C065 Leistungsschalter C066 Relais C067 Sicherheitsrelais C068 Transformator C069 Steckverbinder C070 Kabelsystem
Steuerungskomponenten
### C071 SPS C072 Sicherheits-SPS C073 Industrie-PC C074 Embedded Controller C075 Feldbusmodul C076 I/O-Modul C077 HMI-Bedienpanel C078 Touchpanel C079 Steuerungssoftware C080 Visualisierungssystem
Sensorik
### C081 Positionssensor C082 Näherungssensor C083 Lichtschranke C084 Laserscanner C085 Kamerasystem C086 Drucksensor C087 Temperatursensor C088 Vibrationssensor C089 Drehzahlsensor C090 Kraftsensor
Aktorik
### C091 Magnetventil C092 Linearantrieb C093 Rotationsantrieb C094 Servoregler C095 Ventilsteuerung C096 Aktuatorsteuerung C097 Motorsteuerung C098 Schütz C099 Relaisausgang C100 Leistungsmodul
Sicherheitskomponenten
### C101 Not-Halt Taster C102 Sicherheitslichtgitter C103 Sicherheitslaserscanner C104 Sicherheitsmatte C105 Türverriegelung C106 Zweihandbedienung C107 Zustimmschalter C108 Sicherheitsrelais C109 Sicherheitssteuerung C110 Sicherheitsüberwachung
IT- und Netzwerkkomponenten
### C111 Industrial Switch C112 Router C113 Firewall C114 Edge Computer C115 Gateway C116 Remote-Service-Gateway C117 Datenspeicher C118 Cloud-Schnittstelle C119 Netzwerkinterface C120 Diagnosemodul
2. Energiequellenbibliothek (20 Energiearten)
Struktur
energy_id
name
description
typical_components
typical_hazards
### E01 Mechanische Energie Beschreibung: Bewegungsenergie durch rotierende oder lineare Bewegung.
### E02 Elektrische Energie Beschreibung: Energie durch elektrische Spannung oder Stromfluss.
### E03 Hydraulische Energie Beschreibung: Energie durch Flüssigkeitsdruck.
### E04 Pneumatische Energie Beschreibung: Energie durch Druckluft.
### E05 Thermische Energie Beschreibung: Energie in Form von Wärme.
### E06 Chemische Energie Beschreibung: Energie durch chemische Reaktionen.
### E07 Potenzielle Energie Beschreibung: Energie durch Höhenlage oder gespeicherte Last.
### E08 Kinetische Energie Beschreibung: Energie durch Bewegung von Körpern.
### E09 Strahlungsenergie Beschreibung: Energie durch elektromagnetische Strahlung.
### E10 Laserenergie Beschreibung: konzentrierte optische Strahlung.
### E11 Magnetische Energie Beschreibung: Energie durch magnetische Felder.
### E12 Schallenergie Beschreibung: Energie durch akustische Wellen.
### E13 Vibrationsenergie Beschreibung: Energie durch mechanische Schwingungen.
### E14 Druckenergie Beschreibung: Energie durch gespeicherte Druckkräfte.
### E15 Federenergie Beschreibung: Energie gespeichert in elastischen Komponenten.
### E16 Rotationsenergie Beschreibung: Energie rotierender Bauteile.
### E17 Softwaregesteuerte Energie Beschreibung: Energieflüsse ausgelöst durch Steuerungssoftware.
### E18 Cyber-physische Energie Beschreibung: physische Aktionen ausgelöst durch digitale Systeme.
### E19 KI-gesteuerte Energie Beschreibung: Energieflüsse gesteuert durch autonome Entscheidungslogik.
### E20 Restenergie Beschreibung: gespeicherte Energie nach Abschaltung (z. B. Kondensatoren, Druckspeicher).

View File

@@ -0,0 +1,246 @@
Protection Measures Library — 200 Schutzmaßnahmen vollständig beschrieben
Diese Bibliothek beschreibt typische Schutzmaßnahmen für Maschinen, Anlagen, Software und cyberphysische Systeme. Sie ist so strukturiert, dass sie direkt in eine CERisikobeurteilungsEngine integriert werden kann.
Struktur pro Maßnahme
measure_id
title
category
description
typical_application
typical_verification
Die Maßnahmen sind in drei Hauptgruppen gegliedert: 1. Inherently Safe Design (konstruktive Maßnahmen) 2. Technische Schutzmaßnahmen 3. Benutzerinformation / organisatorische Maßnahmen
1 Inherently Safe Design (M001M050)
### M001 Gefahrenstelle konstruktiv eliminieren Description: Gefährliche Bewegung oder Energiequelle vollständig entfernen. Application: Redesign der Mechanik. Verification: Designreview.
### M002 Bewegungsenergie reduzieren Description: Reduzierung kinetischer Energie bewegter Teile. Application: geringere Geschwindigkeiten. Verification: Berechnung.
### M003 Geschwindigkeit begrenzen Description: Begrenzung maximaler Bewegungsgeschwindigkeit. Application: Roboter oder Fördertechnik. Verification: Systemtest.
### M004 Kraft begrenzen Description: Begrenzung mechanischer Kräfte. Application: kollaborative Systeme. Verification: Kraftmessung.
### M005 Sicherheitsabstände vergrößern Description: Abstand zwischen Mensch und Gefahrenzone erhöhen. Application: Layoutänderung. Verification: Distanzmessung.
### M006 Kinematik ändern Description: Mechanische Bewegung so ändern, dass Gefahr reduziert wird. Application: Gelenkdesign. Verification: Simulation.
### M007 Rotationsbewegung vermeiden Description: gefährliche Drehbewegungen eliminieren. Application: alternative Mechanik. Verification: Designprüfung.
### M008 Scharfe Kanten entfernen Description: Abrunden oder Abdecken von Kanten. Application: Gehäuse. Verification: Sichtprüfung.
### M009 sichere Materialwahl Description: Materialien mit geringer Bruchgefahr verwenden. Application: Strukturteile. Verification: Materialprüfung.
### M010 Gewicht reduzieren Description: Verringerung der Masse beweglicher Teile. Application: Greifer oder Arme. Verification: Berechnung.
### M011 redundante Konstruktion Description: Doppelte mechanische Sicherheit. Application: Tragstrukturen. Verification: Belastungstest.
### M012 mechanische Begrenzung Description: Endanschläge begrenzen Bewegungsbereich. Application: Linearachsen. Verification: Funktionsprüfung.
### M013 sichere Geometrie Description: Gestaltung verhindert Einklemmen. Application: Spaltgrößen. Verification: Designreview.
### M014 stabile Konstruktion Description: Struktur verhindert Umkippen. Application: Maschinenrahmen. Verification: Stabilitätsberechnung.
### M015 kollisionsfreie Bewegungsbahnen Description: Bewegungsplanung vermeidet Kollision. Application: Robotik. Verification: Simulation.
### M016 sichere Greifer Description: Greifer verlieren Werkstück nicht. Application: PickandPlace. Verification: Belastungstest.
### M017 sichere Halterungen Description: Bauteile fest fixieren. Application: Montagevorrichtungen. Verification: Inspektion.
### M018 sichere Werkstückaufnahme Description: stabile Fixierung. Application: CNCMaschinen. Verification: Belastungstest.
### M019 sichere Lageführung Description: Führung verhindert Fehlposition. Application: Fördertechnik. Verification: Test.
### M020 sichere Lastführung Description: Last wird stabil transportiert. Application: Hebesysteme. Verification: Simulation.
### M021 sichere Kraftübertragung Description: Vermeidung von Überlast. Application: Getriebe. Verification: Berechnung.
### M022 sichere Energieübertragung Description: Energiefluss kontrollieren. Application: Motorsteuerung. Verification: Test.
### M023 sichere Hydraulikauslegung Description: Drucksystem korrekt dimensionieren. Verification: Drucktest.
### M024 sichere Pneumatikauslegung Description: sichere Druckbereiche definieren. Verification: Drucktest.
### M025 sichere thermische Auslegung Description: Überhitzung verhindern. Verification: Temperaturmessung.
### M026 Temperaturbegrenzung Description: maximal zulässige Temperatur definieren. Verification: Sensorprüfung.
### M027 passive Kühlung Description: Wärmeabführung ohne aktive Systeme. Verification: Temperaturtest.
### M028 automatische Druckbegrenzung Description: Sicherheitsventil begrenzt Druck. Verification: Funktionstest.
### M029 sichere Sensorposition Description: Sensoren an sicheren Stellen montieren. Verification: Review.
### M030 sichere Kabelführung Description: Kabel vor Beschädigung schützen. Verification: Inspektion.
### M031 sichere Leitungsführung Description: Leitungen stabil verlegen. Verification: Sichtprüfung.
### M032 vibrationsarme Konstruktion Description: Schwingungen minimieren. Verification: Vibrationsmessung.
### M033 ergonomische Gestaltung Description: Bedienung ergonomisch gestalten. Verification: Nutzeranalyse.
### M034 gute Sichtbarkeit Description: Gefahrenbereiche sichtbar machen. Verification: Sichtprüfung.
### M035 intuitive Bedienoberfläche Description: Bedienfehler reduzieren. Verification: Usability Test.
### M036 sichere MenschMaschineInteraktion Description: sichere Interaktionskonzepte. Verification: Risikoanalyse.
### M037 sichere Bedienhöhe Description: ergonomische Bedienhöhe. Verification: Designprüfung.
### M038 sichere Reichweiten Description: Bediener erreicht Gefahrenbereich nicht. Verification: Distanzprüfung.
### M039 sichere Wartungszugänge Description: sichere Wartungspunkte. Verification: Inspektion.
### M040 sichere Montagepunkte Description: stabile Montagepunkte. Verification: Belastungstest.
### M041 sichere Demontagepunkte Description: sichere Demontage ermöglichen. Verification: Test.
### M042 sichere Servicezugänge Description: Servicebereiche sicher gestalten. Verification: Inspektion.
### M043 sichere SoftwareFallbacks Description: definierter Zustand bei Fehler. Verification: Test.
### M044 deterministische Steuerungslogik Description: klar definierte Zustände. Verification: Code Review.
### M045 definierte Zustandsmaschine Description: klar strukturierte Steuerungszustände. Verification: Simulation.
### M046 sichere RestartLogik Description: Neustart nur nach Freigabe. Verification: Funktionstest.
### M047 sichere Fehlermodi Description: Fehler führt zu sicherem Zustand. Verification: Fault Test.
### M048 sichere Energieabschaltung Description: Energiequellen trennen. Verification: Test.
### M049 sichere Energieentladung Description: gespeicherte Energie abbauen. Verification: Messung.
### M050 sichere Notzustände Description: Maschine geht in sicheren Zustand. Verification: Sicherheitsprüfung.
2 Technische Schutzmaßnahmen (M051M140)
### M051 feste trennende Schutzeinrichtung Description: mechanische Barriere verhindert Zugriff. Verification: Sichtprüfung.
### M052 bewegliche Schutzeinrichtung Description: Tür oder Haube schützt Gefahrenbereich. Verification: Funktionstest.
### M053 verriegelte Schutztür Description: Maschine stoppt beim Öffnen. Verification: Verriegelungstest.
### M054 Lichtgitter Description: Unterbricht Maschine bei Zutritt. Verification: Sicherheitstest.
### M055 Laserscanner Description: überwacht Gefahrenzone. Verification: Funktionsprüfung.
### M056 Zweihandbedienung Description: beide Hände erforderlich. Verification: Test.
### M057 Zustimmschalter Description: Bediener muss Schalter aktiv halten. Verification: Test.
### M058 NotHalt Description: sofortige Maschinenabschaltung. Verification: NotHalt Test.
### M059 Sicherheitsrelais Description: sichere Signalverarbeitung. Verification: Funktionsprüfung.
### M060 sichere SPS Description: Steuerung mit Sicherheitsfunktionen. Verification: Validierung.
… (Maßnahmen M061M139 folgen in gleicher Struktur)
### M140 sichere Abschaltung bei Fehler Description: System stoppt automatisch. Verification: FehlerSimulation.
3 Benutzerinformation / organisatorische Maßnahmen (M141M200)
### M141 Warnhinweis Description: Warnung vor Gefahren. Verification: Dokumentenprüfung.
### M142 Gefahrenkennzeichnung Description: visuelle Kennzeichnung. Verification: Inspektion.
### M143 Betriebsanweisung Description: sichere Bedienung beschreiben. Verification: Dokumentenreview.
### M144 Wartungsanweisung Description: Wartungsprozesse dokumentieren. Verification: Review.
### M145 Reinigungsanweisung Description: sichere Reinigung beschreiben. Verification: Review.
### M146 Schulungsprogramm Description: Mitarbeiterschulung. Verification: Trainingsnachweis.
### M147 Sicherheitsunterweisung Description: jährliche Sicherheitsunterweisung. Verification: Protokoll.
### M148 Bedienungsanleitung Description: vollständige Maschinenanleitung. Verification: Dokumentenprüfung.
### M149 Servicehandbuch Description: Anleitung für Techniker. Verification: Review.
### M150 Notfallanweisung Description: Verhalten im Notfall. Verification: Audit.
… (Maßnahmen M151M199 folgen in gleicher Struktur)
### M200 Notfallkontaktliste Description: Kontaktinformationen für Notfälle. Verification: Dokumentenprüfung.

View File

@@ -0,0 +1,322 @@
Roles Library — 20 Rollen vollständig beschrieben
Diese Rollenbibliothek beschreibt typische beteiligte Personengruppen bei Planung, Betrieb, Wartung und Prüfung von Maschinen oder Anlagen. Die Struktur ist so gestaltet, dass sie direkt in eine Compliance, CE oder RisikobeurteilungsEngine integriert werden kann.
Struktur pro Rolle
role_id
title
description
primary_responsibilities
required_training
allowed_access
typical_lifecycle_phases
safety_relevance
### R01 Maschinenbediener (Operator)
**Description:** Person, die die Maschine im täglichen Betrieb bedient.
**Primary Responsibilities:** - Starten und Stoppen der Maschine - Überwachung des Produktionsprozesses - Meldung von Störungen
**Required Training:** - Bedienerschulung - Sicherheitsunterweisung
**Allowed Access:** - Bedienpanel - Start/Stop Funktionen
**Typical Lifecycle Phases:** Normalbetrieb, Produktionsstart, Produktionsstopp
**Safety Relevance:** Hoch
### R02 Einrichter (Setup Technician)
**Description:** Fachpersonal, das Maschinen für neue Produktionsaufträge einrichtet.
**Primary Responsibilities:** - Parametrierung der Maschine - Werkzeugwechsel - Prozessoptimierung
**Required Training:** - Maschineneinrichtung - Sicherheitsfunktionen
**Allowed Access:** - Setup-Modus - Parametrierung
**Typical Lifecycle Phases:** Einrichten, Produktionsstart
**Safety Relevance:** Hoch
### R03 Wartungstechniker
**Description:** Techniker für Inspektion, Wartung und Reparatur.
**Primary Responsibilities:** - Wartung - Austausch von Komponenten - Fehlerdiagnose
**Required Training:** - Wartungsschulung - LockoutTagout Verfahren
**Allowed Access:** - Wartungsmodus - interne Maschinenteile
**Typical Lifecycle Phases:** Wartung, Reparatur
**Safety Relevance:** Sehr hoch
### R04 Servicetechniker
**Description:** Externer oder interner Spezialist für technische Unterstützung.
**Primary Responsibilities:** - Fehleranalyse - Systemupdates - technische Beratung
**Required Training:** - Herstellerschulung
**Allowed Access:** - Servicezugang
**Typical Lifecycle Phases:** Reparatur, Fernwartung
**Safety Relevance:** Hoch
### R05 Reinigungspersonal
**Description:** Personal für Reinigung der Anlage.
**Primary Responsibilities:** - Reinigung der Maschine - Entfernen von Produktionsrückständen
**Required Training:** - Sicherheitsunterweisung
**Allowed Access:** - Reinigungsmodus
**Typical Lifecycle Phases:** Reinigung
**Safety Relevance:** Mittel
### R06 Produktionsleiter
**Description:** Verantwortlich für Produktionsplanung und -überwachung.
**Primary Responsibilities:** - Produktionsplanung - Prozessüberwachung
**Required Training:** - Produktionsmanagement
**Allowed Access:** - Produktionsdaten
**Typical Lifecycle Phases:** Normalbetrieb
**Safety Relevance:** Mittel
### R07 Sicherheitsbeauftragter
**Description:** Verantwortlicher für Arbeitssicherheit.
**Primary Responsibilities:** - Sicherheitsüberwachung - Risikoanalysen
**Required Training:** - Arbeitssicherheit
**Allowed Access:** - Sicherheitsdokumentation
**Typical Lifecycle Phases:** Alle
**Safety Relevance:** Sehr hoch
### R08 Elektriker
**Description:** Fachkraft für elektrische Systeme.
**Primary Responsibilities:** - elektrische Wartung - Fehlersuche
**Required Training:** - Elektrotechnische Ausbildung
**Allowed Access:** - Schaltschrank
**Typical Lifecycle Phases:** Wartung, Reparatur
**Safety Relevance:** Sehr hoch
### R09 Softwareingenieur
**Description:** Entwickler für Steuerungssoftware.
**Primary Responsibilities:** - Softwareentwicklung - Fehlerbehebung
**Required Training:** - Softwareentwicklung
**Allowed Access:** - Steuerungssysteme
**Typical Lifecycle Phases:** Softwareupdate
**Safety Relevance:** Hoch
### R10 Instandhaltungsleiter
**Description:** Leitung der Wartungsabteilung.
**Primary Responsibilities:** - Wartungsplanung - Ressourcenkoordination
**Required Training:** - Instandhaltungsmanagement
**Allowed Access:** - Wartungsberichte
**Typical Lifecycle Phases:** Wartung
**Safety Relevance:** Hoch
### R11 Anlagenfahrer
**Description:** Bediener komplexer Anlagen.
**Primary Responsibilities:** - Anlagenüberwachung
**Required Training:** - Anlagenbedienung
**Allowed Access:** - Steuerungssystem
**Typical Lifecycle Phases:** Normalbetrieb
**Safety Relevance:** Hoch
### R12 Qualitätssicherung
**Description:** Verantwortlich für Qualitätskontrollen.
**Primary Responsibilities:** - Produktprüfung
**Required Training:** - Qualitätsmanagement
**Allowed Access:** - Qualitätsdaten
**Typical Lifecycle Phases:** Produktion
**Safety Relevance:** Niedrig
### R13 Logistikpersonal
**Description:** Verantwortlich für Materialtransport.
**Primary Responsibilities:** - Materialbereitstellung
**Required Training:** - Logistikprozesse
**Allowed Access:** - Transportbereiche
**Typical Lifecycle Phases:** Transport
**Safety Relevance:** Mittel
### R14 Fremdfirma
**Description:** Externe Dienstleister.
**Primary Responsibilities:** - Spezialarbeiten
**Required Training:** - Sicherheitsunterweisung
**Allowed Access:** - begrenzte Bereiche
**Typical Lifecycle Phases:** Wartung
**Safety Relevance:** Hoch
### R15 Besucher
**Description:** Nicht geschulte Personen.
**Primary Responsibilities:** - keine
**Required Training:** - Sicherheitseinweisung
**Allowed Access:** - Besucherzonen
**Typical Lifecycle Phases:** Betrieb
**Safety Relevance:** Mittel
### R16 Auditor
**Description:** Prüfer für Compliance und Sicherheit.
**Primary Responsibilities:** - Audits
**Required Training:** - Auditmethoden
**Allowed Access:** - Dokumentation
**Typical Lifecycle Phases:** Audit
**Safety Relevance:** Mittel
### R17 IT-Administrator
**Description:** Verantwortlich für IT-Systeme.
**Primary Responsibilities:** - Netzwerkverwaltung
**Required Training:** - IT-Sicherheit
**Allowed Access:** - IT-Infrastruktur
**Typical Lifecycle Phases:** Betrieb
**Safety Relevance:** Hoch
### R18 Fernwartungsdienst
**Description:** Externe Remote-Spezialisten.
**Primary Responsibilities:** - Remote Diagnose
**Required Training:** - Fernwartungssysteme
**Allowed Access:** - Remote-Zugang
**Typical Lifecycle Phases:** Fernwartung
**Safety Relevance:** Hoch
### R19 Betreiber
**Description:** Eigentümer oder verantwortlicher Betreiber der Anlage.
**Primary Responsibilities:** - Gesamtverantwortung
**Required Training:** - Betreiberpflichten
**Allowed Access:** - Managementebene
**Typical Lifecycle Phases:** Alle
**Safety Relevance:** Sehr hoch
### R20 Notfallpersonal
**Description:** Einsatzkräfte bei Unfällen.
**Primary Responsibilities:** - Rettung - Erste Hilfe
**Required Training:** - Notfallmanagement
**Allowed Access:** - Notfallzugänge
**Typical Lifecycle Phases:** Notfall
**Safety Relevance:** Kritisch

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,479 @@
package handlers
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func init() {
gin.SetMode(gin.TestMode)
}
// ============================================================================
// Helper: create a gin test context with optional JSON body, headers, params
// ============================================================================
func newTestContext(method, path string, body interface{}, headers map[string]string, params gin.Params) (*httptest.ResponseRecorder, *gin.Context) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
var reqBody *bytes.Reader
if body != nil {
b, _ := json.Marshal(body)
reqBody = bytes.NewReader(b)
} else {
reqBody = bytes.NewReader(nil)
}
c.Request, _ = http.NewRequest(method, path, reqBody)
c.Request.Header.Set("Content-Type", "application/json")
for k, v := range headers {
c.Request.Header.Set(k, v)
}
if params != nil {
c.Params = params
}
return w, c
}
func parseResponse(w *httptest.ResponseRecorder) map[string]interface{} {
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
return resp
}
// ============================================================================
// InitFromProfile Tests
// ============================================================================
func TestInitFromProfile_InvalidProjectID_Returns400(t *testing.T) {
handler := &IACEHandler{}
w, c := newTestContext("POST", "/projects/not-a-uuid/init-from-profile", nil, nil, gin.Params{
{Key: "id", Value: "not-a-uuid"},
})
handler.InitFromProfile(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
resp := parseResponse(w)
if resp["error"] == nil {
t.Error("Expected error message in response")
}
}
func TestInitFromProfile_EmptyProjectID_Returns400(t *testing.T) {
handler := &IACEHandler{}
w, c := newTestContext("POST", "/projects//init-from-profile", nil, nil, gin.Params{
{Key: "id", Value: ""},
})
handler.InitFromProfile(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestInitFromProfile_RequestBinding(t *testing.T) {
// Verify the InitFromProfileRequest properly binds company_profile and compliance_scope
body := `{
"company_profile": {"company_name": "TestCorp GmbH", "contact_email": "info@testcorp.de"},
"compliance_scope": {"machine_name": "Robot XY", "has_ai": true, "applicable_regulations": ["machinery_regulation"]}
}`
var parsed struct {
CompanyProfile json.RawMessage `json:"company_profile"`
ComplianceScope json.RawMessage `json:"compliance_scope"`
}
if err := json.Unmarshal([]byte(body), &parsed); err != nil {
t.Fatalf("Failed to parse request body: %v", err)
}
if parsed.CompanyProfile == nil {
t.Error("company_profile should not be nil")
}
if parsed.ComplianceScope == nil {
t.Error("compliance_scope should not be nil")
}
// Verify scope parsing
var scope struct {
MachineName string `json:"machine_name"`
HasAI bool `json:"has_ai"`
ApplicableRegulations []string `json:"applicable_regulations"`
}
if err := json.Unmarshal(parsed.ComplianceScope, &scope); err != nil {
t.Fatalf("Failed to parse compliance_scope: %v", err)
}
if scope.MachineName != "Robot XY" {
t.Errorf("Expected machine_name 'Robot XY', got %q", scope.MachineName)
}
if !scope.HasAI {
t.Error("Expected has_ai to be true")
}
if len(scope.ApplicableRegulations) != 1 || scope.ApplicableRegulations[0] != "machinery_regulation" {
t.Errorf("Unexpected applicable_regulations: %v", scope.ApplicableRegulations)
}
// Verify profile parsing
var profile struct {
CompanyName string `json:"company_name"`
ContactEmail string `json:"contact_email"`
}
if err := json.Unmarshal(parsed.CompanyProfile, &profile); err != nil {
t.Fatalf("Failed to parse company_profile: %v", err)
}
if profile.CompanyName != "TestCorp GmbH" {
t.Errorf("Expected company_name 'TestCorp GmbH', got %q", profile.CompanyName)
}
if profile.ContactEmail != "info@testcorp.de" {
t.Errorf("Expected contact_email 'info@testcorp.de', got %q", profile.ContactEmail)
}
}
// ============================================================================
// GenerateSingleSection Tests
// ============================================================================
func TestGenerateSingleSection_InvalidProjectID_Returns400(t *testing.T) {
handler := &IACEHandler{}
w, c := newTestContext("POST", "/projects/invalid/tech-file/risk_assessment_report/generate", nil, nil, gin.Params{
{Key: "id", Value: "invalid"},
{Key: "section", Value: "risk_assessment_report"},
})
handler.GenerateSingleSection(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
resp := parseResponse(w)
errMsg, _ := resp["error"].(string)
if errMsg != "invalid project ID" {
t.Errorf("Expected 'invalid project ID' error, got %q", errMsg)
}
}
func TestGenerateSingleSection_EmptySectionType_Returns400(t *testing.T) {
handler := &IACEHandler{}
w, c := newTestContext("POST", "/projects/00000000-0000-0000-0000-000000000001/tech-file//generate", nil, nil, gin.Params{
{Key: "id", Value: "00000000-0000-0000-0000-000000000001"},
{Key: "section", Value: ""},
})
handler.GenerateSingleSection(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
resp := parseResponse(w)
errMsg, _ := resp["error"].(string)
if errMsg != "section type required" {
t.Errorf("Expected 'section type required' error, got %q", errMsg)
}
}
func TestGenerateSingleSection_SectionTitleMapping(t *testing.T) {
// Verify the section title map covers all 19 section types
sectionTitles := map[string]string{
"general_description": "General Description of the Machinery",
"risk_assessment_report": "Risk Assessment Report",
"hazard_log_combined": "Combined Hazard Log",
"essential_requirements": "Essential Health and Safety Requirements",
"design_specifications": "Design Specifications and Drawings",
"test_reports": "Test Reports and Verification Results",
"standards_applied": "Applied Harmonised Standards",
"declaration_of_conformity": "EU Declaration of Conformity",
"component_list": "Component List",
"classification_report": "Regulatory Classification Report",
"mitigation_report": "Mitigation Measures Report",
"verification_report": "Verification Report",
"evidence_index": "Evidence Index",
"instructions_for_use": "Instructions for Use",
"monitoring_plan": "Post-Market Monitoring Plan",
"ai_intended_purpose": "AI System Intended Purpose",
"ai_model_description": "AI Model Description and Training Data",
"ai_risk_management": "AI Risk Management System",
"ai_human_oversight": "AI Human Oversight Measures",
}
if len(sectionTitles) != 19 {
t.Errorf("Expected 19 section types, got %d", len(sectionTitles))
}
for key, title := range sectionTitles {
if key == "" {
t.Error("Section key must not be empty")
}
if title == "" {
t.Errorf("Section title for %q must not be empty", key)
}
}
}
// ============================================================================
// ExportTechFile Tests
// ============================================================================
func TestExportTechFile_InvalidProjectID_Returns400(t *testing.T) {
handler := &IACEHandler{}
w, c := newTestContext("GET", "/projects/invalid/tech-file/export", nil, nil, gin.Params{
{Key: "id", Value: "invalid"},
})
handler.ExportTechFile(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestExportTechFile_FormatQueryParam(t *testing.T) {
// Verify that DefaultQuery correctly parses format parameter
tests := []struct {
name string
queryString string
expectedQuery string
}{
{"default json", "", "json"},
{"explicit pdf", "format=pdf", "pdf"},
{"explicit xlsx", "format=xlsx", "xlsx"},
{"explicit docx", "format=docx", "docx"},
{"explicit md", "format=md", "md"},
{"explicit json", "format=json", "json"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
url := "/projects/test/tech-file/export"
if tt.queryString != "" {
url += "?" + tt.queryString
}
c.Request, _ = http.NewRequest("GET", url, nil)
result := c.DefaultQuery("format", "json")
if result != tt.expectedQuery {
t.Errorf("Expected format %q, got %q", tt.expectedQuery, result)
}
})
}
}
func TestExportTechFile_ContentDispositionHeaders(t *testing.T) {
// Verify the header format for different export types
tests := []struct {
format string
contentType string
}{
{"pdf", "application/pdf"},
{"xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
{"docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
{"md", "text/markdown"},
}
for _, tt := range tests {
t.Run(tt.format, func(t *testing.T) {
if tt.contentType == "" {
t.Errorf("Content-Type for format %q must not be empty", tt.format)
}
})
}
}
// ============================================================================
// CheckCompleteness Tests
// ============================================================================
func TestCheckCompleteness_InvalidProjectID_Returns400(t *testing.T) {
handler := &IACEHandler{}
w, c := newTestContext("POST", "/projects/invalid/completeness-check", nil, nil, gin.Params{
{Key: "id", Value: "not-valid-uuid"},
})
handler.CheckCompleteness(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
// ============================================================================
// getTenantID Tests
// ============================================================================
func TestGetTenantID_MissingHeader_ReturnsError(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/test", nil)
_, err := getTenantID(c)
if err == nil {
t.Error("Expected error for missing X-Tenant-Id header")
}
}
func TestGetTenantID_InvalidUUID_ReturnsError(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/test", nil)
c.Request.Header.Set("X-Tenant-Id", "not-a-uuid")
_, err := getTenantID(c)
if err == nil {
t.Error("Expected error for invalid UUID in X-Tenant-Id header")
}
}
func TestGetTenantID_ValidUUID_ReturnsUUID(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/test", nil)
c.Request.Header.Set("X-Tenant-Id", "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e")
tid, err := getTenantID(c)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if tid.String() != "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e" {
t.Errorf("Expected tenant ID '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e', got %q", tid.String())
}
}
// ============================================================================
// CreateProject / ListProjects — Input Validation
// ============================================================================
func TestCreateProject_MissingTenantID_Returns400(t *testing.T) {
handler := &IACEHandler{}
w, c := newTestContext("POST", "/projects", map[string]string{"machine_name": "Test"}, nil, nil)
handler.CreateProject(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestListProjects_MissingTenantID_Returns400(t *testing.T) {
handler := &IACEHandler{}
w, c := newTestContext("GET", "/projects", nil, nil, nil)
handler.ListProjects(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
// ============================================================================
// Component Handlers — Input Validation
// ============================================================================
func TestCreateComponent_InvalidProjectID_Returns400(t *testing.T) {
handler := &IACEHandler{}
w, c := newTestContext("POST", "/projects/invalid/components", map[string]string{"name": "Test"}, nil, gin.Params{
{Key: "id", Value: "invalid"},
})
handler.CreateComponent(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestListComponents_InvalidProjectID_Returns400(t *testing.T) {
handler := &IACEHandler{}
w, c := newTestContext("GET", "/projects/invalid/components", nil, nil, gin.Params{
{Key: "id", Value: "invalid"},
})
handler.ListComponents(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestUpdateComponent_InvalidProjectID_Returns400(t *testing.T) {
handler := &IACEHandler{}
w, c := newTestContext("PUT", "/projects/invalid/components/abc", map[string]string{"name": "New"}, nil, gin.Params{
{Key: "id", Value: "invalid"},
{Key: "cid", Value: "00000000-0000-0000-0000-000000000001"},
})
handler.UpdateComponent(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestUpdateComponent_InvalidComponentID_Returns400(t *testing.T) {
handler := &IACEHandler{}
w, c := newTestContext("PUT", "/projects/00000000-0000-0000-0000-000000000001/components/invalid", nil, nil, gin.Params{
{Key: "id", Value: "00000000-0000-0000-0000-000000000001"},
{Key: "cid", Value: "invalid"},
})
handler.UpdateComponent(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestDeleteComponent_InvalidProjectID_Returns400(t *testing.T) {
handler := &IACEHandler{}
w, c := newTestContext("DELETE", "/projects/invalid/components/abc", nil, nil, gin.Params{
{Key: "id", Value: "invalid"},
{Key: "cid", Value: "00000000-0000-0000-0000-000000000001"},
})
handler.DeleteComponent(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestDeleteComponent_InvalidComponentID_Returns400(t *testing.T) {
handler := &IACEHandler{}
w, c := newTestContext("DELETE", "/projects/00000000-0000-0000-0000-000000000001/components/invalid", nil, nil, gin.Params{
{Key: "id", Value: "00000000-0000-0000-0000-000000000001"},
{Key: "cid", Value: "invalid"},
})
handler.DeleteComponent(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}

View File

@@ -2,6 +2,7 @@ package handlers
import (
"net/http"
"strconv"
"github.com/breakpilot/ai-compliance-sdk/internal/ucca"
"github.com/gin-gonic/gin"
@@ -24,7 +25,6 @@ func NewRAGHandlers(corpusVersionStore *ucca.CorpusVersionStore) *RAGHandlers {
// AllowedCollections is the whitelist of Qdrant collections that can be queried.
var AllowedCollections = map[string]bool{
"bp_compliance_ce": true,
"bp_compliance_recht": true,
"bp_compliance_gesetze": true,
"bp_compliance_datenschutz": true,
"bp_compliance_gdpr": true,
@@ -32,6 +32,7 @@ var AllowedCollections = map[string]bool{
"bp_dsfa_templates": true,
"bp_dsfa_risks": true,
"bp_legal_templates": true,
"bp_iace_libraries": true,
}
// SearchRequest represents a RAG search request.
@@ -157,3 +158,47 @@ func (h *RAGHandlers) CorpusVersionHistory(c *gin.Context) {
"count": len(versions),
})
}
// HandleScrollChunks scrolls/lists all chunks in a Qdrant collection with pagination.
// GET /sdk/v1/rag/scroll?collection=...&offset=...&limit=...
func (h *RAGHandlers) HandleScrollChunks(c *gin.Context) {
collection := c.Query("collection")
if collection == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter 'collection' is required"})
return
}
if !AllowedCollections[collection] {
c.JSON(http.StatusBadRequest, gin.H{"error": "Unknown collection: " + collection})
return
}
// Parse limit (default 100, max 500)
limit := 100
if limitStr := c.Query("limit"); limitStr != "" {
parsed, err := strconv.Atoi(limitStr)
if err != nil || parsed < 1 {
c.JSON(http.StatusBadRequest, gin.H{"error": "limit must be a positive integer"})
return
}
limit = parsed
}
if limit > 500 {
limit = 500
}
// Offset is optional (empty string = start from beginning)
offset := c.Query("offset")
chunks, nextOffset, err := h.ragClient.ScrollChunks(c.Request.Context(), collection, offset, limit)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "scroll failed: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"chunks": chunks,
"next_offset": nextOffset,
"total": len(chunks),
})
}

View File

@@ -13,11 +13,14 @@ import (
func TestAllowedCollections(t *testing.T) {
allowed := []string{
"bp_compliance_ce",
"bp_compliance_recht",
"bp_compliance_gesetze",
"bp_compliance_datenschutz",
"bp_compliance_gdpr",
"bp_dsfa_corpus",
"bp_dsfa_templates",
"bp_dsfa_risks",
"bp_legal_templates",
"bp_iace_libraries",
}
for _, c := range allowed {
@@ -91,6 +94,72 @@ func TestSearch_WithCollectionParam_BindsCorrectly(t *testing.T) {
}
}
func TestHandleScrollChunks_MissingCollection_Returns400(t *testing.T) {
gin.SetMode(gin.TestMode)
handler := &RAGHandlers{}
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/sdk/v1/rag/scroll", nil)
handler.HandleScrollChunks(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
if resp["error"] == nil {
t.Error("Expected error message in response")
}
}
func TestHandleScrollChunks_InvalidCollection_Returns400(t *testing.T) {
gin.SetMode(gin.TestMode)
handler := &RAGHandlers{}
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/sdk/v1/rag/scroll?collection=bp_evil_collection", nil)
handler.HandleScrollChunks(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestHandleScrollChunks_InvalidLimit_Returns400(t *testing.T) {
gin.SetMode(gin.TestMode)
handler := &RAGHandlers{}
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/sdk/v1/rag/scroll?collection=bp_compliance_ce&limit=abc", nil)
handler.HandleScrollChunks(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestHandleScrollChunks_NegativeLimit_Returns400(t *testing.T) {
gin.SetMode(gin.TestMode)
handler := &RAGHandlers{}
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/sdk/v1/rag/scroll?collection=bp_compliance_ce&limit=-5", nil)
handler.HandleScrollChunks(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestSearch_EmptyCollection_IsAllowed(t *testing.T) {
// Empty collection should be allowed (falls back to default in the handler)
body := `{"query":"test"}`

View File

@@ -0,0 +1,220 @@
package handlers
import (
"net/http"
"github.com/breakpilot/ai-compliance-sdk/internal/ucca"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// RegistrationHandlers handles EU AI Database registration endpoints
type RegistrationHandlers struct {
store *ucca.RegistrationStore
uccaStore *ucca.Store
}
// NewRegistrationHandlers creates new registration handlers
func NewRegistrationHandlers(store *ucca.RegistrationStore, uccaStore *ucca.Store) *RegistrationHandlers {
return &RegistrationHandlers{store: store, uccaStore: uccaStore}
}
// Create creates a new registration
func (h *RegistrationHandlers) Create(c *gin.Context) {
var reg ucca.AIRegistration
if err := c.ShouldBindJSON(&reg); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
return
}
tenantID, _ := uuid.Parse(c.GetHeader("X-Tenant-ID"))
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
reg.TenantID = tenantID
if reg.SystemName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "system_name required"})
return
}
if err := h.store.Create(c.Request.Context(), &reg); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create registration: " + err.Error()})
return
}
c.JSON(http.StatusCreated, reg)
}
// List lists all registrations for the tenant
func (h *RegistrationHandlers) List(c *gin.Context) {
tenantID, _ := uuid.Parse(c.GetHeader("X-Tenant-ID"))
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
registrations, err := h.store.List(c.Request.Context(), tenantID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list registrations: " + err.Error()})
return
}
if registrations == nil {
registrations = []ucca.AIRegistration{}
}
c.JSON(http.StatusOK, gin.H{"registrations": registrations, "total": len(registrations)})
}
// Get returns a single registration
func (h *RegistrationHandlers) Get(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
}
reg, err := h.store.GetByID(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Registration not found"})
return
}
c.JSON(http.StatusOK, reg)
}
// Update updates a registration
func (h *RegistrationHandlers) Update(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
}
existing, err := h.store.GetByID(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Registration not found"})
return
}
var updates ucca.AIRegistration
if err := c.ShouldBindJSON(&updates); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
return
}
// Merge updates into existing
updates.ID = existing.ID
updates.TenantID = existing.TenantID
updates.CreatedAt = existing.CreatedAt
if err := h.store.Update(c.Request.Context(), &updates); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update: " + err.Error()})
return
}
c.JSON(http.StatusOK, updates)
}
// UpdateStatus changes the registration status
func (h *RegistrationHandlers) UpdateStatus(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
}
var body struct {
Status string `json:"status"`
SubmittedBy string `json:"submitted_by"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
validStatuses := map[string]bool{
"draft": true, "ready": true, "submitted": true,
"registered": true, "update_required": true, "withdrawn": true,
}
if !validStatuses[body.Status] {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid status. Valid: draft, ready, submitted, registered, update_required, withdrawn"})
return
}
if err := h.store.UpdateStatus(c.Request.Context(), id, body.Status, body.SubmittedBy); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update status: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"id": id, "status": body.Status})
}
// Prefill creates a registration pre-filled from a UCCA assessment
func (h *RegistrationHandlers) Prefill(c *gin.Context) {
assessmentID, err := uuid.Parse(c.Param("assessment_id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid assessment ID"})
return
}
tenantID, _ := uuid.Parse(c.GetHeader("X-Tenant-ID"))
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
// Load UCCA assessment
assessment, err := h.uccaStore.GetAssessment(c.Request.Context(), assessmentID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Assessment not found"})
return
}
// Pre-fill registration from assessment intake
intake := assessment.Intake
reg := ucca.AIRegistration{
TenantID: tenantID,
SystemName: intake.Title,
SystemDescription: intake.UseCaseText,
IntendedPurpose: intake.UseCaseText,
RiskClassification: string(assessment.RiskLevel),
GPAIClassification: "none",
RegistrationStatus: "draft",
UCCAAssessmentID: &assessmentID,
}
// Map domain to readable text
if intake.Domain != "" {
reg.IntendedPurpose = string(intake.Domain) + ": " + intake.UseCaseText
}
c.JSON(http.StatusOK, reg)
}
// Export generates the EU AI Database submission JSON
func (h *RegistrationHandlers) Export(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
}
reg, err := h.store.GetByID(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Registration not found"})
return
}
exportJSON := h.store.BuildExportJSON(reg)
// Save export data to DB
reg.ExportData = exportJSON
h.store.Update(c.Request.Context(), reg)
c.Header("Content-Type", "application/json")
c.Header("Content-Disposition", "attachment; filename=eu_ai_registration_"+reg.SystemName+".json")
c.Data(http.StatusOK, "application/json", exportJSON)
}

View File

@@ -3,7 +3,9 @@ package handlers
import (
"net/http"
"strconv"
"time"
"github.com/breakpilot/ai-compliance-sdk/internal/academy"
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
"github.com/breakpilot/ai-compliance-sdk/internal/training"
"github.com/gin-gonic/gin"
@@ -14,13 +16,17 @@ import (
type TrainingHandlers struct {
store *training.Store
contentGenerator *training.ContentGenerator
blockGenerator *training.BlockGenerator
ttsClient *training.TTSClient
}
// NewTrainingHandlers creates new training handlers
func NewTrainingHandlers(store *training.Store, contentGenerator *training.ContentGenerator) *TrainingHandlers {
func NewTrainingHandlers(store *training.Store, contentGenerator *training.ContentGenerator, blockGenerator *training.BlockGenerator, ttsClient *training.TTSClient) *TrainingHandlers {
return &TrainingHandlers{
store: store,
contentGenerator: contentGenerator,
blockGenerator: blockGenerator,
ttsClient: ttsClient,
}
}
@@ -212,6 +218,33 @@ func (h *TrainingHandlers) UpdateModule(c *gin.Context) {
c.JSON(http.StatusOK, module)
}
// DeleteModule deletes a training module
// DELETE /sdk/v1/training/modules/:id
func (h *TrainingHandlers) DeleteModule(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid module ID"})
return
}
module, err := h.store.GetModule(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if module == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "module not found"})
return
}
if err := h.store.DeleteModule(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "deleted"})
}
// ============================================================================
// Matrix Endpoints
// ============================================================================
@@ -459,6 +492,48 @@ func (h *TrainingHandlers) UpdateAssignmentProgress(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": string(status), "progress": req.Progress})
}
// UpdateAssignment updates assignment fields (e.g. deadline)
// PUT /sdk/v1/training/assignments/:id
func (h *TrainingHandlers) UpdateAssignment(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"})
return
}
var req struct {
Deadline *string `json:"deadline"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
if req.Deadline != nil {
deadline, err := time.Parse(time.RFC3339, *req.Deadline)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid deadline format (use RFC3339)"})
return
}
if err := h.store.UpdateAssignmentDeadline(c.Request.Context(), id, deadline); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
assignment, err := h.store.GetAssignment(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if assignment == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "assignment not found"})
return
}
c.JSON(http.StatusOK, assignment)
}
// CompleteAssignment marks an assignment as completed
// POST /sdk/v1/training/assignments/:id/complete
func (h *TrainingHandlers) CompleteAssignment(c *gin.Context) {
@@ -1111,3 +1186,679 @@ func (h *TrainingHandlers) PreviewVideoScript(c *gin.Context) {
c.JSON(http.StatusOK, script)
}
// ============================================================================
// Training Block Endpoints (Controls → Schulungsmodule)
// ============================================================================
// ListBlockConfigs returns all block configs for the tenant
// GET /sdk/v1/training/blocks
func (h *TrainingHandlers) ListBlockConfigs(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
configs, err := h.store.ListBlockConfigs(c.Request.Context(), tenantID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"blocks": configs,
"total": len(configs),
})
}
// CreateBlockConfig creates a new block configuration
// POST /sdk/v1/training/blocks
func (h *TrainingHandlers) CreateBlockConfig(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
var req training.CreateBlockConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
config := &training.TrainingBlockConfig{
TenantID: tenantID,
Name: req.Name,
Description: req.Description,
DomainFilter: req.DomainFilter,
CategoryFilter: req.CategoryFilter,
SeverityFilter: req.SeverityFilter,
TargetAudienceFilter: req.TargetAudienceFilter,
RegulationArea: req.RegulationArea,
ModuleCodePrefix: req.ModuleCodePrefix,
FrequencyType: req.FrequencyType,
DurationMinutes: req.DurationMinutes,
PassThreshold: req.PassThreshold,
MaxControlsPerModule: req.MaxControlsPerModule,
}
if config.FrequencyType == "" {
config.FrequencyType = training.FrequencyAnnual
}
if config.DurationMinutes == 0 {
config.DurationMinutes = 45
}
if config.PassThreshold == 0 {
config.PassThreshold = 70
}
if config.MaxControlsPerModule == 0 {
config.MaxControlsPerModule = 20
}
if err := h.store.CreateBlockConfig(c.Request.Context(), config); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, config)
}
// GetBlockConfig returns a single block config
// GET /sdk/v1/training/blocks/:id
func (h *TrainingHandlers) GetBlockConfig(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid block config ID"})
return
}
config, err := h.store.GetBlockConfig(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if config == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "block config not found"})
return
}
c.JSON(http.StatusOK, config)
}
// UpdateBlockConfig updates a block config
// PUT /sdk/v1/training/blocks/:id
func (h *TrainingHandlers) UpdateBlockConfig(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid block config ID"})
return
}
config, err := h.store.GetBlockConfig(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if config == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "block config not found"})
return
}
var req training.UpdateBlockConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.Name != nil {
config.Name = *req.Name
}
if req.Description != nil {
config.Description = *req.Description
}
if req.DomainFilter != nil {
config.DomainFilter = *req.DomainFilter
}
if req.CategoryFilter != nil {
config.CategoryFilter = *req.CategoryFilter
}
if req.SeverityFilter != nil {
config.SeverityFilter = *req.SeverityFilter
}
if req.TargetAudienceFilter != nil {
config.TargetAudienceFilter = *req.TargetAudienceFilter
}
if req.MaxControlsPerModule != nil {
config.MaxControlsPerModule = *req.MaxControlsPerModule
}
if req.DurationMinutes != nil {
config.DurationMinutes = *req.DurationMinutes
}
if req.PassThreshold != nil {
config.PassThreshold = *req.PassThreshold
}
if req.IsActive != nil {
config.IsActive = *req.IsActive
}
if err := h.store.UpdateBlockConfig(c.Request.Context(), config); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, config)
}
// DeleteBlockConfig deletes a block config
// DELETE /sdk/v1/training/blocks/:id
func (h *TrainingHandlers) DeleteBlockConfig(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid block config ID"})
return
}
if err := h.store.DeleteBlockConfig(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "deleted"})
}
// PreviewBlock performs a dry run showing matching controls and proposed roles
// POST /sdk/v1/training/blocks/:id/preview
func (h *TrainingHandlers) PreviewBlock(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid block config ID"})
return
}
preview, err := h.blockGenerator.Preview(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, preview)
}
// GenerateBlock runs the full generation pipeline
// POST /sdk/v1/training/blocks/:id/generate
func (h *TrainingHandlers) GenerateBlock(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid block config ID"})
return
}
var req training.GenerateBlockRequest
if err := c.ShouldBindJSON(&req); err != nil {
// Defaults are fine
req.Language = "de"
req.AutoMatrix = true
}
result, err := h.blockGenerator.Generate(c.Request.Context(), id, req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
}
// GetBlockControls returns control links for a block config
// GET /sdk/v1/training/blocks/:id/controls
func (h *TrainingHandlers) GetBlockControls(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid block config ID"})
return
}
links, err := h.store.GetControlLinksForBlock(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"controls": links,
"total": len(links),
})
}
// ListCanonicalControls returns filtered canonical controls for browsing
// GET /sdk/v1/training/canonical/controls
func (h *TrainingHandlers) ListCanonicalControls(c *gin.Context) {
domain := c.Query("domain")
category := c.Query("category")
severity := c.Query("severity")
targetAudience := c.Query("target_audience")
controls, err := h.store.QueryCanonicalControls(c.Request.Context(),
domain, category, severity, targetAudience,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"controls": controls,
"total": len(controls),
})
}
// GetCanonicalMeta returns aggregated metadata about canonical controls
// GET /sdk/v1/training/canonical/meta
func (h *TrainingHandlers) GetCanonicalMeta(c *gin.Context) {
meta, err := h.store.GetCanonicalControlMeta(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, meta)
}
// ============================================================================
// Media Streaming Endpoint
// ============================================================================
// StreamMedia returns a redirect to a presigned URL for a media file
// GET /sdk/v1/training/media/:mediaId/stream
func (h *TrainingHandlers) StreamMedia(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid media ID"})
return
}
media, err := h.store.GetMedia(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if media == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "media not found"})
return
}
if h.ttsClient == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "media streaming not available"})
return
}
url, err := h.ttsClient.GetPresignedURL(c.Request.Context(), media.Bucket, media.ObjectKey)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate streaming URL: " + err.Error()})
return
}
c.Redirect(http.StatusTemporaryRedirect, url)
}
// ============================================================================
// Certificate Endpoints
// ============================================================================
// GenerateCertificate generates a certificate for a completed assignment
// POST /sdk/v1/training/certificates/generate/:assignmentId
func (h *TrainingHandlers) GenerateCertificate(c *gin.Context) {
assignmentID, err := uuid.Parse(c.Param("assignmentId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"})
return
}
tenantID := rbac.GetTenantID(c)
assignment, err := h.store.GetAssignment(c.Request.Context(), assignmentID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if assignment == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "assignment not found"})
return
}
if assignment.Status != training.AssignmentStatusCompleted {
c.JSON(http.StatusBadRequest, gin.H{"error": "assignment is not completed"})
return
}
if assignment.QuizPassed == nil || !*assignment.QuizPassed {
c.JSON(http.StatusBadRequest, gin.H{"error": "quiz has not been passed"})
return
}
// Generate certificate ID
certID := uuid.New()
if err := h.store.SetCertificateID(c.Request.Context(), assignmentID, certID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Audit log
userID := rbac.GetUserID(c)
h.store.LogAction(c.Request.Context(), &training.AuditLogEntry{
TenantID: tenantID,
UserID: &userID,
Action: training.AuditActionCertificateIssued,
EntityType: training.AuditEntityCertificate,
EntityID: &certID,
Details: map[string]interface{}{
"assignment_id": assignmentID.String(),
"user_name": assignment.UserName,
"module_title": assignment.ModuleTitle,
},
})
// Reload assignment with certificate_id
assignment, _ = h.store.GetAssignment(c.Request.Context(), assignmentID)
c.JSON(http.StatusOK, gin.H{
"certificate_id": certID,
"assignment": assignment,
})
}
// DownloadCertificatePDF generates and returns a PDF certificate
// GET /sdk/v1/training/certificates/:id/pdf
func (h *TrainingHandlers) DownloadCertificatePDF(c *gin.Context) {
certID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid certificate ID"})
return
}
assignment, err := h.store.GetAssignmentByCertificateID(c.Request.Context(), certID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if assignment == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"})
return
}
// Get module for title
module, _ := h.store.GetModule(c.Request.Context(), assignment.ModuleID)
courseName := assignment.ModuleTitle
if module != nil {
courseName = module.Title
}
score := 0
if assignment.QuizScore != nil {
score = int(*assignment.QuizScore)
}
issuedAt := assignment.UpdatedAt
if assignment.CompletedAt != nil {
issuedAt = *assignment.CompletedAt
}
// Use academy PDF generator
pdfBytes, err := academy.GenerateCertificatePDF(academy.CertificateData{
CertificateID: certID.String(),
UserName: assignment.UserName,
CourseName: courseName,
Score: score,
IssuedAt: issuedAt,
ValidUntil: issuedAt.AddDate(1, 0, 0),
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "PDF generation failed: " + err.Error()})
return
}
c.Header("Content-Disposition", "attachment; filename=zertifikat-"+certID.String()[:8]+".pdf")
c.Data(http.StatusOK, "application/pdf", pdfBytes)
}
// ListCertificates returns all certificates for a tenant
// GET /sdk/v1/training/certificates
func (h *TrainingHandlers) ListCertificates(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
certificates, err := h.store.ListCertificates(c.Request.Context(), tenantID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"certificates": certificates,
"total": len(certificates),
})
}
// ============================================================================
// Interactive Video Endpoints
// ============================================================================
// GenerateInteractiveVideo triggers the full interactive video pipeline
// POST /sdk/v1/training/content/:moduleId/generate-interactive
func (h *TrainingHandlers) GenerateInteractiveVideo(c *gin.Context) {
moduleID, err := uuid.Parse(c.Param("moduleId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid module ID"})
return
}
module, err := h.store.GetModule(c.Request.Context(), moduleID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if module == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "module not found"})
return
}
media, err := h.contentGenerator.GenerateInteractiveVideo(c.Request.Context(), *module)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, media)
}
// GetInteractiveManifest returns the interactive video manifest with checkpoints and progress
// GET /sdk/v1/training/content/:moduleId/interactive-manifest
func (h *TrainingHandlers) GetInteractiveManifest(c *gin.Context) {
moduleID, err := uuid.Parse(c.Param("moduleId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid module ID"})
return
}
// Get interactive video media
mediaList, err := h.store.GetMediaForModule(c.Request.Context(), moduleID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Find interactive video
var interactiveMedia *training.TrainingMedia
for i := range mediaList {
if mediaList[i].MediaType == training.MediaTypeInteractiveVideo && mediaList[i].Status == training.MediaStatusCompleted {
interactiveMedia = &mediaList[i]
break
}
}
if interactiveMedia == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "no interactive video found for this module"})
return
}
// Get checkpoints
checkpoints, err := h.store.ListCheckpoints(c.Request.Context(), moduleID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Optional: get assignment ID for progress
assignmentIDStr := c.Query("assignment_id")
// Build manifest entries
entries := make([]training.CheckpointManifestEntry, len(checkpoints))
for i, cp := range checkpoints {
// Get questions for this checkpoint
questions, _ := h.store.GetCheckpointQuestions(c.Request.Context(), cp.ID)
cpQuestions := make([]training.CheckpointQuestion, len(questions))
for j, q := range questions {
cpQuestions[j] = training.CheckpointQuestion{
Question: q.Question,
Options: q.Options,
CorrectIndex: q.CorrectIndex,
Explanation: q.Explanation,
}
}
entry := training.CheckpointManifestEntry{
CheckpointID: cp.ID,
Index: cp.CheckpointIndex,
Title: cp.Title,
TimestampSeconds: cp.TimestampSeconds,
Questions: cpQuestions,
}
// Get progress if assignment_id provided
if assignmentIDStr != "" {
if assignmentID, err := uuid.Parse(assignmentIDStr); err == nil {
progress, _ := h.store.GetCheckpointProgress(c.Request.Context(), assignmentID, cp.ID)
entry.Progress = progress
}
}
entries[i] = entry
}
// Get stream URL
streamURL := ""
if h.ttsClient != nil {
url, err := h.ttsClient.GetPresignedURL(c.Request.Context(), interactiveMedia.Bucket, interactiveMedia.ObjectKey)
if err == nil {
streamURL = url
}
}
manifest := training.InteractiveVideoManifest{
MediaID: interactiveMedia.ID,
StreamURL: streamURL,
Checkpoints: entries,
}
c.JSON(http.StatusOK, manifest)
}
// SubmitCheckpointQuiz handles checkpoint quiz submission
// POST /sdk/v1/training/checkpoints/:checkpointId/submit
func (h *TrainingHandlers) SubmitCheckpointQuiz(c *gin.Context) {
checkpointID, err := uuid.Parse(c.Param("checkpointId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid checkpoint ID"})
return
}
var req training.SubmitCheckpointQuizRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
assignmentID, err := uuid.Parse(req.AssignmentID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"})
return
}
// Get checkpoint questions
questions, err := h.store.GetCheckpointQuestions(c.Request.Context(), checkpointID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if len(questions) == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "no questions found for this checkpoint"})
return
}
// Grade answers
correctCount := 0
feedback := make([]training.CheckpointQuizFeedback, len(questions))
for i, q := range questions {
isCorrect := false
if i < len(req.Answers) && req.Answers[i] == q.CorrectIndex {
isCorrect = true
correctCount++
}
feedback[i] = training.CheckpointQuizFeedback{
Question: q.Question,
Correct: isCorrect,
Explanation: q.Explanation,
}
}
score := float64(correctCount) / float64(len(questions)) * 100
passed := score >= 70 // 70% threshold for checkpoint
// Update progress
progress := &training.CheckpointProgress{
AssignmentID: assignmentID,
CheckpointID: checkpointID,
Passed: passed,
Attempts: 1,
}
if err := h.store.UpsertCheckpointProgress(c.Request.Context(), progress); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Audit log
userID := rbac.GetUserID(c)
h.store.LogAction(c.Request.Context(), &training.AuditLogEntry{
TenantID: rbac.GetTenantID(c),
UserID: &userID,
Action: training.AuditAction("checkpoint_submitted"),
EntityType: training.AuditEntityType("checkpoint"),
EntityID: &checkpointID,
Details: map[string]interface{}{
"assignment_id": assignmentID.String(),
"score": score,
"passed": passed,
"correct": correctCount,
"total": len(questions),
},
})
c.JSON(http.StatusOK, training.SubmitCheckpointQuizResponse{
Passed: passed,
Score: score,
Feedback: feedback,
})
}
// GetCheckpointProgress returns all checkpoint progress for an assignment
// GET /sdk/v1/training/checkpoints/progress/:assignmentId
func (h *TrainingHandlers) GetCheckpointProgress(c *gin.Context) {
assignmentID, err := uuid.Parse(c.Param("assignmentId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid assignment ID"})
return
}
progress, err := h.store.ListCheckpointProgress(c.Request.Context(), assignmentID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"progress": progress,
"total": len(progress),
})
}

View File

@@ -0,0 +1,691 @@
package handlers
import (
"net/http"
"testing"
"github.com/gin-gonic/gin"
)
// newTestContext, parseResponse, and gin.SetMode are defined in iace_handler_test.go
// ============================================================================
// Module Endpoint Tests
// ============================================================================
func TestGetModule_InvalidID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("GET", "/modules/not-a-uuid", nil, nil, gin.Params{{Key: "id", Value: "not-a-uuid"}})
h.GetModule(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestGetModule_EmptyID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("GET", "/modules/", nil, nil, gin.Params{{Key: "id", Value: ""}})
h.GetModule(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestCreateModule_EmptyBody_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("POST", "/modules", nil, nil, nil)
h.CreateModule(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestCreateModule_MissingTitle_Returns400(t *testing.T) {
h := &TrainingHandlers{}
body := map[string]interface{}{"module_code": "T01", "regulation_area": "dsgvo", "frequency_type": "annual"}
w, c := newTestContext("POST", "/modules", body, nil, nil)
h.CreateModule(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestCreateModule_MissingModuleCode_Returns400(t *testing.T) {
h := &TrainingHandlers{}
body := map[string]interface{}{"title": "Test", "regulation_area": "dsgvo", "frequency_type": "annual"}
w, c := newTestContext("POST", "/modules", body, nil, nil)
h.CreateModule(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestCreateModule_MissingRegulationArea_Returns400(t *testing.T) {
h := &TrainingHandlers{}
body := map[string]interface{}{"module_code": "T01", "title": "Test", "frequency_type": "annual"}
w, c := newTestContext("POST", "/modules", body, nil, nil)
h.CreateModule(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestUpdateModule_InvalidID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("PUT", "/modules/bad", map[string]interface{}{"title": "x"}, nil, gin.Params{{Key: "id", Value: "bad"}})
h.UpdateModule(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestDeleteModule_InvalidID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("DELETE", "/modules/bad", nil, nil, gin.Params{{Key: "id", Value: "bad"}})
h.DeleteModule(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestDeleteModule_EmptyID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("DELETE", "/modules/", nil, nil, gin.Params{{Key: "id", Value: ""}})
h.DeleteModule(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
// ============================================================================
// Matrix Endpoint Tests
// ============================================================================
func TestSetMatrixEntry_EmptyBody_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("POST", "/matrix", nil, nil, nil)
h.SetMatrixEntry(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestSetMatrixEntry_MissingRoleCode_Returns400(t *testing.T) {
h := &TrainingHandlers{}
body := map[string]interface{}{"module_id": "00000000-0000-0000-0000-000000000001"}
w, c := newTestContext("POST", "/matrix", body, nil, nil)
h.SetMatrixEntry(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestDeleteMatrixEntry_InvalidModuleID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("DELETE", "/matrix/R1/bad", nil, nil, gin.Params{
{Key: "role", Value: "R1"},
{Key: "moduleId", Value: "bad"},
})
h.DeleteMatrixEntry(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
// ============================================================================
// Assignment Endpoint Tests
// ============================================================================
func TestGetAssignment_InvalidID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("GET", "/assignments/bad", nil, nil, gin.Params{{Key: "id", Value: "bad"}})
h.GetAssignment(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestStartAssignment_InvalidID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("POST", "/assignments/bad/start", nil, nil, gin.Params{{Key: "id", Value: "bad"}})
h.StartAssignment(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestUpdateAssignmentProgress_InvalidID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("POST", "/assignments/bad/progress", map[string]interface{}{"progress": 50}, nil, gin.Params{{Key: "id", Value: "bad"}})
h.UpdateAssignmentProgress(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestUpdateAssignmentProgress_EmptyBody_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("POST", "/assignments/00000000-0000-0000-0000-000000000001/progress", nil, nil,
gin.Params{{Key: "id", Value: "00000000-0000-0000-0000-000000000001"}})
h.UpdateAssignmentProgress(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestCompleteAssignment_InvalidID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("POST", "/assignments/bad/complete", nil, nil, gin.Params{{Key: "id", Value: "bad"}})
h.CompleteAssignment(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestComputeAssignments_EmptyBody_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("POST", "/assignments/compute", nil, nil, nil)
h.ComputeAssignments(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestComputeAssignments_MissingUserID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
body := map[string]interface{}{"user_name": "Test", "user_email": "test@test.de", "roles": []string{"R1"}}
w, c := newTestContext("POST", "/assignments/compute", body, nil, nil)
h.ComputeAssignments(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
// ============================================================================
// Quiz Endpoint Tests
// ============================================================================
func TestGetQuiz_InvalidModuleID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("GET", "/quiz/bad", nil, nil, gin.Params{{Key: "moduleId", Value: "bad"}})
h.GetQuiz(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestSubmitQuiz_InvalidModuleID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("POST", "/quiz/bad/submit", map[string]interface{}{}, nil, gin.Params{{Key: "moduleId", Value: "bad"}})
h.SubmitQuiz(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestSubmitQuiz_EmptyBody_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("POST", "/quiz/00000000-0000-0000-0000-000000000001/submit", nil, nil,
gin.Params{{Key: "moduleId", Value: "00000000-0000-0000-0000-000000000001"}})
h.SubmitQuiz(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestGetQuizAttempts_InvalidAssignmentID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("GET", "/quiz/attempts/bad", nil, nil, gin.Params{{Key: "assignmentId", Value: "bad"}})
h.GetQuizAttempts(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
// ============================================================================
// Content Endpoint Tests
// ============================================================================
func TestGetContent_InvalidModuleID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("GET", "/content/bad", nil, nil, gin.Params{{Key: "moduleId", Value: "bad"}})
h.GetContent(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestPublishContent_InvalidID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("POST", "/content/bad/publish", nil, nil, gin.Params{{Key: "id", Value: "bad"}})
h.PublishContent(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestGenerateContent_EmptyBody_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("POST", "/content/generate", nil, nil, nil)
h.GenerateContent(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestGenerateQuiz_EmptyBody_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("POST", "/content/generate-quiz", nil, nil, nil)
h.GenerateQuiz(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
// ============================================================================
// Media Endpoint Tests
// ============================================================================
func TestGetModuleMedia_InvalidModuleID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("GET", "/media/module/bad", nil, nil, gin.Params{{Key: "moduleId", Value: "bad"}})
h.GetModuleMedia(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestGetMediaURL_InvalidID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("GET", "/media/bad/url", nil, nil, gin.Params{{Key: "id", Value: "bad"}})
h.GetMediaURL(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestPublishMedia_InvalidID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("POST", "/media/bad/publish", nil, nil, gin.Params{{Key: "id", Value: "bad"}})
h.PublishMedia(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestStreamMedia_InvalidID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("GET", "/media/bad/stream", nil, nil, gin.Params{{Key: "id", Value: "bad"}})
h.StreamMedia(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestGenerateAudio_InvalidModuleID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("POST", "/content/bad/generate-audio", nil, nil, gin.Params{{Key: "moduleId", Value: "bad"}})
h.GenerateAudio(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestGenerateVideo_InvalidModuleID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("POST", "/content/bad/generate-video", nil, nil, gin.Params{{Key: "moduleId", Value: "bad"}})
h.GenerateVideo(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestPreviewVideoScript_InvalidModuleID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("POST", "/content/bad/preview-script", nil, nil, gin.Params{{Key: "moduleId", Value: "bad"}})
h.PreviewVideoScript(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
// ============================================================================
// Certificate Endpoint Tests
// ============================================================================
func TestGenerateCertificate_InvalidAssignmentID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("POST", "/certificates/generate/bad", nil, nil, gin.Params{{Key: "assignmentId", Value: "bad"}})
h.GenerateCertificate(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestDownloadCertificatePDF_InvalidID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("GET", "/certificates/bad/pdf", nil, nil, gin.Params{{Key: "id", Value: "bad"}})
h.DownloadCertificatePDF(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestVerifyCertificate_InvalidID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("GET", "/certificates/bad/verify", nil, nil, gin.Params{{Key: "id", Value: "bad"}})
h.VerifyCertificate(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestListCertificates_NilStore_Panics(t *testing.T) {
// This tests that a nil store doesn't silently succeed
defer func() {
if r := recover(); r == nil {
t.Error("Expected panic with nil store")
}
}()
h := &TrainingHandlers{}
_, c := newTestContext("GET", "/certificates", nil, nil, nil)
h.ListCertificates(c)
}
// ============================================================================
// Interactive Video Endpoint Tests (User Journey: Admin generates video)
// ============================================================================
func TestGenerateInteractiveVideo_InvalidModuleID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("POST", "/content/bad/generate-interactive", nil, nil, gin.Params{{Key: "moduleId", Value: "bad"}})
h.GenerateInteractiveVideo(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
resp := parseResponse(w)
if resp["error"] == nil {
t.Error("Response should contain 'error' key")
}
}
func TestGenerateInteractiveVideo_EmptyModuleID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("POST", "/content//generate-interactive", nil, nil, gin.Params{{Key: "moduleId", Value: ""}})
h.GenerateInteractiveVideo(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestGetInteractiveManifest_InvalidModuleID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("GET", "/content/bad/interactive-manifest", nil, nil, gin.Params{{Key: "moduleId", Value: "bad"}})
h.GetInteractiveManifest(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
resp := parseResponse(w)
if resp["error"] == nil {
t.Error("Response should contain 'error' key")
}
}
func TestGetInteractiveManifest_EmptyModuleID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("GET", "/content//interactive-manifest", nil, nil, gin.Params{{Key: "moduleId", Value: ""}})
h.GetInteractiveManifest(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
// ============================================================================
// Checkpoint Quiz Endpoint Tests (User Journey: Learner takes quiz)
// ============================================================================
func TestSubmitCheckpointQuiz_InvalidCheckpointID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
body := map[string]interface{}{
"assignment_id": "00000000-0000-0000-0000-000000000001",
"answers": []int{0, 1, 2},
}
w, c := newTestContext("POST", "/checkpoints/bad/submit", body, nil, gin.Params{{Key: "checkpointId", Value: "bad"}})
h.SubmitCheckpointQuiz(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
resp := parseResponse(w)
if resp["error"] == nil {
t.Error("Response should contain 'error' key")
}
}
func TestSubmitCheckpointQuiz_EmptyCheckpointID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
body := map[string]interface{}{
"assignment_id": "00000000-0000-0000-0000-000000000001",
"answers": []int{0},
}
w, c := newTestContext("POST", "/checkpoints//submit", body, nil, gin.Params{{Key: "checkpointId", Value: ""}})
h.SubmitCheckpointQuiz(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestSubmitCheckpointQuiz_EmptyBody_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("POST", "/checkpoints/00000000-0000-0000-0000-000000000001/submit", nil, nil,
gin.Params{{Key: "checkpointId", Value: "00000000-0000-0000-0000-000000000001"}})
h.SubmitCheckpointQuiz(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestSubmitCheckpointQuiz_InvalidAssignmentID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
body := map[string]interface{}{
"assignment_id": "not-a-uuid",
"answers": []int{0},
}
w, c := newTestContext("POST", "/checkpoints/00000000-0000-0000-0000-000000000001/submit", body, nil,
gin.Params{{Key: "checkpointId", Value: "00000000-0000-0000-0000-000000000001"}})
h.SubmitCheckpointQuiz(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestSubmitCheckpointQuiz_ValidIDs_NilStore_Panics(t *testing.T) {
// When both IDs are valid, handler reaches store → panic with nil store
defer func() {
if r := recover(); r == nil {
t.Error("Expected panic with nil store")
}
}()
h := &TrainingHandlers{}
body := map[string]interface{}{
"assignment_id": "00000000-0000-0000-0000-000000000001",
"answers": []int{0},
}
_, c := newTestContext("POST", "/checkpoints/00000000-0000-0000-0000-000000000001/submit", body, nil,
gin.Params{{Key: "checkpointId", Value: "00000000-0000-0000-0000-000000000001"}})
h.SubmitCheckpointQuiz(c)
}
// ============================================================================
// Checkpoint Progress Endpoint Tests (User Journey: Learner views progress)
// ============================================================================
func TestGetCheckpointProgress_InvalidAssignmentID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("GET", "/checkpoints/progress/bad", nil, nil, gin.Params{{Key: "assignmentId", Value: "bad"}})
h.GetCheckpointProgress(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
resp := parseResponse(w)
if resp["error"] == nil {
t.Error("Response should contain 'error' key")
}
}
func TestGetCheckpointProgress_EmptyAssignmentID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("GET", "/checkpoints/progress/", nil, nil, gin.Params{{Key: "assignmentId", Value: ""}})
h.GetCheckpointProgress(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
// ============================================================================
// Interactive Video Error Format Tests (table-driven)
// ============================================================================
func TestInteractiveEndpoints_InvalidID_ResponseContainsErrorKey(t *testing.T) {
tests := []struct {
name string
method string
handler func(h *TrainingHandlers, c *gin.Context)
params gin.Params
}{
{"GenerateInteractiveVideo", "POST",
func(h *TrainingHandlers, c *gin.Context) { h.GenerateInteractiveVideo(c) },
gin.Params{{Key: "moduleId", Value: "x"}}},
{"GetInteractiveManifest", "GET",
func(h *TrainingHandlers, c *gin.Context) { h.GetInteractiveManifest(c) },
gin.Params{{Key: "moduleId", Value: "x"}}},
{"SubmitCheckpointQuiz", "POST",
func(h *TrainingHandlers, c *gin.Context) { h.SubmitCheckpointQuiz(c) },
gin.Params{{Key: "checkpointId", Value: "x"}}},
{"GetCheckpointProgress", "GET",
func(h *TrainingHandlers, c *gin.Context) { h.GetCheckpointProgress(c) },
gin.Params{{Key: "assignmentId", Value: "x"}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext(tt.method, "/test", nil, nil, tt.params)
tt.handler(h, c)
if w.Code != http.StatusBadRequest {
t.Errorf("%s: Expected 400, got %d", tt.name, w.Code)
}
resp := parseResponse(w)
if resp["error"] == nil {
t.Errorf("%s: response should contain 'error' key", tt.name)
}
})
}
}
// ============================================================================
// Block Endpoint Tests
// ============================================================================
func TestGetBlockConfig_InvalidID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("GET", "/blocks/bad", nil, nil, gin.Params{{Key: "id", Value: "bad"}})
h.GetBlockConfig(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestCreateBlockConfig_EmptyBody_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("POST", "/blocks", nil, nil, nil)
h.CreateBlockConfig(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestCreateBlockConfig_MissingName_Returns400(t *testing.T) {
h := &TrainingHandlers{}
body := map[string]interface{}{"regulation_area": "dsgvo", "module_code_prefix": "BLK"}
w, c := newTestContext("POST", "/blocks", body, nil, nil)
h.CreateBlockConfig(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestUpdateBlockConfig_InvalidID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("PUT", "/blocks/bad", map[string]interface{}{"name": "x"}, nil, gin.Params{{Key: "id", Value: "bad"}})
h.UpdateBlockConfig(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestDeleteBlockConfig_InvalidID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("DELETE", "/blocks/bad", nil, nil, gin.Params{{Key: "id", Value: "bad"}})
h.DeleteBlockConfig(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestPreviewBlock_InvalidID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("POST", "/blocks/bad/preview", nil, nil, gin.Params{{Key: "id", Value: "bad"}})
h.PreviewBlock(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestGenerateBlock_InvalidID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("POST", "/blocks/bad/generate", nil, nil, gin.Params{{Key: "id", Value: "bad"}})
h.GenerateBlock(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
func TestGetBlockControls_InvalidID_Returns400(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext("GET", "/blocks/bad/controls", nil, nil, gin.Params{{Key: "id", Value: "bad"}})
h.GetBlockControls(c)
if w.Code != http.StatusBadRequest {
t.Errorf("Expected 400, got %d", w.Code)
}
}
// ============================================================================
// Response Error Format Tests
// ============================================================================
func TestInvalidID_ResponseContainsErrorKey(t *testing.T) {
tests := []struct {
name string
method string
handler func(h *TrainingHandlers, c *gin.Context)
params gin.Params
}{
{"GetModule", "GET", func(h *TrainingHandlers, c *gin.Context) { h.GetModule(c) }, gin.Params{{Key: "id", Value: "x"}}},
{"DeleteModule", "DELETE", func(h *TrainingHandlers, c *gin.Context) { h.DeleteModule(c) }, gin.Params{{Key: "id", Value: "x"}}},
{"StreamMedia", "GET", func(h *TrainingHandlers, c *gin.Context) { h.StreamMedia(c) }, gin.Params{{Key: "id", Value: "x"}}},
{"GenerateCertificate", "POST", func(h *TrainingHandlers, c *gin.Context) { h.GenerateCertificate(c) }, gin.Params{{Key: "assignmentId", Value: "x"}}},
{"DownloadCertificatePDF", "GET", func(h *TrainingHandlers, c *gin.Context) { h.DownloadCertificatePDF(c) }, gin.Params{{Key: "id", Value: "x"}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := &TrainingHandlers{}
w, c := newTestContext(tt.method, "/test", nil, nil, tt.params)
tt.handler(h, c)
resp := parseResponse(w)
if resp["error"] == nil {
t.Errorf("%s: response should contain 'error' key", tt.name)
}
})
}
}

View File

@@ -1122,6 +1122,114 @@ func (h *UCCAHandlers) GetWizardSchema(c *gin.Context) {
})
}
// ============================================================================
// AI Act Decision Tree Endpoints
// ============================================================================
// GetDecisionTree returns the decision tree structure for the frontend
// GET /sdk/v1/ucca/decision-tree
func (h *UCCAHandlers) GetDecisionTree(c *gin.Context) {
tree := ucca.BuildDecisionTreeDefinition()
c.JSON(http.StatusOK, tree)
}
// EvaluateDecisionTree evaluates the decision tree answers and stores the result
// POST /sdk/v1/ucca/decision-tree/evaluate
func (h *UCCAHandlers) EvaluateDecisionTree(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
var req ucca.DecisionTreeEvalRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.SystemName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "system_name is required"})
return
}
// Evaluate
result := ucca.EvaluateDecisionTree(&req)
result.TenantID = tenantID
// Parse optional project_id
if projectIDStr := c.Query("project_id"); projectIDStr != "" {
if pid, err := uuid.Parse(projectIDStr); err == nil {
result.ProjectID = &pid
}
}
// Store result
if err := h.store.CreateDecisionTreeResult(c.Request.Context(), result); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, result)
}
// ListDecisionTreeResults returns stored decision tree results for a tenant
// GET /sdk/v1/ucca/decision-tree/results
func (h *UCCAHandlers) ListDecisionTreeResults(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
results, err := h.store.ListDecisionTreeResults(c.Request.Context(), tenantID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"results": results, "total": len(results)})
}
// GetDecisionTreeResult returns a single decision tree result by ID
// GET /sdk/v1/ucca/decision-tree/results/:id
func (h *UCCAHandlers) GetDecisionTreeResult(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
result, err := h.store.GetDecisionTreeResult(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if result == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, result)
}
// DeleteDecisionTreeResult deletes a decision tree result
// DELETE /sdk/v1/ucca/decision-tree/results/:id
func (h *UCCAHandlers) DeleteDecisionTreeResult(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
if err := h.store.DeleteDecisionTreeResult(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
// ============================================================================
// Helper functions
// ============================================================================

View File

@@ -21,15 +21,16 @@ type GateDefinition struct {
// CompletenessContext provides all project data needed to evaluate completeness gates.
type CompletenessContext struct {
Project *Project
Components []Component
Classifications []RegulatoryClassification
Hazards []Hazard
Assessments []RiskAssessment
Mitigations []Mitigation
Evidence []Evidence
TechFileSections []TechFileSection
HasAI bool
Project *Project
Components []Component
Classifications []RegulatoryClassification
Hazards []Hazard
Assessments []RiskAssessment
Mitigations []Mitigation
Evidence []Evidence
TechFileSections []TechFileSection
HasAI bool
PatternMatchingPerformed bool // set from audit trail (entity_type="pattern_matching")
}
// CompletenessResult contains the aggregated result of all gate checks.
@@ -135,6 +136,20 @@ func buildGateDefinitions() []GateDefinition {
},
},
// =====================================================================
// Pattern Matching Gate (G09) - Recommended
// =====================================================================
{
ID: "G09",
Category: "onboarding",
Label: "Pattern matching performed",
Required: false,
Recommended: true,
CheckFunc: func(ctx *CompletenessContext) bool {
return ctx.PatternMatchingPerformed
},
},
// =====================================================================
// Classification Gates (G10-G13) - Required
// =====================================================================
@@ -248,14 +263,17 @@ func buildGateDefinitions() []GateDefinition {
Label: "Mitigations verified",
Required: true,
CheckFunc: func(ctx *CompletenessContext) bool {
// All mitigations with status "implemented" must also be verified
// All mitigations must be in a terminal state (verified or rejected).
// Planned and implemented mitigations block export — they haven't been
// verified yet, so the project cannot be considered complete.
if len(ctx.Mitigations) == 0 {
return true
}
for _, m := range ctx.Mitigations {
if m.Status == MitigationStatusImplemented {
// Implemented but not yet verified -> gate fails
if m.Status != MitigationStatusVerified && m.Status != MitigationStatusRejected {
return false
}
}
// All mitigations are either planned, verified, or rejected
return true
},
},

View File

@@ -33,7 +33,7 @@ func TestCompletenessCheck_EmptyContext(t *testing.T) {
// With nil project, most gates fail. However, some auto-pass:
// G06 (AI classification): auto-passes when HasAI=false
// G22 (critical/high mitigated): auto-passes when no critical/high assessments exist
// G23 (mitigations verified): auto-passes when no mitigations with status "implemented"
// G23 (mitigations verified): auto-passes when no mitigations (empty list)
// G42 (AI documents): auto-passes when HasAI=false
// That gives 4 required gates passing even with empty context.
if result.PassedRequired != 4 {
@@ -89,7 +89,8 @@ func TestCompletenessCheck_MinimalValidProject(t *testing.T) {
{ID: uuid.New(), ProjectID: projectID, SectionType: "risk_assessment_report"},
{ID: uuid.New(), ProjectID: projectID, SectionType: "hazard_log_combined"},
},
HasAI: false,
HasAI: false,
PatternMatchingPerformed: true,
}
result := checker.Check(ctx)
@@ -376,11 +377,11 @@ func TestCompletenessCheck_G23_MitigationsVerified(t *testing.T) {
wantG23Passed: false,
},
{
name: "planned mitigations pass G23 (not yet implemented)",
name: "planned mitigations fail G23 (not yet verified)",
mitigations: []Mitigation{
{HazardID: hazardID, Status: MitigationStatusPlanned},
},
wantG23Passed: true,
wantG23Passed: false,
},
{
name: "rejected mitigations pass G23",
@@ -390,12 +391,20 @@ func TestCompletenessCheck_G23_MitigationsVerified(t *testing.T) {
wantG23Passed: true,
},
{
name: "mix of verified planned rejected passes G23",
name: "mix of verified planned rejected fails G23",
mitigations: []Mitigation{
{HazardID: hazardID, Status: MitigationStatusVerified},
{HazardID: hazardID, Status: MitigationStatusPlanned},
{HazardID: hazardID, Status: MitigationStatusRejected},
},
wantG23Passed: false,
},
{
name: "mix of verified and rejected passes G23",
mitigations: []Mitigation{
{HazardID: hazardID, Status: MitigationStatusVerified},
{HazardID: hazardID, Status: MitigationStatusRejected},
},
wantG23Passed: true,
},
}
@@ -422,6 +431,48 @@ func TestCompletenessCheck_G23_MitigationsVerified(t *testing.T) {
}
}
func TestCompletenessCheck_G09_PatternMatchingPerformed(t *testing.T) {
checker := NewCompletenessChecker()
tests := []struct {
name string
performed bool
wantG09Passed bool
}{
{
name: "pattern matching not performed fails G09",
performed: false,
wantG09Passed: false,
},
{
name: "pattern matching performed passes G09",
performed: true,
wantG09Passed: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := &CompletenessContext{
Project: &Project{MachineName: "Test"},
PatternMatchingPerformed: tt.performed,
}
result := checker.Check(ctx)
for _, g := range result.Gates {
if g.ID == "G09" {
if g.Passed != tt.wantG09Passed {
t.Errorf("G09 Passed = %v, want %v", g.Passed, tt.wantG09Passed)
}
return
}
}
t.Error("G09 gate not found in results")
})
}
}
func TestCompletenessCheck_G24_ResidualRiskAccepted(t *testing.T) {
checker := NewCompletenessChecker()
@@ -608,10 +659,10 @@ func TestCompletenessCheck_GateCountsAndCategories(t *testing.T) {
}
result := checker.Check(ctx)
// The buildGateDefinitions function returns exactly 21 gates
// (G01-G08: 8, G10-G13: 4, G20-G24: 5, G30: 1, G40-G42: 3 = 21 total)
if len(result.Gates) != 21 {
t.Errorf("Total gates = %d, want 21", len(result.Gates))
// The buildGateDefinitions function returns exactly 22 gates
// (G01-G08: 8, G09: 1, G10-G13: 4, G20-G24: 5, G30: 1, G40-G42: 3 = 22 total)
if len(result.Gates) != 22 {
t.Errorf("Total gates = %d, want 22", len(result.Gates))
}
// Count required vs recommended
@@ -622,8 +673,8 @@ func TestCompletenessCheck_GateCountsAndCategories(t *testing.T) {
requiredCount++
}
}
// G30 is the only recommended gate (Required=false, Recommended=true)
// All others are required (20 required, 1 recommended)
// G09 and G30 are recommended gates (Required=false, Recommended=true)
// All others are required (20 required, 2 recommended)
if requiredCount != 20 {
t.Errorf("Required gates count = %d, want 20", requiredCount)
}
@@ -632,9 +683,9 @@ func TestCompletenessCheck_GateCountsAndCategories(t *testing.T) {
t.Errorf("TotalRequired = %d, want 20", result.TotalRequired)
}
// TotalRecommended should be 1 (G30)
if result.TotalRecommended != 1 {
t.Errorf("TotalRecommended = %d, want 1", result.TotalRecommended)
// TotalRecommended should be 2 (G09, G30)
if result.TotalRecommended != 2 {
t.Errorf("TotalRecommended = %d, want 2", result.TotalRecommended)
}
_ = recommendedCount
@@ -645,7 +696,7 @@ func TestCompletenessCheck_GateCountsAndCategories(t *testing.T) {
}
expectedCategories := map[string]int{
"onboarding": 8,
"onboarding": 9, // G01-G08 + G09 (recommended)
"classification": 4,
"hazard_risk": 5,
"evidence": 1,

View File

@@ -0,0 +1,209 @@
package iace
// ComponentLibraryEntry represents a reusable machine component template from the library.
type ComponentLibraryEntry struct {
ID string `json:"id"`
NameDE string `json:"name_de"`
NameEN string `json:"name_en"`
Category string `json:"category"`
DescriptionDE string `json:"description_de,omitempty"`
TypicalHazardCategories []string `json:"typical_hazard_categories"`
TypicalEnergySources []string `json:"typical_energy_sources"`
MapsToComponentType string `json:"maps_to_component_type"`
Tags []string `json:"tags"`
SortOrder int `json:"sort_order"`
}
// EnergySourceEntry represents an energy source type used in machinery.
type EnergySourceEntry struct {
ID string `json:"id"`
NameDE string `json:"name_de"`
NameEN string `json:"name_en"`
DescriptionDE string `json:"description_de,omitempty"`
TypicalComponents []string `json:"typical_components"`
TypicalHazardCategories []string `json:"typical_hazard_categories"`
Tags []string `json:"tags"`
SortOrder int `json:"sort_order"`
}
// GetComponentLibrary returns the complete built-in component library with 120 entries
// across 11 categories, covering typical industrial machine components.
func GetComponentLibrary() []ComponentLibraryEntry {
return []ComponentLibraryEntry{
// ── Category: mechanical (C001-C020) ────────────────────────────────────
{ID: "C001", NameDE: "Roboterarm", NameEN: "Robot Arm", Category: "mechanical", DescriptionDE: "Mehrgelenkiger Industrierobotearm fuer Pick-and-Place, Schweissen oder Montage.", TypicalHazardCategories: []string{"mechanical_hazard", "ergonomic"}, TypicalEnergySources: []string{"EN01", "EN02"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "rotating_part", "high_force"}, SortOrder: 1},
{ID: "C002", NameDE: "Greifer", NameEN: "Gripper", Category: "mechanical", DescriptionDE: "Pneumatischer oder elektrischer Greifer zum Greifen und Halten von Werkstuecken.", TypicalHazardCategories: []string{"mechanical_hazard", "pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN01", "EN05"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "clamping_part", "pinch_point"}, SortOrder: 2},
{ID: "C003", NameDE: "Foerderband", NameEN: "Conveyor Belt", Category: "mechanical", DescriptionDE: "Endlosband zum Transport von Werkstuecken zwischen Arbeitsstationen.", TypicalHazardCategories: []string{"mechanical_hazard", "ergonomic"}, TypicalEnergySources: []string{"EN01", "EN02"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "rotating_part", "entanglement_risk"}, SortOrder: 3},
{ID: "C004", NameDE: "Drehtisch", NameEN: "Rotary Table", Category: "mechanical", DescriptionDE: "Rotierender Arbeitstisch fuer Bearbeitungs- oder Montageprozesse.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN02"}, MapsToComponentType: "mechanical", Tags: []string{"rotating_part", "high_force"}, SortOrder: 4},
{ID: "C005", NameDE: "Linearachse", NameEN: "Linear Axis", Category: "mechanical", DescriptionDE: "Linearfuehrung fuer praezise translatorische Bewegungen.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN01"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "crush_point"}, SortOrder: 5},
{ID: "C006", NameDE: "Spindel", NameEN: "Spindle", Category: "mechanical", DescriptionDE: "Hochdrehende Spindel fuer Fräs-, Bohr- oder Schleifoperationen.", TypicalHazardCategories: []string{"mechanical_hazard", "noise_vibration"}, TypicalEnergySources: []string{"EN02"}, MapsToComponentType: "mechanical", Tags: []string{"rotating_part", "high_speed", "cutting_part"}, SortOrder: 6},
{ID: "C007", NameDE: "Saegeblatt", NameEN: "Saw Blade", Category: "mechanical", DescriptionDE: "Rotierendes oder oszillierendes Schneidwerkzeug.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN02"}, MapsToComponentType: "mechanical", Tags: []string{"cutting_part", "rotating_part", "high_speed"}, SortOrder: 7},
{ID: "C008", NameDE: "Pressenstoessel", NameEN: "Press Ram", Category: "mechanical", DescriptionDE: "Auf- und abfahrender Stoessel einer Presse zum Umformen.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN01", "EN05"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "high_force", "crush_point"}, SortOrder: 8},
{ID: "C009", NameDE: "Walze", NameEN: "Roller", Category: "mechanical", DescriptionDE: "Zylindrische Walze zum Foerdern, Pressen oder Kalandrieren.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN02"}, MapsToComponentType: "mechanical", Tags: []string{"rotating_part", "entanglement_risk", "pinch_point"}, SortOrder: 9},
{ID: "C010", NameDE: "Kettenantrieb", NameEN: "Chain Drive", Category: "mechanical", DescriptionDE: "Kette und Kettenrad zur Kraftuebertragung.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN01"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "entanglement_risk"}, SortOrder: 10},
{ID: "C011", NameDE: "Zahnradgetriebe", NameEN: "Gear Transmission", Category: "mechanical", DescriptionDE: "Zahnradpaar oder -satz zur Drehzahl-/Drehmomentanpassung.", TypicalHazardCategories: []string{"mechanical_hazard", "noise_vibration"}, TypicalEnergySources: []string{"EN02"}, MapsToComponentType: "mechanical", Tags: []string{"rotating_part", "pinch_point"}, SortOrder: 11},
{ID: "C012", NameDE: "Kupplung", NameEN: "Clutch", Category: "mechanical", DescriptionDE: "Mechanische Kupplung zur An-/Abkopplung von Antriebsstraengen.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN02"}, MapsToComponentType: "mechanical", Tags: []string{"rotating_part"}, SortOrder: 12},
{ID: "C013", NameDE: "Bremse", NameEN: "Brake", Category: "mechanical", DescriptionDE: "Mechanische oder elektromagnetische Bremse zum Stillsetzen von Antrieben.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN01"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "stored_energy"}, SortOrder: 13},
{ID: "C014", NameDE: "Hubwerk", NameEN: "Hoist", Category: "mechanical", DescriptionDE: "Hebezeug zum vertikalen Bewegen von Lasten.", TypicalHazardCategories: []string{"mechanical_hazard", "ergonomic"}, TypicalEnergySources: []string{"EN01", "EN03"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "high_force", "gravity_risk"}, SortOrder: 14},
{ID: "C015", NameDE: "Werkzeugwechsler", NameEN: "Tool Changer", Category: "mechanical", DescriptionDE: "Automatischer Werkzeugwechsler fuer CNC-Maschinen.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN01", "EN05"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "pinch_point"}, SortOrder: 15},
{ID: "C016", NameDE: "Schweisskopf", NameEN: "Welding Head", Category: "mechanical", DescriptionDE: "Schweisskopf fuer MIG/MAG, WIG oder Laserschweissen.", TypicalHazardCategories: []string{"mechanical_hazard", "thermal_hazard", "electrical_hazard"}, TypicalEnergySources: []string{"EN03", "EN07"}, MapsToComponentType: "mechanical", Tags: []string{"high_temperature", "radiation_risk"}, SortOrder: 16},
{ID: "C017", NameDE: "Schraubstation", NameEN: "Screwdriving Station", Category: "mechanical", DescriptionDE: "Automatische Schraubeinheit fuer Montageprozesse.", TypicalHazardCategories: []string{"mechanical_hazard", "noise_vibration"}, TypicalEnergySources: []string{"EN02"}, MapsToComponentType: "mechanical", Tags: []string{"rotating_part"}, SortOrder: 17},
{ID: "C018", NameDE: "Stanzen-Werkzeug", NameEN: "Punching Tool", Category: "mechanical", DescriptionDE: "Stanzwerkzeug zum Ausschneiden von Formen aus Blech oder Folie.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN01"}, MapsToComponentType: "mechanical", Tags: []string{"cutting_part", "high_force", "crush_point"}, SortOrder: 18},
{ID: "C019", NameDE: "Biegewerkzeug", NameEN: "Bending Tool", Category: "mechanical", DescriptionDE: "Werkzeug zum Biegen von Blech oder Profilen.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN01"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "high_force", "crush_point"}, SortOrder: 19},
{ID: "C020", NameDE: "Vibrationsfoerderer", NameEN: "Vibratory Feeder", Category: "mechanical", DescriptionDE: "Schwingfoerderer zum Sortieren und Zufuehren von Kleinteilen.", TypicalHazardCategories: []string{"mechanical_hazard", "noise_vibration"}, TypicalEnergySources: []string{"EN01"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "vibration_source"}, SortOrder: 20},
// ── Category: structural (C021-C030) ────────────────────────────────────
{ID: "C021", NameDE: "Maschinenrahmen", NameEN: "Machine Frame", Category: "structural", DescriptionDE: "Tragender Rahmen als Grundstruktur der Maschine.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{}, MapsToComponentType: "mechanical", Tags: []string{"structural_part"}, SortOrder: 21},
{ID: "C022", NameDE: "Schutzgehaeuse", NameEN: "Protective Enclosure", Category: "structural", DescriptionDE: "Feste Verkleidung zum Schutz vor Gefahrstellen.", TypicalHazardCategories: []string{}, TypicalEnergySources: []string{}, MapsToComponentType: "mechanical", Tags: []string{"structural_part", "guard"}, SortOrder: 22},
{ID: "C023", NameDE: "Schutztuer", NameEN: "Safety Door", Category: "structural", DescriptionDE: "Verriegelte Tuer mit Zugangsschutz zum Gefahrbereich.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{}, MapsToComponentType: "mechanical", Tags: []string{"structural_part", "guard", "interlocked"}, SortOrder: 23},
{ID: "C024", NameDE: "Arbeitstisch", NameEN: "Work Table", Category: "structural", DescriptionDE: "Feststehende oder hoehenverstellbare Arbeitsflaeche.", TypicalHazardCategories: []string{"ergonomic"}, TypicalEnergySources: []string{}, MapsToComponentType: "mechanical", Tags: []string{"structural_part"}, SortOrder: 24},
{ID: "C025", NameDE: "Kabelkanal", NameEN: "Cable Tray", Category: "structural", DescriptionDE: "Fuehrung und Schutz fuer elektrische Leitungen und Kabel.", TypicalHazardCategories: []string{"electrical_hazard"}, TypicalEnergySources: []string{}, MapsToComponentType: "mechanical", Tags: []string{"structural_part"}, SortOrder: 25},
{ID: "C026", NameDE: "Schwingungsdaempfer", NameEN: "Vibration Damper", Category: "structural", DescriptionDE: "Daempfungselement zur Reduzierung von Maschinenschwingungen.", TypicalHazardCategories: []string{"noise_vibration"}, TypicalEnergySources: []string{}, MapsToComponentType: "mechanical", Tags: []string{"structural_part"}, SortOrder: 26},
{ID: "C027", NameDE: "Fundamentplatte", NameEN: "Foundation Plate", Category: "structural", DescriptionDE: "Fundamentplatte zur Aufstellung und Verankerung der Maschine.", TypicalHazardCategories: []string{}, TypicalEnergySources: []string{}, MapsToComponentType: "mechanical", Tags: []string{"structural_part"}, SortOrder: 27},
{ID: "C028", NameDE: "Schutzgitter", NameEN: "Safety Fence", Category: "structural", DescriptionDE: "Feststehende Schutzeinrichtung als Umzaeunung des Gefahrbereichs.", TypicalHazardCategories: []string{}, TypicalEnergySources: []string{}, MapsToComponentType: "mechanical", Tags: []string{"structural_part", "guard"}, SortOrder: 28},
{ID: "C029", NameDE: "Aufstiegsleiter", NameEN: "Access Ladder", Category: "structural", DescriptionDE: "Fest montierte Leiter fuer Wartungszugang in der Hoehe.", TypicalHazardCategories: []string{"ergonomic", "mechanical_hazard"}, TypicalEnergySources: []string{"EN03"}, MapsToComponentType: "mechanical", Tags: []string{"structural_part", "gravity_risk"}, SortOrder: 29},
{ID: "C030", NameDE: "Plattform/Buehne", NameEN: "Platform/Walkway", Category: "structural", DescriptionDE: "Begehbare Plattform fuer Bedienung oder Wartung in der Hoehe.", TypicalHazardCategories: []string{"ergonomic", "mechanical_hazard"}, TypicalEnergySources: []string{"EN03"}, MapsToComponentType: "mechanical", Tags: []string{"structural_part", "gravity_risk"}, SortOrder: 30},
// ── Category: drive (C031-C040) ─────────────────────────────────────────
{ID: "C031", NameDE: "Elektromotor (Drehstrom)", NameEN: "AC Motor", Category: "drive", DescriptionDE: "Drehstrom-Asynchronmotor als Hauptantrieb.", TypicalHazardCategories: []string{"electrical_hazard", "mechanical_hazard", "noise_vibration"}, TypicalEnergySources: []string{"EN02", "EN04"}, MapsToComponentType: "electrical", Tags: []string{"rotating_part", "high_voltage", "high_force"}, SortOrder: 31},
{ID: "C032", NameDE: "Servomotor", NameEN: "Servo Motor", Category: "drive", DescriptionDE: "Hochdynamischer Servomotor fuer praezise Positionierung.", TypicalHazardCategories: []string{"electrical_hazard", "mechanical_hazard"}, TypicalEnergySources: []string{"EN02", "EN04"}, MapsToComponentType: "electrical", Tags: []string{"rotating_part", "high_speed"}, SortOrder: 32},
{ID: "C033", NameDE: "Schrittmotor", NameEN: "Stepper Motor", Category: "drive", DescriptionDE: "Schrittmotor fuer inkrementelle Positionierung.", TypicalHazardCategories: []string{"electrical_hazard"}, TypicalEnergySources: []string{"EN02", "EN04"}, MapsToComponentType: "electrical", Tags: []string{"rotating_part"}, SortOrder: 33},
{ID: "C034", NameDE: "Frequenzumrichter", NameEN: "Frequency Converter", Category: "drive", DescriptionDE: "Frequenzumrichter zur stufenlosen Drehzahlregelung.", TypicalHazardCategories: []string{"electrical_hazard", "emc_hazard"}, TypicalEnergySources: []string{"EN04"}, MapsToComponentType: "electrical", Tags: []string{"high_voltage", "stored_energy"}, SortOrder: 34},
{ID: "C035", NameDE: "Getriebemotor", NameEN: "Gear Motor", Category: "drive", DescriptionDE: "Motor mit integriertem Getriebe fuer hohes Drehmoment bei niedriger Drehzahl.", TypicalHazardCategories: []string{"mechanical_hazard", "electrical_hazard"}, TypicalEnergySources: []string{"EN02", "EN04"}, MapsToComponentType: "electrical", Tags: []string{"rotating_part", "high_force"}, SortOrder: 35},
{ID: "C036", NameDE: "Linearmotor", NameEN: "Linear Motor", Category: "drive", DescriptionDE: "Elektromagnetischer Direktantrieb fuer lineare Bewegung.", TypicalHazardCategories: []string{"electrical_hazard", "mechanical_hazard"}, TypicalEnergySources: []string{"EN01", "EN04"}, MapsToComponentType: "electrical", Tags: []string{"moving_part", "high_speed"}, SortOrder: 36},
{ID: "C037", NameDE: "Torque-Motor", NameEN: "Torque Motor", Category: "drive", DescriptionDE: "Direktantriebsmotor fuer hohe Drehmomente ohne Getriebe.", TypicalHazardCategories: []string{"electrical_hazard", "mechanical_hazard"}, TypicalEnergySources: []string{"EN02", "EN04"}, MapsToComponentType: "electrical", Tags: []string{"rotating_part", "high_force"}, SortOrder: 37},
{ID: "C038", NameDE: "Elektrischer Stellantrieb", NameEN: "Electric Actuator", Category: "drive", DescriptionDE: "Elektrischer Antrieb fuer Ventile, Klappen oder Schieber.", TypicalHazardCategories: []string{"electrical_hazard"}, TypicalEnergySources: []string{"EN01", "EN04"}, MapsToComponentType: "actuator", Tags: []string{"moving_part"}, SortOrder: 38},
{ID: "C039", NameDE: "Spindelantrieb", NameEN: "Spindle Drive", Category: "drive", DescriptionDE: "Kugelgewindetrieb fuer praezise Linearbewegung.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN01"}, MapsToComponentType: "mechanical", Tags: []string{"moving_part", "crush_point"}, SortOrder: 39},
{ID: "C040", NameDE: "Riemenantrieb", NameEN: "Belt Drive", Category: "drive", DescriptionDE: "Riemen und Riemenscheiben zur Kraftuebertragung.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN02"}, MapsToComponentType: "mechanical", Tags: []string{"rotating_part", "entanglement_risk"}, SortOrder: 40},
// ── Category: hydraulic (C041-C050) ─────────────────────────────────────
{ID: "C041", NameDE: "Hydraulikpumpe", NameEN: "Hydraulic Pump", Category: "hydraulic", DescriptionDE: "Pumpe zur Erzeugung des hydraulischen Drucks im System.", TypicalHazardCategories: []string{"pneumatic_hydraulic", "noise_vibration"}, TypicalEnergySources: []string{"EN05"}, MapsToComponentType: "actuator", Tags: []string{"hydraulic_part", "high_pressure"}, SortOrder: 41},
{ID: "C042", NameDE: "Hydraulikzylinder", NameEN: "Hydraulic Cylinder", Category: "hydraulic", DescriptionDE: "Linearaktuator zur Erzeugung hoher Kraefte.", TypicalHazardCategories: []string{"pneumatic_hydraulic", "mechanical_hazard"}, TypicalEnergySources: []string{"EN05"}, MapsToComponentType: "actuator", Tags: []string{"hydraulic_part", "moving_part", "high_force", "high_pressure"}, SortOrder: 42},
{ID: "C043", NameDE: "Hydraulikventil", NameEN: "Hydraulic Valve", Category: "hydraulic", DescriptionDE: "Steuer- oder Regelventil im Hydraulikkreislauf.", TypicalHazardCategories: []string{"pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN05"}, MapsToComponentType: "actuator", Tags: []string{"hydraulic_part", "high_pressure"}, SortOrder: 43},
{ID: "C044", NameDE: "Hydraulikspeicher", NameEN: "Hydraulic Accumulator", Category: "hydraulic", DescriptionDE: "Druckspeicher zur Pufferung von Druckspitzen.", TypicalHazardCategories: []string{"pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN05"}, MapsToComponentType: "actuator", Tags: []string{"hydraulic_part", "stored_energy", "high_pressure"}, SortOrder: 44},
{ID: "C045", NameDE: "Hydraulikschlauch", NameEN: "Hydraulic Hose", Category: "hydraulic", DescriptionDE: "Flexible Schlauchleitung fuer Hydraulikfluid.", TypicalHazardCategories: []string{"pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN05"}, MapsToComponentType: "actuator", Tags: []string{"hydraulic_part", "high_pressure"}, SortOrder: 45},
{ID: "C046", NameDE: "Hydraulikfilter", NameEN: "Hydraulic Filter", Category: "hydraulic", DescriptionDE: "Filter zur Reinigung des Hydraulikfluids.", TypicalHazardCategories: []string{"pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN05"}, MapsToComponentType: "actuator", Tags: []string{"hydraulic_part"}, SortOrder: 46},
{ID: "C047", NameDE: "Hydraulikkuehler", NameEN: "Hydraulic Cooler", Category: "hydraulic", DescriptionDE: "Kuehlaggregat zur Temperierung des Hydraulikoels.", TypicalHazardCategories: []string{"pneumatic_hydraulic", "thermal_hazard"}, TypicalEnergySources: []string{"EN05", "EN07"}, MapsToComponentType: "actuator", Tags: []string{"hydraulic_part", "high_temperature"}, SortOrder: 47},
{ID: "C048", NameDE: "Hydraulik-Proportionalventil", NameEN: "Hydraulic Proportional Valve", Category: "hydraulic", DescriptionDE: "Proportionalventil fuer stufenlose Steuerung von Druck und Volumen.", TypicalHazardCategories: []string{"pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN05"}, MapsToComponentType: "actuator", Tags: []string{"hydraulic_part", "high_pressure"}, SortOrder: 48},
{ID: "C049", NameDE: "Hydraulik-Druckminderer", NameEN: "Hydraulic Pressure Reducer", Category: "hydraulic", DescriptionDE: "Druckregelventil zur Begrenzung des Systemdrucks.", TypicalHazardCategories: []string{"pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN05"}, MapsToComponentType: "actuator", Tags: []string{"hydraulic_part", "high_pressure"}, SortOrder: 49},
{ID: "C050", NameDE: "Hydraulik-Absperrventil", NameEN: "Hydraulic Shut-off Valve", Category: "hydraulic", DescriptionDE: "Handventil zum sicheren Absperren von Leitungsabschnitten.", TypicalHazardCategories: []string{"pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN05"}, MapsToComponentType: "actuator", Tags: []string{"hydraulic_part"}, SortOrder: 50},
// ── Category: pneumatic (C051-C060) ─────────────────────────────────────
{ID: "C051", NameDE: "Pneumatikzylinder", NameEN: "Pneumatic Cylinder", Category: "pneumatic", DescriptionDE: "Druckluftbetriebener Linearaktuator.", TypicalHazardCategories: []string{"pneumatic_hydraulic", "mechanical_hazard"}, TypicalEnergySources: []string{"EN06"}, MapsToComponentType: "actuator", Tags: []string{"pneumatic_part", "moving_part", "stored_energy"}, SortOrder: 51},
{ID: "C052", NameDE: "Kompressor", NameEN: "Compressor", Category: "pneumatic", DescriptionDE: "Druckluftkompressor zur Erzeugung von Druckluft.", TypicalHazardCategories: []string{"pneumatic_hydraulic", "noise_vibration"}, TypicalEnergySources: []string{"EN06"}, MapsToComponentType: "actuator", Tags: []string{"pneumatic_part", "high_pressure", "noise_source"}, SortOrder: 52},
{ID: "C053", NameDE: "Pneumatikventil", NameEN: "Pneumatic Valve", Category: "pneumatic", DescriptionDE: "Steuerventil zur Druckluftverteilung.", TypicalHazardCategories: []string{"pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN06"}, MapsToComponentType: "actuator", Tags: []string{"pneumatic_part"}, SortOrder: 53},
{ID: "C054", NameDE: "Druckluftaufbereitung", NameEN: "Air Treatment Unit", Category: "pneumatic", DescriptionDE: "Wartungseinheit mit Filter, Regler und Oeler.", TypicalHazardCategories: []string{"pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN06"}, MapsToComponentType: "actuator", Tags: []string{"pneumatic_part"}, SortOrder: 54},
{ID: "C055", NameDE: "Vakuumsauger", NameEN: "Vacuum Suction Cup", Category: "pneumatic", DescriptionDE: "Vakuumgreifer zum beruehrungslosen Heben von Werkstuecken.", TypicalHazardCategories: []string{"pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN06"}, MapsToComponentType: "actuator", Tags: []string{"pneumatic_part"}, SortOrder: 55},
{ID: "C056", NameDE: "Vakuumerzeuger", NameEN: "Vacuum Generator", Category: "pneumatic", DescriptionDE: "Venturi-Erzeuger oder Vakuumpumpe.", TypicalHazardCategories: []string{"pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN06"}, MapsToComponentType: "actuator", Tags: []string{"pneumatic_part"}, SortOrder: 56},
{ID: "C057", NameDE: "Druckluftmotor", NameEN: "Pneumatic Motor", Category: "pneumatic", DescriptionDE: "Druckluftbetriebener Rotationsmotor.", TypicalHazardCategories: []string{"pneumatic_hydraulic", "mechanical_hazard"}, TypicalEnergySources: []string{"EN06"}, MapsToComponentType: "actuator", Tags: []string{"pneumatic_part", "rotating_part"}, SortOrder: 57},
{ID: "C058", NameDE: "Pneumatik-Absperrventil", NameEN: "Pneumatic Shut-off Valve", Category: "pneumatic", DescriptionDE: "Handventil zum sicheren Absperren der Druckluft.", TypicalHazardCategories: []string{"pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN06"}, MapsToComponentType: "actuator", Tags: []string{"pneumatic_part"}, SortOrder: 58},
{ID: "C059", NameDE: "Pneumatik-Drosselventil", NameEN: "Pneumatic Flow Control", Category: "pneumatic", DescriptionDE: "Drosselventil zur Geschwindigkeitsregelung von Pneumatikzylindern.", TypicalHazardCategories: []string{"pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN06"}, MapsToComponentType: "actuator", Tags: []string{"pneumatic_part"}, SortOrder: 59},
{ID: "C060", NameDE: "Druckluftspeicher", NameEN: "Air Reservoir", Category: "pneumatic", DescriptionDE: "Druckluftbehaelter als Energiespeicher.", TypicalHazardCategories: []string{"pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN06"}, MapsToComponentType: "actuator", Tags: []string{"pneumatic_part", "stored_energy", "high_pressure"}, SortOrder: 60},
// ── Category: electrical (C061-C070) ────────────────────────────────────
{ID: "C061", NameDE: "Schaltschrank", NameEN: "Control Cabinet", Category: "electrical", DescriptionDE: "Zentraler Schaltschrank mit Sicherungen, Schuetzen und Steuerung.", TypicalHazardCategories: []string{"electrical_hazard"}, TypicalEnergySources: []string{"EN04"}, MapsToComponentType: "electrical", Tags: []string{"high_voltage", "electrical_part"}, SortOrder: 61},
{ID: "C062", NameDE: "Stromversorgung (Netzteil)", NameEN: "Power Supply", Category: "electrical", DescriptionDE: "AC/DC-Wandler oder Schaltnetzteil fuer die Maschinensteuerung.", TypicalHazardCategories: []string{"electrical_hazard"}, TypicalEnergySources: []string{"EN04"}, MapsToComponentType: "electrical", Tags: []string{"high_voltage", "electrical_part"}, SortOrder: 62},
{ID: "C063", NameDE: "Transformator", NameEN: "Transformer", Category: "electrical", DescriptionDE: "Spannungswandler fuer die Versorgung verschiedener Spannungsebenen.", TypicalHazardCategories: []string{"electrical_hazard", "thermal_hazard"}, TypicalEnergySources: []string{"EN04"}, MapsToComponentType: "electrical", Tags: []string{"high_voltage", "electrical_part"}, SortOrder: 63},
{ID: "C064", NameDE: "Schuetz/Relais", NameEN: "Contactor/Relay", Category: "electrical", DescriptionDE: "Elektromechanisches Schaltgeraet fuer Lastkreise.", TypicalHazardCategories: []string{"electrical_hazard"}, TypicalEnergySources: []string{"EN04"}, MapsToComponentType: "electrical", Tags: []string{"electrical_part"}, SortOrder: 64},
{ID: "C065", NameDE: "Sicherungsautomat", NameEN: "Circuit Breaker", Category: "electrical", DescriptionDE: "Leitungsschutzschalter oder Motorschutzschalter.", TypicalHazardCategories: []string{"electrical_hazard"}, TypicalEnergySources: []string{"EN04"}, MapsToComponentType: "electrical", Tags: []string{"electrical_part"}, SortOrder: 65},
{ID: "C066", NameDE: "FI-Schutzschalter", NameEN: "Residual Current Device", Category: "electrical", DescriptionDE: "Fehlerstromschutzschalter zum Personenschutz.", TypicalHazardCategories: []string{"electrical_hazard"}, TypicalEnergySources: []string{"EN04"}, MapsToComponentType: "electrical", Tags: []string{"electrical_part", "safety_device"}, SortOrder: 66},
{ID: "C067", NameDE: "USV (Unterbrechungsfreie Stromversorgung)", NameEN: "Uninterruptible Power Supply", Category: "electrical", DescriptionDE: "Batteriepuffer fuer sicherheitsrelevante Verbraucher.", TypicalHazardCategories: []string{"electrical_hazard"}, TypicalEnergySources: []string{"EN04", "EN08"}, MapsToComponentType: "electrical", Tags: []string{"electrical_part", "stored_energy"}, SortOrder: 67},
{ID: "C068", NameDE: "Energiekette/Schleppkette", NameEN: "Cable Carrier", Category: "electrical", DescriptionDE: "Fuehrung fuer Kabel und Schlaeuche an bewegten Achsen.", TypicalHazardCategories: []string{"electrical_hazard", "mechanical_hazard"}, TypicalEnergySources: []string{}, MapsToComponentType: "electrical", Tags: []string{"electrical_part", "moving_part"}, SortOrder: 68},
{ID: "C069", NameDE: "Erdungssystem", NameEN: "Grounding System", Category: "electrical", DescriptionDE: "Schutz- und Funktionserdung der Maschine.", TypicalHazardCategories: []string{"electrical_hazard"}, TypicalEnergySources: []string{"EN04"}, MapsToComponentType: "electrical", Tags: []string{"electrical_part"}, SortOrder: 69},
{ID: "C070", NameDE: "Hauptschalter", NameEN: "Main Switch", Category: "electrical", DescriptionDE: "Zentraler Netztrennschalter der Maschine.", TypicalHazardCategories: []string{"electrical_hazard"}, TypicalEnergySources: []string{"EN04"}, MapsToComponentType: "electrical", Tags: []string{"electrical_part", "high_voltage"}, SortOrder: 70},
// ── Category: control (C071-C080) ───────────────────────────────────────
{ID: "C071", NameDE: "SPS (Speicherprogrammierbare Steuerung)", NameEN: "PLC (Programmable Logic Controller)", Category: "control", DescriptionDE: "Zentrale Maschinensteuerung mit SPS-Programm.", TypicalHazardCategories: []string{"software_fault", "configuration_error"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"has_software", "programmable"}, SortOrder: 71},
{ID: "C072", NameDE: "Sicherheits-SPS", NameEN: "Safety PLC", Category: "control", DescriptionDE: "Redundante Sicherheitssteuerung bis SIL 3 / PL e.", TypicalHazardCategories: []string{"safety_function_failure", "software_fault"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"has_software", "programmable", "safety_device"}, SortOrder: 72},
{ID: "C073", NameDE: "HMI (Bedienterminal)", NameEN: "HMI (Human Machine Interface)", Category: "control", DescriptionDE: "Bedienpanel mit Touchscreen zur Maschinensteuerung.", TypicalHazardCategories: []string{"hmi_error", "mode_confusion"}, TypicalEnergySources: []string{}, MapsToComponentType: "hmi", Tags: []string{"has_software", "user_interface"}, SortOrder: 73},
{ID: "C074", NameDE: "Industrierechner (IPC)", NameEN: "Industrial PC", Category: "control", DescriptionDE: "Industrie-PC fuer komplexe Steuerungs- und Datenverarbeitungsaufgaben.", TypicalHazardCategories: []string{"software_fault", "configuration_error"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"has_software", "programmable", "networked"}, SortOrder: 74},
{ID: "C075", NameDE: "Motion Controller", NameEN: "Motion Controller", Category: "control", DescriptionDE: "Achscontroller fuer synchronisierte Mehrachsbewegungen.", TypicalHazardCategories: []string{"software_fault", "mechanical_hazard"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"has_software", "programmable"}, SortOrder: 75},
{ID: "C076", NameDE: "Sicherheitsrelais", NameEN: "Safety Relay", Category: "control", DescriptionDE: "Sicherheitsschaltgeraet fuer Not-Halt, Schutztuer etc.", TypicalHazardCategories: []string{"safety_function_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"safety_device"}, SortOrder: 76},
{ID: "C077", NameDE: "Antriebsregler", NameEN: "Drive Controller", Category: "control", DescriptionDE: "Intelligenter Antriebsregler mit integrierten Sicherheitsfunktionen.", TypicalHazardCategories: []string{"software_fault", "electrical_hazard"}, TypicalEnergySources: []string{"EN04"}, MapsToComponentType: "controller", Tags: []string{"has_software", "programmable"}, SortOrder: 77},
{ID: "C078", NameDE: "Remote I/O", NameEN: "Remote I/O Module", Category: "control", DescriptionDE: "Dezentrales Ein-/Ausgangsmodul im Feldbus.", TypicalHazardCategories: []string{"communication_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"networked"}, SortOrder: 78},
{ID: "C079", NameDE: "Bedienpult", NameEN: "Control Desk", Category: "control", DescriptionDE: "Zentrales Bedienpult mit Tastern, Schaltern und Anzeigen.", TypicalHazardCategories: []string{"hmi_error", "mode_confusion"}, TypicalEnergySources: []string{}, MapsToComponentType: "hmi", Tags: []string{"user_interface"}, SortOrder: 79},
{ID: "C080", NameDE: "Datenschreiber/Logger", NameEN: "Data Logger", Category: "control", DescriptionDE: "Geraet zur Aufzeichnung von Prozessparametern.", TypicalHazardCategories: []string{"logging_audit_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"has_software"}, SortOrder: 80},
// ── Category: sensor (C081-C090) ────────────────────────────────────────
{ID: "C081", NameDE: "Positionssensor", NameEN: "Position Sensor", Category: "sensor", DescriptionDE: "Induktiver, kapazitiver oder optischer Positionssensor.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part"}, SortOrder: 81},
{ID: "C082", NameDE: "Kamerasystem", NameEN: "Camera System", Category: "sensor", DescriptionDE: "Industriekamera fuer Bildverarbeitung und Qualitaetskontrolle.", TypicalHazardCategories: []string{"sensor_spoofing", "false_classification"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part", "networked"}, SortOrder: 82},
{ID: "C083", NameDE: "Kraftsensor", NameEN: "Force Sensor", Category: "sensor", DescriptionDE: "Dehnungsmessstreifen oder piezoelektrischer Kraftsensor.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part"}, SortOrder: 83},
{ID: "C084", NameDE: "Temperatursensor", NameEN: "Temperature Sensor", Category: "sensor", DescriptionDE: "Thermocouple oder PT100 zur Temperaturueberwachung.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part"}, SortOrder: 84},
{ID: "C085", NameDE: "Drucksensor", NameEN: "Pressure Sensor", Category: "sensor", DescriptionDE: "Sensor zur Ueberwachung von Druck in Hydraulik- oder Pneumatiksystemen.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part"}, SortOrder: 85},
{ID: "C086", NameDE: "Drehgeber/Encoder", NameEN: "Rotary Encoder", Category: "sensor", DescriptionDE: "Absolut- oder Inkrementaldrehgeber zur Winkel-/Positionsmessung.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part"}, SortOrder: 86},
{ID: "C087", NameDE: "Laserscanner", NameEN: "Laser Scanner", Category: "sensor", DescriptionDE: "Sicherheits-Laserscanner zur Ueberwachung von Schutzzonen.", TypicalHazardCategories: []string{"sensor_spoofing", "safety_function_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part", "safety_device"}, SortOrder: 87},
{ID: "C088", NameDE: "Beschleunigungssensor", NameEN: "Accelerometer", Category: "sensor", DescriptionDE: "Sensor zur Vibrations- und Beschleunigungsmessung.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part"}, SortOrder: 88},
{ID: "C089", NameDE: "Durchflusssensor", NameEN: "Flow Sensor", Category: "sensor", DescriptionDE: "Sensor zur Ueberwachung des Volumenstrom.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part"}, SortOrder: 89},
{ID: "C090", NameDE: "Fuellstandsensor", NameEN: "Level Sensor", Category: "sensor", DescriptionDE: "Sensor zur Ueberwachung des Fuellstands in Tanks und Behaeltern.", TypicalHazardCategories: []string{"sensor_spoofing"}, TypicalEnergySources: []string{}, MapsToComponentType: "sensor", Tags: []string{"sensor_part"}, SortOrder: 90},
// ── Category: actuator (C091-C100) ──────────────────────────────────────
{ID: "C091", NameDE: "Magnetventil", NameEN: "Solenoid Valve", Category: "actuator", DescriptionDE: "Elektromagnetisch betaetigtes Ventil fuer Pneumatik oder Hydraulik.", TypicalHazardCategories: []string{"pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN05", "EN06"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part"}, SortOrder: 91},
{ID: "C092", NameDE: "Linearantrieb (elektrisch)", NameEN: "Electric Linear Actuator", Category: "actuator", DescriptionDE: "Elektrischer Linearantrieb fuer Positionieraufgaben.", TypicalHazardCategories: []string{"mechanical_hazard", "electrical_hazard"}, TypicalEnergySources: []string{"EN01", "EN04"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part", "moving_part"}, SortOrder: 92},
{ID: "C093", NameDE: "Proportionalventil", NameEN: "Proportional Valve", Category: "actuator", DescriptionDE: "Stetig regelbares Ventil fuer praezise Drucksteuerung.", TypicalHazardCategories: []string{"pneumatic_hydraulic"}, TypicalEnergySources: []string{"EN05", "EN06"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part"}, SortOrder: 93},
{ID: "C094", NameDE: "Heizelement", NameEN: "Heating Element", Category: "actuator", DescriptionDE: "Elektrisches Heizelement fuer Temperierung von Werkzeugen oder Medien.", TypicalHazardCategories: []string{"thermal_hazard", "electrical_hazard"}, TypicalEnergySources: []string{"EN07"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part", "high_temperature"}, SortOrder: 94},
{ID: "C095", NameDE: "Kuehlaggregat", NameEN: "Cooling Unit", Category: "actuator", DescriptionDE: "Kuehlanlage fuer Prozesse oder Schaltschraenke.", TypicalHazardCategories: []string{"thermal_hazard"}, TypicalEnergySources: []string{"EN07"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part"}, SortOrder: 95},
{ID: "C096", NameDE: "Luefter/Geblaese", NameEN: "Fan/Blower", Category: "actuator", DescriptionDE: "Luefter zur Kuehlung oder Absaugung.", TypicalHazardCategories: []string{"mechanical_hazard", "noise_vibration"}, TypicalEnergySources: []string{"EN02"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part", "rotating_part"}, SortOrder: 96},
{ID: "C097", NameDE: "Dosierpumpe", NameEN: "Dosing Pump", Category: "actuator", DescriptionDE: "Praezisionspumpe zur Dosierung von Fluessigkeiten oder Klebstoffen.", TypicalHazardCategories: []string{"pneumatic_hydraulic", "material_environmental"}, TypicalEnergySources: []string{"EN05"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part"}, SortOrder: 97},
{ID: "C098", NameDE: "Elektromagnet", NameEN: "Electromagnet", Category: "actuator", DescriptionDE: "Elektromagnet fuer Halten, Spannen oder Foerdern.", TypicalHazardCategories: []string{"electrical_hazard", "emc_hazard"}, TypicalEnergySources: []string{"EN04"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part", "stored_energy"}, SortOrder: 98},
{ID: "C099", NameDE: "Piezo-Aktuator", NameEN: "Piezo Actuator", Category: "actuator", DescriptionDE: "Piezoelektrischer Aktuator fuer hochpraezise Mikrobewegungen.", TypicalHazardCategories: []string{"electrical_hazard"}, TypicalEnergySources: []string{"EN04"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part"}, SortOrder: 99},
{ID: "C100", NameDE: "Spannvorrichtung", NameEN: "Clamping Device", Category: "actuator", DescriptionDE: "Mechanische, pneumatische oder hydraulische Spannvorrichtung.", TypicalHazardCategories: []string{"mechanical_hazard"}, TypicalEnergySources: []string{"EN01", "EN05", "EN06"}, MapsToComponentType: "actuator", Tags: []string{"actuator_part", "clamping_part", "pinch_point"}, SortOrder: 100},
// ── Category: safety (C101-C110) ────────────────────────────────────────
{ID: "C101", NameDE: "Not-Halt-Taster", NameEN: "Emergency Stop Button", Category: "safety", DescriptionDE: "Pilzfoermiger Taster fuer den sofortigen Maschinenstopp.", TypicalHazardCategories: []string{"safety_function_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"safety_device", "emergency_stop"}, SortOrder: 101},
{ID: "C102", NameDE: "Lichtgitter / Lichtvorhang", NameEN: "Light Curtain", Category: "safety", DescriptionDE: "Optoelektronische Schutzeinrichtung zur Zugangsueberwachung.", TypicalHazardCategories: []string{"safety_function_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"safety_device", "interlocked"}, SortOrder: 102},
{ID: "C103", NameDE: "Sicherheits-Laserscanner", NameEN: "Safety Laser Scanner", Category: "safety", DescriptionDE: "Scanner zur Ueberwachung von Schutzfeldern und Warnfeldern.", TypicalHazardCategories: []string{"safety_function_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"safety_device", "sensor_part"}, SortOrder: 103},
{ID: "C104", NameDE: "Sicherheitsschalter (Tuerkontakt)", NameEN: "Safety Switch (Door Contact)", Category: "safety", DescriptionDE: "Positions- oder Schluesseltransferschalter an Schutztueren.", TypicalHazardCategories: []string{"safety_function_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"safety_device", "interlocked"}, SortOrder: 104},
{ID: "C105", NameDE: "Zuhaltung", NameEN: "Guard Locking Device", Category: "safety", DescriptionDE: "Elektromechanische Zuhaltung fuer Schutztueren.", TypicalHazardCategories: []string{"safety_function_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"safety_device", "interlocked"}, SortOrder: 105},
{ID: "C106", NameDE: "Zweihandschaltung", NameEN: "Two-Hand Control", Category: "safety", DescriptionDE: "Zweihandschaltung zur sicheren Ausloesung von Hubbewegungen.", TypicalHazardCategories: []string{"safety_function_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"safety_device"}, SortOrder: 106},
{ID: "C107", NameDE: "Sicherheitsmagnet / RFID-Schalter", NameEN: "Safety RFID Switch", Category: "safety", DescriptionDE: "Manipulationssicherer Sicherheitsschalter mit RFID-Codierung.", TypicalHazardCategories: []string{"safety_function_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"safety_device", "interlocked"}, SortOrder: 107},
{ID: "C108", NameDE: "Schaltmatte / Trittmatte", NameEN: "Safety Mat", Category: "safety", DescriptionDE: "Druckempfindliche Matte zur Personenerkennung in Gefahrzonen.", TypicalHazardCategories: []string{"safety_function_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"safety_device"}, SortOrder: 108},
{ID: "C109", NameDE: "Seilzugschalter", NameEN: "Pull-Wire Switch", Category: "safety", DescriptionDE: "Seilzug-Notschalter entlang von Foerderstrecken.", TypicalHazardCategories: []string{"safety_function_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"safety_device", "emergency_stop"}, SortOrder: 109},
{ID: "C110", NameDE: "Zustimmtaster", NameEN: "Enabling Device", Category: "safety", DescriptionDE: "Dreistufiger Zustimmtaster fuer den Einrichtbetrieb.", TypicalHazardCategories: []string{"safety_function_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "controller", Tags: []string{"safety_device"}, SortOrder: 110},
// ── Category: it_network (C111-C120) ────────────────────────────────────
{ID: "C111", NameDE: "Industrie-Switch (managed)", NameEN: "Managed Industrial Switch", Category: "it_network", DescriptionDE: "Managed Ethernet Switch fuer das Maschinennetzwerk.", TypicalHazardCategories: []string{"communication_failure", "unauthorized_access"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"networked", "it_component"}, SortOrder: 111},
{ID: "C112", NameDE: "Industrie-Router", NameEN: "Industrial Router", Category: "it_network", DescriptionDE: "Router zur Segmentierung und Absicherung des Maschinennetzwerks.", TypicalHazardCategories: []string{"communication_failure", "unauthorized_access"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"networked", "it_component"}, SortOrder: 112},
{ID: "C113", NameDE: "Industrie-Firewall", NameEN: "Industrial Firewall", Category: "it_network", DescriptionDE: "Firewall zum Schutz des OT-Netzwerks vor externen Angriffen.", TypicalHazardCategories: []string{"unauthorized_access"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"networked", "it_component", "security_device"}, SortOrder: 113},
{ID: "C114", NameDE: "IoT-Gateway", NameEN: "IoT Gateway", Category: "it_network", DescriptionDE: "Gateway fuer die Anbindung von Maschinen an Cloud/Edge.", TypicalHazardCategories: []string{"communication_failure", "unauthorized_access"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"networked", "it_component", "has_software"}, SortOrder: 114},
{ID: "C115", NameDE: "Edge-Computing-Einheit", NameEN: "Edge Computing Unit", Category: "it_network", DescriptionDE: "Lokale Recheneinheit fuer Datenvorverarbeitung und KI-Inferenz.", TypicalHazardCategories: []string{"software_fault", "communication_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"networked", "it_component", "has_software", "has_ai"}, SortOrder: 115},
{ID: "C116", NameDE: "WLAN Access Point (Industrie)", NameEN: "Industrial WiFi Access Point", Category: "it_network", DescriptionDE: "Drahtloser Netzwerkzugang im Maschinenumfeld.", TypicalHazardCategories: []string{"communication_failure", "unauthorized_access"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"networked", "it_component", "wireless"}, SortOrder: 116},
{ID: "C117", NameDE: "OPC UA Server", NameEN: "OPC UA Server", Category: "it_network", DescriptionDE: "OPC UA Kommunikationsserver fuer Maschine-zu-Maschine-Vernetzung.", TypicalHazardCategories: []string{"communication_failure", "unauthorized_access"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"networked", "it_component", "has_software"}, SortOrder: 117},
{ID: "C118", NameDE: "VPN-Appliance", NameEN: "VPN Appliance", Category: "it_network", DescriptionDE: "VPN-Geraet fuer sichere Fernzugriffe auf die Maschinensteuerung.", TypicalHazardCategories: []string{"unauthorized_access"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"networked", "it_component", "security_device"}, SortOrder: 118},
{ID: "C119", NameDE: "KI-Inferenzmodul", NameEN: "AI Inference Module", Category: "it_network", DescriptionDE: "Dediziertes KI-Modul (GPU/TPU) fuer Echtzeit-Inferenz.", TypicalHazardCategories: []string{"false_classification", "model_drift", "unintended_bias"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"has_ai", "has_software", "networked"}, SortOrder: 119},
{ID: "C120", NameDE: "Feldbus-Koppler", NameEN: "Fieldbus Coupler", Category: "it_network", DescriptionDE: "Koppler fuer PROFINET, EtherCAT oder andere Feldbussysteme.", TypicalHazardCategories: []string{"communication_failure"}, TypicalEnergySources: []string{}, MapsToComponentType: "network", Tags: []string{"networked", "it_component"}, SortOrder: 120},
}
}
// GetEnergySources returns the complete built-in energy source library with 20 entries.
func GetEnergySources() []EnergySourceEntry {
return []EnergySourceEntry{
{ID: "EN01", NameDE: "Kinetische Energie (translatorisch)", NameEN: "Kinetic Energy (Translational)", DescriptionDE: "Energie durch lineare Bewegung von Maschinenteilen.", TypicalComponents: []string{"C001", "C002", "C005", "C008"}, TypicalHazardCategories: []string{"mechanical_hazard"}, Tags: []string{"kinetic", "translational"}, SortOrder: 1},
{ID: "EN02", NameDE: "Kinetische Energie (rotatorisch)", NameEN: "Kinetic Energy (Rotational)", DescriptionDE: "Energie durch Drehbewegung von Wellen, Spindeln, Motoren.", TypicalComponents: []string{"C004", "C006", "C009", "C031"}, TypicalHazardCategories: []string{"mechanical_hazard"}, Tags: []string{"kinetic", "rotational"}, SortOrder: 2},
{ID: "EN03", NameDE: "Potentielle Energie (Lage)", NameEN: "Potential Energy (Gravitational)", DescriptionDE: "Energie durch angehobene Lasten oder Maschinenteile.", TypicalComponents: []string{"C014", "C029", "C030"}, TypicalHazardCategories: []string{"mechanical_hazard"}, Tags: []string{"potential", "gravitational"}, SortOrder: 3},
{ID: "EN04", NameDE: "Elektrische Energie", NameEN: "Electrical Energy", DescriptionDE: "Netz- oder Batteriespannung in Schaltschraenken und Antrieben.", TypicalComponents: []string{"C061", "C062", "C063", "C070"}, TypicalHazardCategories: []string{"electrical_hazard"}, Tags: []string{"electrical_energy"}, SortOrder: 4},
{ID: "EN05", NameDE: "Hydraulische Energie", NameEN: "Hydraulic Energy", DescriptionDE: "Druckenergie in Hydrauliksystemen.", TypicalComponents: []string{"C041", "C042", "C043", "C044"}, TypicalHazardCategories: []string{"pneumatic_hydraulic"}, Tags: []string{"hydraulic_pressure"}, SortOrder: 5},
{ID: "EN06", NameDE: "Pneumatische Energie", NameEN: "Pneumatic Energy", DescriptionDE: "Druckenergie in Druckluftsystemen.", TypicalComponents: []string{"C051", "C052", "C053", "C060"}, TypicalHazardCategories: []string{"pneumatic_hydraulic"}, Tags: []string{"pneumatic_pressure"}, SortOrder: 6},
{ID: "EN07", NameDE: "Thermische Energie", NameEN: "Thermal Energy", DescriptionDE: "Waerme- oder Kaelteenergie in Prozessen oder Kuehlsystemen.", TypicalComponents: []string{"C094", "C095", "C047"}, TypicalHazardCategories: []string{"thermal_hazard"}, Tags: []string{"thermal"}, SortOrder: 7},
{ID: "EN08", NameDE: "Gespeicherte Energie (elektrisch)", NameEN: "Stored Energy (Electrical)", DescriptionDE: "Energie in Kondensatoren, Batterien oder USV-Systemen.", TypicalComponents: []string{"C067", "C034"}, TypicalHazardCategories: []string{"electrical_hazard"}, Tags: []string{"stored_energy", "electrical_energy"}, SortOrder: 8},
{ID: "EN09", NameDE: "Gespeicherte Energie (mechanisch)", NameEN: "Stored Energy (Mechanical)", DescriptionDE: "Energie in Federn, Schwungraedern oder Gegengewichten.", TypicalComponents: []string{"C013"}, TypicalHazardCategories: []string{"mechanical_hazard"}, Tags: []string{"stored_energy", "mechanical"}, SortOrder: 9},
{ID: "EN10", NameDE: "Gespeicherte Energie (hydraulisch)", NameEN: "Stored Energy (Hydraulic)", DescriptionDE: "Restdruck in Hydraulikspeichern und -leitungen.", TypicalComponents: []string{"C044", "C045"}, TypicalHazardCategories: []string{"pneumatic_hydraulic"}, Tags: []string{"stored_energy", "hydraulic_pressure"}, SortOrder: 10},
{ID: "EN11", NameDE: "Gespeicherte Energie (pneumatisch)", NameEN: "Stored Energy (Pneumatic)", DescriptionDE: "Restdruck in Druckluftbehaeltern und -leitungen.", TypicalComponents: []string{"C060", "C051"}, TypicalHazardCategories: []string{"pneumatic_hydraulic"}, Tags: []string{"stored_energy", "pneumatic_pressure"}, SortOrder: 11},
{ID: "EN12", NameDE: "Strahlung (optisch/Laser)", NameEN: "Radiation (Optical/Laser)", DescriptionDE: "Licht- oder Laserstrahlung bei Schneid-, Schweiß- oder Messprozessen.", TypicalComponents: []string{"C016", "C087"}, TypicalHazardCategories: []string{"thermal_hazard"}, Tags: []string{"radiation"}, SortOrder: 12},
{ID: "EN13", NameDE: "Strahlung (elektromagnetisch)", NameEN: "Radiation (Electromagnetic)", DescriptionDE: "EMV-Stoerungen durch Frequenzumrichter, Motoren oder Funksender.", TypicalComponents: []string{"C034", "C098", "C116"}, TypicalHazardCategories: []string{"emc_hazard"}, Tags: []string{"radiation", "electromagnetic"}, SortOrder: 13},
{ID: "EN14", NameDE: "Schallenergie", NameEN: "Acoustic Energy", DescriptionDE: "Laermemission durch Antriebe, Kompressoren oder Bearbeitungsprozesse.", TypicalComponents: []string{"C052", "C006", "C020"}, TypicalHazardCategories: []string{"noise_vibration"}, Tags: []string{"acoustic"}, SortOrder: 14},
{ID: "EN15", NameDE: "Vibrationsenergie", NameEN: "Vibration Energy", DescriptionDE: "Mechanische Schwingungen durch rotierende oder oszillierende Teile.", TypicalComponents: []string{"C020", "C006"}, TypicalHazardCategories: []string{"noise_vibration"}, Tags: []string{"vibration"}, SortOrder: 15},
{ID: "EN16", NameDE: "Chemische Energie", NameEN: "Chemical Energy", DescriptionDE: "Gefahrstoffe, Klebstoffe, Kuehlschmierstoffe oder Hydraulikoele.", TypicalComponents: []string{"C097", "C046"}, TypicalHazardCategories: []string{"material_environmental"}, Tags: []string{"chemical"}, SortOrder: 16},
{ID: "EN17", NameDE: "Magnetische Energie", NameEN: "Magnetic Energy", DescriptionDE: "Magnetfelder durch Elektromagnete oder Permanentmagnete.", TypicalComponents: []string{"C098"}, TypicalHazardCategories: []string{"emc_hazard"}, Tags: []string{"magnetic"}, SortOrder: 17},
{ID: "EN18", NameDE: "Datenenergie (Cyber)", NameEN: "Data/Cyber Energy", DescriptionDE: "Logische Steuerungsdaten und Kommunikation als Angriffsvektor.", TypicalComponents: []string{"C111", "C112", "C114", "C119"}, TypicalHazardCategories: []string{"unauthorized_access", "firmware_corruption"}, Tags: []string{"cyber", "data"}, SortOrder: 18},
{ID: "EN19", NameDE: "KI-Modell-Energie", NameEN: "AI Model Energy", DescriptionDE: "Trainierte Modellparameter als potenzielle Fehlerquelle bei Inferenz.", TypicalComponents: []string{"C119", "C115"}, TypicalHazardCategories: []string{"false_classification", "model_drift", "data_poisoning", "unintended_bias"}, Tags: []string{"ai_model", "cyber"}, SortOrder: 19},
{ID: "EN20", NameDE: "Ergonomische Belastung", NameEN: "Ergonomic Load", DescriptionDE: "Koerperliche Belastung durch ungünstige Arbeitshaltungen oder Gewichte.", TypicalComponents: []string{"C024", "C029", "C030"}, TypicalHazardCategories: []string{"ergonomic"}, Tags: []string{"ergonomic"}, SortOrder: 20},
}
}
// ValidComponentLibraryCategories returns all valid component library categories.
func ValidComponentLibraryCategories() []string {
return []string{
"mechanical", "structural", "drive", "hydraulic", "pneumatic",
"electrical", "control", "sensor", "actuator", "safety", "it_network",
}
}

View File

@@ -0,0 +1,131 @@
package iace
import "testing"
// TestGetComponentLibrary_EntryCount verifies the component library has exactly 120 entries.
func TestGetComponentLibrary_EntryCount(t *testing.T) {
entries := GetComponentLibrary()
if len(entries) != 120 {
t.Fatalf("GetComponentLibrary returned %d entries, want 120", len(entries))
}
}
// TestGetComponentLibrary_UniqueIDs verifies all component IDs are unique.
func TestGetComponentLibrary_UniqueIDs(t *testing.T) {
entries := GetComponentLibrary()
seen := make(map[string]bool)
for _, e := range entries {
if e.ID == "" {
t.Errorf("component has empty ID: %s", e.NameDE)
}
if seen[e.ID] {
t.Errorf("duplicate component ID: %s", e.ID)
}
seen[e.ID] = true
}
}
// TestGetComponentLibrary_ValidCategories verifies all categories are valid.
func TestGetComponentLibrary_ValidCategories(t *testing.T) {
validCats := make(map[string]bool)
for _, c := range ValidComponentLibraryCategories() {
validCats[c] = true
}
for _, e := range GetComponentLibrary() {
if !validCats[e.Category] {
t.Errorf("component %s has invalid category %q", e.ID, e.Category)
}
}
}
// TestGetComponentLibrary_NonEmptyFields verifies required fields are filled.
func TestGetComponentLibrary_NonEmptyFields(t *testing.T) {
for _, e := range GetComponentLibrary() {
if e.NameDE == "" {
t.Errorf("component %s: NameDE is empty", e.ID)
}
if e.NameEN == "" {
t.Errorf("component %s: NameEN is empty", e.ID)
}
if e.MapsToComponentType == "" {
t.Errorf("component %s: MapsToComponentType is empty", e.ID)
}
if len(e.Tags) == 0 {
t.Errorf("component %s: Tags is empty", e.ID)
}
}
}
// TestGetComponentLibrary_CategoryDistribution verifies expected category counts.
func TestGetComponentLibrary_CategoryDistribution(t *testing.T) {
counts := make(map[string]int)
for _, e := range GetComponentLibrary() {
counts[e.Category]++
}
expected := map[string]int{
"mechanical": 20,
"structural": 10,
"drive": 10,
"hydraulic": 10,
"pneumatic": 10,
"electrical": 10,
"control": 10,
"sensor": 10,
"actuator": 10,
"safety": 10,
"it_network": 10,
}
for cat, want := range expected {
got := counts[cat]
if got != want {
t.Errorf("category %s: got %d entries, want %d", cat, got, want)
}
}
}
// TestGetEnergySources_EntryCount verifies the energy source library has exactly 20 entries.
func TestGetEnergySources_EntryCount(t *testing.T) {
entries := GetEnergySources()
if len(entries) != 20 {
t.Fatalf("GetEnergySources returned %d entries, want 20", len(entries))
}
}
// TestGetEnergySources_UniqueIDs verifies all energy source IDs are unique.
func TestGetEnergySources_UniqueIDs(t *testing.T) {
entries := GetEnergySources()
seen := make(map[string]bool)
for _, e := range entries {
if e.ID == "" {
t.Errorf("energy source has empty ID: %s", e.NameDE)
}
if seen[e.ID] {
t.Errorf("duplicate energy source ID: %s", e.ID)
}
seen[e.ID] = true
}
}
// TestGetEnergySources_NonEmptyFields verifies required fields are filled.
func TestGetEnergySources_NonEmptyFields(t *testing.T) {
for _, e := range GetEnergySources() {
if e.NameDE == "" {
t.Errorf("energy source %s: NameDE is empty", e.ID)
}
if e.NameEN == "" {
t.Errorf("energy source %s: NameEN is empty", e.ID)
}
if len(e.Tags) == 0 {
t.Errorf("energy source %s: Tags is empty", e.ID)
}
}
}
// TestGetEnergySources_AllHaveTags verifies every energy source has at least one tag.
func TestGetEnergySources_AllHaveTags(t *testing.T) {
for _, e := range GetEnergySources() {
if len(e.Tags) == 0 {
t.Errorf("energy source %s has no tags", e.ID)
}
}
}

View File

@@ -48,185 +48,194 @@ func GetControlsLibrary() []ControlLibraryEntry {
{ID: "CTRL.REQ.028", Domain: "REQ", Title: "Kalibrierungsanforderungen definiert", Description: "Anforderungen an Kalibrierung und Messkettenpruefung fuer sicherheitsrelevante Sensoren sind spezifiziert.", PriorityHint: "medium", MapsToHazardCategories: []string{"safety_function_failure", "mechanical_hazard"}, EvidenceExamples: []string{"Kalibrierplan", "Kalibrieranforderungen"}, ReductionType: "design", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.REQ.029", Domain: "REQ", Title: "Produktlebenszyklusanforderungen erfasst", Description: "Anforderungen fuer den gesamten Produktlebenszyklus (Inbetriebnahme bis Ausserbetriebnahme) sind dokumentiert.", PriorityHint: "medium", MapsToHazardCategories: []string{"maintenance_hazard"}, EvidenceExamples: []string{"Lebenszyklusplan", "End-of-Life-Konzept"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.REQ.030", Domain: "REQ", Title: "Notfallplanung als Anforderung", Description: "Anforderungen fuer Notfallbetrieb, Degradation und Wiederanlauf nach Ausfall sind spezifiziert.", PriorityHint: "medium", MapsToHazardCategories: []string{"safety_function_failure", "communication_failure"}, EvidenceExamples: []string{"Notfallplan-Anforderungen", "Degradationskonzept"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
// ── Domain ARCH (Architecture & Design) ───────────────────────────────
{ID: "CTRL.ARCH.001", Domain: "ARCH", Title: "Redundanzkonzept implementiert", Description: "Sicherheitsrelevante Funktionen sind durch Redundanz abgesichert, sodass ein Einzelfehler nicht zum Ausfall fuehrt.", PriorityHint: "critical", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"Redundanzkonzept", "FMEA-Nachweis"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.ARCH.002", Domain: "ARCH", Title: "Fail-Safe-Verhalten definiert", Description: "Das System geht bei Erkennung eines Fehlers in einen sicheren Zustand ueber, der keine Gefaehrdung verursacht.", PriorityHint: "critical", MapsToHazardCategories: []string{"safety_function_failure", "software_fault"}, EvidenceExamples: []string{"Fail-Safe-Konzept", "Safe-State-Beschreibung"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.ARCH.003", Domain: "ARCH", Title: "Diagnostics Coverage sichergestellt", Description: "Die Diagnosedeckung (DC) fuer alle sicherheitsrelevanten Hardwarekomponenten ist gemaess SIL/PL-Anforderung erreicht.", PriorityHint: "critical", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"DC-Nachweis", "Diagnosekonzept"}, ReductionType: "design", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.ARCH.004", Domain: "ARCH", Title: "Diverse Softwareimplementierung", Description: "Fuer SIL3/SIL4-Anforderungen werden zwei unabhaengig entwickelte Softwareversionen eingesetzt.", PriorityHint: "critical", MapsToHazardCategories: []string{"software_fault", "safety_function_failure"}, EvidenceExamples: []string{"Diversitaetsnachweis", "Entwicklungsprozess-Dokument"}, ReductionType: "design", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.ARCH.005", Domain: "ARCH", Title: "Watchdog-Timer implementiert", Description: "Ein hardwarebasierter Watchdog-Timer ueberwacht die korrekte Ausfuehrung der Safety-Software.", PriorityHint: "critical", MapsToHazardCategories: []string{"timing_error", "software_fault"}, EvidenceExamples: []string{"Watchdog-Designdokument", "Testnachweis"}, ReductionType: "design", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.ARCH.006", Domain: "ARCH", Title: "Safe-State-Transition spezifiziert", Description: "Alle Zustandsuebergaenge in den sicheren Zustand sind vollstaendig definiert und getestet.", PriorityHint: "critical", MapsToHazardCategories: []string{"safety_function_failure", "mode_confusion"}, EvidenceExamples: []string{"Zustandsdiagramm", "Transition-Testprotokoll"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.ARCH.007", Domain: "ARCH", Title: "Fehlererkennung implementiert", Description: "Mechanismen zur Erkennung von Einzel- und Mehrfachfehlern (z.B. CRC, ECC, Parity) sind implementiert.", PriorityHint: "critical", MapsToHazardCategories: []string{"software_fault", "electrical_hazard"}, EvidenceExamples: []string{"Fehlererkennungskonzept", "CRC-Testnachweis"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.ARCH.008", Domain: "ARCH", Title: "Modulares Design umgesetzt", Description: "Die Systemarchitektur ist modular aufgebaut, sodass sicherheitsrelevante Module klar abgegrenzt sind.", PriorityHint: "critical", MapsToHazardCategories: []string{"software_fault", "integration_error"}, EvidenceExamples: []string{"Architekturdiagramm", "Modulbeschreibung"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.ARCH.009", Domain: "ARCH", Title: "Partitionierung safety/non-safety", Description: "Safety- und Non-Safety-Softwareteile sind klar partitioniert und gegenseitige Beeinflussung ist unterbunden.", PriorityHint: "critical", MapsToHazardCategories: []string{"safety_function_failure", "software_fault"}, EvidenceExamples: []string{"Partitionierungsnachweis", "MPU-Konfiguration"}, ReductionType: "design", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.ARCH.010", Domain: "ARCH", Title: "Hardware-Absicherung sicherheitsrelevanter Funktionen", Description: "Sicherheitsrelevante Hardware-Pfade sind durch dedizierte Schutzschaltungen gegen Fehler abgesichert.", PriorityHint: "critical", MapsToHazardCategories: []string{"electrical_hazard", "safety_function_failure"}, EvidenceExamples: []string{"HW-Schutzkonzept", "Schaltplan-Review"}, ReductionType: "design", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.ARCH.011", Domain: "ARCH", Title: "Sicherer Bootvorgang implementiert", Description: "Der Bootvorgang prueft die Integritaet der Software bevor sicherheitsrelevante Funktionen aktiviert werden.", PriorityHint: "high", MapsToHazardCategories: []string{"firmware_corruption", "software_fault"}, EvidenceExamples: []string{"Secure-Boot-Konzept", "Testprotokoll"}, ReductionType: "design", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.ARCH.012", Domain: "ARCH", Title: "Speicherpartitionierung umgesetzt", Description: "Speicherbereiche fuer Safety- und Non-Safety-Code sind durch Hardware (MPU/MMU) getrennt.", PriorityHint: "high", MapsToHazardCategories: []string{"software_fault"}, EvidenceExamples: []string{"MPU-Konfigurationsdokument", "Speicher-Map"}, ReductionType: "design", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.ARCH.013", Domain: "ARCH", Title: "Zufaellige Fehlerrate reduziert", Description: "Massnahmen zur Reduzierung der zufaelligen Hardwarefehlerrate (PFH/PFD) sind im Design beruecksichtigt.", PriorityHint: "high", MapsToHazardCategories: []string{"safety_function_failure", "electrical_hazard"}, EvidenceExamples: []string{"PFH-Berechnung", "FMEDA-Nachweis"}, ReductionType: "design", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.ARCH.014", Domain: "ARCH", Title: "Sicherheitsarchitektur dokumentiert", Description: "Die Sicherheitsarchitektur ist vollstaendig dokumentiert und durch Review freigegeben.", PriorityHint: "high", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"Sicherheitsarchitektur-Dokument", "Review-Protokoll"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.ARCH.015", Domain: "ARCH", Title: "Redundante Kommunikation", Description: "Sicherheitsrelevante Kommunikationspfade sind redundant ausgelegt oder mit Fehlererkennungsmechanismen abgesichert.", PriorityHint: "high", MapsToHazardCategories: []string{"communication_failure", "safety_function_failure"}, EvidenceExamples: []string{"Kommunikationsarchitektur", "Redundanznachweis"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.ARCH.016", Domain: "ARCH", Title: "Independent Safety Monitor", Description: "Ein unabhaengiger Safety-Monitor ueberwacht die Hauptsicherheitsfunktion und kann diese deaktivieren.", PriorityHint: "high", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"Monitor-Designdokument", "Unabhaengigkeitsnachweis"}, ReductionType: "design", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.ARCH.017", Domain: "ARCH", Title: "Safety-Integrity-Architektur nachgewiesen", Description: "Die Systemarchitektur erfuellt die Anforderungen des zugewiesenen SIL/PL gemaess IEC 61508 oder ISO 13849.", PriorityHint: "high", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"SIL-Architekturnachweis", "PL-Zertifikat"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.ARCH.018", Domain: "ARCH", Title: "Diversitaere Hardware eingesetzt", Description: "Fuer kritische Sicherheitsfunktionen werden unterschiedliche Hardwaretypen eingesetzt, um Systematikfehler zu vermeiden.", PriorityHint: "high", MapsToHazardCategories: []string{"safety_function_failure", "electrical_hazard"}, EvidenceExamples: []string{"Diversitaetsnachweis", "BOM-Review"}, ReductionType: "design", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.ARCH.019", Domain: "ARCH", Title: "Systemgrenzen definiert", Description: "Die Systemgrenzen und Schnittstellen zu anderen Systemen und Betreibern sind klar definiert.", PriorityHint: "high", MapsToHazardCategories: []string{"integration_error"}, EvidenceExamples: []string{"Systemgrenzendokument", "Interface-Control-Document"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.ARCH.020", Domain: "ARCH", Title: "Energiemanagement-Architektur", Description: "Die Architektur stellt sicher, dass bei Energieausfall oder -schwankung ein sicherer Zustand erreicht wird.", PriorityHint: "high", MapsToHazardCategories: []string{"electrical_hazard", "safety_function_failure"}, EvidenceExamples: []string{"Energiemanagement-Konzept", "UPS-Design"}, ReductionType: "design", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.ARCH.021", Domain: "ARCH", Title: "Schutzmassnahmen Hardware implementiert", Description: "Hardware-Schutzmassnahmen (Ueberstrom, Ueberspannung, thermischer Schutz) sind im Design integriert.", PriorityHint: "medium", MapsToHazardCategories: []string{"electrical_hazard", "thermal_hazard"}, EvidenceExamples: []string{"HW-Schutzschaltungs-Nachweis", "Pruefprotokoll"}, ReductionType: "design", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.ARCH.022", Domain: "ARCH", Title: "Thermisches Design umgesetzt", Description: "Waermemanagement und thermisches Design stellen den Betrieb innerhalb der zulaessigen Temperaturgrenzen sicher.", PriorityHint: "medium", MapsToHazardCategories: []string{"thermal_hazard"}, EvidenceExamples: []string{"Thermisches Design-Dokument", "Temperatursimulation"}, ReductionType: "design", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.ARCH.023", Domain: "ARCH", Title: "EMV-Design beruecksichtigt", Description: "EMV-gerechtes Design (Schirmung, Filterung, Layoutregeln) wurde bei der Leiterplattenentwicklung angewendet.", PriorityHint: "medium", MapsToHazardCategories: []string{"emc_hazard", "electrical_hazard"}, EvidenceExamples: []string{"EMV-Design-Review", "EMV-Testbericht"}, ReductionType: "design", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.ARCH.024", Domain: "ARCH", Title: "Mechanisches Schutzkonzept umgesetzt", Description: "Mechanische Schutzmassnahmen (Gehaeuse, IP-Schutzart, Vibrationsschutz) sind dem Einsatzbereich angepasst.", PriorityHint: "medium", MapsToHazardCategories: []string{"mechanical_hazard", "environmental_hazard"}, EvidenceExamples: []string{"IP-Zertifikat", "Vibrationstest-Protokoll"}, ReductionType: "design", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.ARCH.025", Domain: "ARCH", Title: "Wartungsarchitektur geplant", Description: "Die Systemarchitektur unterstuetzt planmaessige Wartung ohne Deaktivierung aller Sicherheitsfunktionen.", PriorityHint: "medium", MapsToHazardCategories: []string{"maintenance_hazard"}, EvidenceExamples: []string{"Wartungskonzept", "Architektur-Review"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.ARCH.026", Domain: "ARCH", Title: "Testpunkte im Design vorgesehen", Description: "Das Hardware-Design enthaelt dedizierte Testpunkte fuer Produktionstest und Instandhaltung.", PriorityHint: "medium", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"Testpunkt-Plan", "Layout-Review"}, ReductionType: "design", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.ARCH.027", Domain: "ARCH", Title: "Sicherer Zustand definiert und erreichbar", Description: "Der sichere Zustand des Systems ist eindeutig definiert und unter allen Fehlerbedingungen erreichbar.", PriorityHint: "medium", MapsToHazardCategories: []string{"safety_function_failure", "mode_confusion"}, EvidenceExamples: []string{"Safe-State-Definition", "Zustandsautomat"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.ARCH.028", Domain: "ARCH", Title: "Safe-State-Uebergang getestet", Description: "Der Uebergang in den sicheren Zustand wurde unter realen Fehlerbedingungen erprobt und dokumentiert.", PriorityHint: "medium", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"Safe-State-Testprotokoll", "Fehler-Injektionstest"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.ARCH.029", Domain: "ARCH", Title: "Back-to-Back-Test-Architektur unterstuetzt", Description: "Die Architektur ermoeglicht Back-to-Back-Tests zwischen diversitaeren Implementierungen.", PriorityHint: "medium", MapsToHazardCategories: []string{"software_fault"}, EvidenceExamples: []string{"B2B-Test-Konzept", "Vergleichstest-Protokoll"}, ReductionType: "design", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.ARCH.030", Domain: "ARCH", Title: "Diagnosemassnahmen vollstaendig", Description: "Alle Diagnosemassnahmen sind vollstaendig spezifiziert, implementiert und auf DC-Zielwert ueberprueft.", PriorityHint: "medium", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"Diagnosekonzept", "DC-Berechnung"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl"}},
// ── Domain SWDEV (Software Development) ───────────────────────────────
{ID: "CTRL.SWDEV.001", Domain: "SWDEV", Title: "SW-Safety-Plan erstellt", Description: "Ein Software-Safety-Plan mit Zielen, Aktivitaeten und Verantwortlichkeiten ist dokumentiert und freigegeben.", PriorityHint: "critical", MapsToHazardCategories: []string{"software_fault", "safety_function_failure"}, EvidenceExamples: []string{"SW-Safety-Plan", "Freigabeprotokoll"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.SWDEV.002", Domain: "SWDEV", Title: "MISRA-C Compliance sichergestellt", Description: "Der C-Quellcode erfuellt die MISRA-C-Regeln fuer sicherheitskritische Software.", PriorityHint: "critical", MapsToHazardCategories: []string{"software_fault"}, EvidenceExamples: []string{"MISRA-Pruefbericht", "Abweichungsliste"}, ReductionType: "design", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.SWDEV.003", Domain: "SWDEV", Title: "Statische Code-Analyse durchgefuehrt", Description: "Statische Code-Analyse mit einem qualifizierten Tool wurde durchgefuehrt und alle Befunde bewertet.", PriorityHint: "critical", MapsToHazardCategories: []string{"software_fault"}, EvidenceExamples: []string{"Statische-Analyse-Bericht", "Tool-Qualifikationsnachweis"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.SWDEV.004", Domain: "SWDEV", Title: "Code-Review-Prozess etabliert", Description: "Alle sicherheitsrelevanten Codeaenderungen werden durch einen unabhaengigen Review freigegeben.", PriorityHint: "critical", MapsToHazardCategories: []string{"software_fault"}, EvidenceExamples: []string{"Review-Protokolle", "Review-Checkliste"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.SWDEV.005", Domain: "SWDEV", Title: "Defensiv-Programmierung angewendet", Description: "Defensive Programmiertechniken (Eingabevalidierung, Assertions, Fehlerbehandlung) sind konsequent eingesetzt.", PriorityHint: "critical", MapsToHazardCategories: []string{"software_fault", "integration_error"}, EvidenceExamples: []string{"Coding-Standard", "Code-Review-Nachweis"}, ReductionType: "protective", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.SWDEV.006", Domain: "SWDEV", Title: "Unit-Tests fuer Safety-Funktionen", Description: "Alle sicherheitsrelevanten Softwaremodule haben dokumentierte Unit-Tests mit definierten Abbruchkriterien.", PriorityHint: "critical", MapsToHazardCategories: []string{"software_fault"}, EvidenceExamples: []string{"Unit-Test-Berichte", "Testplan"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.SWDEV.007", Domain: "SWDEV", Title: "Code-Coverage-Analyse durchgefuehrt", Description: "Die Testabdeckung (Statement, Branch, MC/DC) wurde gemessen und entspricht den SIL-Anforderungen.", PriorityHint: "critical", MapsToHazardCategories: []string{"software_fault"}, EvidenceExamples: []string{"Coverage-Bericht", "MC/DC-Nachweis"}, ReductionType: "protective", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.SWDEV.008", Domain: "SWDEV", Title: "Konfigurations-Management implementiert", Description: "Alle Softwareartefakte stehen unter Konfigurationsmanagement mit nachvollziehbarer Versionshistorie.", PriorityHint: "critical", MapsToHazardCategories: []string{"configuration_error", "software_fault"}, EvidenceExamples: []string{"CM-Plan", "Repository-Nachweis"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.SWDEV.009", Domain: "SWDEV", Title: "Compiler-Warnungen aktiviert und bewertet", Description: "Alle Compiler-Warnungen sind aktiviert und alle Warnungen sind bewertet und beseitigt oder dokumentiert begründet.", PriorityHint: "critical", MapsToHazardCategories: []string{"software_fault"}, EvidenceExamples: []string{"Compiler-Log", "Warnungsliste"}, ReductionType: "protective", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.SWDEV.010", Domain: "SWDEV", Title: "RTOS-Nutzung qualifiziert", Description: "Das eingesetzte Echtzeitbetriebssystem ist fuer den Sicherheitsintegritaetslevel qualifiziert.", PriorityHint: "critical", MapsToHazardCategories: []string{"timing_error", "software_fault"}, EvidenceExamples: []string{"RTOS-Qualifikationsnachweis", "Tool-Qualifikation"}, ReductionType: "design", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.SWDEV.011", Domain: "SWDEV", Title: "Speicherschutz aktiviert", Description: "Hardware-Speicherschutz (MPU) ist aktiviert und verhindert unerlaubten Speicherzugriff zwischen Modulen.", PriorityHint: "high", MapsToHazardCategories: []string{"software_fault"}, EvidenceExamples: []string{"MPU-Konfiguration", "Testnachweis"}, ReductionType: "protective", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.SWDEV.012", Domain: "SWDEV", Title: "Stack-Analyse durchgefuehrt", Description: "Der maximale Stack-Verbrauch aller Tasks wurde analysiert und Stackgroessen sind angemessen dimensioniert.", PriorityHint: "high", MapsToHazardCategories: []string{"software_fault", "timing_error"}, EvidenceExamples: []string{"Stack-Analyse-Bericht", "WCSA-Nachweis"}, ReductionType: "design", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.SWDEV.013", Domain: "SWDEV", Title: "Deadlock-Analyse durchgefuehrt", Description: "Potenzielle Deadlocks und Race-Conditions wurden analysiert und ausgeschlossen.", PriorityHint: "high", MapsToHazardCategories: []string{"timing_error", "software_fault"}, EvidenceExamples: []string{"Deadlock-Analyse", "Synchronisationsnachweis"}, ReductionType: "protective", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.SWDEV.014", Domain: "SWDEV", Title: "WCET-Analyse durchgefuehrt", Description: "Worst-Case Execution Times aller sicherheitsrelevanten Tasks wurden analysiert und sind innerhalb der Zeitbudgets.", PriorityHint: "high", MapsToHazardCategories: []string{"timing_error"}, EvidenceExamples: []string{"WCET-Analyse-Bericht", "Timing-Nachweis"}, ReductionType: "design", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.SWDEV.015", Domain: "SWDEV", Title: "SW-Integrationsstrategie definiert", Description: "Die Software-Integrationsstrategie (Bottom-Up, Top-Down) ist geplant und dokumentiert.", PriorityHint: "high", MapsToHazardCategories: []string{"integration_error", "software_fault"}, EvidenceExamples: []string{"Integrationsplan", "Teststrategie"}, ReductionType: "protective", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.SWDEV.016", Domain: "SWDEV", Title: "SW-Sicherheitstests spezifiziert", Description: "Software-Sicherheitstests sind spezifiziert und abdecken alle sicherheitsrelevanten Funktionen und Fehlerszenarien.", PriorityHint: "high", MapsToHazardCategories: []string{"software_fault", "safety_function_failure"}, EvidenceExamples: []string{"Sicherheitstest-Spezifikation", "Testprotokoll"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.SWDEV.017", Domain: "SWDEV", Title: "Coding-Standards definiert", Description: "Einheitliche Coding-Standards sind definiert, kommuniziert und ihre Einhaltung wird automatisch ueberprueft.", PriorityHint: "high", MapsToHazardCategories: []string{"software_fault"}, EvidenceExamples: []string{"Coding-Standard-Dokument", "Linter-Konfiguration"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.SWDEV.018", Domain: "SWDEV", Title: "Dokumentation SW-Design vollstaendig", Description: "Die Software-Design-Dokumentation ist vollstaendig, aktuell und durch Review freigegeben.", PriorityHint: "high", MapsToHazardCategories: []string{"software_fault"}, EvidenceExamples: []string{"SW-Design-Dokument", "Doxygen-Ausgabe"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.SWDEV.019", Domain: "SWDEV", Title: "Fehlerbehandlung vollstaendig implementiert", Description: "Alle Fehlerzustaende sind vollstaendig behandelt — kein unbehandelter Fehler kann zum Systemabsturz fuehren.", PriorityHint: "high", MapsToHazardCategories: []string{"software_fault", "safety_function_failure"}, EvidenceExamples: []string{"Fehlerbehandlungs-Matrix", "Code-Review-Nachweis"}, ReductionType: "protective", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.SWDEV.020", Domain: "SWDEV", Title: "Modul-Interface-Definition vorhanden", Description: "Alle Software-Modul-Interfaces sind vollstaendig und formell spezifiziert.", PriorityHint: "high", MapsToHazardCategories: []string{"integration_error"}, EvidenceExamples: []string{"Interface-Spezifikation", "API-Dokumentation"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.SWDEV.021", Domain: "SWDEV", Title: "Versionskontrolle fuer alle Artefakte", Description: "Alle Softwareartefakte (Code, Tests, Dokumentation) werden in einem Versionskontrollsystem verwaltet.", PriorityHint: "medium", MapsToHazardCategories: []string{"configuration_error"}, EvidenceExamples: []string{"Git-Repository", "Taggin-Policy"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.SWDEV.022", Domain: "SWDEV", Title: "Build-Reproduzierbarkeit sichergestellt", Description: "Der Build-Prozess ist vollstaendig automatisiert und reproduzierbar (gleiche Eingaben erzeugen gleiche Ausgaben).", PriorityHint: "medium", MapsToHazardCategories: []string{"configuration_error", "software_fault"}, EvidenceExamples: []string{"Build-Skript", "CI/CD-Nachweis"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.SWDEV.023", Domain: "SWDEV", Title: "Abhaengigkeitsanalyse durchgefuehrt", Description: "Alle externen Softwareabhaengigkeiten wurden analysiert, bewertet und ihre Kompatibilitaet ist sichergestellt.", PriorityHint: "medium", MapsToHazardCategories: []string{"software_fault", "update_failure"}, EvidenceExamples: []string{"SBOM", "Abhaengigkeits-Review"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.SWDEV.024", Domain: "SWDEV", Title: "Sicheres API-Design umgesetzt", Description: "APIs sind nach Security-by-Design-Prinzipien entwickelt und gegen Missbrauch abgesichert.", PriorityHint: "medium", MapsToHazardCategories: []string{"unauthorized_access", "software_fault"}, EvidenceExamples: []string{"API-Sicherheits-Review", "Threat-Model"}, ReductionType: "design", Applicable: []string{"sw", "ai"}},
{ID: "CTRL.SWDEV.025", Domain: "SWDEV", Title: "Input-Validierung implementiert", Description: "Alle externen Eingaben werden validiert und ungueltige Eingaben werden sicher behandelt.", PriorityHint: "medium", MapsToHazardCategories: []string{"software_fault", "unauthorized_access"}, EvidenceExamples: []string{"Validierungs-Nachweis", "Fuzzing-Testbericht"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.SWDEV.026", Domain: "SWDEV", Title: "Logging in sicherheitsrelevanter SW", Description: "Sicherheitsrelevante Software-Ereignisse werden protokolliert und sind fuer die Diagnose verfuegbar.", PriorityHint: "medium", MapsToHazardCategories: []string{"logging_audit_failure"}, EvidenceExamples: []string{"Logging-Konzept", "Log-Beispiele"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.SWDEV.027", Domain: "SWDEV", Title: "Exception-Handling vollstaendig", Description: "Alle moeglichen Ausnahmen (Exceptions) sind behandelt und fuehren zu einem definierten sicheren Zustand.", PriorityHint: "medium", MapsToHazardCategories: []string{"software_fault"}, EvidenceExamples: []string{"Exception-Handler-Review", "Testprotokoll"}, ReductionType: "protective", Applicable: []string{"sw"}},
{ID: "CTRL.SWDEV.028", Domain: "SWDEV", Title: "SW-Update-Mechanismus sicher implementiert", Description: "Der Software-Update-Mechanismus validiert Signatur und Integritaet vor der Installation.", PriorityHint: "medium", MapsToHazardCategories: []string{"update_failure", "firmware_corruption"}, EvidenceExamples: []string{"Update-Mechanismus-Design", "Signaturpruefungsnachweis"}, ReductionType: "design", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.SWDEV.029", Domain: "SWDEV", Title: "Selbsttest-Routinen implementiert", Description: "Das System fuehrt beim Start und zyklisch Selbsttests der sicherheitsrelevanten Funktionen durch.", PriorityHint: "medium", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"Selbsttest-Konzept", "Testabdeckungsnachweis"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.SWDEV.030", Domain: "SWDEV", Title: "Parameterpruefung implementiert", Description: "Alle Konfigurationsparameter werden beim Start und nach Aenderungen auf Gueltigkeit geprueft.", PriorityHint: "medium", MapsToHazardCategories: []string{"configuration_error", "software_fault"}, EvidenceExamples: []string{"Parameterpruef-Nachweis", "Out-of-Range-Testprotokoll"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.SWDEV.031", Domain: "SWDEV", Title: "Transiente Fehlertoleranz implementiert", Description: "Die Software toleriert transiente Fehler (Bitflips, EMV-Stoerungen) und stellt den korrekten Betrieb wieder her.", PriorityHint: "medium", MapsToHazardCategories: []string{"software_fault", "emc_hazard"}, EvidenceExamples: []string{"Fehlertoleranz-Konzept", "Soft-Error-Testnachweis"}, ReductionType: "protective", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.SWDEV.032", Domain: "SWDEV", Title: "Watchdog-SW korrekt implementiert", Description: "Die Software-Watchdog-Behandlung verhindert falsche Bediening und stellt echte Ueberwachung sicher.", PriorityHint: "medium", MapsToHazardCategories: []string{"timing_error", "software_fault"}, EvidenceExamples: []string{"Watchdog-SW-Review", "Watchdog-Testprotokoll"}, ReductionType: "design", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.SWDEV.033", Domain: "SWDEV", Title: "Sicherer Neustart implementiert", Description: "Der Systemstart nach Fehler oder Watchdog-Reset fuehrt zu einem sicheren Ausgangszustand.", PriorityHint: "medium", MapsToHazardCategories: []string{"software_fault", "safety_function_failure"}, EvidenceExamples: []string{"Reset-Handling-Dokument", "Neustart-Testprotokoll"}, ReductionType: "protective", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.SWDEV.034", Domain: "SWDEV", Title: "Boot-Integritaetspruefung implementiert", Description: "Beim Bootvorgang wird die Integritaet aller sicherheitsrelevanten Softwarekomponenten verifiziert.", PriorityHint: "medium", MapsToHazardCategories: []string{"firmware_corruption", "software_fault"}, EvidenceExamples: []string{"Boot-Integritaets-Nachweis", "CRC-Testprotokoll"}, ReductionType: "design", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.SWDEV.035", Domain: "SWDEV", Title: "Sicherheits-SW-Architektur dokumentiert", Description: "Die Sicherheitssoftware-Architektur ist vollstaendig dokumentiert inkl. Datenfluessen und Kontrollfluss.", PriorityHint: "medium", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"SW-Architektur-Dokument", "Datenflussdiagramm"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.SWDEV.036", Domain: "SWDEV", Title: "SW-Lifecycle-Plan erstellt", Description: "Der Software-Lifecycle-Plan definiert alle Phasen von Planung bis Ausserdienststellung.", PriorityHint: "medium", MapsToHazardCategories: []string{"software_fault"}, EvidenceExamples: []string{"SW-Lifecycle-Plan", "Meilensteinplan"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.SWDEV.037", Domain: "SWDEV", Title: "Baseline-Management implementiert", Description: "Konfigurationsbaselines werden zu definierten Meilensteinen erstellt und versioniert.", PriorityHint: "medium", MapsToHazardCategories: []string{"configuration_error"}, EvidenceExamples: []string{"Baseline-Register", "CM-Tool-Nachweis"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.SWDEV.038", Domain: "SWDEV", Title: "SW-Verifizierungsplan erstellt", Description: "Der Software-Verifizierungsplan legt Methoden, Werkzeuge und Verantwortlichkeiten fuer alle Teststufen fest.", PriorityHint: "medium", MapsToHazardCategories: []string{"software_fault"}, EvidenceExamples: []string{"Verifizierungsplan", "Testmatrix"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.SWDEV.039", Domain: "SWDEV", Title: "Qualitaetsmetriken definiert und gemessen", Description: "Softwarequalitaetsmetriken (Complexity, Coupling, Defectrate) werden erhoben und bewertet.", PriorityHint: "medium", MapsToHazardCategories: []string{"software_fault"}, EvidenceExamples: []string{"Qualitaetsmetriken-Bericht", "Code-Analyse-Tool-Ausgabe"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.SWDEV.040", Domain: "SWDEV", Title: "Threat-Modeling fuer SW durchgefuehrt", Description: "Ein Threat-Modeling (STRIDE, PASTA) wurde fuer die Software-Architektur durchgefuehrt und Massnahmen abgeleitet.", PriorityHint: "medium", MapsToHazardCategories: []string{"unauthorized_access", "software_fault"}, EvidenceExamples: []string{"Threat-Model-Dokument", "Massnahmenplan"}, ReductionType: "design", Applicable: []string{"sw", "ai"}},
// ── Domain VER (Verification & Validation) ────────────────────────────
{ID: "CTRL.VER.001", Domain: "VER", Title: "Fault-Injection-Test durchgefuehrt", Description: "Fehlerinjektionstests wurden systematisch durchgefuehrt um die Fehlerreaktionen des Systems zu validieren.", PriorityHint: "critical", MapsToHazardCategories: []string{"safety_function_failure", "software_fault"}, EvidenceExamples: []string{"Fault-Injection-Testprotokoll", "Testplan"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.VER.002", Domain: "VER", Title: "WCET-Messung validiert", Description: "Worst-Case Execution Times wurden messtechnisch validiert und entsprechen den Analyseergebnissen.", PriorityHint: "critical", MapsToHazardCategories: []string{"timing_error"}, EvidenceExamples: []string{"WCET-Messprokoll", "Trace-Analyse"}, ReductionType: "design", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.VER.003", Domain: "VER", Title: "HIL-Test durchgefuehrt", Description: "Hardware-in-the-Loop-Tests wurden mit repraesentativen Testfaellen und Fehlerszenarien durchgefuehrt.", PriorityHint: "critical", MapsToHazardCategories: []string{"integration_error", "safety_function_failure"}, EvidenceExamples: []string{"HIL-Testprotokoll", "HIL-Testplan"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.VER.004", Domain: "VER", Title: "Boundary-Value-Test durchgefuehrt", Description: "Grenzwertanalyse und Aequivalenzklassentest wurden fuer alle Eingangsparameter durchgefuehrt.", PriorityHint: "critical", MapsToHazardCategories: []string{"software_fault", "safety_boundary_violation"}, EvidenceExamples: []string{"Boundary-Test-Protokoll", "Testspezifikation"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.VER.005", Domain: "VER", Title: "Penetrationstest durchgefuehrt", Description: "Penetrationstests durch qualifizierte Tester wurden durchgefuehrt und alle Findings bewertet.", PriorityHint: "critical", MapsToHazardCategories: []string{"unauthorized_access"}, EvidenceExamples: []string{"Penetrationstest-Bericht", "Finding-Tracking"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.VER.006", Domain: "VER", Title: "Regressionstest etabliert", Description: "Automatisierte Regressionstests werden bei jeder Codeaenderung ausgefuehrt um Regressionen zu erkennen.", PriorityHint: "critical", MapsToHazardCategories: []string{"software_fault"}, EvidenceExamples: []string{"Regressionstest-Protokoll", "CI-Pipeline-Nachweis"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.VER.007", Domain: "VER", Title: "SIL-Verifikation abgeschlossen", Description: "Die Verifikation des geforderten SIL/PL wurde durch qualifizierte Institution durchgefuehrt und positiv bewertet.", PriorityHint: "critical", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"SIL-Verifikationsbericht", "Zertifikat"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.VER.008", Domain: "VER", Title: "Sicherheitsfunktions-Test bestanden", Description: "Alle spezifizierten Sicherheitsfunktionen wurden systematisch getestet und alle Tests bestanden.", PriorityHint: "critical", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"Sicherheitsfunktions-Testprotokoll", "Akzeptanzkriterien-Nachweis"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.VER.009", Domain: "VER", Title: "Lasttest durchgefuehrt", Description: "Lasttests unter maximaler Systembelastung wurden durchgefuehrt und alle Sicherheitsfunktionen behalten ihr Verhalten.", PriorityHint: "critical", MapsToHazardCategories: []string{"timing_error", "software_fault"}, EvidenceExamples: []string{"Lasttest-Protokoll", "Ressourcenauslastungs-Bericht"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.VER.010", Domain: "VER", Title: "Stresstest unter Extrembedingungen", Description: "Stresstests unter Extrembedingungen (Temperatur, Spannung, Vibration) wurden erfolgreich durchgefuehrt.", PriorityHint: "high", MapsToHazardCategories: []string{"environmental_hazard", "safety_function_failure"}, EvidenceExamples: []string{"Stresstest-Protokoll", "Klimatest-Bericht"}, ReductionType: "design", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.VER.011", Domain: "VER", Title: "EMV-Test bestanden", Description: "EMV-Tests nach anwendbaren Normen wurden in akkreditiertem Labor durchgefuehrt und bestanden.", PriorityHint: "high", MapsToHazardCategories: []string{"emc_hazard", "electrical_hazard"}, EvidenceExamples: []string{"EMV-Pruefbericht", "Laborzertifikat"}, ReductionType: "protective", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.VER.012", Domain: "VER", Title: "Umgebungstest abgeschlossen", Description: "Umgebungstests (Klimatest, Vibration, Schock) nach anwendbaren Normen wurden bestanden.", PriorityHint: "high", MapsToHazardCategories: []string{"environmental_hazard", "mechanical_hazard"}, EvidenceExamples: []string{"Umgebungstest-Berichte", "Klimatestprotokoll"}, ReductionType: "design", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.VER.013", Domain: "VER", Title: "Interface-Test durchgefuehrt", Description: "Alle definierten System-Interfaces wurden auf Konformitaet und Fehlverhalten getestet.", PriorityHint: "high", MapsToHazardCategories: []string{"integration_error", "communication_failure"}, EvidenceExamples: []string{"Interface-Testprotokoll", "Konformitaetsnachweis"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.VER.014", Domain: "VER", Title: "Integrations-Qualifikation abgeschlossen", Description: "Die Systemintegration wurde schrittweise qualifiziert und alle Integrationstest bestanden.", PriorityHint: "high", MapsToHazardCategories: []string{"integration_error"}, EvidenceExamples: []string{"Integrationstest-Protokoll", "Qualifikationsplan"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.VER.015", Domain: "VER", Title: "System-Abnahmetest bestanden", Description: "Der System-Abnahmetest wurde durch Kunden und/oder Zertifizierungsstelle erfolgreich durchgefuehrt.", PriorityHint: "high", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"SAT-Protokoll", "Abnahme-Zertifikat"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.VER.016", Domain: "VER", Title: "Sicherheitsnachweise vollstaendig", Description: "Alle erforderlichen Sicherheitsnachweise sind vollstaendig, aktuell und von der Zertifizierungsstelle anerkannt.", PriorityHint: "high", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"Safety-Case", "Zertifikat"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.VER.017", Domain: "VER", Title: "Unabhaengige Verifikation durchgefuehrt", Description: "Eine unabhaengige Verifikation (IV&V) durch eine separate Stelle wurde fuer alle SIL3/4-Anforderungen durchgefuehrt.", PriorityHint: "high", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"IV&V-Bericht", "Unabhaengigkeitsnachweis"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.VER.018", Domain: "VER", Title: "Safety-Case dokumentiert", Description: "Der Safety-Case stellt strukturiert dar wie alle Sicherheitsziele durch Massnahmen und Nachweise erfuellt sind.", PriorityHint: "high", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"Safety-Case-Dokument", "Argumentation-Baum"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.VER.019", Domain: "VER", Title: "Code-Coverage-Nachweis erbracht", Description: "Der geforderte Code-Coverage-Grad (Statement/Branch/MC/DC) wurde nachgewiesen und dokumentiert.", PriorityHint: "high", MapsToHazardCategories: []string{"software_fault"}, EvidenceExamples: []string{"Coverage-Bericht", "MC/DC-Nachweis"}, ReductionType: "protective", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.VER.020", Domain: "VER", Title: "Fehlermodell-Test abgeschlossen", Description: "Tests basierend auf dem Fehlermodell (Fehlerguppe, Fehlerrate) wurden durchgefuehrt und ausgewertet.", PriorityHint: "medium", MapsToHazardCategories: []string{"safety_function_failure", "software_fault"}, EvidenceExamples: []string{"Fehlermodell-Test-Protokoll", "FMEA-Nachweis"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.VER.021", Domain: "VER", Title: "Feldbus-Konformitaetstest bestanden", Description: "Konformitaetstests fuer alle eingesetzten Feldbuskommunikationen wurden erfolgreich abgeschlossen.", PriorityHint: "medium", MapsToHazardCategories: []string{"communication_failure", "integration_error"}, EvidenceExamples: []string{"Konformitaetstest-Zertifikat", "Pruefprotokoll"}, ReductionType: "protective", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.VER.022", Domain: "VER", Title: "Kalibrierungsverifikation abgeschlossen", Description: "Die Kalibrierung aller sicherheitsrelevanten Messwerterfassungen wurde verifiziert und dokumentiert.", PriorityHint: "medium", MapsToHazardCategories: []string{"safety_function_failure", "mechanical_hazard"}, EvidenceExamples: []string{"Kalibrierprotokoll", "Messketten-Nachweis"}, ReductionType: "design", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.VER.023", Domain: "VER", Title: "Dokumentenpruefung durchgefuehrt", Description: "Alle sicherheitsrelevanten Dokumente wurden auf Vollstaendigkeit, Konsistenz und Korrektheit geprueft.", PriorityHint: "medium", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"Dokumentenpruef-Protokoll", "Review-Checkliste"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.VER.024", Domain: "VER", Title: "Zuverlaessigkeitstest abgeschlossen", Description: "Zuverlaessigkeitstests (HALT, ALT) wurden durchgefuehrt und die Lebensdaueranforderungen bestanden.", PriorityHint: "medium", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"HALT-Bericht", "MTTF-Nachweis"}, ReductionType: "design", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.VER.025", Domain: "VER", Title: "Lebenszyklustest durchgefuehrt", Description: "Tests ueber den vollstaendigen Produktlebenszyklus (Alterung, Zyklen) wurden abgeschlossen.", PriorityHint: "medium", MapsToHazardCategories: []string{"safety_function_failure", "maintenance_hazard"}, EvidenceExamples: []string{"Lebenszyklustest-Protokoll", "Alterungstest-Bericht"}, ReductionType: "protective", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.VER.026", Domain: "VER", Title: "Sicherheits-Audit abgeschlossen", Description: "Ein Sicherheits-Audit durch qualifizierte Auditoren wurde durchgefuehrt und alle Befunde behandelt.", PriorityHint: "medium", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"Audit-Bericht", "Finding-Trackingprotokoll"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.VER.027", Domain: "VER", Title: "Kompatibilitaetstest durchgefuehrt", Description: "Kompatibilitaetstests mit allen unterstuetzten Systemkonfigurationen und Softwareversionen wurden abgeschlossen.", PriorityHint: "medium", MapsToHazardCategories: []string{"integration_error", "configuration_error"}, EvidenceExamples: []string{"Kompatibilitaetstest-Matrix", "Testprotokoll"}, ReductionType: "protective", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.VER.028", Domain: "VER", Title: "Software-Review abgeschlossen", Description: "Formelle Software-Reviews (Inspektion, Walkthrough) wurden fuer alle sicherheitsrelevanten Module durchgefuehrt.", PriorityHint: "medium", MapsToHazardCategories: []string{"software_fault"}, EvidenceExamples: []string{"Review-Protokoll", "Findings-Liste"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.VER.029", Domain: "VER", Title: "Factory-Acceptance-Test bestanden", Description: "Der Factory Acceptance Test (FAT) beim Hersteller wurde erfolgreich durchgefuehrt und dokumentiert.", PriorityHint: "medium", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"FAT-Protokoll", "Abnahme-Zertifikat"}, ReductionType: "protective", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.VER.030", Domain: "VER", Title: "Site-Acceptance-Test bestanden", Description: "Der Site Acceptance Test (SAT) am Einsatzort wurde erfolgreich durchgefuehrt und dokumentiert.", PriorityHint: "medium", MapsToHazardCategories: []string{"safety_function_failure", "integration_error"}, EvidenceExamples: []string{"SAT-Protokoll", "Inbetriebnahme-Zertifikat"}, ReductionType: "design", Applicable: []string{"fw", "ctrl"}},
// ── Domain CYBER (OT-Cybersecurity) ───────────────────────────────────
{ID: "CTRL.CYBER.001", Domain: "CYBER", Title: "Netzwerksegmentierung implementiert", Description: "OT- und IT-Netzwerke sind segmentiert und der Zugriff zwischen Zonen ist durch Firewalls kontrolliert.", PriorityHint: "critical", MapsToHazardCategories: []string{"unauthorized_access", "communication_failure"}, EvidenceExamples: []string{"Netzwerkdiagramm", "Firewall-Regelwerk"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.CYBER.002", Domain: "CYBER", Title: "Signierte Firmware-Updates", Description: "Alle Firmware-Updates werden vor der Installation kryptografisch auf Authentizitaet und Integritaet geprueft.", PriorityHint: "critical", MapsToHazardCategories: []string{"firmware_corruption", "unauthorized_access"}, EvidenceExamples: []string{"Signaturpruefungsnachweis", "Update-Prozess-Dokument"}, ReductionType: "protective", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.CYBER.003", Domain: "CYBER", Title: "MFA fuer Admin-Zugang", Description: "Multi-Faktor-Authentifizierung ist fuer alle administrativen Zugaenge zum System verpflichtend.", PriorityHint: "critical", MapsToHazardCategories: []string{"unauthorized_access"}, EvidenceExamples: []string{"MFA-Konfigurationsnachweis", "Zugangskontroll-Policy"}, ReductionType: "design", Applicable: []string{"sw", "ctrl"}},
{ID: "CTRL.CYBER.004", Domain: "CYBER", Title: "IDS/IPS implementiert", Description: "Ein Intrusion Detection/Prevention System ueberwacht den Netzwerkverkehr auf Angriffsmuster.", PriorityHint: "critical", MapsToHazardCategories: []string{"unauthorized_access", "communication_failure"}, EvidenceExamples: []string{"IDS-Konfigurationsnachweis", "Alert-Testprotokoll"}, ReductionType: "protective", Applicable: []string{"sw", "ctrl"}},
{ID: "CTRL.CYBER.005", Domain: "CYBER", Title: "SBOM vorhanden und aktuell", Description: "Eine vollstaendige Software Bill of Materials (SBOM) ist vorhanden und wird bei Releases aktualisiert.", PriorityHint: "critical", MapsToHazardCategories: []string{"firmware_corruption", "unauthorized_access"}, EvidenceExamples: []string{"SBOM-Dokument", "SBOM-Tool-Nachweis"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.CYBER.006", Domain: "CYBER", Title: "Schwachstellen-Scan durchgefuehrt", Description: "Regelmaessige Schwachstellen-Scans werden durchgefuehrt und Findings zeitgerecht behoben.", PriorityHint: "critical", MapsToHazardCategories: []string{"unauthorized_access", "firmware_corruption"}, EvidenceExamples: []string{"Scan-Bericht", "Remediation-Tracking"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.CYBER.007", Domain: "CYBER", Title: "Patch-Management etabliert", Description: "Ein strukturierter Patch-Management-Prozess stellt zeitnahe Behebung von Sicherheitsschwachstellen sicher.", PriorityHint: "critical", MapsToHazardCategories: []string{"unauthorized_access", "firmware_corruption"}, EvidenceExamples: []string{"Patch-Management-Policy", "Patch-Status-Report"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.CYBER.008", Domain: "CYBER", Title: "Sichere Kommunikationsprotokolle (TLS/DTLS)", Description: "Alle Netzwerkkommunikationen nutzen TLS 1.2+ oder DTLS und schwache Ciphersuites sind deaktiviert.", PriorityHint: "critical", MapsToHazardCategories: []string{"unauthorized_access", "communication_failure"}, EvidenceExamples: []string{"TLS-Konfigurationsnachweis", "SSL-Labs-Bericht"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.CYBER.009", Domain: "CYBER", Title: "Firewall-Regeln minimiert", Description: "Firewall-Regeln folgen dem Whitelist-Prinzip — nur explizit erlaubter Verkehr ist zugelassen.", PriorityHint: "critical", MapsToHazardCategories: []string{"unauthorized_access"}, EvidenceExamples: []string{"Firewall-Regelwerk-Review", "Penetrationstest-Nachweis"}, ReductionType: "design", Applicable: []string{"sw", "ctrl"}},
{ID: "CTRL.CYBER.010", Domain: "CYBER", Title: "Portabsicherung implementiert", Description: "Alle nicht benoetigten Netzwerkports und Dienste sind deaktiviert und dokumentiert.", PriorityHint: "critical", MapsToHazardCategories: []string{"unauthorized_access"}, EvidenceExamples: []string{"Port-Scan-Bericht", "Service-Haertungsdokument"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.CYBER.011", Domain: "CYBER", Title: "VPN fuer Remote-Zugang", Description: "Fernzugriff auf OT-Systeme erfolgt ausschliesslich ueber verschluesselte VPN-Verbindungen.", PriorityHint: "high", MapsToHazardCategories: []string{"unauthorized_access"}, EvidenceExamples: []string{"VPN-Konfigurationsnachweis", "Remote-Access-Policy"}, ReductionType: "design", Applicable: []string{"sw", "ctrl"}},
{ID: "CTRL.CYBER.012", Domain: "CYBER", Title: "Passwort-Policy durchgesetzt", Description: "Eine Passwort-Policy mit Komplexitaetsanforderungen und Ablauffristen ist implementiert und technisch erzwungen.", PriorityHint: "high", MapsToHazardCategories: []string{"unauthorized_access"}, EvidenceExamples: []string{"Passwort-Policy", "Technischer Nachweis"}, ReductionType: "protective", Applicable: []string{"sw", "ctrl"}},
{ID: "CTRL.CYBER.013", Domain: "CYBER", Title: "Brute-Force-Schutz aktiviert", Description: "Account-Lockout nach mehrfach fehlgeschlagenen Login-Versuchen ist implementiert und getestet.", PriorityHint: "high", MapsToHazardCategories: []string{"unauthorized_access"}, EvidenceExamples: []string{"Brute-Force-Test-Nachweis", "Konfigurationsprotokoll"}, ReductionType: "design", Applicable: []string{"sw", "ctrl"}},
{ID: "CTRL.CYBER.014", Domain: "CYBER", Title: "Session-Management sicher implementiert", Description: "Session-Tokens sind kryptografisch stark, Timeouts sind konfiguriert und Session-Fixation verhindert.", PriorityHint: "high", MapsToHazardCategories: []string{"unauthorized_access"}, EvidenceExamples: []string{"Session-Management-Review", "Penetrationstest-Nachweis"}, ReductionType: "protective", Applicable: []string{"sw"}},
{ID: "CTRL.CYBER.015", Domain: "CYBER", Title: "Kryptografisch sichere Zufallszahlen", Description: "Kryptografisch sichere Zufallszahlengeneratoren (CSPRNG) werden fuer alle sicherheitsrelevanten Operationen genutzt.", PriorityHint: "high", MapsToHazardCategories: []string{"unauthorized_access", "firmware_corruption"}, EvidenceExamples: []string{"CSPRNG-Nachweis", "Code-Review-Protokoll"}, ReductionType: "design", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.CYBER.016", Domain: "CYBER", Title: "PKI/Zertifikatsverwaltung etabliert", Description: "Eine Public Key Infrastructure (PKI) verwaltet alle Zertifikate mit definierten Lebenszyklen.", PriorityHint: "high", MapsToHazardCategories: []string{"unauthorized_access", "communication_failure"}, EvidenceExamples: []string{"PKI-Konzept", "Zertifikatsregister"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.CYBER.017", Domain: "CYBER", Title: "Sicheres Schluesselspeichern (HSM/TPM)", Description: "Kryptografische Schluessel werden sicher in Hardware Security Modules (HSM) oder TPM-Chips gespeichert.", PriorityHint: "high", MapsToHazardCategories: []string{"unauthorized_access", "firmware_corruption"}, EvidenceExamples: []string{"HSM-Konfigurationsnachweis", "Key-Management-Policy"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.CYBER.018", Domain: "CYBER", Title: "Code-Signing implementiert", Description: "Alle ausfuehrbaren Softwarekomponenten sind mit verifizierbaren digitalen Signaturen versehen.", PriorityHint: "high", MapsToHazardCategories: []string{"firmware_corruption", "unauthorized_access"}, EvidenceExamples: []string{"Code-Signing-Prozess", "Signaturpruefungsnachweis"}, ReductionType: "protective", Applicable: []string{"sw", "fw"}},
{ID: "CTRL.CYBER.019", Domain: "CYBER", Title: "Integrity-Monitoring aktiviert", Description: "Integritaets-Monitoring erkennt unautorisierten Zugriff auf kritische System- und Konfigurationsdateien.", PriorityHint: "high", MapsToHazardCategories: []string{"unauthorized_access", "configuration_error"}, EvidenceExamples: []string{"FIM-Konfigurationsnachweis", "Alert-Testprotokoll"}, ReductionType: "design", Applicable: []string{"sw", "ctrl"}},
{ID: "CTRL.CYBER.020", Domain: "CYBER", Title: "Log-Management etabliert", Description: "Zentralisiertes Log-Management stellt sichere Aufbewahrung und Unversehrtheit von Sicherheitslogs sicher.", PriorityHint: "high", MapsToHazardCategories: []string{"logging_audit_failure"}, EvidenceExamples: []string{"Log-Management-Konzept", "SIEM-Nachweis"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.CYBER.021", Domain: "CYBER", Title: "SIEM-Integration vorhanden", Description: "Sicherheitsrelevante Ereignisse werden an ein SIEM-System weitergeleitet und korreliert.", PriorityHint: "medium", MapsToHazardCategories: []string{"unauthorized_access", "logging_audit_failure"}, EvidenceExamples: []string{"SIEM-Integrationsnachweis", "Usecase-Dokumentation"}, ReductionType: "design", Applicable: []string{"sw", "ctrl"}},
{ID: "CTRL.CYBER.022", Domain: "CYBER", Title: "Vulnerability-Disclosure-Policy vorhanden", Description: "Eine oeffentliche Vulnerability Disclosure Policy (VDP) ist etabliert und ein Meldeprozess definiert.", PriorityHint: "medium", MapsToHazardCategories: []string{"unauthorized_access"}, EvidenceExamples: []string{"VDP-Dokument", "Bug-Bounty-Policy"}, ReductionType: "protective", Applicable: []string{"sw", "ai"}},
{ID: "CTRL.CYBER.023", Domain: "CYBER", Title: "Incident-Response-Plan vorhanden", Description: "Ein Cyber-Incident-Response-Plan definiert Eskalation, Rollen und Massnahmen fuer Sicherheitsvorfaelle.", PriorityHint: "medium", MapsToHazardCategories: []string{"unauthorized_access", "firmware_corruption"}, EvidenceExamples: []string{"Incident-Response-Plan", "Uebungsprotokoll"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.CYBER.024", Domain: "CYBER", Title: "Penetrationstest OT durchgefuehrt", Description: "OT-spezifische Penetrationstests unter Beruecksichtigung von Verduegbarkeitsanforderungen wurden durchgefuehrt.", PriorityHint: "medium", MapsToHazardCategories: []string{"unauthorized_access", "communication_failure"}, EvidenceExamples: []string{"OT-Pentest-Bericht", "Finding-Tracking"}, ReductionType: "protective", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.CYBER.025", Domain: "CYBER", Title: "Security-by-Default konfiguriert", Description: "Alle Systeme werden mit sicheren Standardkonfigurationen ausgeliefert und unsichere Defaults sind deaktiviert.", PriorityHint: "medium", MapsToHazardCategories: []string{"unauthorized_access", "configuration_error"}, EvidenceExamples: []string{"Default-Konfigurationsnachweis", "Haertungs-Checkliste"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.CYBER.026", Domain: "CYBER", Title: "Least-Privilege-Prinzip umgesetzt", Description: "Alle Benutzer, Prozesse und Dienste haben nur die Mindestberechtigungen die fuer ihre Funktion notwendig sind.", PriorityHint: "medium", MapsToHazardCategories: []string{"unauthorized_access"}, EvidenceExamples: []string{"Berechtigungs-Matrix", "Access-Review-Protokoll"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.CYBER.027", Domain: "CYBER", Title: "Role-Based Access Control implementiert", Description: "RBAC stellt sicher dass Zugriffsrechte rollenbasiert vergeben und regelmaessig geprueft werden.", PriorityHint: "medium", MapsToHazardCategories: []string{"unauthorized_access"}, EvidenceExamples: []string{"RBAC-Konzept", "Rollen-Dokumentation"}, ReductionType: "design", Applicable: []string{"sw", "ctrl", "ai"}},
{ID: "CTRL.CYBER.028", Domain: "CYBER", Title: "Authentifizierung an allen Schnittstellen", Description: "Alle Kommunikationsschnittstellen erfordern starke Authentifizierung vor dem Datenzugriff.", PriorityHint: "medium", MapsToHazardCategories: []string{"unauthorized_access", "communication_failure"}, EvidenceExamples: []string{"Schnittstellen-Auth-Nachweis", "API-Security-Review"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.CYBER.029", Domain: "CYBER", Title: "Datenverschluesselung im Transit", Description: "Alle uebertragenen Daten sind Ende-zu-Ende verschluesselt, auch im internen Netzwerk.", PriorityHint: "medium", MapsToHazardCategories: []string{"unauthorized_access", "communication_failure"}, EvidenceExamples: []string{"Verschluesselungsnachweis", "Traffic-Analyse"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.CYBER.030", Domain: "CYBER", Title: "Datenverschluesselung at Rest", Description: "Alle gespeicherten sensitiven Daten sind verschluesselt und Schluessel werden sicher verwaltet.", PriorityHint: "medium", MapsToHazardCategories: []string{"unauthorized_access"}, EvidenceExamples: []string{"Verschluesselungskonzept", "Key-Management-Nachweis"}, ReductionType: "protective", Applicable: []string{"sw", "ctrl", "ai"}},
{ID: "CTRL.CYBER.031", Domain: "CYBER", Title: "Sicherer Fernzugriff kontrolliert", Description: "Fernzugriff ist auf bekannte Nutzer und Systeme beschraenkt und wird vollstaendig protokolliert.", PriorityHint: "medium", MapsToHazardCategories: []string{"unauthorized_access"}, EvidenceExamples: []string{"Fernzugriffs-Policy", "Access-Log-Nachweis"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.CYBER.032", Domain: "CYBER", Title: "Deaktivierung ungenutzter Dienste", Description: "Alle nicht benoetigten Netzwerkdienste, Protokolle und Benutzerschnittstellen sind deaktiviert.", PriorityHint: "medium", MapsToHazardCategories: []string{"unauthorized_access"}, EvidenceExamples: []string{"Service-Inventar", "Haertungsnachweis"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.CYBER.033", Domain: "CYBER", Title: "Haertung Betriebssystem durchgefuehrt", Description: "Das Betriebssystem wurde gemaess anerkannten Haertungsleitfaeden (CIS Benchmark, BSI) konfiguriert.", PriorityHint: "medium", MapsToHazardCategories: []string{"unauthorized_access", "configuration_error"}, EvidenceExamples: []string{"Haertungs-Protokoll", "CIS-Benchmark-Bericht"}, ReductionType: "design", Applicable: []string{"sw", "ctrl"}},
{ID: "CTRL.CYBER.034", Domain: "CYBER", Title: "Container-Sicherheit implementiert", Description: "Container-Images werden regelmaessig auf Schwachstellen gescannt und laufen mit minimalen Berechtigungen.", PriorityHint: "medium", MapsToHazardCategories: []string{"unauthorized_access", "software_fault"}, EvidenceExamples: []string{"Container-Scan-Bericht", "Pod-Security-Policy"}, ReductionType: "protective", Applicable: []string{"sw", "ai"}},
{ID: "CTRL.CYBER.035", Domain: "CYBER", Title: "Supply-Chain-Sicherheit beruecksichtigt", Description: "Software-Lieferketten-Risiken wurden bewertet und Massnahmen zur Absicherung sind implementiert.", PriorityHint: "medium", MapsToHazardCategories: []string{"firmware_corruption", "unauthorized_access"}, EvidenceExamples: []string{"Supply-Chain-Risikoanalyse", "SBOM-Nachweis"}, ReductionType: "design", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.CYBER.036", Domain: "CYBER", Title: "IEC-62443-Compliance-Assessment durchgefuehrt", Description: "Eine Konformitaetsbewertung gegenueber IEC 62443 fuer OT-Systeme wurde durchgefuehrt.", PriorityHint: "medium", MapsToHazardCategories: []string{"unauthorized_access", "communication_failure"}, EvidenceExamples: []string{"IEC-62443-Gap-Assessment", "Compliance-Bericht"}, ReductionType: "protective", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.CYBER.037", Domain: "CYBER", Title: "Zero-Trust-Architektur umgesetzt", Description: "Zero-Trust-Prinzipien (Never Trust Always Verify) werden fuer alle Netzwerkzugriffe angewendet.", PriorityHint: "medium", MapsToHazardCategories: []string{"unauthorized_access"}, EvidenceExamples: []string{"Zero-Trust-Architektur-Dokument", "Implementierungsnachweis"}, ReductionType: "design", Applicable: []string{"sw", "ctrl"}},
{ID: "CTRL.CYBER.038", Domain: "CYBER", Title: "Security-Training fuer Mitarbeiter", Description: "Regelmaessige Security-Awareness-Trainings werden fuer alle Mitarbeiter mit OT-Systemzugang durchgefuehrt.", PriorityHint: "medium", MapsToHazardCategories: []string{"unauthorized_access"}, EvidenceExamples: []string{"Trainingsprotokoll", "Teilnehmerliste"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.CYBER.039", Domain: "CYBER", Title: "Sicherheits-Audit OT abgeschlossen", Description: "OT-spezifisches Sicherheits-Audit durch qualifizierte Auditoren wurde durchgefuehrt und Befunde behoben.", PriorityHint: "medium", MapsToHazardCategories: []string{"unauthorized_access", "configuration_error"}, EvidenceExamples: []string{"OT-Audit-Bericht", "Finding-Closing-Nachweis"}, ReductionType: "design", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.CYBER.040", Domain: "CYBER", Title: "Cyber-Resilience-Plan vorhanden", Description: "Ein Cyber-Resilience-Plan definiert Massnahmen fuer den Weiterbetrieb und die schnelle Wiederherstellung nach Cyberangriffen.", PriorityHint: "medium", MapsToHazardCategories: []string{"unauthorized_access", "communication_failure"}, EvidenceExamples: []string{"Resilience-Plan", "BCP-Dokument"}, ReductionType: "protective", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
// ── Domain DOC (Documentation & CE-Akte) ──────────────────────────────
{ID: "CTRL.DOC.001", Domain: "DOC", Title: "Risikobewertung dokumentiert", Description: "Die vollstaendige Risikobewertung inkl. Risikomatrix, Massnahmen und Restrisiken ist dokumentiert und freigegeben.", PriorityHint: "critical", MapsToHazardCategories: []string{"safety_function_failure", "software_fault", "mechanical_hazard", "electrical_hazard"}, EvidenceExamples: []string{"Risikobewertungs-Dokument", "Freigabeprotokoll"}, ReductionType: "information", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.DOC.002", Domain: "DOC", Title: "Technical-File vollstaendig", Description: "Das Technical File (Technische Dokumentation) fuer die CE-Kennzeichnung ist vollstaendig und aktuell.", PriorityHint: "critical", MapsToHazardCategories: []string{"safety_function_failure", "software_fault", "electrical_hazard"}, EvidenceExamples: []string{"Technical-File-Checkliste", "CE-Dossier"}, ReductionType: "information", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.DOC.003", Domain: "DOC", Title: "Konformitaetserklaerung erstellt", Description: "Die EU-Konformitaetserklaerung (DoC) wurde erstellt, unterzeichnet und liegt dem Produkt bei.", PriorityHint: "critical", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"EU-Konformitaetserklaerung", "CE-Kennzeichen-Nachweis"}, ReductionType: "information", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.DOC.004", Domain: "DOC", Title: "Betriebsanleitung verfasst", Description: "Eine vollstaendige Betriebsanleitung in der Landessprache des Anwenders liegt vor und deckt alle Betriebsmodi ab.", PriorityHint: "critical", MapsToHazardCategories: []string{"hmi_error", "maintenance_hazard", "safety_function_failure"}, EvidenceExamples: []string{"Betriebsanleitung", "Sprachversionen-Nachweis"}, ReductionType: "information", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.DOC.005", Domain: "DOC", Title: "Wartungsanleitung vorhanden", Description: "Eine vollstaendige Wartungsanleitung mit Wartungsintervallen, Ersatzteilen und Pruefanweisungen liegt vor.", PriorityHint: "critical", MapsToHazardCategories: []string{"maintenance_hazard"}, EvidenceExamples: []string{"Wartungsanleitung", "Wartungsintervall-Tabelle"}, ReductionType: "information", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.DOC.006", Domain: "DOC", Title: "Installationsanleitung vorhanden", Description: "Eine vollstaendige Installationsanleitung mit Sicherheitshinweisen und Pruefanweisungen fuer die Inbetriebnahme liegt vor.", PriorityHint: "critical", MapsToHazardCategories: []string{"integration_error", "safety_function_failure"}, EvidenceExamples: []string{"Installationsanleitung", "Inbetriebnahme-Checkliste"}, ReductionType: "information", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.DOC.007", Domain: "DOC", Title: "Sicherheitsdatenblatt vorhanden", Description: "Ein Sicherheitsdatenblatt mit relevanten Sicherheitsinformationen liegt vor und ist aktuell.", PriorityHint: "critical", MapsToHazardCategories: []string{"safety_function_failure", "electrical_hazard", "thermal_hazard"}, EvidenceExamples: []string{"Sicherheitsdatenblatt", "REACH-Konformitaet"}, ReductionType: "information", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.DOC.008", Domain: "DOC", Title: "Sicherheitsfunktions-Beschreibung vorhanden", Description: "Alle Sicherheitsfunktionen sind vollstaendig beschrieben inkl. Funktionsprinzip, Ausloesekriterien und Wirkung.", PriorityHint: "critical", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"Sicherheitsfunktions-Beschreibung", "Funktionale-Sicherheits-Konzept"}, ReductionType: "information", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.DOC.009", Domain: "DOC", Title: "CE-Kennzeichnung angebracht", Description: "Das CE-Kennzeichen ist am Produkt angebracht und die zugehoerige Doku ist vollstaendig.", PriorityHint: "critical", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"CE-Kennzeichen-Foto", "Konformitaetserklaerung"}, ReductionType: "information", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.DOC.010", Domain: "DOC", Title: "Pruefberichte archiviert", Description: "Alle Pruef- und Testberichte sind vollstaendig archiviert und fuer 10 Jahre verfuegbar.", PriorityHint: "critical", MapsToHazardCategories: []string{"safety_function_failure", "electrical_hazard", "emc_hazard"}, EvidenceExamples: []string{"Archivierungsnachweis", "Dokumentenregister"}, ReductionType: "information", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.DOC.011", Domain: "DOC", Title: "Normenkonformitaetsmatrix erstellt", Description: "Eine Normenkonformitaetsmatrix zeigt fuer jede anwendbare Norm den Konformitaetsnachweis.", PriorityHint: "high", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"Normenmatrix", "Gap-Analyse"}, ReductionType: "information", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.DOC.012", Domain: "DOC", Title: "Aenderungsdokumentation vollstaendig", Description: "Alle Produktaenderungen sind vollstaendig dokumentiert und die Auswirkungen auf die Zertifizierung bewertet.", PriorityHint: "high", MapsToHazardCategories: []string{"configuration_error", "software_fault"}, EvidenceExamples: []string{"Aenderungsprotokoll", "Impact-Analyse"}, ReductionType: "information", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.DOC.013", Domain: "DOC", Title: "Software-Release-Notes vorhanden", Description: "Fuer jedes Software-Release existieren vollstaendige Release-Notes mit Aenderungen, Fixes und Bekannten-Problemen.", PriorityHint: "high", MapsToHazardCategories: []string{"software_fault", "update_failure"}, EvidenceExamples: []string{"Release-Notes", "Changelog"}, ReductionType: "information", Applicable: []string{"sw", "fw", "ai"}},
{ID: "CTRL.DOC.014", Domain: "DOC", Title: "Konfigurationshandbuch erstellt", Description: "Ein vollstaendiges Konfigurationshandbuch mit allen Parametern und deren sicheren Wertebereichen liegt vor.", PriorityHint: "high", MapsToHazardCategories: []string{"configuration_error"}, EvidenceExamples: []string{"Konfigurationshandbuch", "Parameter-Referenz"}, ReductionType: "information", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.DOC.015", Domain: "DOC", Title: "Schulungsunterlagen vorhanden", Description: "Schulungsunterlagen fuer Bediener, Instandhalter und Administratoren sind vorhanden und aktuell.", PriorityHint: "high", MapsToHazardCategories: []string{"hmi_error", "maintenance_hazard"}, EvidenceExamples: []string{"Schulungsunterlagen", "Kompetenzmatrix"}, ReductionType: "information", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.DOC.016", Domain: "DOC", Title: "Lagerungsanweisung vorhanden", Description: "Eine Lagerungsanweisung mit Bedingungen und Hoechstlagerdauer liegt vor.", PriorityHint: "high", MapsToHazardCategories: []string{"environmental_hazard"}, EvidenceExamples: []string{"Lagerungsanweisung", "Verpackungsvorschrift"}, ReductionType: "information", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.DOC.017", Domain: "DOC", Title: "Entsorgungshinweise vorhanden", Description: "Hinweise zur umweltgerechten Entsorgung (WEEE, RoHS) sind im Produkt dokumentiert.", PriorityHint: "high", MapsToHazardCategories: []string{"environmental_hazard"}, EvidenceExamples: []string{"Entsorgungshinweis", "WEEE-Konformitaetsnachweis"}, ReductionType: "information", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.DOC.018", Domain: "DOC", Title: "HARA-Dokumentation vollstaendig", Description: "Die HARA-Dokumentation ist vollstaendig, durch Review freigegeben und alle Massnahmen sind rueckverfolgbar.", PriorityHint: "high", MapsToHazardCategories: []string{"safety_function_failure", "mechanical_hazard", "electrical_hazard"}, EvidenceExamples: []string{"HARA-Dokument", "Review-Protokoll"}, ReductionType: "information", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.DOC.019", Domain: "DOC", Title: "SIL-Nachweisdokumentation vollstaendig", Description: "Die vollstaendige SIL-Nachweisdokumentation gemaess IEC 61508 ist vorhanden und durch Zertifizierungsstelle anerkannt.", PriorityHint: "high", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"SIL-Zertifikat", "Nachweisdokumentation"}, ReductionType: "information", Applicable: []string{"sw", "fw", "ctrl"}},
{ID: "CTRL.DOC.020", Domain: "DOC", Title: "Baumustererpruefung durchgefuehrt", Description: "Eine Baumustererpruefung durch eine notifizierte Stelle wurde durchgefuehrt und ein Zertifikat ausgestellt.", PriorityHint: "high", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"Baumustererpruefungs-Zertifikat", "Pruefbericht"}, ReductionType: "information", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.DOC.021", Domain: "DOC", Title: "Erstinbetriebnahme-Protokoll vorhanden", Description: "Ein Erstinbetriebnahme-Protokoll dokumentiert alle Pruefschritte und deren Ergebnisse.", PriorityHint: "medium", MapsToHazardCategories: []string{"safety_function_failure", "integration_error"}, EvidenceExamples: []string{"Inbetriebnahme-Protokoll", "Abnahme-Checkliste"}, ReductionType: "information", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.DOC.022", Domain: "DOC", Title: "Wartungsprotokoll gefuehrt", Description: "Wartungsprotokolle werden bei jeder Wartung erstellt und archiviert.", PriorityHint: "medium", MapsToHazardCategories: []string{"maintenance_hazard"}, EvidenceExamples: []string{"Wartungsprotokoll", "Wartungsbuch"}, ReductionType: "information", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.DOC.023", Domain: "DOC", Title: "Kalibrierprotokoll aktuell", Description: "Kalibrierprotokolle fuer alle sicherheitsrelevanten Messgeraete sind aktuell und archiviert.", PriorityHint: "medium", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"Kalibrierprotokoll", "Kalibrierzertifikat"}, ReductionType: "information", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.DOC.024", Domain: "DOC", Title: "Vorfallsprotokoll gefuehrt", Description: "Alle Sicherheitsvorfaelle und sicherheitsrelevanten Ereignisse werden protokolliert und ausgewertet.", PriorityHint: "medium", MapsToHazardCategories: []string{"safety_function_failure", "software_fault"}, EvidenceExamples: []string{"Vorfallsprotokoll", "Root-Cause-Analyse"}, ReductionType: "information", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.DOC.025", Domain: "DOC", Title: "Audits und Zertifikate archiviert", Description: "Alle Audit-Berichte und Zertifikate sind vollstaendig archiviert und fuer die vorgeschriebene Dauer verfuegbar.", PriorityHint: "medium", MapsToHazardCategories: []string{"safety_function_failure"}, EvidenceExamples: []string{"Zertifikate-Register", "Archivierungsnachweis"}, ReductionType: "information", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.DOC.026", Domain: "DOC", Title: "Zulieferer-Dokumentation vorhanden", Description: "Alle relevanten Zulieferer-Dokumentationen (Datenblatt, Zertifikat, Konformitaetserklaerung) sind vorhanden.", PriorityHint: "medium", MapsToHazardCategories: []string{"software_fault", "electrical_hazard"}, EvidenceExamples: []string{"Zulieferer-Dokumentenregister", "Komponentenzertifikate"}, ReductionType: "information", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.DOC.027", Domain: "DOC", Title: "Lebenszyklusplan dokumentiert", Description: "Der Produktlebenszyklusplan von Entwicklung bis Ausserbetriebnahme ist dokumentiert und verbindlich.", PriorityHint: "medium", MapsToHazardCategories: []string{"maintenance_hazard", "update_failure"}, EvidenceExamples: []string{"Lebenszyklusplan", "EOL-Strategie"}, ReductionType: "information", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.DOC.028", Domain: "DOC", Title: "Obsoleszenz-Management dokumentiert", Description: "Ein Obsoleszenz-Management-Plan identifiziert Risiken durch auslaufende Komponenten und definiert Massnahmen.", PriorityHint: "medium", MapsToHazardCategories: []string{"update_failure", "software_fault"}, EvidenceExamples: []string{"Obsoleszenz-Plan", "Risiko-Register"}, ReductionType: "information", Applicable: []string{"fw", "ctrl"}},
{ID: "CTRL.DOC.029", Domain: "DOC", Title: "Schnittstellendokumentation vollstaendig", Description: "Alle Systemschnittstellen sind vollstaendig dokumentiert inkl. Protokolle, Datenformate und Fehlercodes.", PriorityHint: "medium", MapsToHazardCategories: []string{"integration_error", "communication_failure"}, EvidenceExamples: []string{"Schnittstellendokumentation", "API-Referenz"}, ReductionType: "information", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
{ID: "CTRL.DOC.030", Domain: "DOC", Title: "Post-Market-Surveillance-Plan vorhanden", Description: "Ein Post-Market-Surveillance-Plan definiert wie Felddaten, Vorfaelle und Kundenrueckmeldungen systematisch ausgewertet werden.", PriorityHint: "medium", MapsToHazardCategories: []string{"safety_function_failure", "software_fault"}, EvidenceExamples: []string{"PMS-Plan", "PMSR-Bericht"}, ReductionType: "information", Applicable: []string{"sw", "fw", "ctrl", "ai"}},
}
}
// GetProtectiveMeasureLibrary returns the complete built-in protective measures library
// with 160 entries organized by reduction type (design, protection, information)
// following the ISO 12100 three-step method for risk reduction.
//
// Each entry is categorized by sub-type and hazard category, with German-language
// names, descriptions, and practical examples for industrial machinery safety.
func GetProtectiveMeasureLibrary() []ProtectiveMeasureEntry {
return []ProtectiveMeasureEntry{
// ====================================================================
// Type 1: Design (ReductionType: "design") — Inhaerent sichere Konstruktion
// 50 entries: M001-M050
// ====================================================================
{ID: "M001", ReductionType: "design", SubType: "geometry", Name: "Gefahrstelle konstruktiv eliminieren", Description: "Durch konstruktive Gestaltung wird die Gefahrstelle vollstaendig beseitigt, sodass eine Gefaehrdung gar nicht erst entstehen kann.", HazardCategory: "mechanical", Examples: []string{"Quetschstelle durch Geometrieaenderung entfernen", "Einzugsstelle durch vergroesserten Spalt eliminieren", "Scherstelle durch Kinematikanpassung vermeiden"}},
{ID: "M002", ReductionType: "design", SubType: "force_energy", Name: "Bewegungsenergie reduzieren", Description: "Die kinetische Energie beweglicher Maschinenteile wird durch konstruktive Massnahmen auf ein sicheres Niveau begrenzt.", HazardCategory: "mechanical", Examples: []string{"Masse beweglicher Teile verringern", "Hublaenge verkuerzen", "Traegheitsmoment durch Leichtbau reduzieren"}},
{ID: "M003", ReductionType: "design", SubType: "force_energy", Name: "Geschwindigkeit reduzieren", Description: "Die Verfahrgeschwindigkeit wird konstruktiv auf ein Niveau begrenzt, bei dem keine gefaehrlichen Verletzungen auftreten koennen.", HazardCategory: "mechanical", Examples: []string{"Maximale Achsgeschwindigkeit mechanisch begrenzen", "Getriebeuebersetzung anpassen", "Drehzahlbegrenzer einbauen"}},
{ID: "M004", ReductionType: "design", SubType: "force_energy", Name: "Kraft begrenzen", Description: "Die maximal auftretende Kraft wird durch die Konstruktion so weit begrenzt, dass keine gefaehrlichen Verletzungen moeglich sind.", HazardCategory: "mechanical", Examples: []string{"Federbelastete Kraftbegrenzung einsetzen", "Antriebsdrehmoment begrenzen", "Nachgiebige Elemente verwenden"}},
{ID: "M005", ReductionType: "design", SubType: "geometry", Name: "Sicherheitsabstaende vergroessern", Description: "Konstruktive Abstaende zwischen Gefahrstellen und zugaenglichen Bereichen werden so dimensioniert, dass ein Erreichen der Gefahrstelle ausgeschlossen ist.", HazardCategory: "mechanical", Examples: []string{"Greifabstand an Walzen vergroessern", "Abstand zu heissen Oberflaechen erhoehen", "Sicherheitsabstand an Pressen einhalten"}},
{ID: "M006", ReductionType: "design", SubType: "force_energy", Name: "Kinematik aendern", Description: "Die Bewegungsart oder Bewegungsrichtung wird so umgestaltet, dass die Gefaehrdung durch die Maschinenbewegung entfaellt.", HazardCategory: "mechanical", Examples: []string{"Linearbewegung statt Rotation einsetzen", "Bewegungsrichtung von Bedienerseite wegfuehren", "Parallele statt gegenlaefige Walzen verwenden"}},
{ID: "M007", ReductionType: "design", SubType: "geometry", Name: "Rotationsbewegung vermeiden", Description: "Rotierende Teile werden durch alternative Konstruktionsloesungen ersetzt, um Einzugs- und Wickelgefaehrdungen zu beseitigen.", HazardCategory: "mechanical", Examples: []string{"Linearantrieb statt Drehantrieb verwenden", "Riemenantrieb durch Zahnstange ersetzen", "Drehbewegung kapseln"}},
{ID: "M008", ReductionType: "design", SubType: "geometry", Name: "Scharfe Kanten entfernen", Description: "Alle zugaenglichen Kanten und Ecken werden abgerundet oder entgratet, um Schnitt- und Stichverletzungen auszuschliessen.", HazardCategory: "mechanical", Examples: []string{"Radien an Blechkanten anbringen", "Entgratung aller Stanzteile sicherstellen", "Abgerundete Gehaeusekanten vorsehen"}},
{ID: "M009", ReductionType: "design", SubType: "material", Name: "Sichere Materialwahl", Description: "Die verwendeten Werkstoffe werden so gewaehlt, dass sie keine zusaetzlichen Gefaehrdungen verursachen und den Betriebsbelastungen standhalten.", HazardCategory: "material_environmental", Examples: []string{"Nicht-toxische Kunststoffe waehlen", "Korrosionsbestaendige Legierungen einsetzen", "Feuerfeste Materialien an heissen Stellen verwenden"}},
{ID: "M010", ReductionType: "design", SubType: "force_energy", Name: "Gewicht reduzieren", Description: "Das Gewicht beweglicher Maschinenteile wird minimiert, um die potenzielle Verletzungsschwere bei Kollisionen zu verringern.", HazardCategory: "mechanical", Examples: []string{"Leichtbauwerkstoffe fuer bewegliche Arme einsetzen", "Hohlprofile statt Vollmaterial verwenden", "Gegengewichte durch Ausgleichsfedern ersetzen"}},
{ID: "M011", ReductionType: "design", SubType: "force_energy", Name: "Redundante Konstruktion", Description: "Sicherheitskritische Bauteile werden mehrfach ausgefuehrt, sodass beim Versagen einer Komponente die Sicherheitsfunktion erhalten bleibt.", HazardCategory: "mechanical", Examples: []string{"Doppelte Tragseile an Hebezeugen", "Zweifache Verriegelungsmechanismen", "Redundante Bremssysteme vorsehen"}},
{ID: "M012", ReductionType: "design", SubType: "force_energy", Name: "Mechanische Begrenzung", Description: "Feste mechanische Anschlaege oder Endlagen begrenzen den Bewegungsbereich und verhindern ein Ueberfahren sicherheitskritischer Positionen.", HazardCategory: "mechanical", Examples: []string{"Feste Endanschlaege an Linearachsen", "Mechanische Hublimitierung an Pressen", "Drehwinkelbegrenzung an Drehachsen"}},
{ID: "M013", ReductionType: "design", SubType: "geometry", Name: "Sichere Geometrie", Description: "Die Bauteilgeometrie wird so gestaltet, dass Quetsch-, Scher- und Einzugsstellen vermieden werden.", HazardCategory: "mechanical", Examples: []string{"Abgerundete Formteile anstelle scharfkantiger verwenden", "Spaltmasse an Fuehrungen einhalten", "Konische statt zylindrische Aufnahmen waehlen"}},
{ID: "M014", ReductionType: "design", SubType: "material", Name: "Stabile Konstruktion", Description: "Die Konstruktion wird auf ausreichende Festigkeit und Stabilitaet ausgelegt, um ein strukturelles Versagen unter allen Betriebsbedingungen zu verhindern.", HazardCategory: "mechanical", Examples: []string{"Sicherheitsfaktoren bei Tragstrukturen einhalten", "Kippstabilitaet rechnerisch nachweisen", "Dauerfestigkeit der Schweissnaehte pruefen"}},
{ID: "M015", ReductionType: "design", SubType: "geometry", Name: "Kollisionsfreie Bewegungsbahnen", Description: "Die Bewegungsbahnen werden so geplant, dass Kollisionen mit Personen oder Gegenstaenden konstruktiv ausgeschlossen sind.", HazardCategory: "mechanical", Examples: []string{"Verfahrwege ausserhalb des Bedienerbereichs legen", "Ueberlappungsfreie Arbeitsbereiche definieren", "Bewegungsbahnen in der Simulation pruefen"}},
{ID: "M016", ReductionType: "design", SubType: "geometry", Name: "Sichere Greifer", Description: "Greifersysteme werden so konstruiert, dass ein unkontrolliertes Freisetzen des Werkstuecks und Quetschungen ausgeschlossen sind.", HazardCategory: "mechanical", Examples: []string{"Formschluessige Greiferbacken verwenden", "Federbelastete Greifer fuer Fail-Safe-Zustand", "Weiche Greiferbacken fuer empfindliche Teile"}},
{ID: "M017", ReductionType: "design", SubType: "geometry", Name: "Sichere Halterungen", Description: "Halterungen und Befestigungen sind so konstruiert, dass ein unbeabsichtigtes Loesen auch unter dynamischen Belastungen ausgeschlossen ist.", HazardCategory: "mechanical", Examples: []string{"Selbstsichernde Muttern verwenden", "Formschluessige Klemmverbindungen einsetzen", "Sicherungsstifte an Bolzenverbindungen vorsehen"}},
{ID: "M018", ReductionType: "design", SubType: "geometry", Name: "Sichere Werkstueckaufnahme", Description: "Werkstueckaufnahmen werden so gestaltet, dass ein Herausschleudern des Werkstuecks bei allen Betriebszustaenden verhindert wird.", HazardCategory: "mechanical", Examples: []string{"Spannvorrichtung mit Formschluss ausstatten", "Automatische Spannkontrolle integrieren", "Niederzugspanner an Fraestischen verwenden"}},
{ID: "M019", ReductionType: "design", SubType: "geometry", Name: "Sichere Lagefuehrung", Description: "Fuehrungselemente stellen sicher, dass sich bewegliche Teile nur in der vorgesehenen Bahn bewegen und kein seitliches Ausbrechen moeglich ist.", HazardCategory: "mechanical", Examples: []string{"Praezise Linearfuehrungen mit Spielausgleich", "Kugelfuehrungen fuer spielfreie Bewegung", "Gleitfuehrungen mit Anschlag"}},
{ID: "M020", ReductionType: "design", SubType: "geometry", Name: "Sichere Lastfuehrung", Description: "Das Fuehrungssystem fuer Lasten ist so dimensioniert, dass ein unkontrolliertes Herabfallen oder Abrutschen der Last verhindert wird.", HazardCategory: "mechanical", Examples: []string{"Fangvorrichtung an Hubtischen einbauen", "Kettensperre bei Kettenfoerderern vorsehen", "Lasthalteventile in Hubzylindern verwenden"}},
{ID: "M021", ReductionType: "design", SubType: "geometry", Name: "Sichere Kraftuebertragung", Description: "Kraftuebertragungselemente sind so dimensioniert und gesichert, dass ein Bruch oder Loesen keine Gefaehrdung verursacht.", HazardCategory: "mechanical", Examples: []string{"Wellensicherungen gegen Axialverschiebung", "Bruchsichere Kupplungen dimensionieren", "Sicherheitswellen mit Sollbruchstelle verwenden"}},
{ID: "M022", ReductionType: "design", SubType: "force_energy", Name: "Sichere Energieuebertragung", Description: "Energieleitungen und -uebertragungselemente werden so verlegt und dimensioniert, dass Leckagen oder Brueche keine Gefaehrdung darstellen.", HazardCategory: "electrical", Examples: []string{"Schleppketten fuer flexible Leitungen einsetzen", "Zugentlastungen an Steckverbindungen anbringen", "Doppelwandige Druckleitungen verwenden"}},
{ID: "M023", ReductionType: "design", SubType: "fluid_design", Name: "Sichere Hydraulikdimensionierung", Description: "Das Hydrauliksystem wird so ausgelegt, dass Druckspitzen und Leitungsbrueche keine unkontrollierten Bewegungen verursachen.", HazardCategory: "pneumatic_hydraulic", Examples: []string{"Druckspeicher mit Berstscheibe absichern", "Schlauchbruchsicherungen einbauen", "Hydraulikleitungen druckfest dimensionieren"}},
{ID: "M024", ReductionType: "design", SubType: "fluid_design", Name: "Sichere Pneumatikdimensionierung", Description: "Pneumatische Systeme werden so ausgelegt, dass ein Druckverlust oder Leitungsbruch zu einem sicheren Zustand fuehrt.", HazardCategory: "pneumatic_hydraulic", Examples: []string{"Federrueckstellung bei Druckausfall vorsehen", "Druckbegrenzer in Versorgungsleitungen einbauen", "Auffangbehaelter fuer Kondensat installieren"}},
{ID: "M025", ReductionType: "design", SubType: "fluid_design", Name: "Sichere thermische Auslegung", Description: "Die thermische Belastung aller Komponenten wird bei der Konstruktion beruecksichtigt, um Ueberhitzung und Brandgefahr zu vermeiden.", HazardCategory: "thermal", Examples: []string{"Waermeableitung durch Kuehlrippen sicherstellen", "Thermisch belastete Bauteile mit Sicherheitsreserve auslegen", "Brandlast durch Materialwahl minimieren"}},
{ID: "M026", ReductionType: "design", SubType: "fluid_design", Name: "Temperaturbegrenzung", Description: "Durch konstruktive Massnahmen wird sichergestellt, dass zugaengliche Oberflaechen keine gefaehrlichen Temperaturen erreichen.", HazardCategory: "thermal", Examples: []string{"Isolierungen an heissen Rohrleitungen anbringen", "Thermische Abschirmungen an Oefen vorsehen", "Oberflaechentemperatur unter 43 Grad Celsius halten"}},
{ID: "M027", ReductionType: "design", SubType: "fluid_design", Name: "Passive Kuehlung", Description: "Waerme wird ohne aktive Komponenten abgefuehrt, sodass kein Ausfall der Kuehlung zu einer Ueberhitzung fuehren kann.", HazardCategory: "thermal", Examples: []string{"Natuerliche Konvektion durch Rippendesign foerdern", "Waermeleitrohre (Heatpipes) einsetzen", "Waermeableitung ueber Maschinengestell realisieren"}},
{ID: "M028", ReductionType: "design", SubType: "fluid_design", Name: "Automatische Druckbegrenzung", Description: "Druckbehaelter und -leitungen sind mit passiven Begrenzungselementen ausgestattet, die ein Ueberschreiten des zulaessigen Drucks verhindern.", HazardCategory: "pneumatic_hydraulic", Examples: []string{"Berstscheiben an Druckbehaeltern montieren", "Ueberdruckventile in Hydraulikkreisen integrieren", "Druckabsicherung in Pneumatikleitungen vorsehen"}},
{ID: "M029", ReductionType: "design", SubType: "geometry", Name: "Sichere Sensorposition", Description: "Sensoren werden so positioniert, dass sie zuverlaessig messen und gleichzeitig vor mechanischer Beschaedigung geschuetzt sind.", HazardCategory: "software_control", Examples: []string{"Sensoren in geschuetzten Nischen montieren", "Abgeschirmte Sensorgehaeuse verwenden", "Sensoren ausserhalb des Gefahrbereichs platzieren"}},
{ID: "M030", ReductionType: "design", SubType: "geometry", Name: "Sichere Kabelfuehrung", Description: "Elektrische Leitungen werden so gefuehrt, dass sie vor mechanischer Beschaedigung, Feuchtigkeit und Hitze geschuetzt sind.", HazardCategory: "electrical", Examples: []string{"Kabelkanaele mit Deckel verwenden", "Leitungen in Schleppketten fuehren", "Kabel mit Knickschutz an Durchfuehrungen versehen"}},
{ID: "M031", ReductionType: "design", SubType: "geometry", Name: "Sichere Leitungsfuehrung", Description: "Hydraulik- und Pneumatikleitungen werden so verlegt, dass sie vor Beschaedigung geschuetzt sind und Leckagen erkannt werden koennen.", HazardCategory: "pneumatic_hydraulic", Examples: []string{"Leitungen in geschuetzten Kanaelen verlegen", "Auffangwannen unter Hydraulikleitungen montieren", "Farbcodierte Leitungen fuer schnelle Identifikation"}},
{ID: "M032", ReductionType: "design", SubType: "noise_vibration", Name: "Vibrationsarme Konstruktion", Description: "Durch konstruktive Massnahmen werden Vibrationen und Koerperschall an der Quelle minimiert.", HazardCategory: "noise_vibration", Examples: []string{"Schwingungsdaempfer an Motoren einbauen", "Unwuchtfreie Rotoren konstruieren", "Elastische Maschinenlagerung vorsehen"}},
{ID: "M033", ReductionType: "design", SubType: "ergonomics", Name: "Ergonomische Gestaltung", Description: "Die Maschine wird so gestaltet, dass eine koerperlich belastungsarme und gesundheitsschonende Bedienung moeglich ist.", HazardCategory: "ergonomic", Examples: []string{"Arbeitshoehe an Bedienergroesse anpassbar machen", "Gewicht von Handwerkzeugen unter 2,5 kg halten", "Griffgestaltung nach ergonomischen Richtlinien"}},
{ID: "M034", ReductionType: "design", SubType: "ergonomics", Name: "Gute Sichtbarkeit", Description: "Alle sicherheitsrelevanten Bereiche sind vom Bedienstandort aus gut einsehbar, damit Gefaehrdungen rechtzeitig erkannt werden.", HazardCategory: "ergonomic", Examples: []string{"Transparente Schutzhauben verwenden", "Spiegel an unuebersichtlichen Stellen montieren", "Kamerabasierte Sichthilfen installieren"}},
{ID: "M035", ReductionType: "design", SubType: "ergonomics", Name: "Intuitive Bedienoberflaeche", Description: "Bedienelemente und Anzeigen sind so gestaltet, dass Fehlbedienungen durch unklare Zuordnung vermieden werden.", HazardCategory: "ergonomic", Examples: []string{"Einheitliche Farbcodierung fuer Bedienfunktionen", "Logische Anordnung der Bedienelemente", "Beschriftung in Landessprache und mit Piktogrammen"}},
{ID: "M036", ReductionType: "design", SubType: "ergonomics", Name: "Sichere Mensch-Maschine-Interaktion", Description: "Die Schnittstelle zwischen Bediener und Maschine ist so konzipiert, dass gefaehrliche Missverstaendnisse ausgeschlossen sind.", HazardCategory: "ergonomic", Examples: []string{"Eindeutige Statusindikatoren an der Maschine anbringen", "Akustische und optische Rueckmeldung bei Gefahrsituationen", "Bestaetigung vor Ausfuehrung kritischer Befehle fordern"}},
{ID: "M037", ReductionType: "design", SubType: "ergonomics", Name: "Sichere Bedienhoehe", Description: "Bedienelemente sind in einer Hoehe angebracht, die ein sicheres und ermuedungsfreies Arbeiten ermoeglicht.", HazardCategory: "ergonomic", Examples: []string{"Hauptbedienfeld auf Ellbogenhoehe montieren", "Not-Halt in Greifhoehe anbringen", "Schwenkbare Bedientableaus fuer flexible Positionierung"}},
{ID: "M038", ReductionType: "design", SubType: "ergonomics", Name: "Sichere Reichweiten", Description: "Alle sicherheitsrelevanten Bedienelemente befinden sich innerhalb der ergonomisch guenstigen Reichweite des Bedieners.", HazardCategory: "ergonomic", Examples: []string{"Haeufig genutzte Taster im Nahbereich anordnen", "Selten genutzte Schalter im Fernbereich platzieren", "Reichweitendiagramme bei der Planung anwenden"}},
{ID: "M039", ReductionType: "design", SubType: "ergonomics", Name: "Sichere Wartungszugaenge", Description: "Wartungsbereiche sind so gestaltet, dass Wartungsarbeiten gefahrlos und ohne Demontage sicherheitsrelevanter Schutzeinrichtungen durchgefuehrt werden koennen.", HazardCategory: "ergonomic", Examples: []string{"Wartungsklappen mit Sicherheitsverriegelung vorsehen", "Inspektionsoeffnungen an kritischen Stellen anbringen", "Ausreichende Arbeitsflaeche im Wartungsbereich sicherstellen"}},
{ID: "M040", ReductionType: "design", SubType: "ergonomics", Name: "Sichere Montagepunkte", Description: "Montagepunkte fuer Baugruppen sind so konstruiert, dass ein sicheres Handling waehrend Montage und Demontage gewaehrleistet ist.", HazardCategory: "ergonomic", Examples: []string{"Anschlagpunkte fuer Hebezeuge vorsehen", "Passstifte fuer lagegenaue Montage verwenden", "Montagehilfen fuer schwere Baugruppen bereitstellen"}},
{ID: "M041", ReductionType: "design", SubType: "ergonomics", Name: "Sichere Demontagepunkte", Description: "Demontagepunkte sind so markiert und konstruiert, dass ein sicherer Rueckbau ohne unbeabsichtigtes Freisetzen von Energie moeglich ist.", HazardCategory: "ergonomic", Examples: []string{"Ablassoeffnungen fuer Betriebsfluide vorsehen", "Entlueftungspunkte fuer Druckbehaelter kennzeichnen", "Abziehgewinde fuer Pressverbindungen einplanen"}},
{ID: "M042", ReductionType: "design", SubType: "ergonomics", Name: "Sichere Servicezugaenge", Description: "Servicebereiche sind bei abgeschalteter Maschine sicher zugaenglich und alle gespeicherten Energien koennen kontrolliert abgebaut werden.", HazardCategory: "ergonomic", Examples: []string{"Servicetreppen und -plattformen montieren", "Beleuchtung im Servicebereich installieren", "Ableitungsmechanismen fuer gespeicherte Energie bereitstellen"}},
{ID: "M043", ReductionType: "design", SubType: "control_design", Name: "Sichere Software-Fallbacks", Description: "Die Steuerungssoftware enthaelt definierte Rueckfallstrategien, die bei Softwarefehlern einen sicheren Maschinenzustand herstellen.", HazardCategory: "software_control", Examples: []string{"Standardwerte bei Sensorausfall verwenden", "Sicherer Stopp bei unplausiblen Eingangsdaten", "Automatischer Wechsel in den Notbetrieb bei Softwarefehler"}},
{ID: "M044", ReductionType: "design", SubType: "control_design", Name: "Deterministische Steuerungslogik", Description: "Die Steuerungslogik ist vollstaendig deterministisch aufgebaut, sodass bei identischen Eingaben immer identische Ausgaben erzeugt werden.", HazardCategory: "software_control", Examples: []string{"Keine Zufallselemente in Sicherheitsfunktionen verwenden", "Feste Zykluszeiten fuer alle sicherheitsrelevanten Tasks", "Deadlock-freie Programmstruktur sicherstellen"}},
{ID: "M045", ReductionType: "design", SubType: "control_design", Name: "Definierte Zustandsmaschine", Description: "Alle Maschinenzustaende und Uebergaenge sind vollstaendig definiert, dokumentiert und gegen unzulaessige Zustandswechsel abgesichert.", HazardCategory: "software_control", Examples: []string{"Zustandsdiagramm fuer alle Betriebsmodi erstellen", "Ungueltige Zustandsuebergaenge softwareseitig blockieren", "Maschinenzustand dauerhaft anzeigen"}},
{ID: "M046", ReductionType: "design", SubType: "control_design", Name: "Sichere Restart-Logik", Description: "Ein Neustart der Maschine nach Stopp oder Fehler ist nur durch bewusste Bedienerhandlung moeglich und fuehrt in einen sicheren Ausgangszustand.", HazardCategory: "software_control", Examples: []string{"Automatischen Wiederanlauf nach Netzausfall verhindern", "Quittierungspflicht vor Neustart einbauen", "Schrittweisen Anlauf nach Fehler erzwingen"}},
{ID: "M047", ReductionType: "design", SubType: "control_design", Name: "Sichere Fehlermodi", Description: "Jeder erkannte Fehler fuehrt die Maschine in einen vordefinierten sicheren Zustand, ohne dass eine Bedienerinteraktion erforderlich ist.", HazardCategory: "software_control", Examples: []string{"Fail-Safe-Verhalten bei Sensorausfall implementieren", "Fail-Stop bei unbekanntem Fehlerzustand", "Fehlerkatalog mit zugeordneten sicheren Zustaenden pflegen"}},
{ID: "M048", ReductionType: "design", SubType: "control_design", Name: "Sichere Energieabschaltung", Description: "Die Maschine kann jederzeit sicher und vollstaendig von allen Energiequellen getrennt werden.", HazardCategory: "electrical", Examples: []string{"Hauptschalter mit Absperrmoeglichkeit vorsehen", "Pneumatik-Absperrventil am Maschineneingang installieren", "Alle Energiequellen mit eindeutiger Beschriftung versehen"}},
{ID: "M049", ReductionType: "design", SubType: "control_design", Name: "Sichere Energieentladung", Description: "Nach dem Abschalten werden alle gespeicherten Energien kontrolliert abgebaut, bevor der Zugang zum Gefahrbereich freigegeben wird.", HazardCategory: "electrical", Examples: []string{"Kondensatoren ueber Entladewiderstaende entladen", "Druckspeicher ueber Entlastungsventil entspannen", "Federspeicher kontrolliert loesen"}},
{ID: "M050", ReductionType: "design", SubType: "control_design", Name: "Sichere Notzustaende", Description: "Fuer alle vorhersehbaren Notsituationen sind definierte Zustaende festgelegt, die automatisch oder durch Bedienereingriff hergestellt werden.", HazardCategory: "general", Examples: []string{"Not-Halt-Zustand mit definierten Achspositionen", "Notzustand bei Brand mit automatischer Loeschausloesung", "Evakuierungszustand mit geoeffneten Schutztoren"}},
// ====================================================================
// Type 2: Protection (ReductionType: "protection") — Technische Schutzmassnahmen
// 70 entries: M051-M120
// ====================================================================
{ID: "M051", ReductionType: "protection", SubType: "fixed_guard", Name: "Feste trennende Schutzeinrichtung", Description: "Eine fest montierte physische Barriere verhindert den Zugang zum Gefahrbereich und kann nur mit Werkzeug entfernt werden.", HazardCategory: "mechanical", Examples: []string{"Verschraubte Schutzverkleidung an Antrieben", "Fest montierter Schutzzaun um Roboterzelle", "Schutzhaube ueber Riementrieb"}},
{ID: "M052", ReductionType: "protection", SubType: "movable_guard", Name: "Bewegliche Schutzeinrichtung", Description: "Eine bewegliche Schutzeinrichtung kann ohne Werkzeug geoeffnet werden und ist mit einer Verriegelung gekoppelt, die die Maschine bei geoeffneter Tuer abschaltet.", HazardCategory: "mechanical", Examples: []string{"Schwenkbare Schutzhaube mit Sicherheitsschalter", "Schiebetueren mit Verriegelungssystem", "Klappbare Schutzabdeckung mit Positionsschalter"}},
{ID: "M053", ReductionType: "protection", SubType: "movable_guard", Name: "Verriegelte Schutztuer", Description: "Die Schutztuer ist mit einer Sicherheitsverriegelung versehen, die ein Oeffnen nur bei stillstehender Maschine erlaubt.", HazardCategory: "mechanical", Examples: []string{"Zuhaltung mit Zeitverzoegerung an Schutztuer", "Elektromagnetische Zuhaltung mit Freigabebestaetigung", "Bolzenverriegelung mit sicherer Positionsueberwachung"}},
{ID: "M054", ReductionType: "protection", SubType: "electro_sensitive", Name: "Lichtgitter", Description: "Ein optoelektronisches Schutzfeld erkennt das Eingreifen in den Gefahrbereich und loest einen sicheren Maschinenstopp aus.", HazardCategory: "mechanical", Examples: []string{"Typ-4-Lichtgitter an Pressenzufuehrung", "Handschutz-Lichtgitter mit 14mm Aufloesung", "Koerperschutz-Lichtgitter mit 30mm Aufloesung"}},
{ID: "M055", ReductionType: "protection", SubType: "electro_sensitive", Name: "Laserscanner", Description: "Ein flaechenabdeckendes Laserscansystem ueberwacht den Bodenbereich vor der Maschine und unterscheidet zwischen Warn- und Schutzfeld.", HazardCategory: "mechanical", Examples: []string{"Sicherheitslaserscanner an mobiler Plattform", "Bodennaher Laserscanner an Palettieranlage", "Multizonen-Laserscanner mit anpassbaren Feldern"}},
{ID: "M056", ReductionType: "protection", SubType: "electrical_protection", Name: "Zweihandbedienung", Description: "Die Maschine kann nur ausgeloest werden, wenn beide Haende des Bedieners gleichzeitig an den Bedienelementen sind, sodass sie sich nicht im Gefahrbereich befinden koennen.", HazardCategory: "mechanical", Examples: []string{"Zweihand-Sicherheitsschaltung an Exzenterpresse", "Synchrone Tastenbetaetigung mit Zeitfenster unter 0,5s", "Typ-IIIC-Zweihandsteuerung installieren"}},
{ID: "M057", ReductionType: "protection", SubType: "electrical_protection", Name: "Zustimmschalter", Description: "Ein Dreistufenschalter erlaubt das Verfahren der Maschine im Einrichtbetrieb nur solange der Bediener den Taster aktiv gedrueckt haelt.", HazardCategory: "mechanical", Examples: []string{"Dreistufigen Zustimmtaster am Handgeraet verbauen", "Zustimmfunktion fuer Roboter-Teachbetrieb aktivieren", "Panikausloesung bei Durchdruecken oder Loslassen"}},
{ID: "M058", ReductionType: "protection", SubType: "emergency_stop", Name: "Not-Halt", Description: "Rot-gelbe Pilztaster sind an allen Bedienstellen und Zugaengen angebracht und fuehren die Maschine in den sicheren Stopp-Zustand.", HazardCategory: "general", Examples: []string{"Not-Halt-Taster an jedem Bedienfeld montieren", "Not-Halt-Reissleine entlang langer Foerderstrecken", "Drahtlos verbundenen Not-Halt-Taster fuer mobile Bedienung"}},
{ID: "M059", ReductionType: "protection", SubType: "electrical_protection", Name: "Sicherheitsrelais", Description: "Zwangsgefuehrte Sicherheitsrelais ueberwachen Schutzeinrichtungen und schalten die Maschine bei Ausfall zuverlaessig ab.", HazardCategory: "electrical", Examples: []string{"Not-Halt-Sicherheitsrelais mit Querschlusserkennung", "Tuerverriegelungs-Auswertegeraet installieren", "Lichtgitter-Sicherheitsrelais mit Anlauftestung"}},
{ID: "M060", ReductionType: "protection", SubType: "safety_control", Name: "Sichere SPS", Description: "Eine fehlersichere speicherprogrammierbare Steuerung fuehrt alle sicherheitsrelevanten Funktionen redundant und diversitaer aus.", HazardCategory: "software_control", Examples: []string{"Zweikanalige SPS mit Kreuzvergleich einsetzen", "SIL-3-zertifizierte Safety-SPS installieren", "Sicherheitsprogramm mit Diversitaet ausfuehren"}},
{ID: "M061", ReductionType: "protection", SubType: "safety_control", Name: "Sicherheitssteuerung", Description: "Eine dedizierte Sicherheitssteuerung uebernimmt ausschliesslich die sicherheitsrelevanten Steuerungsfunktionen getrennt von der Standardsteuerung.", HazardCategory: "software_control", Examples: []string{"Separaten Safety-Controller fuer Achsueberwachung einsetzen", "Sicherheitssteuerung mit eigenem Diagnosesystem betreiben", "Safety-Netzwerk von Standard-Netzwerk trennen"}},
{ID: "M062", ReductionType: "protection", SubType: "electrical_protection", Name: "Sichere Positionsueberwachung", Description: "Die Position sicherheitskritischer Achsen wird redundant ueberwacht und bei Abweichung wird ein sicherer Zustand hergestellt.", HazardCategory: "mechanical", Examples: []string{"Doppelte Absolutwertgeber an Servoachsen", "Sichere Positionsauswertung ueber Safety-SPS", "Referenzfahrt mit sicherer Endlageerkennung"}},
{ID: "M063", ReductionType: "protection", SubType: "electrical_protection", Name: "Sichere Geschwindigkeitsueberwachung", Description: "Die Geschwindigkeit beweglicher Maschinenteile wird sicher ueberwacht und bei Ueberschreitung des Grenzwerts wird ein Stopp ausgeloest.", HazardCategory: "mechanical", Examples: []string{"Sichere Drehzahlueberwachung an Spindeln", "Geschwindigkeitsbegrenzung im Einrichtbetrieb ueberwachen", "Zweikanalige Geschwindigkeitsmessung implementieren"}},
{ID: "M064", ReductionType: "protection", SubType: "electrical_protection", Name: "Sichere Stillstandsueberwachung", Description: "Der Stillstand sicherheitskritischer Achsen wird sicher erkannt und der Zugang zum Gefahrbereich erst nach bestaetigtem Stillstand freigegeben.", HazardCategory: "mechanical", Examples: []string{"Sichere Stillstandserkennung ueber Geberrauswertung", "Stillstandsfreigabe fuer Schutztueroeffnung implementieren", "Zeitverzoegerte Freigabe nach Motorabschaltung"}},
{ID: "M065", ReductionType: "protection", SubType: "fluid_protection", Name: "Sicherheitsventil", Description: "Ein Sicherheitsventil oeffnet bei Ueberschreitung des zulaessigen Drucks und leitet das Medium kontrolliert ab.", HazardCategory: "pneumatic_hydraulic", Examples: []string{"Federbelastetes Sicherheitsventil am Druckbehaelter", "Ueberstroemventil in Hydraulikkreislauf einbauen", "Entlueftungsventil bei Pneumatikueberdruck vorsehen"}},
{ID: "M066", ReductionType: "protection", SubType: "fluid_protection", Name: "Druckbegrenzungsventil", Description: "Ein voreingestelltes Druckbegrenzungsventil schuetzt das System vor unzulaessig hohem Druck im gesamten Betriebsbereich.", HazardCategory: "pneumatic_hydraulic", Examples: []string{"Druckbegrenzungsventil in Hydraulik-Hauptleitung", "Reduzierventil in Pneumatik-Zuluftleitung", "Druckwaechterschaltung mit Abschaltung bei Ueberdruck"}},
{ID: "M067", ReductionType: "protection", SubType: "electrical_protection", Name: "Sicherheitskupplung", Description: "Eine Sicherheitskupplung trennt die Kraftuebertragung bei Ueberschreitung des zulaessigen Drehmoments und schuetzt so Mensch und Maschine.", HazardCategory: "mechanical", Examples: []string{"Rutschkupplung in Antriebsstrang einbauen", "Brechbolzenkupplung als Sollbruchstelle vorsehen", "Elektronisch ueberwachte Sicherheitskupplung verwenden"}},
{ID: "M068", ReductionType: "protection", SubType: "electrical_protection", Name: "Mechanischer Anschlag", Description: "Feste Anschlaege begrenzen den Verfahrweg und verhindern ein Ueberfahren der zulaessigen Endposition.", HazardCategory: "mechanical", Examples: []string{"Hartanschlag an Linearachse montieren", "Stossabsorber am Verfahrwegende einbauen", "Anschlagpuffer an Schwenkachse anbringen"}},
{ID: "M069", ReductionType: "protection", SubType: "electrical_protection", Name: "Bremssystem", Description: "Ein zuverlaessiges Bremssystem verzoegert und stoppt bewegliche Maschinenteile bei Abschaltung oder im Notfall.", HazardCategory: "mechanical", Examples: []string{"Federspeicherbremse an Vertikalachse montieren", "Haltebremse an jeder Antriebsachse vorsehen", "Redundante Bremse mit diversitaerem Wirkprinzip einsetzen"}},
{ID: "M070", ReductionType: "protection", SubType: "electrical_protection", Name: "Energieabschaltung", Description: "Die sichere Energieabschaltung trennt die Maschine zuverlaessig von allen Energiequellen und stellt sicher, dass keine Restenergie vorhanden ist.", HazardCategory: "electrical", Examples: []string{"Reparaturschalter mit Vorhangschloss-Absperrung", "Pneumatik-Absperrventil mit Entlueftung", "Hydraulik-Absperrung mit Druckentlastung"}},
{ID: "M071", ReductionType: "protection", SubType: "electrical_protection", Name: "Lockout-Tagout", Description: "Ein definiertes Verfahren zur Verriegelung und Kennzeichnung von Energiequellen stellt sicher, dass waehrend Wartungsarbeiten keine unbeabsichtigte Energiezufuhr erfolgt.", HazardCategory: "electrical", Examples: []string{"Absperrvorrichtungen fuer alle Energiequellen bereitstellen", "Persoenliche Sicherheitsschloesser an jeden Wartungsarbeiter ausgeben", "LOTO-Verfahrensanweisung an der Maschine aushangen"}},
{ID: "M072", ReductionType: "protection", SubType: "safety_control", Name: "Automatische Abschaltung", Description: "Die Maschine wird bei Erkennung eines Fehlers oder einer gefaehrlichen Situation automatisch in den sicheren Zustand gebracht.", HazardCategory: "software_control", Examples: []string{"Automatische Abschaltung bei Uebertemperatur", "Selbsttaetige Stillsetzung bei Schwingungsueberschreitung", "Automatischer Stopp bei Sensorausfall"}},
{ID: "M073", ReductionType: "protection", SubType: "electro_sensitive", Name: "Sicherheitslichtschranke", Description: "Einstrahl-Sicherheitslichtschranken ueberwachen den Zugang zu Gefahrbereichen und loesen bei Unterbrechung einen sicheren Stopp aus.", HazardCategory: "mechanical", Examples: []string{"Einstrahl-Lichtschranke an Maschinenzugang", "Umlenkspiegel-Lichtschranke fuer L-foermigen Bereich", "Reflex-Lichtschranke an Auswurfoeffnung"}},
{ID: "M074", ReductionType: "protection", SubType: "pressure_sensitive", Name: "Sicherheitsmatte", Description: "Druckempfindliche Bodenmatten erkennen das Betreten des Gefahrbereichs und loesen einen sicheren Maschinenstopp aus.", HazardCategory: "mechanical", Examples: []string{"Sicherheitsmatte vor Roboterzelle auslegen", "Schaltmatte unter Palettierstation installieren", "Trittflaeche mit Personenerkennung am Maschinenzugang"}},
{ID: "M075", ReductionType: "protection", SubType: "fixed_guard", Name: "Schutzzaun", Description: "Ein feststehender Schutzzaun umgibt den Gefahrbereich vollstaendig und kann nur mit Werkzeug demontiert werden.", HazardCategory: "mechanical", Examples: []string{"Stahlgitterzaun um Roboterzelle montieren", "Polycarbonat-Schutzwand an Drehmaschine befestigen", "Maschendrahtzaun um Foerderanlage aufstellen"}},
{ID: "M076", ReductionType: "protection", SubType: "movable_guard", Name: "Verriegelungssystem", Description: "Ein kombiniertes Verriegelungs- und Zuhaltesystem stellt sicher, dass Schutztueren nur im sicheren Maschinenzustand geoeffnet werden koennen.", HazardCategory: "mechanical", Examples: []string{"Sicherheits-Tuerschalter mit Zuhaltung montieren", "RFID-basierte Zuhalteeinrichtung einsetzen", "Codierte Sicherheitsschalter mit Manipulationsschutz"}},
{ID: "M077", ReductionType: "protection", SubType: "cybersecurity", Name: "Zugangskontrolle", Description: "Ein physisches oder elektronisches Zugangskontrollsystem stellt sicher, dass nur autorisierte Personen die Maschine bedienen koennen.", HazardCategory: "cyber_network", Examples: []string{"Schluesselsystem fuer Betriebsartenwahl einsetzen", "RFID-Chipkarten fuer Maschinenzugang verwenden", "Biometrische Zugangskontrolle am Schaltschrank"}},
{ID: "M078", ReductionType: "protection", SubType: "cybersecurity", Name: "Sichere Authentifizierung", Description: "Alle Benutzer muessen sich vor der Nutzung von Steuerungsfunktionen sicher authentifizieren, um unberechtigten Zugriff zu verhindern.", HazardCategory: "cyber_network", Examples: []string{"Passwortschutz am HMI-Panel aktivieren", "Mehrstufige Authentifizierung fuer Parametrierung einrichten", "Zeitlich begrenzte Sitzungen am Bedienterminal"}},
{ID: "M079", ReductionType: "protection", SubType: "cybersecurity", Name: "Rollenbasierte Rechte", Description: "Zugriffsrechte auf Maschinenfunktionen und Parameter sind nach Benutzerrollen differenziert und einschraenkt.", HazardCategory: "cyber_network", Examples: []string{"Bediener-Rolle mit eingeschraenkten Rechten definieren", "Instandhalter-Rolle mit erweitertem Parameterzugriff", "Administrator-Rolle fuer Konfigurationsaenderungen"}},
{ID: "M080", ReductionType: "protection", SubType: "cybersecurity", Name: "Sichere Netzwerksegmentierung", Description: "Das Maschinennetzwerk ist von anderen Netzwerken getrennt, um ein Uebergreifen von Angriffen auf sicherheitskritische Steuerungen zu verhindern.", HazardCategory: "cyber_network", Examples: []string{"OT-Netzwerk vom IT-Netzwerk per VLAN trennen", "Safety-Netzwerk physisch vom Standard-Netzwerk isolieren", "DMZ zwischen Office- und Produktionsnetzwerk einrichten"}},
{ID: "M081", ReductionType: "protection", SubType: "cybersecurity", Name: "Firewall", Description: "Eine konfigurierte Firewall schuetzt das Steuerungsnetzwerk vor unautorisiertem Netzwerkverkehr.", HazardCategory: "cyber_network", Examples: []string{"Industrie-Firewall vor SPS-Netzwerk installieren", "Deep-Packet-Inspection fuer Industrieprotokolle aktivieren", "Whitelisting fuer erlaubte Kommunikationsverbindungen"}},
{ID: "M082", ReductionType: "protection", SubType: "cybersecurity", Name: "Sichere Fernwartung", Description: "Der Fernzugriff auf die Maschine erfolgt ausschliesslich ueber verschluesselte und authentifizierte Verbindungen mit zeitlicher Begrenzung.", HazardCategory: "cyber_network", Examples: []string{"VPN-Verbindung fuer Fernwartung einrichten", "Fernzugriff nur nach lokaler Freigabe ermoeglichen", "Session-Timeout fuer Fernwartungszugang konfigurieren"}},
{ID: "M083", ReductionType: "protection", SubType: "cybersecurity", Name: "Verschluesselte Kommunikation", Description: "Alle sicherheitsrelevanten Kommunikationsverbindungen sind verschluesselt, um Abhoeren und Manipulation zu verhindern.", HazardCategory: "cyber_network", Examples: []string{"TLS-Verschluesselung fuer HMI-Kommunikation aktivieren", "OPC UA mit Security Mode Sign&Encrypt konfigurieren", "MQTT-Kommunikation mit TLS absichern"}},
{ID: "M084", ReductionType: "protection", SubType: "cybersecurity", Name: "Signierte Updates", Description: "Software- und Firmware-Updates werden vor der Installation auf ihre Authentizitaet und Integritaet geprueft.", HazardCategory: "cyber_network", Examples: []string{"Code-Signing fuer Steuerungssoftware implementieren", "Firmware-Updates nur mit gueltiger Signatur akzeptieren", "Hash-Pruefung vor jeder Update-Installation durchfuehren"}},
{ID: "M085", ReductionType: "protection", SubType: "cybersecurity", Name: "Sichere Updatepruefung", Description: "Vor dem Einspielen eines Updates wird automatisch geprueft, ob es mit der bestehenden Konfiguration kompatibel und sicher ist.", HazardCategory: "cyber_network", Examples: []string{"Kompatibilitaetspruefung vor Update-Installation ausfuehren", "Automatische Plausibilitaetspruefung der Update-Datei", "Pruefprotokoll fuer jedes eingespieltes Update erstellen"}},
{ID: "M086", ReductionType: "protection", SubType: "cybersecurity", Name: "Update-Rollback", Description: "Ein Mechanismus ermoeglicht das automatische Zuruecksetzen auf die vorherige Softwareversion, falls ein Update fehlschlaegt.", HazardCategory: "cyber_network", Examples: []string{"Dual-Boot-System fuer Safety-SPS bereitstellen", "Automatischen Rollback bei gescheitertem Update ausfuehren", "Backup der alten Firmware vor jedem Update erstellen"}},
{ID: "M087", ReductionType: "protection", SubType: "cybersecurity", Name: "Integritaetspruefung", Description: "Die Integritaet der Steuerungssoftware und Konfiguration wird regelmaessig automatisch ueberprueft.", HazardCategory: "cyber_network", Examples: []string{"CRC-Pruefung des Sicherheitsprogramms bei jedem Start", "Laufende Hash-Verifizierung der Firmware im Betrieb", "Zyklische Integritaetspruefung der Parameterdaten"}},
{ID: "M088", ReductionType: "protection", SubType: "cybersecurity", Name: "Sichere Bootkette", Description: "Der Startvorgang der Steuerung wird durch eine sichere Bootkette geschuetzt, die nur vertrauenswuerdige Software laed.", HazardCategory: "cyber_network", Examples: []string{"Secure Boot im BIOS der Steuerung aktivieren", "Chain-of-Trust vom Bootloader bis zur Applikation aufbauen", "Hardware-Sicherheitsmodul fuer Boot-Verifizierung einsetzen"}},
{ID: "M089", ReductionType: "protection", SubType: "cybersecurity", Name: "Sichere Firmware", Description: "Die Firmware der Steuerungskomponenten ist gegen unbefugte Aenderung geschuetzt und wird regelmaessig aktualisiert.", HazardCategory: "cyber_network", Examples: []string{"Schreibschutz fuer Firmware aktivieren", "Firmware-Versionskontrolle implementieren", "Nur vom Hersteller freigegebene Firmware einsetzen"}},
{ID: "M090", ReductionType: "protection", SubType: "cybersecurity", Name: "Sichere Parameteraenderung", Description: "Aenderungen an sicherheitsrelevanten Maschinenparametern erfordern eine Authentifizierung und werden protokolliert.", HazardCategory: "cyber_network", Examples: []string{"Parameterschutz ueber Zugangsstufen realisieren", "Aenderungsprotokoll fuer sicherheitsrelevante Parameter fuehren", "Vier-Augen-Prinzip bei Safety-Parameteraenderungen umsetzen"}},
{ID: "M091", ReductionType: "protection", SubType: "monitoring", Name: "Audit-Trail", Description: "Alle sicherheitsrelevanten Aktionen und Ereignisse werden mit Zeitstempel und Benutzerkennung unveraenderbar protokolliert.", HazardCategory: "cyber_network", Examples: []string{"Sicherheitsereignisse in geschuetztem Logfile speichern", "Benutzerzugriffe auf Safety-Parameter aufzeichnen", "Aenderungshistorie fuer Sicherheitskonfiguration fuehren"}},
{ID: "M092", ReductionType: "protection", SubType: "monitoring", Name: "Alarmierung", Description: "Bei sicherheitsrelevanten Zustandsaenderungen werden optische und akustische Alarme ausgeloest.", HazardCategory: "general", Examples: []string{"Signalleuchte bei Warnung gelb, bei Gefahr rot schalten", "Akustischen Alarm bei Schutztuerverletzung ausloesen", "Blinkende Warnleuchte vor Maschinenstart aktivieren"}},
{ID: "M093", ReductionType: "protection", SubType: "monitoring", Name: "Zustandsueberwachung", Description: "Der Maschinenzustand wird kontinuierlich ueberwacht und Abweichungen vom Normalzustand werden fruehzeitig erkannt.", HazardCategory: "general", Examples: []string{"Condition-Monitoring-System fuer Lager installieren", "Trendanalyse fuer Motorstroeme durchfuehren", "Zustandsbasierte Wartungsplanung implementieren"}},
{ID: "M094", ReductionType: "protection", SubType: "monitoring", Name: "Temperaturueberwachung", Description: "Temperatursensoren ueberwachen sicherheitskritische Stellen und loesen bei Grenzwertueberschreitung eine Schutzreaktion aus.", HazardCategory: "thermal", Examples: []string{"Temperaturfuehler an Motorwicklungen installieren", "Thermoelement an Hydraulikoeltank montieren", "Infrarot-Temperatursensor an Lagergehaeuse anbringen"}},
{ID: "M095", ReductionType: "protection", SubType: "monitoring", Name: "Vibrationsueberwachung", Description: "Schwingungssensoren erkennen erhoehte Vibrationen an rotierenden Teilen und warnen vor drohendem Komponentenversagen.", HazardCategory: "noise_vibration", Examples: []string{"Beschleunigungssensoren an Hauptlagern anbringen", "Schwingungsanalyse-System fuer Spindeln einrichten", "Grenzwertbasierte Vibrationswarnung konfigurieren"}},
{ID: "M096", ReductionType: "protection", SubType: "fluid_protection", Name: "Druckueberwachung", Description: "Der Druck in hydraulischen und pneumatischen Systemen wird kontinuierlich ueberwacht und bei Grenzwertueberschreitung wird eine Schutzreaktion ausgeloest.", HazardCategory: "pneumatic_hydraulic", Examples: []string{"Druckschalter in Hydraulik-Hauptleitung einbauen", "Pneumatik-Druckueberwachung mit Abschaltung konfigurieren", "Differenzdruck-Ueberwachung an Filtern installieren"}},
{ID: "M097", ReductionType: "protection", SubType: "monitoring", Name: "Stromueberwachung", Description: "Die Stromaufnahme sicherheitskritischer Antriebe wird ueberwacht und bei Ueberlast oder Kurzschluss wird abgeschaltet.", HazardCategory: "electrical", Examples: []string{"Motorschutzschalter an jedem Antrieb vorsehen", "Strommessung an Servoachsen fuer Kollisionserkennung nutzen", "Fehlerstrom-Schutzeinrichtung installieren"}},
{ID: "M098", ReductionType: "protection", SubType: "emergency_stop", Name: "Notabschaltung", Description: "Eine Notabschaltung trennt die Maschine sofort von der Energieversorgung, wenn eine unmittelbare Gefahr fuer Leib und Leben besteht.", HazardCategory: "general", Examples: []string{"Notabschaltung der Stromversorgung am Hauptschalter", "Schnellablass fuer Hydraulikdruck bei Notabschaltung", "Automatische Entlueftung der Pneumatik bei Notstopp"}},
{ID: "M099", ReductionType: "protection", SubType: "monitoring", Name: "Sicherheitsueberwachung", Description: "Alle sicherheitsrelevanten Funktionen werden zyklisch auf korrekte Funktion geprueft und Fehler werden erkannt.", HazardCategory: "general", Examples: []string{"Zyklischen Funktionstest der Sicherheitskette durchfuehren", "Online-Diagnose fuer Sicherheitsrelais aktivieren", "Periodische Pruefung der Not-Halt-Funktion"}},
{ID: "M100", ReductionType: "protection", SubType: "electrical_protection", Name: "Sichere Energieentladung", Description: "Nach dem Abschalten entladen sich alle gespeicherten Energien kontrolliert, bevor der Zugang zum Gefahrbereich freigegeben wird.", HazardCategory: "electrical", Examples: []string{"Entladewiderstaende fuer Zwischenkreiskondensatoren vorsehen", "Entlueftungsventil fuer Druckspeicher automatisch betaetigen", "Federspeicherbremse nach Energieabschaltung kontrolliert loesen"}},
{ID: "M101", ReductionType: "protection", SubType: "monitoring", Name: "Automatische Diagnose", Description: "Integrierte Diagnosefunktionen erkennen Fehler in sicherheitsrelevanten Komponenten und melden diese dem Bediener.", HazardCategory: "software_control", Examples: []string{"Automatische Sensordiagnose bei Maschinenstart", "Fehlerspeicher mit Diagnose-Codes am HMI anzeigen", "Remote-Diagnose fuer Sicherheitssteuerung einrichten"}},
{ID: "M102", ReductionType: "protection", SubType: "monitoring", Name: "Selbsttest", Description: "Sicherheitsrelevante Baugruppen fuehren bei jedem Start oder zyklisch automatische Selbsttests durch und melden erkannte Fehler.", HazardCategory: "software_control", Examples: []string{"Anlauftestung des Lichtgitters bei jedem Maschinenstart", "ROM-CRC-Pruefung bei Steuerungsstart durchfuehren", "Zyklischer Relaistest mit Rueckmeldesignal"}},
{ID: "M103", ReductionType: "protection", SubType: "safety_control", Name: "Watchdog", Description: "Ein Watchdog-Timer ueberwacht die Steuerungssoftware und loest bei Ausbleiben des Ruecksetzsignals einen sicheren Zustand aus.", HazardCategory: "software_control", Examples: []string{"Hardware-Watchdog auf Safety-SPS aktivieren", "Fenster-Watchdog mit engem Zeitfenster konfigurieren", "Externen Watchdog fuer diversitaere Ueberwachung einsetzen"}},
{ID: "M104", ReductionType: "protection", SubType: "monitoring", Name: "Redundante Sensorik", Description: "Sicherheitskritische Messstellen werden mit mindestens zwei unabhaengigen Sensoren ueberwacht und die Signale auf Plausibilitaet verglichen.", HazardCategory: "software_control", Examples: []string{"Zweikanalige Positionserfassung an Sicherheitsachse", "Diversitaere Druckmessung mit unterschiedlichen Messprinzipien", "Redundante Temperatursensoren an kritischen Stellen"}},
{ID: "M105", ReductionType: "protection", SubType: "monitoring", Name: "Redundante Aktorik", Description: "Sicherheitskritische Aktoren sind redundant ausgefuehrt, sodass bei Ausfall eines Aktors die Sicherheitsfunktion erhalten bleibt.", HazardCategory: "software_control", Examples: []string{"Doppelte Abschaltventile in Reihe schalten", "Redundante Bremsen mit diversitaerem Wirkprinzip einsetzen", "Zweikanalige Schuetze fuer Motorabschaltung verwenden"}},
{ID: "M106", ReductionType: "protection", SubType: "cybersecurity", Name: "Sichere Kommunikationsprotokolle", Description: "Fuer die Kommunikation zwischen Steuerungskomponenten werden sichere und authentifizierte Industrieprotokolle eingesetzt.", HazardCategory: "cyber_network", Examples: []string{"PROFIsafe fuer sichere SPS-Kommunikation einsetzen", "CIP Safety fuer EtherNet/IP-Netzwerke konfigurieren", "FSoE fuer EtherCAT-Safety-Kommunikation verwenden"}},
{ID: "M107", ReductionType: "protection", SubType: "cybersecurity", Name: "Zugriffskontrollsystem", Description: "Ein umfassendes Zugriffskontrollsystem regelt und dokumentiert physische und logische Zugriffe auf alle Maschinenkomponenten.", HazardCategory: "cyber_network", Examples: []string{"Zentrale Benutzerverwaltung fuer alle Steuerungssysteme", "Physische Schluesselsysteme fuer Schaltschraenke einsetzen", "Zugriffsprotokolle fuer alle Steuerungszugriffe fuehren"}},
{ID: "M108", ReductionType: "protection", SubType: "monitoring", Name: "Anomalieerkennung", Description: "Ein System zur Erkennung ungewoehnlicher Verhaltensmuster im Maschinenbetrieb warnt fruehzeitig vor potenziellen Sicherheitsproblemen.", HazardCategory: "ai_specific", Examples: []string{"Maschinelles Lernen fuer Normalbetriebsprofil einsetzen", "Statistische Prozesskontrolle fuer Sicherheitsparameter", "Netzwerk-Anomalieerkennung fuer Industrieprotokolle"}},
{ID: "M109", ReductionType: "protection", SubType: "monitoring", Name: "Sicherheitsmonitor", Description: "Ein unabhaengiger Sicherheitsmonitor ueberwacht die Einhaltung aller Sicherheitsbedingungen parallel zur Standardsteuerung.", HazardCategory: "software_control", Examples: []string{"Unabhaengigen Safety-Monitor fuer Roboterbewegung einsetzen", "Externen Sicherheitsmonitor fuer Drehzahlueberwachung nutzen", "Separaten Sicherheitsrechner fuer Plausibilitaetspruefung"}},
{ID: "M110", ReductionType: "protection", SubType: "cybersecurity", Name: "Sichere Konfigurationsverwaltung", Description: "Alle Konfigurationsaenderungen an sicherheitsrelevanten Systemen werden versioniert, dokumentiert und auf Konsistenz geprueft.", HazardCategory: "cyber_network", Examples: []string{"Versionsverwaltung fuer Safety-SPS-Programme einrichten", "Konfigurationsaenderungen nur ueber freigegebenen Prozess", "Automatische Konfigurationssicherung bei jeder Aenderung"}},
{ID: "M111", ReductionType: "protection", SubType: "cybersecurity", Name: "Sichere API", Description: "Programmierschnittstellen zur Maschinensteuerung sind gegen unautorisierte Nutzung und fehlerhafte Eingaben geschuetzt.", HazardCategory: "cyber_network", Examples: []string{"API-Zugriff nur mit gueltigem Token ermoeglichen", "Eingabevalidierung fuer alle API-Parameter durchfuehren", "Rate-Limiting fuer Steuerungs-API konfigurieren"}},
{ID: "M112", ReductionType: "protection", SubType: "cybersecurity", Name: "Sichere Datenvalidierung", Description: "Alle eingehenden Daten und Befehle werden auf Gueltigkeit, Plausibilitaet und zulaessigen Wertebereich geprueft.", HazardCategory: "cyber_network", Examples: []string{"Wertebereichspruefung fuer Sollwert-Vorgaben implementieren", "Plausibilitaetspruefung fuer Sensordaten durchfuehren", "Typenpruefung fuer Kommunikationspakete aktivieren"}},
{ID: "M113", ReductionType: "protection", SubType: "cybersecurity", Name: "Sichere Protokollierung", Description: "Alle sicherheitsrelevanten Vorgaenge werden manipulationssicher protokolliert und fuer Audits aufbewahrt.", HazardCategory: "cyber_network", Examples: []string{"Append-Only-Log fuer Sicherheitsereignisse einrichten", "Logdaten auf separatem geschuetztem Speicher ablegen", "Regelnmaessigen Log-Export fuer Archivierung durchfuehren"}},
{ID: "M114", ReductionType: "protection", SubType: "cybersecurity", Name: "Sichere Zeitstempel", Description: "Alle protokollierten Ereignisse erhalten zuverlaessige Zeitstempel aus einer synchronisierten und gesicherten Zeitquelle.", HazardCategory: "cyber_network", Examples: []string{"NTP-Synchronisation fuer alle Steuerungskomponenten aktivieren", "Hardwareuhr mit Batteriesicherung verwenden", "Zeitsynchronisation der Safety-SPS ueberwachen"}},
{ID: "M115", ReductionType: "protection", SubType: "safety_control", Name: "Sichere Speicherverwaltung", Description: "Die Steuerungssoftware verwendet sichere Speicherverwaltung, um Datenverlust und speicherbasierte Angriffe zu verhindern.", HazardCategory: "software_control", Examples: []string{"Speicherschutz fuer Safety-Programmbereiche aktivieren", "Stack-Overflow-Schutz implementieren", "Dynamische Speicherallokation in Safety-Software vermeiden"}},
{ID: "M116", ReductionType: "protection", SubType: "safety_control", Name: "Sichere Ressourcenverwaltung", Description: "Steuerungsressourcen werden so verwaltet, dass Engpaesse bei CPU, Speicher oder Kommunikation keine Sicherheitsfunktion beeintraechtigen.", HazardCategory: "software_control", Examples: []string{"CPU-Auslastung der Safety-SPS ueberwachen", "Priorisierung der sicherheitsrelevanten Tasks sicherstellen", "Kommunikationsbandbreite fuer Safety-Protokolle reservieren"}},
{ID: "M117", ReductionType: "protection", SubType: "safety_control", Name: "Sichere Fehlerbehandlung", Description: "Jeder erkannte Fehler wird systematisch behandelt und in einen definierten sicheren Zustand ueberfuehrt.", HazardCategory: "software_control", Examples: []string{"Exception-Handler fuer alle kritischen Codebereiche implementieren", "Fehlerkategorien mit zugeordneten Reaktionen definieren", "Unbehandelte Fehler loesen sicheren Stopp aus"}},
{ID: "M118", ReductionType: "protection", SubType: "safety_control", Name: "Sichere Recovery", Description: "Nach einem Fehler oder Ausfall wird die Maschine in einen definierten Ausgangszustand zurueckgesetzt, bevor der Betrieb fortgesetzt werden kann.", HazardCategory: "software_control", Examples: []string{"Referenzfahrt nach Steuerungswiederanlauf erzwingen", "Konfigurationscheck nach Recovery durchfuehren", "Schrittweisen Wiederanlauf mit Bedienerquittierung"}},
{ID: "M119", ReductionType: "protection", SubType: "safety_control", Name: "Sichere Degradationsstrategie", Description: "Bei Teilausfaellen wird die Maschine in einen Modus mit reduzierter Leistung aber erhaltener Sicherheit uebergefuehrt.", HazardCategory: "software_control", Examples: []string{"Reduzierte Geschwindigkeit bei Sensorausfall aktivieren", "Einschraenkung auf Grundfunktionen bei Teilausfall", "Automatischer Wechsel von Automatik auf Handbetrieb"}},
{ID: "M120", ReductionType: "protection", SubType: "safety_control", Name: "Sichere Abschaltung bei Fehler", Description: "Bei schwerwiegenden Fehlern wird die Maschine automatisch und sicher abgeschaltet, um Personen- und Sachschaeden zu vermeiden.", HazardCategory: "software_control", Examples: []string{"Stopp-Kategorie 0 bei kritischem Sicherheitsfehler ausfuehren", "Kontrolliertes Herunterfahren bei erkanntem Komponentenausfall", "Energietrennung bei nicht behebbarem Fehler einleiten"}},
// ====================================================================
// Type 3: Information (ReductionType: "information") — Benutzerinformation
// 40 entries: M121-M160
// ====================================================================
{ID: "M121", ReductionType: "information", SubType: "signage", Name: "Warnhinweis", Description: "Gut sichtbare Warnhinweise weisen den Bediener auf bestehende Restrisiken und erforderliche Vorsichtsmassnahmen hin.", HazardCategory: "general", Examples: []string{"Warnschild fuer Quetschgefahr an Schutztuer anbringen", "Hinweis auf automatischen Anlauf an der Maschine", "Warnaufkleber fuer heisse Oberflaeche am Gehaeuse"}},
{ID: "M122", ReductionType: "information", SubType: "signage", Name: "Gefahrenkennzeichnung", Description: "Gefahrbereiche und -stellen werden durch normgerechte Kennzeichnung dauerhaft markiert.", HazardCategory: "general", Examples: []string{"Bodenmarkierung um Gefahrbereich anbringen", "Warnmarkierung an rotierenden Teilen aufkleben", "Gefahrenzone mit gelb-schwarzer Schraffur kennzeichnen"}},
{ID: "M123", ReductionType: "information", SubType: "manual", Name: "Betriebsanweisung", Description: "Eine verstaendliche Betriebsanweisung beschreibt den sicheren Betrieb der Maschine in allen vorgesehenen Betriebsarten.", HazardCategory: "general", Examples: []string{"Betriebsanweisung am Bedienplatz aushangen", "Taeglich zu beachtende Sicherheitshinweise dokumentieren", "Betriebsarten und zugehoerige Schutzfunktionen beschreiben"}},
{ID: "M124", ReductionType: "information", SubType: "manual", Name: "Wartungsanweisung", Description: "Detaillierte Wartungsanweisungen beschreiben alle planmaessigen Wartungsarbeiten mit den erforderlichen Sicherheitsmassnahmen.", HazardCategory: "general", Examples: []string{"Schritt-fuer-Schritt-Wartungsanleitung erstellen", "Erforderliche Sicherheitsmassnahmen vor jeder Wartungstaetikeit", "Wartungsintervalle und Pruefpunkte auflisten"}},
{ID: "M125", ReductionType: "information", SubType: "manual", Name: "Reinigungsanweisung", Description: "Reinigungsanweisungen geben die sichere Vorgehensweise bei der Reinigung der Maschine und ihrer Komponenten vor.", HazardCategory: "general", Examples: []string{"Zulaessige Reinigungsmittel auflisten", "Reinigungsprozedur bei abgeschalteter Maschine beschreiben", "Bereiche mit besonderen Reinigungsanforderungen kennzeichnen"}},
{ID: "M126", ReductionType: "information", SubType: "training", Name: "Schulungsprogramm", Description: "Ein strukturiertes Schulungsprogramm vermittelt allen Bedienern die sicherheitsrelevanten Kenntnisse und Fertigkeiten.", HazardCategory: "general", Examples: []string{"Einweisungsprogramm fuer neue Maschinenbediener erstellen", "Jaehrliche Auffrischungsschulung durchfuehren", "Schulungsnachweise dokumentieren und archivieren"}},
{ID: "M127", ReductionType: "information", SubType: "training", Name: "Sicherheitsunterweisung", Description: "Regelmaessige Sicherheitsunterweisungen informieren die Beschaeftigten ueber Gefaehrdungen und die korrekte Nutzung von Schutzeinrichtungen.", HazardCategory: "general", Examples: []string{"Arbeitsplatzspezifische Sicherheitsunterweisung durchfuehren", "Not-Halt-Uebung mit allen Bedienern regelmaessig durchfuehren", "Unterweisung zum sicheren Umgang mit Gefahrstoffen"}},
{ID: "M128", ReductionType: "information", SubType: "manual", Name: "Bedienungsanleitung", Description: "Die Bedienungsanleitung beschreibt umfassend die sichere Bedienung, Installation, Inbetriebnahme und Ausserbetriebnahme der Maschine.", HazardCategory: "general", Examples: []string{"Vollstaendige Bedienungsanleitung in Landessprache bereitstellen", "Bebilderte Schritt-fuer-Schritt-Anleitungen erstellen", "Kapitel zu bestimmungsgemaesser Verwendung aufnehmen"}},
{ID: "M129", ReductionType: "information", SubType: "manual", Name: "Servicehandbuch", Description: "Das Servicehandbuch stellt Servicetechnikern alle Informationen fuer sichere Diagnose, Reparatur und Instandsetzung bereit.", HazardCategory: "general", Examples: []string{"Schaltplaene und Hydraulikplaene beifuegen", "Diagnoseprozeduren fuer alle Fehlercodes beschreiben", "Ersatzteillisten mit sicherheitsrelevanten Komponenten kennzeichnen"}},
{ID: "M130", ReductionType: "information", SubType: "manual", Name: "Notfallanweisung", Description: "Die Notfallanweisung beschreibt die Sofortmassnahmen bei Unfaellen und Stoerungen an der Maschine.", HazardCategory: "general", Examples: []string{"Notfallplan an der Maschine aushangen", "Erste-Hilfe-Massnahmen bei maschinenspezifischen Verletzungen", "Verhalten bei Medienaustritt beschreiben"}},
{ID: "M131", ReductionType: "information", SubType: "ppe", Name: "PSA-Vorgaben", Description: "Vorgeschriebene Persoenliche Schutzausruestung wird fuer jeden Arbeitsbereich und jede Taetikeit festgelegt.", HazardCategory: "general", Examples: []string{"Gehoerschutz ab 85 dB(A) vorschreiben", "Schutzbrille bei Spanabnahme fordern", "Sicherheitsschuhe im gesamten Produktionsbereich verlangen"}},
{ID: "M132", ReductionType: "information", SubType: "signage", Name: "Sicherheitskennzeichnung", Description: "Normgerechte Sicherheitskennzeichnung informiert ueber Gebote, Verbote, Warnungen und Rettungswege.", HazardCategory: "general", Examples: []string{"Gebotszeichen fuer Gehoerschutz anbringen", "Verbotszeichen an gesperrten Zugaengen befestigen", "Rettungszeichen fuer Notausgaenge und Erste-Hilfe-Stellen"}},
{ID: "M133", ReductionType: "information", SubType: "organizational", Name: "Zugangsbeschraenkung", Description: "Organisatorische Regelungen legen fest, wer Zugang zum Gefahrbereich erhalten darf und unter welchen Bedingungen.", HazardCategory: "general", Examples: []string{"Zugangsberechtigungsliste fuer Gefahrbereiche fuehren", "Begleitpflicht fuer Besucher im Produktionsbereich anordnen", "Zugangsverbot fuer nicht unterwiesene Personen kennzeichnen"}},
{ID: "M134", ReductionType: "information", SubType: "training", Name: "Benutzerrollenbeschreibung", Description: "Fuer jede Benutzerrolle sind die Aufgaben, Verantwortlichkeiten und erforderlichen Qualifikationen dokumentiert.", HazardCategory: "general", Examples: []string{"Rollenbeschreibung fuer Maschinenbediener erstellen", "Qualifikationsanforderungen fuer Instandhalter definieren", "Verantwortlichkeiten des Schichtfuehrers dokumentieren"}},
{ID: "M135", ReductionType: "information", SubType: "organizational", Name: "Wartungsplan", Description: "Ein detaillierter Wartungsplan legt die Intervalle und den Umfang aller planmaessigen Wartungstaetigkeiten fest.", HazardCategory: "general", Examples: []string{"Taeglich zu pruefende Sicherheitseinrichtungen auflisten", "Woechentlichen Schmierplan erstellen", "Jaehrliche Hauptinspektion planen und dokumentieren"}},
{ID: "M136", ReductionType: "information", SubType: "organizational", Name: "Inspektionsplan", Description: "Der Inspektionsplan definiert Art und Haeufigkeit der Inspektionen sicherheitsrelevanter Maschinenkomponenten.", HazardCategory: "general", Examples: []string{"Tagesinspektion fuer Schutzeinrichtungen festlegen", "Quartalinspektion fuer Hydraulikleitungen einplanen", "Jaehrliche Sicherheitstechnische Pruefung terminieren"}},
{ID: "M137", ReductionType: "information", SubType: "organizational", Name: "Pruefplan", Description: "Der Pruefplan legt fest, welche Pruefungen an sicherheitsrelevanten Bauteilen in welchen Intervallen durchzufuehren sind.", HazardCategory: "general", Examples: []string{"Wiederkehrende Pruefung von Druckbehaeltern planen", "Pruefung der elektrischen Sicherheit nach DGUV V3 terminieren", "Belastungstest fuer Hebezeuge jaehrlich durchfuehren"}},
{ID: "M138", ReductionType: "information", SubType: "organizational", Name: "Softwareupdateanleitung", Description: "Die Anleitung beschreibt das sichere Vorgehen beim Einspielen von Software- und Firmware-Updates auf der Maschinensteuerung.", HazardCategory: "software_control", Examples: []string{"Updateprozedur mit Sicherheitshinweisen dokumentieren", "Backup-Erstellung vor jedem Update vorschreiben", "Funktionspruefung nach Update beschreiben"}},
{ID: "M139", ReductionType: "information", SubType: "organizational", Name: "Cyber-Security-Hinweise", Description: "Hinweise zur IT-Sicherheit informieren den Betreiber ueber Massnahmen zum Schutz der Maschinensteuerung vor Cyberangriffen.", HazardCategory: "cyber_network", Examples: []string{"Passwortrichtlinien fuer Steuerungssysteme kommunizieren", "Hinweise zur Netzwerksicherheit bereitstellen", "Regelmaessige Aktualisierung der Sicherheitssoftware empfehlen"}},
{ID: "M140", ReductionType: "information", SubType: "organizational", Name: "Passwortpolicy", Description: "Eine Passwortrichtlinie definiert Mindestanforderungen an Passwoerter fuer den Zugriff auf Steuerungssysteme.", HazardCategory: "cyber_network", Examples: []string{"Mindestlaenge und Komplexitaetsanforderungen festlegen", "Regelmaessigen Passwortwechsel vorschreiben", "Standardpasswoerter bei Inbetriebnahme aendern lassen"}},
{ID: "M141", ReductionType: "information", SubType: "organizational", Name: "Backup-Anweisung", Description: "Die Backup-Anweisung regelt die regelmaessige Sicherung von Steuerungsprogrammen und Konfigurationsdaten.", HazardCategory: "software_control", Examples: []string{"Woechentliches Backup der SPS-Programme vorschreiben", "Sicherung der Parameterdaten nach jeder Aenderung", "Backup-Medien an sicherem Ort aufbewahren"}},
{ID: "M142", ReductionType: "information", SubType: "organizational", Name: "Incident-Reporting", Description: "Ein Verfahren zur Meldung und Dokumentation von Sicherheitsvorfaellen stellt sicher, dass alle Vorfaelle erfasst und ausgewertet werden.", HazardCategory: "general", Examples: []string{"Meldeformular fuer Sicherheitsvorfaelle bereitstellen", "Meldekette fuer schwere Vorfaelle definieren", "Vorfallanalyse und Massnahmenverfolgung dokumentieren"}},
{ID: "M143", ReductionType: "information", SubType: "organizational", Name: "Stoerungsanweisung", Description: "Die Stoerungsanweisung beschreibt das sichere Vorgehen bei typischen Maschinenstoerungen und deren Behebung.", HazardCategory: "general", Examples: []string{"Haeufige Stoerungsbilder mit Abhilfemassnahmen auflisten", "Sicherheitshinweise vor jeder Stoerungsbeseitigung angeben", "Eskalationsprozess bei nicht behebbaren Stoerungen"}},
{ID: "M144", ReductionType: "information", SubType: "organizational", Name: "Reset-Anweisung", Description: "Die Reset-Anweisung beschreibt die sichere Vorgehensweise zum Quittieren von Fehlern und Wiederanlauf der Maschine.", HazardCategory: "software_control", Examples: []string{"Reset-Prozedur nach Not-Halt dokumentieren", "Voraussetzungen fuer sicheren Wiederanlauf beschreiben", "Pruefschritte vor Produktionsfreigabe nach Reset"}},
{ID: "M145", ReductionType: "information", SubType: "organizational", Name: "Konfigurationsrichtlinie", Description: "Die Konfigurationsrichtlinie legt fest, wie sicherheitsrelevante Parameter dokumentiert, geaendert und freigegeben werden.", HazardCategory: "software_control", Examples: []string{"Parameterliste mit zulaessigen Wertebereichen erstellen", "Aenderungsantragsformular fuer Safety-Parameter bereitstellen", "Freigabeprozess fuer Konfigurationsaenderungen dokumentieren"}},
{ID: "M146", ReductionType: "information", SubType: "organizational", Name: "Update-Freigabeprozess", Description: "Ein definierter Freigabeprozess stellt sicher, dass Software-Updates vor dem Einspielen auf Sicherheit geprueft und freigegeben werden.", HazardCategory: "software_control", Examples: []string{"Testprozedur fuer Updates auf Testmaschine beschreiben", "Freigabeformular mit Unterschrift des Sicherheitsbeauftragten", "Dokumentation der Testerbgebnisse vor Produktivstart"}},
{ID: "M147", ReductionType: "information", SubType: "organizational", Name: "Aenderungsmanagement", Description: "Ein strukturiertes Aenderungsmanagement stellt sicher, dass alle technischen Aenderungen auf ihre Sicherheitswirkung geprueft werden.", HazardCategory: "general", Examples: []string{"Aenderungsantrag mit Sicherheitsbewertung einreichen", "Risikobeurteilung bei jeder technischen Aenderung aktualisieren", "Aenderungshistorie lueckenlos fuehren"}},
{ID: "M148", ReductionType: "information", SubType: "organizational", Name: "Sicherheitsprotokoll", Description: "Ein Sicherheitsprotokoll dokumentiert alle durchgefuehrten Sicherheitspruefungen und deren Ergebnisse.", HazardCategory: "general", Examples: []string{"Pruefprotokoll fuer wiederkehrende Sicherheitspruefungen", "Abnahmeprotokoll nach Umbaumassnahmen", "Funktionspruefungsprotokoll fuer Schutzeinrichtungen"}},
{ID: "M149", ReductionType: "information", SubType: "organizational", Name: "Auditverfahren", Description: "Ein Auditverfahren regelt die regelmaessige Ueberpruefung der Wirksamkeit aller Sicherheitsmassnahmen.", HazardCategory: "general", Examples: []string{"Internes Sicherheitsaudit jaehrlich durchfuehren", "Auditcheckliste fuer Maschinensicherheit erstellen", "Massnahmen aus Auditfeststellungen nachverfolgen"}},
{ID: "M150", ReductionType: "information", SubType: "manual", Name: "Betreiberhandbuch", Description: "Das Betreiberhandbuch fasst alle organisatorischen Pflichten und Verantwortlichkeiten des Maschinenbetreibers zusammen.", HazardCategory: "general", Examples: []string{"Betreiberpflichten aus Maschinenrichtlinie zusammenfassen", "Pruef- und Wartungspflichten dokumentieren", "Verantwortlichkeiten fuer Sicherheitsbeauftragte festlegen"}},
{ID: "M151", ReductionType: "information", SubType: "signage", Name: "Gefahrenpiktogramme", Description: "Normgerechte Piktogramme warnen sprachunabhaengig vor spezifischen Gefaehrdungen an der Maschine.", HazardCategory: "general", Examples: []string{"Piktogramm fuer Handverletzungsgefahr anbringen", "Elektrogefahr-Symbol am Schaltschrank befestigen", "Laserwarnsymbol an Laserbearbeitungsanlage"}},
{ID: "M152", ReductionType: "information", SubType: "marking", Name: "Sicherheitsdiagramme", Description: "Sicherheitsdiagramme visualisieren Sicherheitskreise, Schutzeinrichtungen und deren Zusammenwirken an der Maschine.", HazardCategory: "general", Examples: []string{"Schaltschema des Sicherheitskreises im Schaltschrank aushangen", "Pneumatikschaltplan mit Sicherheitsventilen bereitstellen", "Blockschaltbild der Sicherheitssteuerung dokumentieren"}},
{ID: "M153", ReductionType: "information", SubType: "organizational", Name: "Checklisten", Description: "Standardisierte Checklisten unterstuetzen die systematische Durchfuehrung sicherheitsrelevanter Taetigkeiten.", HazardCategory: "general", Examples: []string{"Taeglich abzuarbeitende Sicherheits-Checkliste erstellen", "Checkliste fuer Schichtwechsel bereitstellen", "Checkliste fuer Wiederanlauf nach Stillstand"}},
{ID: "M154", ReductionType: "information", SubType: "organizational", Name: "Wartungscheckliste", Description: "Eine Wartungscheckliste stellt sicher, dass alle Wartungspunkte systematisch abgearbeitet und dokumentiert werden.", HazardCategory: "general", Examples: []string{"Checkliste fuer woechentliche Wartung erstellen", "Pruefpunkte fuer Oelstand und Filterzustand auflisten", "Wartungsergebnis mit Datum und Unterschrift dokumentieren"}},
{ID: "M155", ReductionType: "information", SubType: "organizational", Name: "Inspektionscheckliste", Description: "Die Inspektionscheckliste leitet die systematische Sichtpruefung aller sicherheitsrelevanten Maschinenkomponenten.", HazardCategory: "general", Examples: []string{"Sichtpruefung der Schutzeinrichtungen auf Vollstaendigkeit", "Funktionspruefung der Not-Halt-Einrichtungen", "Kontrolle der Sicherheitskennzeichnung auf Lesbarkeit"}},
{ID: "M156", ReductionType: "information", SubType: "organizational", Name: "Inbetriebnahmecheckliste", Description: "Die Inbetriebnahmecheckliste stellt sicher, dass vor dem ersten Betrieb alle Sicherheitsvoraussetzungen erfuellt sind.", HazardCategory: "general", Examples: []string{"Schutzeinrichtungen auf korrekte Montage pruefen", "Sicherheitsfunktionen einzeln testen und dokumentieren", "Erdung und Potenzialausgleich messen"}},
{ID: "M157", ReductionType: "information", SubType: "organizational", Name: "Stilllegungscheckliste", Description: "Die Stilllegungscheckliste regelt die sichere Ausserbetriebnahme und Entsorgung der Maschine.", HazardCategory: "general", Examples: []string{"Energiequellen sicher trennen und sichern", "Betriebsstoffe umweltgerecht entsorgen", "Sicherheitsrelevante Komponenten deaktivieren und kennzeichnen"}},
{ID: "M158", ReductionType: "information", SubType: "organizational", Name: "Entsorgungsanleitung", Description: "Die Entsorgungsanleitung beschreibt die fachgerechte und sichere Entsorgung der Maschine und ihrer Betriebsstoffe.", HazardCategory: "material_environmental", Examples: []string{"Gefahrstoffe identifizieren und Entsorgungsweg festlegen", "Recyclingfaehige Materialien kennzeichnen", "Entsorgungsnachweis fuer Betriebsfluide verlangen"}},
{ID: "M159", ReductionType: "information", SubType: "signage", Name: "Sicherheitsplakat", Description: "Ein gut sichtbares Sicherheitsplakat fasst die wichtigsten Sicherheitsregeln fuer den Arbeitsbereich zusammen.", HazardCategory: "general", Examples: []string{"Sicherheitsplakat am Maschinenzugang aushangen", "Wichtigste Verhaltensregeln im Notfall darstellen", "Piktogramme fuer vorgeschriebene PSA abbilden"}},
{ID: "M160", ReductionType: "information", SubType: "organizational", Name: "Notfallkontaktliste", Description: "Eine aktuelle Notfallkontaktliste stellt sicher, dass im Notfall alle relevanten Ansprechpartner schnell erreichbar sind.", HazardCategory: "general", Examples: []string{"Notfallnummern am Bedienplatz aushangen", "Kontaktdaten von Sicherheitsbeauftragten und Ersthelfern", "Herstellerhotline fuer technische Notfaelle bereithalten"}},
}
}

Some files were not shown because too many files have changed in this diff Show More