261 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
Benjamin Admin
8a05fcc2f0 refactor: split control library into components, add generator UI
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 47s
CI/CD / test-python-backend-compliance (push) Successful in 36s
CI/CD / test-python-document-crawler (push) Successful in 24s
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
- Extract ControlForm, ControlDetail, GeneratorModal, helpers into
  separate component files (max ~470 lines each, was 1210)
- Add Collection selector in Generator modal
- Add Job History view in Generator modal
- Add Review Queue button with counter badge
- Add review mode navigation (prev/next through review items)
- Add vitest tests for helpers (getDomain, constants, options)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 18:52:42 +01:00
Benjamin Admin
9812ff46f3 feat: add 7-stage control generator pipeline with 3 license rules
- control_generator.py: RAG→License→Structure/Reform→Harmonize→Anchor→Store→Mark pipeline
  with Anthropic Claude API (primary) + Ollama fallback for LLM reformulation
- anchor_finder.py: RAG-based + DuckDuckGo anchor search for open references
- control_generator_routes.py: REST API for generate, job status, review queue, processed stats
- 046_control_generator.sql: job tracking, chunk tracking, blocked sources tables;
  extends canonical_controls with license_rule, source_original_text, source_citation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 18:42:40 +01:00
Benjamin Admin
30236c0001 docs: add post-push deploy monitoring to CLAUDE.md
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 44s
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 12s
CI/CD / Deploy (push) Successful in 2s
After every push to gitea, Claude now automatically polls health
endpoints and notifies the user when the deployment is ready for testing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 13:39:12 +01:00
Benjamin Admin
b4d2be83eb Merge gitea/main: resolve ci.yaml conflict, keep Coolify deploy
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 39s
CI/CD / test-python-document-crawler (push) Successful in 30s
CI/CD / test-python-dsms-gateway (push) Successful in 24s
CI/CD / validate-canonical-controls (push) Successful in 15s
CI/CD / Deploy (push) Successful in 3s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 13:26:17 +01:00
Benjamin Admin
38c7cf0a00 Merge branch 'main' of ssh://gitea.meghsakha.com:22222/Benjamin_Boenisch/breakpilot-compliance 2026-03-13 13:23:30 +01:00
Benjamin Admin
399fa62267 docs: update all docs to reflect Coolify deployment model
Replace Hetzner references with Coolify. Deployment is now:
- Core + Compliance: Push gitea → Coolify auto-deploys
- Lehrer: stays local on Mac Mini

Updated: CLAUDE.md, MkDocs CI/CD pipeline, MkDocs index.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 12:09:51 +01:00
f1710fdb9e fix: migrate deployment from Hetzner to Coolify (#1)
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 39s
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) Successful in 2s
## Summary
- Add Coolify deployment configuration (docker-compose, healthchecks, network setup)
- Replace deploy-hetzner CI job with Coolify webhook deploy
- Externalize postgres, qdrant, S3 for Coolify environment

## All changes since branch creation
- Coolify docker-compose with Traefik labels and healthchecks
- CI pipeline: deploy-hetzner → deploy-coolify (simple webhook curl)
- SQLAlchemy 2.x text() compatibility fixes
- Alpine-compatible Dockerfile fixes

Co-authored-by: Sharang Parnerkar <parnerkarsharang@gmail.com>
Reviewed-on: #1
2026-03-13 10:45:35 +00:00
Benjamin Admin
499ddc04d5 chore: trigger redeploy via Gitea Actions CI/CD
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 37s
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 22s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / deploy-hetzner (push) Successful in 15s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 09:54:23 +01:00
Benjamin Admin
f738ca8c52 fix: make compliance router imports resilient to individual module failures
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 23s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / deploy-hetzner (push) Successful in 17s
Replaced bare imports with safe_import_router pattern — if one sub-router
fails to import (e.g. missing dependency), other routers still load.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 09:46:52 +01:00
Benjamin Admin
c530898963 fix: replace Python 3.10+ union type syntax with typing.Optional for Pydantic v2 compat
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) Successful in 37s
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 12s
CI/CD / deploy-hetzner (push) Has been cancelled
from __future__ import annotations breaks Pydantic BaseModel runtime type
evaluation. Replaced str | None → Optional[str], list[str] → List[str] etc.
in control_generator.py, anchor_finder.py, control_generator_routes.py.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 09:36:14 +01:00
Benjamin Admin
cdafc4d9f4 feat: auto-run SQL migrations on backend startup
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 26s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / deploy-hetzner (push) Successful in 2m35s
Adds migration_runner.py that executes pending migrations from
migrations/ directory when backend-compliance starts. Tracks applied
migrations in _migration_history table.

Handles existing databases: detects if tables from migrations 001-045
already exist and seeds the history table accordingly, so only new
migrations (046+) are applied.

Skippable via SKIP_MIGRATIONS=true env var.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 09:14:18 +01:00
Benjamin Admin
de19ef0684 feat(control-generator): 7-stage pipeline for RAG→LLM→Controls generation
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) Successful in 45s
CI/CD / test-python-document-crawler (push) Has been cancelled
CI/CD / test-python-dsms-gateway (push) Has been cancelled
CI/CD / validate-canonical-controls (push) Has been cancelled
CI/CD / deploy-hetzner (push) Has been cancelled
CI/CD / test-python-backend-compliance (push) Has been cancelled
Implements the Control Generator Pipeline that systematically generates
canonical security controls from 150k+ RAG chunks across all compliance
collections (BSI, NIST, OWASP, ENISA, EU laws, German laws).

Three license rules enforced throughout:
- Rule 1 (free_use): Laws/Public Domain — original text preserved
- Rule 2 (citation_required): CC-BY/CC-BY-SA — text with citation
- Rule 3 (restricted): BSI/ISO — full reformulation, no source traces

New files:
- Migration 046: job tracking, chunk tracking, blocked sources tables
- control_generator.py: 7-stage pipeline (scan→classify→structure/reform→harmonize→anchor→store→mark)
- anchor_finder.py: RAG + DuckDuckGo open-source reference search
- control_generator_routes.py: REST API (generate, review, stats, blocked-sources)
- test_control_generator.py: license mapping, rule enforcement, anchor filtering tests

Modified:
- __init__.py: register control_generator_router
- route.ts: proxy generator/review/stats endpoints
- page.tsx: Generator modal, stats panel, state filter, review queue, license badges

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 09:03:37 +01:00
Benjamin Admin
c87f07c99a feat: seed 10 canonical controls + CRUD endpoints + frontend 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 39s
CI/CD / test-python-backend-compliance (push) Successful in 39s
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 12s
CI/CD / deploy-hetzner (push) Successful in 1m37s
- Migration 045: Seed 10 controls (AUTH, NET, SUP, LOG, WEB, DATA, CRYP, REL)
  with 39 open-source anchors into the database
- Backend: POST/PUT/DELETE endpoints for canonical controls CRUD
- Frontend proxy: PUT and DELETE methods added to canonical route
- Frontend: Control Library with create/edit/delete UI, full form with
  open anchor management, scope, requirements, evidence, test procedures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 00:28:21 +01:00
Benjamin Admin
453eec9ed8 fix: correct canonical control proxy paths to include /compliance prefix
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 1m4s
CI/CD / test-python-document-crawler (push) Successful in 27s
CI/CD / test-python-dsms-gateway (push) Successful in 24s
CI/CD / validate-canonical-controls (push) Successful in 14s
CI/CD / deploy-hetzner (push) Successful in 1m49s
The backend mounts the compliance router at /api/compliance, so canonical
control endpoints are at /api/compliance/v1/canonical/*, not /api/v1/canonical/*.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 20:49:06 +01:00
Benjamin Admin
050f353192 feat(canonical-controls): Canonical Control Library — rechtssichere Security 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 40s
CI/CD / test-python-backend-compliance (push) Successful in 41s
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 18s
CI/CD / deploy-hetzner (push) Successful in 2m26s
Eigenstaendig formulierte Security Controls mit unabhaengiger Taxonomie
und Open-Source-Verankerung (OWASP, NIST, ENISA). Keine BSI-Nomenklatur.

- Migration 044: 5 DB-Tabellen (frameworks, controls, sources, licenses, mappings)
- 10 Seed Controls mit 39 Open-Source-Referenzen
- License Gate: Quellen-Berechtigungspruefung (analysis/excerpt/embeddings/product)
- Too-Close-Detektor: 5 Metriken (exact-phrase, token-overlap, ngram, embedding, LCS)
- REST API: 8 Endpoints unter /v1/canonical/
- Go Loader mit Multi-Index (ID, domain, severity, framework)
- Frontend: Control Library Browser + Provenance Wiki
- CI/CD: validate-controls.py Job (schema, no-leak, open-anchors)
- 67 Tests (8 Go + 59 Python), alle PASS
- MkDocs Dokumentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 19:55:06 +01:00
Benjamin Admin
8442115e7c fix(rag): Fix bash compatibility + missing mkdir in phase functions
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 42s
CI/CD / test-python-document-crawler (push) Successful in 29s
CI/CD / test-python-dsms-gateway (push) Successful in 24s
CI/CD / deploy-hetzner (push) Successful in 17s
- Replace ${var,,} (bash 4+) with $(echo | tr) for macOS bash 3.2 compat
- Add mkdir -p to phase_gesetze, phase_eu, phase_templates, phase_datenschutz,
  phase_dach — prevents download failures when running phases individually

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 16:44:15 +01:00
Benjamin Admin
999cc81c78 feat(rag): Phase J — Security Guidelines & Standards (NIST, OWASP, ENISA)
Some checks failed
CI/CD / go-lint (push) Has been cancelled
CI/CD / python-lint (push) Has been cancelled
CI/CD / nodejs-lint (push) Has been cancelled
CI/CD / test-go-ai-compliance (push) Has been cancelled
CI/CD / test-python-backend-compliance (push) Has been cancelled
CI/CD / test-python-document-crawler (push) Has been cancelled
CI/CD / test-python-dsms-gateway (push) Has been cancelled
CI/CD / deploy-hetzner (push) Has been cancelled
Add phase_security() with 15 documents across 3 sub-phases:
- J1: 7 NIST standards (SP 800-53, 800-218, 800-63, 800-207, 8259A/B, AI RMF)
- J2: 6 OWASP projects (Top 10, API Security, ASVS, MASVS, SAMM, Mobile Top 10)
- J3: 2 ENISA guides (Procurement Hospitals, Cloud Security SMEs)

All documents are commercially licensed (Public Domain / CC BY / CC BY-SA).
Wire up 'security' phase in dispatcher and workflow yaml.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 16:14:44 +01:00
Benjamin Admin
ff66612beb fix(rag): Make download failures non-fatal — prevent set -e from aborting entire 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 43s
CI/CD / test-python-backend-compliance (push) Successful in 38s
CI/CD / test-python-document-crawler (push) Successful in 30s
CI/CD / test-python-dsms-gateway (push) Successful in 23s
CI/CD / deploy-hetzner (push) Successful in 17s
download_pdf() and extract_gesetz_html() now return 0 on failure and clean up
partial files. This prevents set -euo pipefail from aborting the entire script
when a single download fails (e.g. EUR-Lex timeout, BSI redirect).

Root cause of H2 EU loop only processing 1 document in Run #724: first failed
download_pdf returned 1, triggering set -e script abort.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 15:56:23 +01:00
Benjamin Admin
42ec3cad6d feat(rag): Phase I DACH-Erweiterung — Gesetze, Templates, Urteile
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 56s
CI/CD / test-python-backend-compliance (push) Successful in 49s
CI/CD / test-python-document-crawler (push) Successful in 32s
CI/CD / test-python-dsms-gateway (push) Successful in 25s
CI/CD / deploy-hetzner (push) Successful in 17s
New ingestion phase 'dach' adds missing documents from DACH catalog:

I1: UStG (Retention), MStV (Impressum)
I2: DSK Muster-VVT, DSK KP5 DSFA, BfDI Beispiel-VVT (DL-DE/BY-2.0)
I3: BSI IT-Grundschutz Kompendium 2024 (CC BY-SA 4.0)
I4: 7 Gerichtsentscheidungen as Praxisanker:
  - DE: LG Bonn 1&1, BGH Planet49, BGH Art.82 (2x)
  - AT: OGH Schutzzweck, OGH Art.15+82 EuGH-Vorlage
  - CH: BVGer DSG-Auskunft, BGer Datensperre

Trigger: workflow_dispatch phase=dach

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 14:36:59 +01:00
Benjamin Admin
9945a62a50 fix(rag): docker cp into /workspace_scripts, then copy at runtime
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 39s
CI/CD / test-python-document-crawler (push) Successful in 28s
CI/CD / test-python-dsms-gateway (push) Successful in 24s
CI/CD / deploy-hetzner (push) Successful in 18s
docker cp fails when target dir doesn't exist in a created container.
Copy scripts to /workspace_scripts, then cp them at container start.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 14:24:36 +01:00
Benjamin Admin
eef1c2e7d3 fix(rag): Use docker cp to inject checked-out scripts
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 40s
CI/CD / test-python-document-crawler (push) Successful in 29s
CI/CD / test-python-dsms-gateway (push) Successful in 24s
CI/CD / deploy-hetzner (push) Successful in 17s
The runner container can't access host paths directly, so the
deploy dir scripts were always stale. Now uses docker create +
docker cp + docker start to copy the freshly checked-out scripts
into the ingestion container before starting it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 13:37:57 +01:00
Benjamin Admin
a0e2a35e66 fix(rag): Git pull deploy dir before 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 40s
CI/CD / test-python-backend-compliance (push) Successful in 44s
CI/CD / test-python-document-crawler (push) Successful in 29s
CI/CD / test-python-dsms-gateway (push) Successful in 23s
CI/CD / deploy-hetzner (push) Successful in 18s
The RAG workflow mounts scripts from /opt/breakpilot-compliance/scripts
(deploy dir) but this may not have the latest fixes if CI hasn't
deployed yet. Add explicit git pull before running ingestion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 13:13:33 +01:00
Benjamin Admin
57f390190d fix(rag): Arithmetic error, dedup auth, EGBGB timeout
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 41s
CI/CD / test-python-document-crawler (push) Successful in 27s
CI/CD / test-python-dsms-gateway (push) Successful in 21s
CI/CD / deploy-hetzner (push) Successful in 19s
- collection_count() returns 0 (not ?) on failure — fixes arithmetic error
- Pass QDRANT_API_KEY to ingestion container for dedup checks
- Include api-key header in collection_count() and dedup scroll queries
- Lower large-file threshold to 256KB (EGBGB 310KB was timing out)
- More targeted EGBGB XML extraction (Art. 246a + Anlage only)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 12:05:07 +01:00
Benjamin Admin
cf60c39658 fix(scope-engine): Normalize UPPERCASE trigger docs to lowercase ScopeDocumentType
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 56s
CI/CD / test-python-backend-compliance (push) Successful in 42s
CI/CD / test-python-document-crawler (push) Successful in 24s
CI/CD / test-python-dsms-gateway (push) Successful in 26s
CI/CD / deploy-hetzner (push) Successful in 2m57s
Critical bug fix: mandatoryDocuments in Hard-Trigger-Rules used UPPERCASE
names (VVT, TOM, DSE) that never matched lowercase ScopeDocumentType keys
(vvt, tom, dsi). This meant no trigger documents were ever recognized as
mandatory in buildDocumentScope().

- Add normalizeDocType() mapping function with alias support
  (DSE→dsi, LOESCHKONZEPT→lf, DSR_PROZESS→betroffenenrechte, etc.)
- Fix buildDocumentScope() to use normalized doc types
- Fix estimateEffort() to use lowercase keys matching ScopeDocumentType
- Add 2 tests for UPPERCASE normalization and alias resolution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 09:39:31 +01:00
Benjamin Admin
c88653b221 fix(rag): Dedup check, BGB split, GewO timeout, arithmetic fix
- Add Qdrant dedup check in upload_file() — skip if regulation_id already exists
- Split BGB (2.7MB) into 5 targeted parts via XML extraction:
  AGB §§305-310, Fernabsatz §§312-312k, Kaufrecht §§433-480,
  Widerruf §§355-361, Digitale Produkte §§327-327u
- Lower large-file threshold 512KB→384KB (fixes GewO 432KB timeout)
- Fix arithmetic syntax error when collection_count returns "?"
- Replace EGBGB PDF (was empty) with XML extraction
- Add unzip to Alpine container for XML archives

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 09:39:09 +01:00
Benjamin Admin
87d06c8b20 fix(rag): Handle large file uploads + don't abort on individual failures
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 1m5s
CI/CD / test-python-backend-compliance (push) Successful in 43s
CI/CD / test-python-document-crawler (push) Successful in 33s
CI/CD / test-python-dsms-gateway (push) Successful in 27s
CI/CD / deploy-hetzner (push) Successful in 17s
- Extended timeout (15 min) for files > 500KB (BGB is 1.5MB)
- upload_file returns 0 even on failure so set -e doesn't kill script
- Failed uploads are still counted and reported in summary

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 23:33:28 +01:00
Benjamin Admin
0b47612272 fix(rag): Always run download phase before ingestion phases
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 37s
CI/CD / test-python-document-crawler (push) Successful in 26s
CI/CD / test-python-dsms-gateway (push) Successful in 23s
CI/CD / deploy-hetzner (push) Successful in 20s
The gesetze phase failed because it expects text files created by the
download phase. Now the workflow automatically runs download first for
any phase that depends on it. Also adds git and python3 to the alpine
container for repo cloning and text extraction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 23:13:33 +01:00
Benjamin Admin
c14b31b3bc fix(docker): Ensure public dir exists in Next.js builds + Hetzner compose fixes
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 38s
CI/CD / test-python-document-crawler (push) Successful in 29s
CI/CD / test-python-dsms-gateway (push) Successful in 20s
CI/CD / deploy-hetzner (push) Successful in 1m43s
- admin-compliance/Dockerfile: mkdir -p public before build
- developer-portal/Dockerfile: mkdir -p public before build
  (fixes "failed to calculate checksum /app/public: not found")
- docker-compose.hetzner.yml: Override core-health-check to exit
  immediately (Core doesn't run on Hetzner)
- Network override: external:false (auto-create breakpilot-network)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 19:11:26 +01:00
Benjamin Admin
0b836f7e2d fix(ci): Run docker compose from helper container with deploy dir mounted
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 41s
CI/CD / test-python-document-crawler (push) Successful in 25s
CI/CD / test-python-dsms-gateway (push) Successful in 21s
CI/CD / deploy-hetzner (push) Successful in 1m27s
The runner container has Docker socket but no host filesystem access.
docker compose needs to read YAML files, so run build+deploy inside
a helper container that has both Docker socket and the deploy dir mounted.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 18:31:19 +01:00
Benjamin Admin
18d9eec654 fix(ci): Use --entrypoint sh for alpine/git (default entrypoint is git)
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) Successful in 35s
CI/CD / test-python-backend-compliance (push) Successful in 38s
CI/CD / test-python-document-crawler (push) Successful in 27s
CI/CD / test-python-dsms-gateway (push) Successful in 27s
CI/CD / deploy-hetzner (push) Failing after 6s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 18:14:58 +01:00
Benjamin Admin
339505feed fix(ci): Fix Hetzner deploy — host filesystem access + network + dependencies
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) Successful in 37s
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 21s
CI/CD / deploy-hetzner (push) Failing after 7s
Problems fixed:
1. Deploy step couldn't access /opt/breakpilot-compliance (host path not
   mounted in runner container). Now uses alpine/git helper container with
   host bind-mount for git ops, then docker compose with host paths.
2. breakpilot-network was external:true but Core doesn't run on Hetzner.
   Override in hetzner.yml creates the network automatically.
3. core-health-check blocks startup waiting for Core. Override in
   hetzner.yml makes it exit immediately.
4. RAG ingestion script now respects RAG_URL/QDRANT_URL env vars.
5. RAG workflow discovers network dynamically from running containers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 18:11:05 +01:00
Benjamin Admin
23b9808bf3 debug(ci): Discovery step to find RAG service on Hetzner
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) Successful in 36s
CI/CD / test-python-backend-compliance (push) Successful in 40s
CI/CD / test-python-document-crawler (push) Successful in 27s
CI/CD / test-python-dsms-gateway (push) Successful in 26s
CI/CD / deploy-hetzner (push) Failing after 1s
Temporary commit to discover Docker container names and networks
on Hetzner, since breakpilot-network doesn't exist there.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 17:58:46 +01:00
Benjamin Admin
c3654bc9ea fix(ci): Spawn ingestion container on breakpilot-network
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) Successful in 36s
CI/CD / test-python-backend-compliance (push) Successful in 36s
CI/CD / test-python-document-crawler (push) Successful in 49s
CI/CD / test-python-dsms-gateway (push) Successful in 23s
CI/CD / deploy-hetzner (push) Failing after 1s
Instead of trying to connect the runner to breakpilot-network,
spawn a new alpine container directly on it via docker run.
Added debug output for network/container visibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 17:53:06 +01:00
Benjamin Admin
363bf9606a fix(ci): Connect runner to breakpilot-network for RAG ingestion
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) Successful in 38s
CI/CD / test-python-backend-compliance (push) Successful in 36s
CI/CD / test-python-document-crawler (push) Successful in 28s
CI/CD / test-python-dsms-gateway (push) Successful in 22s
CI/CD / deploy-hetzner (push) Failing after 1s
- Join breakpilot-network so bp-core-rag-service is reachable
- Make RAG_URL/QDRANT_URL in script respect env vars (${VAR:-default})
- Remove complex fallback logic — fail fast if network not available

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 17:48:13 +01:00
Benjamin Admin
e88c0aeeb3 fix(ci): RAG ingestion uses git-cloned workspace instead of deploy dir
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) Successful in 39s
CI/CD / test-python-backend-compliance (push) Successful in 44s
CI/CD / test-python-document-crawler (push) Successful in 31s
CI/CD / test-python-dsms-gateway (push) Successful in 26s
CI/CD / deploy-hetzner (push) Failing after 2s
The runner container doesn't always have /opt/breakpilot-compliance mounted.
Use the git-cloned workspace (current dir) and add multi-fallback for RAG API
URL (container network → localhost → host.docker.internal).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 17:43:37 +01:00
Benjamin Admin
ebe7e90bd8 feat(rag): Expand Phase H to Layer 1 Safe Core (~60 documents)
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) Successful in 40s
CI/CD / test-python-backend-compliance (push) Successful in 39s
CI/CD / test-python-document-crawler (push) Successful in 29s
CI/CD / test-python-dsms-gateway (push) Successful in 25s
CI/CD / deploy-hetzner (push) Failing after 1s
Phase H now includes:
- 16 German laws (PAngV, VSBG, ProdHaftG, BDSG, HGB, AO, DDG, TKG, etc.)
- 15 EUR-Lex EU laws (DSGVO, Consumer Rights Dir, Sale of Goods Dir,
  E-Commerce Dir, Unfair Terms Dir, DMA, NIS2, Product Liability Dir, etc.)
- 2 NIST frameworks (CSF 2.0, Privacy Framework 1.0)
- 1 HLEG Ethics Guidelines

Updated rag-sources.md with complete inventory of already-ingested vs
new documents, plus Layer 2-5 TODO roadmap.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 16:54:07 +01:00
Benjamin Admin
995de9e0f4 fix(ci): RAG ingestion uses docker:27-cli with host network access
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) Successful in 47s
CI/CD / test-python-backend-compliance (push) Successful in 47s
CI/CD / test-python-document-crawler (push) Successful in 30s
CI/CD / test-python-dsms-gateway (push) Successful in 25s
CI/CD / deploy-hetzner (push) Failing after 2s
Runner needs access to /opt/breakpilot-compliance and Docker network
for RAG service (bp-core-rag-service:8097). Falls back to
host.docker.internal if container network unavailable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 16:17:16 +01:00
Benjamin Admin
4e08364bc6 feat(ci): Add manual RAG ingestion workflow for Gitea Actions
Some checks failed
CI/CD / go-lint (push) Has been cancelled
CI/CD / python-lint (push) Has been cancelled
CI/CD / nodejs-lint (push) Has been cancelled
CI/CD / test-go-ai-compliance (push) Has been cancelled
CI/CD / test-python-backend-compliance (push) Has been cancelled
CI/CD / test-python-document-crawler (push) Has been cancelled
CI/CD / test-python-dsms-gateway (push) Has been cancelled
CI/CD / deploy-hetzner (push) Has been cancelled
Adds workflow_dispatch-triggered job to run ingest-legal-corpus.sh
on Hetzner. Supports phase selection (verbraucherschutz, gesetze, eu, etc.).

Usage: Gitea UI → Actions → "RAG Ingestion" → Run (select phase)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 16:14:44 +01:00
Benjamin Admin
7f38df9d9c feat(scope): Split HT-H01 B2B/B2C + register Verbraucherschutz document types + RAG ingestion
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) Successful in 38s
CI/CD / test-python-backend-compliance (push) Successful in 39s
CI/CD / test-python-document-crawler (push) Successful in 27s
CI/CD / test-python-dsms-gateway (push) Successful in 24s
CI/CD / deploy-hetzner (push) Has been cancelled
- Split HT-H01 into HT-H01a (B2C/Hybrid mit Verbraucherschutzpflichten) und
  HT-H01b (reiner B2B mit Basis-Pflichten). B2B-Webshops bekommen keine
  Widerrufsbelehrung/Preisangaben/Fernabsatz mehr.
- Add excludeWhen/requireWhen to HardTriggerRule for conditional trigger logic
- Register 6 neue ScopeDocumentType: widerrufsbelehrung, preisangaben,
  fernabsatz_info, streitbeilegung, produktsicherheit, ai_act_doku
- Full DOCUMENT_SCOPE_MATRIX L1-L4 for all new types
- Align HardTriggerRule interface with actual engine field names
- Add Phase H (Verbraucherschutz) to RAG ingestion script:
  10 deutsche Gesetze + 4 EU-Verordnungen + HLEG Ethics Guidelines
- Add scripts/rag-sources.md with license documentation
- 9 new tests for B2B/B2C trigger split, all 326 tests pass

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 16:03:49 +01:00
Benjamin Admin
cb48b8289e fix(sdk): Align scope types with engine output + project isolation + optional block progress
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) Successful in 46s
CI/CD / test-python-backend-compliance (push) Successful in 42s
CI/CD / test-python-document-crawler (push) Successful in 29s
CI/CD / test-python-dsms-gateway (push) Successful in 25s
CI/CD / deploy-hetzner (push) Failing after 2s
Type alignment (root cause of client-side crash):
- RiskFlag: id/title/description → severity/category/message/recommendation
- ScopeGap: id/title/recommendation/relatedDocuments → gapType/currentState/targetState/effort
- NextAction: id/priority:number/effortDays → actionType/priority:string/estimatedEffort
- ScopeReasoning: details → factors + impact
- TriggeredHardTrigger: {rule: HardTriggerRule} → flat fields (ruleId, description, etc.)
- All UI components updated to match engine output shape

Project isolation:
- Scope localStorage key now includes projectId (prevents data leak between projects)

Optional block progress:
- Blocks with only optional questions now show green checkmark when any question answered

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:58:29 +01:00
Benjamin Admin
46048554cb fix(sdk): Fix ScopeDecisionTab crash — type mismatches with backend types
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) Successful in 38s
CI/CD / test-python-backend-compliance (push) Successful in 37s
CI/CD / test-python-document-crawler (push) Successful in 24s
CI/CD / test-python-dsms-gateway (push) Successful in 20s
CI/CD / deploy-hetzner (push) Failing after 5s
- DEPTH_LEVEL_COLORS: simple strings → objects with {bg, border, badge, text} Tailwind classes
- decision.reasoning: render as mapped array instead of direct JSX child
- trigger.X → trigger.rule.X for TriggeredHardTrigger properties
- doc.isMandatory → doc.required, doc.depthDescription → doc.depth
- doc.effortEstimate → doc.estimatedEffort, doc.triggeredByHardTrigger → doc.triggeredBy
- decision.gapAnalysis → decision.gaps (matching ScopeDecision type)
- getSeverityBadge: uppercase severity ('LOW'|'MEDIUM'|'HIGH'|'CRITICAL')
- Also includes CLAUDE.md and DEVELOPER.md CI/CD documentation updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 09:07:41 +01:00
Benjamin Admin
a673cb0ce4 fix(sdk): Prevent auto-save from overwriting completed profile status
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) Successful in 41s
CI/CD / test-python-backend-compliance (push) Successful in 39s
CI/CD / test-python-document-crawler (push) Successful in 25s
CI/CD / test-python-dsms-gateway (push) Successful in 21s
CI/CD / deploy-hetzner (push) Failing after 1s
The auto-save timers (SDK context + backend) were firing after
completeAndSaveProfile(), resetting isComplete back to false.

Fix: skip auto-save when currentStep===99 (completed), cancel pending
timers before completing, and await backend save before updating
SDK context.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 08:50:28 +01:00
Benjamin Admin
7afbcfd9f5 fix(sdk): Auto-save company profile to SDK context and backend
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) Successful in 51s
CI/CD / test-python-backend-compliance (push) Successful in 38s
CI/CD / test-python-document-crawler (push) Successful in 27s
CI/CD / test-python-dsms-gateway (push) Successful in 28s
CI/CD / deploy-hetzner (push) Failing after 6s
Profile data was lost when navigating away because it was only saved
to SDK context on explicit button click (Next/Save). Scope data
persisted because it auto-synced on every change.

Added two debounced auto-save mechanisms:
- SDK context sync (500ms) — survives in-app navigation
- Backend save (2s) — survives page reload/session change

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 08:33:12 +01:00
Benjamin Admin
091f093e1b fix(ci): Add missing ReportingHandlers + fix Python 3.9 compat
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) Successful in 44s
CI/CD / test-python-backend-compliance (push) Successful in 48s
CI/CD / test-python-document-crawler (push) Successful in 32s
CI/CD / test-python-dsms-gateway (push) Successful in 27s
CI/CD / deploy-hetzner (push) Failing after 9s
- Create reporting_handlers.go with ReportingHandlers struct and 4
  endpoint methods (GetExecutiveReport, GetComplianceScore,
  GetUpcomingDeadlines, GetRiskOverview) to fix build failure
- Fix gap_analysis/analyzer.py: use Optional[list[str]] instead of
  list[str] | None for Python 3.9 compatibility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 00:21:50 +01:00
Benjamin Admin
5d99d5d47a feat(ci): Automatisches Deploy auf Hetzner via Gitea Actions
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 38s
CI/CD / test-python-backend-compliance (push) Successful in 36s
CI/CD / test-python-document-crawler (push) Successful in 26s
CI/CD / test-python-dsms-gateway (push) Successful in 26s
CI/CD / deploy-hetzner (push) Has been skipped
- Gitea Actions CI um deploy-hetzner Job erweitert
- Automatischer Build + Deploy bei Push auf main (nach Tests)
- docker-compose.hetzner.yml Override (amd64 statt arm64)
- Deploy-Dir: /opt/breakpilot-compliance/
- Baut parallel: admin, backend, ai-sdk, developer-portal
- Health Checks nach Deploy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 00:08:27 +01:00
Benjamin Admin
e3a877b549 feat(sdk): Advisory-Board Wizard auf Kachel-UI umstellen + use-cases/new konsolidieren
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 39s
CI / test-python-backend-compliance (push) Successful in 44s
CI / test-python-document-crawler (push) Successful in 29s
CI / test-python-dsms-gateway (push) Successful in 24s
Advisory-Board page komplett auf tile-basierte UI umgestellt (wie use-cases/new):
- 11 Kachel-Konstanten (50+ Datenkategorien, 16 Zwecke, Hosting, Transfer, etc.)
- Array-basiertes Formular statt einzelner Booleans
- Client-seitige Risikobewertung entfernt — nur UCCA-Backend
- Edit-Modus via ?edit=id (laedt Assessment, sendet PUT)
- use-cases/new durch Redirect zu advisory-board ersetzt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 23:43:56 +01:00
Benjamin Admin
237c05a94c fix: Profil-State nach Backend-Load in SDK-Context sync + alle Tests fixen
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 36s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 26s
CI / test-python-dsms-gateway (push) Successful in 20s
- Company Profile: setCompanyProfile() und COMPLETE_STEP dispatch nach Backend-Load,
  damit Sidebar-Checkmarks nach Refresh erhalten bleiben
- compliance-scope-engine.test.ts: Property-Namen anpassen (composite_score, risk_score, etc.)
- dsfa/types.test.ts: 9 Sections (0-8), 7 required, 2 optional
- api-client.test.ts: SDKApiClient-Klasse statt nicht-existierendem Singleton
- types.test.ts: use-case-assessment statt use-case-workshop
- vitest.config.ts: E2E-Verzeichnis aus Vitest excluden

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 22:58:36 +01:00
Benjamin Admin
b1a0dd3615 docs: SDK-Flow, MkDocs und Entwicklerdoku aktualisieren
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 36s
CI / test-python-backend-compliance (push) Successful in 40s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 20s
- flow-data.ts: Profil-Summary und Use-Case-Wizard-Details dokumentiert
- stammdaten.md: Step 99 Summary-Seite dokumentiert, Dokumente-generieren entfernt
- vorbereitung-module.md: UCCA 8-Schritte-Wizard mit Kachel-UI dokumentiert

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 22:30:31 +01:00
Benjamin Admin
6e7d0d9b14 feat(sdk): Profil-Summary + Use-Case-Workshop komplett auf Kachel-UI umstellen
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 38s
CI / test-python-backend-compliance (push) Successful in 41s
CI / test-python-document-crawler (push) Successful in 25s
CI / test-python-dsms-gateway (push) Successful in 22s
- Profil: Summary-Seite (Step 99) nach Abschluss statt direkter Sprung zu Scope
- Profil: "Dokumente generieren" Block entfernt
- Use Cases Step 1: Branche aus Profil auto-abgeleitet, 21 KI-Kategorien als Kacheln
- Use Cases Step 2: ~60 Datenkategorien in 10 Gruppen als Kacheln (Art. 9 orange)
- Use Cases Step 3: Rechtsgrundlage entfernt (SDK ermittelt), 16 Zweck-Kacheln
- Use Cases Step 4: Automatisierungsgrad als Single-Select-Kacheln
- Use Cases Step 5: Hosting/Region/Modellnutzung als Kacheln statt Dropdowns
- Use Cases Step 6: Transfer-Ziele + Mechanismus als Kacheln statt Checkbox/Dropdown
- Use Cases Step 7: Aufbewahrungsdauer als Kacheln statt Zahlenfeld
- Use Cases Step 8: Compliance-Dokumente als Multi-Select-Kacheln

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 19:03:47 +01:00
Benjamin Admin
24afed69c1 Fix Scope evaluation crash: align property names between engine, types, and components
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 35s
CI / test-python-backend-compliance (push) Successful in 39s
CI / test-python-document-crawler (push) Successful in 27s
CI / test-python-dsms-gateway (push) Successful in 25s
The engine used short property names (risk, complexity, assurance, composite) while
the ComplianceScores interface defined (risk_score, complexity_score, assurance_need,
composite_score). Components used yet another convention (riskScore, level, hardTriggers).
The main crash was DEPTH_LEVEL_COLORS[decision.level] where decision.level was undefined
(correct property: decision.determinedLevel).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 16:47:01 +01:00
Benjamin Admin
579fe1b5e1 fix(scope): Evaluierung crasht (answerValue→value), Profil-Persistenz, Block-Umbenennungen
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 36s
CI / test-python-backend-compliance (push) Successful in 42s
CI / test-python-document-crawler (push) Successful in 27s
CI / test-python-dsms-gateway (push) Successful in 25s
- compliance-scope-engine: answerValue→value (Property existierte nicht, Crash bei Evaluierung)
- company-profile: saveProfileDraft synct jetzt Redux-State (Daten bleiben bei Navigation)
- Scope-Bloecke umbenannt: Kunden & Nutzer, Datenverarbeitung, Hosting & Verarbeitung, Website und Services
- org_cert_target + data_volume als Hidden Scoring Questions (Duplikate entfernt)
- ai_risk_assessment: boolean→single mit Ja/Nein/Noch nicht
- 6 neue Abteilungs-Datenkategorien: IT, Recht, Produktion, Logistik, Einkauf, Facility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 16:33:59 +01:00
Benjamin Admin
ee6743c7c6 chore: Test-Artefakte (.coverage, test_*.db) zur .gitignore hinzufuegen
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 50s
CI / test-python-backend-compliance (push) Successful in 42s
CI / test-python-document-crawler (push) Successful in 26s
CI / test-python-dsms-gateway (push) Successful in 26s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 15:59:44 +01:00
Benjamin Admin
a6818b39c5 feat(scope+vvt): Datenkategorien in Scope Block 9 verschieben, VVT Generator-Tab entfernen
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 36s
CI / test-python-backend-compliance (push) Successful in 36s
CI / test-python-document-crawler (push) Successful in 26s
CI / test-python-dsms-gateway (push) Successful in 20s
- ScopeQuestionBlockId um 'datenkategorien_detail' erweitert
- Block 9 mit 6 Abteilungs-Datenkategorie-Fragen (dk_dept_hr, dk_dept_recruiting, etc.)
- ScopeWizardTab: aufklappbare Kacheln fuer Block 9, gefiltert nach Block 8 Abteilungswahl
- VVT: Generator-Tab komplett entfernt (3 statt 4 Tabs)
- VVT Verzeichnis: "Aus Scope-Analyse generieren" Button mit Preview + Uebernehmen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 14:47:20 +01:00
Benjamin Admin
e3fb81fc0d feat(vvt): Aufklappbare Abteilungskacheln mit Datenkategorien + Wiki-Infoboxen
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 35s
CI / test-python-backend-compliance (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 23s
Step 2 im VVT-Generator: Ja/Nein-Buttons durch expandierbare Kacheln ersetzt.
Pro Abteilung werden typische Datenkategorien als Checkboxen angezeigt (isTypical
vorausgefuellt), Art. 9 Kategorien orange hervorgehoben mit DSGVO-Warnung.
7 neue Wiki-Artikel fuer Datenkategorien pro Geschaeftsbereich (HR, Finanzen,
Vertrieb, Marketing, Support, IT, Produktion).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 13:11:00 +01:00
Benjamin Admin
3512963006 fix(compliance-scope): Race Condition bei State-Persistenz beheben
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 35s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 20s
SDK Context laedt State asynchron vom Server. Die Page las bei Mount
sdkState.complianceScope (noch null), fiel auf leeres localStorage
zurueck, und der Save-Effect ueberschrieb dann den echten State mit
leeren Daten. Fix: sdkState.complianceScope wird jetzt reaktiv
beobachtet, und leere States werden nie zurueckgeschrieben.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 12:11:36 +01:00
Benjamin Admin
85b3cc3421 fix(company-profile): CE-Kennzeichnung und Pruefzyklus entfernen
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 37s
CI / test-python-backend-compliance (push) Successful in 36s
CI / test-python-document-crawler (push) Successful in 27s
CI / test-python-dsms-gateway (push) Successful in 21s
CE-Kennzeichnung aus Zertifizierungsliste entfernt und den Pruefzyklus-
Abschnitt aus dem Legal-Framework-Step entfernt, da beides nicht relevant.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 11:50:25 +01:00
Benjamin Admin
f6019ecba9 feat(compliance-scope): Pflicht/Optional-Klassifikation, offene Fragen sichtbar + Navigation
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 37s
CI / test-python-backend-compliance (push) Successful in 35s
CI / test-python-document-crawler (push) Successful in 26s
CI / test-python-dsms-gateway (push) Successful in 20s
Block-Sidebar zeigt gruen/orange Status pro Block, klickbare Zusammenfassung
offener Pflichtfragen unter dem Fortschrittsbalken, und visuelles Highlighting
(linker Rand) fuer unbeantwortete Pflichtfragen. Sidebar-Haken wird gesetzt
wenn alle Pflichtfragen beantwortet und Auswertung vorhanden.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 11:45:46 +01:00
Benjamin Admin
1f91e05600 fix+test+docs: Archivierte Projekte, Vitest-Tests & Regulations-Doku
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 38s
CI / test-python-backend-compliance (push) Successful in 39s
CI / test-python-document-crawler (push) Successful in 26s
CI / test-python-dsms-gateway (push) Successful in 21s
- fix(ProjectSelector): Archivierte Projekte anklickbar machen, doppelten
  "Neues Projekt" Button entfernen
- test: 32 Vitest-Tests fuer scope-to-facts und supervisory-authority-resolver
- docs(flow-data): Scope-Step outputs + Obligations inputs erweitert
- docs(developer-portal): Feature-Highlight "Automatische Regulierungs-Ableitung"
- docs(mkdocs): Neuer Abschnitt Regulierungs-Ableitung in obligations.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 11:07:35 +01:00
Benjamin Admin
3c0c1e49da feat(company-profile): Branchen-Erweiterung, Multi-Select & Zertifizierungen
Multi-Branche-Auswahl im CompanyProfile, erweiterte allowed-facts fuer
Drafting Engine, Demo-Daten und TOM-Generator Anpassungen.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 11:03:21 +01:00
Benjamin Admin
5da93c5d10 feat(regulations): Automatische Ableitung anwendbarer Gesetze & Aufsichtsbehoerden
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 35s
CI / test-python-backend-compliance (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 27s
CI / test-python-dsms-gateway (push) Successful in 21s
Nach Abschluss von Profil + Scope werden jetzt automatisch die anwendbaren
Regulierungen (DSGVO, NIS2, AI Act, DORA) ermittelt und die zustaendigen
Aufsichtsbehoerden (Landes-DSB, BSI, BaFin) aus Bundesland + Branche abgeleitet.

- Neues scope-to-facts.ts: Mapping CompanyProfile+Scope → Go SDK Payload
- Neues supervisory-authority-resolver.ts: 16 Landes-DSB + nationale Behoerden
- ScopeDecisionTab: Regulierungs-Report mit Aufsichtsbehoerden-Karten
- Obligations-Seite: Echte Daten statt Dummy in handleAutoProfiling()
- Neue Types: ApplicableRegulation, RegulationAssessmentResult, SupervisoryAuthorityInfo

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:29:24 +01:00
Benjamin Admin
fa4cda7627 refactor(scope+profile): Duplikate eliminieren, UI vereinheitlichen
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 36s
CI / test-python-backend-compliance (push) Successful in 35s
CI / test-python-document-crawler (push) Successful in 25s
CI / test-python-dsms-gateway (push) Successful in 20s
- Profil: Steps 6 (VVT) + 7 (KI) entfernt, nach Scope verschoben
- Profil: Steps 9→7 renummeriert (Rechtlicher Rahmen→6, Maschinenbau→7)
- Profil: Branche + Jahresumsatz von Dropdown auf Tile-Grid umgestellt
- Profil: usesAI/aiUseCases aus CompanyProfile Interface entfernt
- Scope: 5 Duplikate aus Block 1 entfernt (Branche, MA, Umsatz, Modell, DSB)
- Scope: 2 Duplikate aus Block 6 entfernt (Angebote, Webshop)
- Scope: Block 7 (KI-Systeme) + Block 8 (VVT) neu hinzugefuegt
- Scope: "Aus Profil" Info-Box zeigt Profildaten in betroffenen Blocks
- Scope: Hidden Scoring fuer entfernte Fragen bleibt erhalten via Auto-Fill

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 08:52:09 +01:00
Benjamin Admin
90d99bba08 feat(use-case-workshop): UCCA-Ergebnis-Panel nach Abschluss anzeigen
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 35s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 25s
CI / test-python-dsms-gateway (push) Successful in 19s
Nach Wizard-Abschluss wird ein Ergebnis-Panel angezeigt:
- Bei UCCA-API-Erfolg: AssessmentResultCard mit Regeln, Kontrollen, Architektur
- Bei API-Fehler: Lokale Risikobewertung mit Score, Massnahmen, Regulations
- Badge zeigt Quelle (API vs Lokal)
- Nutzer kann Ergebnis pruefen bevor "Use Case speichern" geklickt wird

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 08:06:43 +01:00
Benjamin Admin
2c35775b44 feat(use-case-workshop): 8-Schritt-Wizard mit UCCA-API-Integration
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 34s
CI / test-python-backend-compliance (push) Successful in 38s
CI / test-python-document-crawler (push) Successful in 25s
CI / test-python-dsms-gateway (push) Successful in 24s
Workshop von 5 auf 8 Schritte erweitert: Datenkategorien (Art.9, Sonstige),
Verarbeitungszweck (Rechtsgrundlage), Technologie (Glossar, Modell-Nutzung),
Automatisierung (Beispiele, Art.22), Hosting/Transfer, Datenhaltung/Vertraege,
Zusammenfassung mit automatischer Risikobewertung und UCCA-API-Aufruf.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 07:56:01 +01:00
Benjamin Admin
aaf95cf894 feat(use-case-wizard): Sonstige Datentypen, bessere Erklaerungen und Glossar
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 51s
CI / test-python-backend-compliance (push) Successful in 40s
CI / test-python-document-crawler (push) Successful in 26s
CI / test-python-dsms-gateway (push) Successful in 19s
- Step 2: Dynamische Textfelder fuer eigene Datentypen (Kennzeichen, VIN etc.)
- Step 4: Konkrete Beispiele fuer Automatisierungsgrade + Art. 22 DSGVO Info
- Step 5: Ausfuehrliche Erklaerungen mit Beispielen + aufklappbares Glossar (ML, DL, NLP, LLM, RAG, Fine-Tuning)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 07:24:05 +01:00
Benjamin Admin
9f41ed4f8e fix: CREATE audit table IF NOT EXISTS before ALTER in migration 042
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 39s
CI / test-python-backend-compliance (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 27s
CI / test-python-dsms-gateway (push) Successful in 22s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 23:54:20 +01:00
Benjamin Admin
e7fab73a3a fix(company-profile): Projekt-aware Persistenz — Daten werden jetzt pro Projekt gespeichert
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 36s
CI / test-python-backend-compliance (push) Successful in 35s
CI / test-python-document-crawler (push) Successful in 27s
CI / test-python-dsms-gateway (push) Successful in 21s
Problem: Company Profile nutzte hartcodiertes tenant_id=default ohne project_id.
Beim Wechsel zwischen Projekten wurden immer die gleichen (oder keine) Daten geladen.

Aenderungen:
- Migration 042: project_id Spalte + UNIQUE(tenant_id, project_id) Constraint,
  fehlende Spalten (offering_urls, Adressfelder) nachgetragen
- Backend: Alle Queries nutzen WHERE tenant_id + project_id IS NOT DISTINCT FROM
- Proxy: project_id Query-Parameter wird durchgereicht
- Frontend: projectId aus SDK-Context, profileApiUrl() Helper fuer alle API-Aufrufe
- "Weiter" speichert jetzt immer den Draft (war schon so, ging aber ins Leere)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 23:48:15 +01:00
Benjamin Admin
f8917ee6fd fix(company-profile): Religion-Label und Art.9-Duplikate bereinigen
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 35s
CI / test-python-backend-compliance (push) Successful in 33s
CI / test-python-document-crawler (push) Successful in 28s
CI / test-python-dsms-gateway (push) Successful in 20s
- "Religioese Ueberzeugungen" → "Religion" umbenannt (Konfession ≠ Ueberzeugung)
- Gesundheit/Religion aus Lohnbuchhaltung art9_relevant entfernt,
  da bereits in Personalverwaltung erfasst (keine Doppelabfrage)
- Info-Texte angepasst mit Hinweis auf Personalverwaltung

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 23:30:52 +01:00
Benjamin Admin
51a208a2e1 feat(company-profile): KI-Systeme als eigener Wizard-Schritt mit Vorlagen
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 34s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 25s
CI / test-python-dsms-gateway (push) Successful in 20s
Schritt 6 (Verarbeitung & KI) aufgeteilt: Step 6 zeigt nur noch
Verarbeitungstaetigkeiten, Step 7 ist ein neuer KI-Systeme-Schritt mit
18 vorgefertigten Vorlagen in 7 Kategorien (Text-KI, Office, Code, Bild,
Uebersetzung, CRM, Intern). Jede Vorlage hat anklickbare Einsatzzweck-Chips,
Datenschutz-Warnhinweise und vorausgefuellte Felder. Link zum AI-Act-Modul
am Ende des Schritts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 23:06:23 +01:00
Benjamin Admin
1c59996f32 feat(wiki): Enrich wiki with DACH court decisions and 18 new articles
- Update all 10 existing articles with real source URLs (EuGH, BAG, DSK, BfDI)
- Add 18 new articles covering:
  - EuGH C-184/20 (wide interpretation Art. 9)
  - EuGH C-667/21 (cumulative legal basis)
  - EuGH C-34/21 (§26 BDSG unconstitutional)
  - EuGH C-634/21 (SCHUFA scoring)
  - EuGH C-582/14 (IP addresses as personal data)
  - Biometric data, indirect Art. 9 data in daily practice
  - Retention periods overview
  - Video surveillance and GPS tracking at workplace
  - Communication data (email/chat, Fernmeldegeheimnis)
  - Financial data, PCI DSS, SEPA
  - Minors (Art. 8 DSGVO)
  - Austria DSG specifics, Switzerland revDSG
  - AI training data and GDPR/AI Act
  - "Forced" special categories
- Add 3 new categories (EuGH-Leiturteile, Aufbewahrungsfristen, DACH-Besonderheiten)
- Add code block rendering to markdown renderer
- Add Clock, Globe, Gavel icons to icon map
- Total: 11 categories, 28 articles, all with verified source URLs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:43:23 +01:00
Benjamin Admin
61064fdcba fix: Cast empty ARRAY[] to text[] in wiki migration
PostgreSQL requires explicit type cast for empty array literals.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:12:54 +01:00
Benjamin Admin
11d4c2fd36 feat: Add Compliance Wiki as internal admin knowledge base
Migration 040 with wiki_categories + wiki_articles tables, 10 seed
articles across 8 categories (DSGVO, Art. 9, AVV, HinSchG etc.).
Read-only FastAPI API, Next.js proxy, and two-column frontend with
full-text search.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:01:27 +01:00
Benjamin Admin
3d22935065 feat(company-profile): Datenkategorien-Infoboxen, AVV-Hinweise, Qualifikationsdaten
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 34s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 23s
- Info-Tooltips fuer alle Datenkategorien mit typischen Beispieldaten
- Neue Kategorie: Qualifikations-/Schulungsdaten (Fortbildungen, Zertifikate)
- Externer Dienstleister bei Lohn-/Gehaltsabrechnung (AVV-relevant)
- Externer Dienstleister bei Website-Betrieb (AVV nach Art. 28 DSGVO)
- Arbeitszeiterfassung als Pflicht markiert (§3 ArbZG)
- Gesundheitsdaten-Abgrenzung: Krankenkassenname ist KEIN Art. 9 Datum
- Bewerbermanagement: Religion als Art. 9 ergaenzt
- Online-Shop/SaaS Beschreibungen praezisiert + Warnbanner bei Doppelauswahl
- Rechtsgrundlage aus Firmenprofil entfernt (gehoert in VVT-Schritt)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:26:49 +01:00
Benjamin Admin
09cfb79840 feat: Projekt-Verwaltung verbessern — Archivieren, Loeschen, Wiederherstellen
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 35s
CI / test-python-backend-compliance (push) Successful in 38s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 24s
- Backend: Restore-Endpoint (POST /projects/{id}/restore) und
  Hard-Delete-Endpoint (DELETE /projects/{id}/permanent) hinzugefuegt
- Frontend: Dreistufiger Dialog (Archivieren / Endgueltig loeschen mit
  Bestaetigungsdialog) statt einfachem Loeschen
- Archivierte Projekte aufklappbar in der Projektliste mit
  Wiederherstellen-Button
- CustomerTypeSelector entfernt (redundant seit Multi-Projekt)
- Default tenantId von 'default' auf UUID geaendert (Backend-400-Fix)
- SQL-Cast :state::jsonb durch CAST(:state AS jsonb) ersetzt (SQLAlchemy-Fix)
- snake_case/camelCase-Mapping fuer Backend-Response (NaN-Datum-Fix)
- projectInfo wird beim Laden vom Backend geholt (Header zeigt Projektname)
- API-Client erzeugt sich on-demand (Race-Condition-Fix fuer Projektliste)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:48:02 +01:00
Benjamin Admin
d53cf21b95 docs: Projekt-API in API-Docs, Developer Portal + MKDocs ergaenzen
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 38s
CI / test-python-backend-compliance (push) Successful in 1m0s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 20s
- endpoints.ts: Neues Modul "Projekte — Multi-Projekt-Verwaltung" (5 Endpoints)
- Developer Portal: projectId im Beispiel-Code, Multi-Projekt als Feature
- multi-tenancy.md: Verweis auf multi-project.md + neue Tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 15:59:27 +01:00
Benjamin Admin
0c83e765d9 fix(sdk): show Header + Sidebar on project list page too
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 31s
CI / test-python-backend-compliance (push) Successful in 35s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 20s
The ProjectSelector page now renders inside the standard SDK layout
with header, sidebar and navigation — consistent with all other pages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 15:24:17 +01:00
Benjamin Admin
9b59044663 fix(proxy): correct backend URL path to /api/compliance/v1/projects
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 33s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 27s
The backend routes are nested under /api/compliance/ prefix, not /api/.
Also includes Suspense boundary fix for useSearchParams in layout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 15:11:59 +01:00
Benjamin Admin
d787e58341 fix(migration): handle missing sdk_states table in migration 039
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 31s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 19s
The sdk_states table may not exist yet if no state has been saved via
the frontend. Wrap sdk_states alterations in a conditional DO block.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 15:06:06 +01:00
Benjamin Admin
4b778f2f85 fix(sdk): wrap useSearchParams in Suspense boundary for Next.js 15
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 35s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 19s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:59:30 +01:00
Benjamin Admin
0affa4eb66 feat(sdk): Multi-Projekt-Architektur — mehrere Projekte pro Tenant
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 33s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 19s
Jeder Tenant kann jetzt mehrere Compliance-Projekte anlegen (z.B. verschiedene
Produkte, Tochterunternehmen). CompanyProfile ist pro Projekt kopierbar und
danach unabhaengig editierbar. Multi-Tab-Support via separater BroadcastChannel
und localStorage Keys pro Projekt.

- Migration 039: compliance_projects Tabelle, sdk_states.project_id
- Backend: FastAPI CRUD-Routes fuer Projekte mit Tenant-Isolation
- Frontend: ProjectSelector UI, SDKProvider mit projectId, URL ?project=
- State API: UPSERT auf (tenant_id, project_id) mit Abwaertskompatibilitaet
- Tests: pytest fuer Model-Validierung, Row-Konvertierung, Tenant-Isolation
- Docs: MKDocs Seite, CLAUDE.md, Backend README

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:53:50 +01:00
Benjamin Admin
d3fc4cdaaa feat(sdk): Logo-Navigation, stabile Versionsnummer V001 + Firmenname im Header
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 45s
CI / test-python-backend-compliance (push) Successful in 35s
CI / test-python-document-crawler (push) Successful in 25s
CI / test-python-dsms-gateway (push) Successful in 21s
- Logo-Klick fuehrt zurueck zur Startseite (Neues/Bestehendes Projekt)
- Neue projectVersion im SDK State (inkrementiert nur bei explizitem Speichern)
- Header zeigt Firmenname + V001-Format statt auto-inkrementierende Sync-Version
- Sidebar Logo von Link auf Button umgestellt mit customerType-Reset

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 08:14:33 +01:00
Benjamin Admin
de486aeab0 feat(sdk): Step 6 nach Abteilungen + aktivitätsspezifische Datenkategorien
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 34s
CI / test-python-backend-compliance (push) Successful in 38s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 21s
- Verarbeitungstätigkeiten nach 11+ Abteilungen gruppiert (Personal, Finanzen, Vertrieb, Marketing, IT, Recht, Einkauf, Produktion, Logistik, Kundenservice, Facility)
- Branchenspezifische Abteilungen (E-Commerce, Gesundheit, Finanz, Bildung, Immobilien)
- 3-Stufen Datenkategorien: primär (sichtbar), weitere (aufklappbar), Art. 9 (rot, nur wenn plausibel relevant)
- Bundesland/Kanton-Dropdown für DE (16), AT (9), CH (26) statt Freitext

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 23:53:52 +01:00
Benjamin Admin
e96f623af0 feat(sdk): Step 6 Wizard rebuilt — Verarbeitungstätigkeiten statt IT-Systeme
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 33s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 28s
CI / test-python-dsms-gateway (push) Successful in 21s
- Verarbeitungstätigkeiten-Checkliste nach Branche (Art. 30 DSGVO)
- DSGVO-Standard Datenkategorien als Checkboxen (10 + 5 Art. 9)
- Rechtsgrundlage pro Verarbeitungstätigkeit (Art. 6 Abs. 1)
- KI-Systeme vereinfacht: risk_category und human_oversight entfernt (Tool-Output)
- Full-width Layout, Mitarbeiterzahl-Dropdown entfernt, Adressfelder ergänzt
- Konzern-Hinweis bei Jahresumsatz, Regulierungen aus Zielmarkt entfernt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 23:31:19 +01:00
Benjamin Admin
53ff0722a4 fix(backend): SQLAlchemy text() fuer alle raw SQL + UI-Verbesserungen
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 32s
CI / test-python-backend-compliance (push) Successful in 39s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 18s
- CRITICAL: Alle db.execute() Aufrufe in company_profile_routes.py
  und generation_routes.py mit text() gewrapped (SQLAlchemy 2.x)
- Geschaeftsmodell-Kacheln: Nur Kurztext, Beschreibung bei Klick
- "Warum diese Fragen" in Hauptbereich unter Ueberschrift verschoben
- Sidebar-Box entfernt fuer mehr Platz

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 22:53:29 +01:00
Benjamin Admin
2abf0b4cac feat(sdk): Company Profile Wizard verbessert
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 32s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 25s
CI / test-python-dsms-gateway (push) Successful in 19s
- B2B2C als Geschaeftsmodell hinzugefuegt
- URL-Felder bei Offering-Auswahl (Website, Shop, App, SaaS) — optional
- Schritt-spezifische Erklaerungen in "Warum diese Fragen?"
- Firmenname ohne Rechtsform, Templates bauen automatisch zusammen
- Gruendungsjahr springt auf 2000 statt 1800
- SDK-Abdeckung Panel und Profil-loeschen Button entfernt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 22:41:15 +01:00
Benjamin Admin
fd45545fbe feat(sdk): Auto-Save bei Schrittwechsel + Session-Header
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 34s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 19s
Company Profile Wizard speichert jetzt bei jedem Schrittwechsel (Weiter/Zurueck)
als Draft (is_complete: false). Shared buildProfilePayload() vermeidet Duplikation.
SDKHeader zeigt Version, letzten Schritt, Sync-Status und Bearbeiter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 22:16:44 +01:00
Benjamin Admin
504b1a1207 fix(sdk): Remove non-existent setCurrentModule() from roadmap page
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 29s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 17s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 14:38:46 +01:00
Benjamin Admin
66d32cd744 fix(sdk): Re-add useEffect import removed by mistake
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 27s
CI / test-python-backend-compliance (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 20s
CI / test-python-dsms-gateway (push) Successful in 16s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 14:30:38 +01:00
Benjamin Admin
7fa0349fe4 fix(sdk): Portfolio/Workshop crash + Audit-LLM 403 + Change-Requests Sidebar
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 29s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 17s
- Remove non-existent setCurrentModule() calls from portfolio/workshop pages
- Move change-requests from app/(sdk)/sdk/ to app/sdk/ for sidebar layout
- Seed compliance_officer RBAC role for default admin user (audit-llm 403)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 14:25:41 +01:00
Benjamin Admin
6ff9f1f2f3 feat(api-docs): API-Exposure-Klassifikation — Intern vs. Oeffentlich
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 36s
CI / test-python-backend-compliance (push) Successful in 33s
CI / test-python-document-crawler (push) Successful in 20s
CI / test-python-dsms-gateway (push) Successful in 16s
Alle ~640 Endpoints nach Netzwerk-Exposition klassifiziert: public (5 Module, ~30 EP),
partner/Integration (6 Module, ~25 EP), internal (25+ Module, ~550 EP), admin/Wartung (4 EP).
Badges, Filter und Stats in API-Docs + Developer Portal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 13:31:16 +01:00
Benjamin Admin
56758e8b55 fix(mock-data): Fake-Daten bei leerer DB entfernt — ISMS 0%, Dashboard keine simulierten Trends, Compliance-Hub keine Fallback-Zahlen
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 29s
CI / test-python-backend-compliance (push) Successful in 33s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 17s
- ISMS Overview: 14% → 0% bei leerer DB, "not_started" Status, alle Kapitel 0%
- Dashboard: 12-Monate simulierte Trend-Historie entfernt
- Compliance-Hub: Hardcoded Fallback-Statistiken (474/180/95/120/79/44/558/19) → 0
- SQLAlchemy Bug: `is not None` → `.isnot(None)` in SoA-Query
- Hardcoded chapter_7/8_status="pass" → berechnet aus Findings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 23:25:25 +01:00
Benjamin Admin
0c75182fb3 fix(proxy): 3 kaputte Proxy-Pfade + 21x localhost→backend-compliance Fallback
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 30s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 18s
- compliance-scope: /api/v1/compliance-scope → /api/compliance/v1/compliance-scope
- modules (4 Dateien): /api/modules → /api/compliance/modules
- 21 Proxy-Dateien: localhost:8002 → backend-compliance:8002 Fallback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 22:57:03 +01:00
Benjamin Admin
4a60b3a744 fix(isms): Proxy-Pfad /api/isms → /api/compliance/isms
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 29s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 20s
CI / test-python-dsms-gateway (push) Successful in 18s
Readiness-Check Button funktionierte nicht weil der ISMS-Proxy
das /compliance Segment im Backend-Pfad fehlte.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 19:20:27 +01:00
Benjamin Admin
95fcba34cd fix(quality): Ruff/CVE/TS-Fixes, 104 neue Tests, Complexity-Refactoring
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 30s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 17s
- Ruff: 144 auto-fixes (unused imports, == None → is None), F821/F811/F841 manuell
- CVEs: python-multipart>=0.0.22, weasyprint>=68.0, pillow>=12.1.1, npm audit fix (0 vulns)
- TS: 5 tote Drafting-Engine-Dateien entfernt, allowed-facts/sanitizer/StepHeader/context fixes
- Tests: +104 (ISMS 58, Evidence 18, VVT 14, Generation 14) → 1449 passed
- Refactoring: collect_ci_evidence (F→A), row_to_response (E→A), extract_requirements (E→A)
- Dead Code: pca-platform, 7 Go-Handler, dsr_api.py, duplicate Schemas entfernt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 19:00:33 +01:00
Benjamin Admin
6509e64dd9 feat(sdk): API-Referenz Frontend + Backend-Konsolidierung (Shared Utilities, CRUD Factory)
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 18s
- API-Referenz Seite (/sdk/api-docs) mit ~690 Endpoints, Suche, Filter, Modul-Index
- Shared db_utils.py (row_to_dict) + tenant_utils Integration in 6 Route-Dateien
- CRUD Factory (crud_factory.py) fuer zukuenftige Module
- Version-Route Auto-Registration in versioning_utils.py
- 1338 Tests bestanden, -232 Zeilen Duplikat-Code

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 17:07:43 +01:00
Benjamin Admin
7ec6b9f6c0 fix(cleanup): ISMS Bugfix, 13 tote AI-Endpoints entfernt, Compliance-Hub Proxy fix
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 33s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 18s
- ISMS: markStepCompleted entfernt (existiert nicht in SDKContext, verursachte Application Error)
- AI Routes: 13 ungenutzte Endpoints entfernt (ai_routes.py 1266→379 Zeilen, -887)
- Schemas: 12 ungenutzte AI-Schema-Klassen entfernt (-108 Zeilen)
- Compliance-Hub: 5 Fetch-URLs von /api/admin/... auf /api/sdk/v1/... umgestellt
- Tests: 1361 passed, 0 Regressionen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 15:13:19 +01:00
Benjamin Admin
9e65dff7d6 feat(isms): ISO 27001 Frontend, Proxy, Sidebar, Flow-Data, Architecture, MkDocs
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 33s
CI / test-python-backend-compliance (push) Successful in 29s
CI / test-python-document-crawler (push) Successful in 20s
CI / test-python-dsms-gateway (push) Successful in 16s
ISMS-Modul mit 6 Tabs (Uebersicht, Policies, SoA, Ziele, Audits/Findings/CAPA,
Management-Reviews) fuer alle 39 Backend-Endpoints. Readiness-Check identifiziert
potenzielle Major/Minor-Findings vor externer Zertifizierung.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 14:53:31 +01:00
Benjamin Admin
1e84df9769 feat(sdk): Multi-Tenancy, Versionierung, Change-Requests, Dokumentengenerierung (Phase 1-6)
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 18s
6-Phasen-Implementation fuer cloud-faehiges, mandantenfaehiges Compliance SDK:

Phase 1: Multi-Tenancy Fix
- Shared tenant_utils.py Dependency (UUID-Validierung, kein "default" mehr)
- VVT tenant_id Column + tenant-scoped Queries
- DSFA/Vendor DEFAULT_TENANT_ID von "default" auf UUID migriert
- Migration 035

Phase 2: Stammdaten-Erweiterung
- Company Profile um JSONB-Felder erweitert (processing_systems, ai_systems, technical_contacts)
- Regulierungs-Flags (NIS2, AI Act, ISO 27001)
- GET /template-context Endpoint
- Migration 036

Phase 3: Dokument-Versionierung
- 5 Versions-Tabellen (DSFA, VVT, TOM, Loeschfristen, Obligations)
- Shared versioning_utils.py Helper
- /{id}/versions Endpoints auf allen 5 Dokumenttypen
- Migration 037

Phase 4: Change-Request System
- Zentrale CR-Inbox mit CRUD + Accept/Reject/Edit Workflow
- Regelbasierte CR-Engine (VVT DPIA → DSFA CR, Datenkategorien → Loeschfristen CR)
- Audit-Trail
- Migration 038

Phase 5: Dokumentengenerierung
- 5 Template-Generatoren (DSFA, VVT, TOM, Loeschfristen, Obligations)
- Preview + Apply Endpoints (erzeugt CRs, keine direkten Dokumente)

Phase 6: Frontend-Integration
- Change-Request Inbox Page mit Stats, Filtern, Modals
- VersionHistory Timeline-Komponente
- SDKSidebar CR-Badge (60s Polling)
- Company Profile: 2 neue Wizard-Steps + "Dokumente generieren" CTA

Docs: 5 neue MkDocs-Seiten, CLAUDE.md aktualisiert
Tests: 97 neue Tests (alle bestanden)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 14:12:34 +01:00
Benjamin Admin
ef9aed666f fix(reporting): Replace deleted dsgvo/vendor/incidents store imports with direct SQL
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 18s
The reporting module imported packages deleted in the previous commit.
Replaced with direct SQL queries against the compliance schema tables.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 09:53:46 +01:00
Benjamin Admin
37166c966f feat(sdk): Audit-Dashboard + RBAC-Admin Frontends, UCCA/Go Cleanup
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 33s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 18s
CI / test-python-dsms-gateway (push) Successful in 16s
- Remove 5 unused UCCA routes (wizard, stats, dsb-pool) from Go main.go
- Delete 64 deprecated Go handlers (DSGVO, Vendors, Incidents, Drafting)
- Delete legacy proxy routes (dsgvo, vendors)
- Add LLM Audit Dashboard (3 tabs: Log, Nutzung, Compliance) with export
- Add RBAC Admin UI (5 tabs: Mandanten, Namespaces, Rollen, Benutzer, LLM-Policies)
- Add proxy routes for audit-llm and rbac to Go backend
- Add Workshop, Portfolio, Roadmap proxy routes and frontends
- Add LLM Audit + RBAC Admin to SDKSidebar

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 09:45:56 +01:00
Benjamin Admin
3467bce222 feat(obligations): Go PARTIAL DEPRECATED, Python x-user-id, UCCA Proxy Headers, 62 Tests
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 31s
CI / test-python-backend-compliance (push) Successful in 29s
CI / test-python-document-crawler (push) Successful in 19s
CI / test-python-dsms-gateway (push) Successful in 26s
- Go obligations_handlers.go: CRUD-Overlap als deprecated markiert, AI-Features (Assess/Gap/TOM/Export) bleiben aktiv
- Python obligation_routes.py: x-user-id Header + Audit-Logging an 4 Write-Endpoints
- 3 UCCA Proxy-Dateien: Default X-Tenant-ID + X-User-ID Headers
- Tests von 39 auf 62 erweitert (+23 Route-Integration-Tests mit mock_db/TestClient)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 21:31:14 +01:00
Benjamin Admin
a5e4801b09 fix(escalations): Tenant/User-ID Defaults + Routing-Klarheit
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 19s
CI / test-python-dsms-gateway (push) Successful in 16s
- escalations/route.ts: X-Tenant-Id + X-User-Id Default-Header ergaenzt,
  X-User-Id aus Request weitergeleitet
- escalation_routes.py: DEFAULT_TENANT_ID Konstante (9282a473-...) statt 'default'
- test_escalation_routes.py: vollstaendige Test-Suite ergaenzt (+337 Zeilen)
- main.go + escalation_handlers.go: DEPRECATED-Kommentare — UCCA-Escalations
  bleiben fuer Assessment-Review, Haupt-Escalation-System ist Python-Backend

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 21:15:02 +01:00
Benjamin Admin
8f3fb84b61 docs: Infrastruktur-Migration 2026-03-06 in Docs + CLAUDE.md nachziehen
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 31s
CI / test-python-backend-compliance (push) Successful in 27s
CI / test-python-document-crawler (push) Successful in 20s
CI / test-python-dsms-gateway (push) Successful in 17s
- CLAUDE.md: Voraussetzungen auf externe Hetzner-Services aktualisiert
  (PostgreSQL 46.225.100.82:54321, Qdrant qdrant-dev.breakpilot.ai, MinIO Hetzner)
- docs-src/index.md: PostgreSQL-Zeile auf externe Instanz aktualisiert
- docs-src/document-crawler/index.md: DB-Verbindung auf externe PG aktualisiert
- Zusatz: training_*, ucca_*, academy_* Tabellen + update_updated_at_column()
  Funktion auf externe DB nachmigriert (waren beim ersten Dump nicht erfasst)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 21:10:24 +01:00
Benjamin Admin
2dd86e97be feat(incidents): Go Incidents nach Python migrieren, Proxy umleiten, 50 Tests
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 34s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 18s
- incident_routes.py: 15 Endpoints (CRUD, Risk Assessment, Art. 33/34 Notifications, Measures, Timeline, Close, Stats)
- Neuer Endpoint PUT /{id}/status (nicht in Go vorhanden, Frontend braucht ihn)
- Proxy von ai-compliance-sdk:8090 auf backend-compliance:8002 umgeleitet
- Go incidents_handlers.go + main.go als DEPRECATED markiert
- 50/50 Tests bestanden

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 20:50:00 +01:00
Benjamin Admin
8742cb7f5a docs: Qdrant und MinIO/Object-Storage Referenzen aktualisieren
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 41s
CI / test-python-dsms-gateway (push) Successful in 19s
- Qdrant: lokaler Container → qdrant-dev.breakpilot.ai (gehostet, API-Key)
- MinIO: bp-core-minio → Hetzner Object Storage (nbg1.your-objectstorage.com)
- CLAUDE.md, MkDocs, ARCHITECTURE.md, training.md, ci-cd-pipeline.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 20:18:29 +01:00
Benjamin Admin
6a940344c2 feat(dsfa): Go DSFA deprecated, URL-Fix, fehlende Endpoints + 145 Tests
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 34s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 27s
CI / test-python-dsms-gateway (push) Successful in 18s
- Go: DEPRECATED-Kommentare an allen 6 DSFA-Handlern + Route-Block
- api.ts: URL-Fix /dsgvo/dsfas → /dsfa (Detail-Seite war komplett kaputt)
- Python: Section-Update, Workflow (submit/approve), Export (JSON+CSV), UCCA-Stubs
- Tests: 145/145 bestanden (Schema + Route-Integration mit TestClient+SQLite)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 19:41:12 +01:00
Benjamin Admin
095eff26d9 feat(dsr): Go DSR deprecated, Python Export-Endpoint, Frontend an Backend-APIs anbinden
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 34s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 17s
- Go: DEPRECATED-Kommentare an allen DSR-Handlern und Routes
- Python: GET /dsr/export?format=csv|json (Semikolon-CSV, 12 Spalten)
- API-Client: 12 neue Funktionen (verify, assign, extend, complete, reject, communications, exception-checks, history)
- Detail-Seite: Alle Actions verdrahtet (keine Coming-soon-Alerts mehr), Communications + Art.17(3)-Checks + Audit-Log live
- Haupt-Seite: CSV-Export-Button im Header
- Tests: 54/54 bestanden (4 neue Export-Tests)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 18:21:43 +01:00
Benjamin Admin
3593a4ff78 feat(tom): TOM-Backend in Python erstellen, Frontend von In-Memory auf DB migrieren
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 33s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 15s
- Migration 034: compliance_tom_state + compliance_tom_measures Tabellen
- Python Routes: State CRUD, Measures CRUD, Bulk-Upsert, Stats, CSV/JSON-Export
- Frontend-Proxy: In-Memory Storage durch Proxy zu backend-compliance ersetzt
- Go TOM-Handler als DEPRECATED markiert (Source of Truth ist jetzt Python)
- 44 Tests (alle bestanden)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 17:35:44 +01:00
Benjamin Admin
4cbfea5c1d feat(vvt): Go-Features nach Python portieren (Source of Truth)
Review-Daten (last_reviewed_at, next_review_at), created_by, DSFA-Link,
CSV-Export mit Semikolon-Trennung, overdue_review_count in Stats.
Go-VVT-Handler als DEPRECATED markiert. 32 Tests bestanden.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 17:14:38 +01:00
Benjamin Admin
885b97d422 feat(infra): Qdrant + MinIO auf externe Hetzner-Services migrieren
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 33s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 20s
CI / test-python-dsms-gateway (push) Successful in 17s
- docker-compose.yml: QDRANT_HOST/PORT → QDRANT_URL (qdrant-dev.breakpilot.ai) + QDRANT_API_KEY
- docker-compose.yml: MINIO bp-core-minio → nbg1.your-objectstorage.com (Hetzner)
- .env.example: QDRANT_URL + QDRANT_API_KEY ergaenzt, MinIO-Hinweis
- architecture-data.ts: PostgreSQL/Qdrant/MinIO auf externe Dienste aktualisiert
  - PostgreSQL 17 @ 46.225.100.82:54321 (migriert 2026-03-06)
  - Qdrant Cloud @ qdrant-dev.breakpilot.ai (migriert 2026-03-06)
  - Hetzner Object Storage @ nbg1.your-objectstorage.com (migriert 2026-03-06)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 14:33:04 +01:00
Benjamin Admin
ee359885a8 Switch MinIO from local to Hetzner Object Storage
Migrate compliance-tts-service S3 config to Hetzner Object Storage
(nbg1.your-objectstorage.com) with HTTPS. Add MINIO_SECURE env
support to TTS main.py StorageClient initialization.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:07:28 +01:00
Benjamin Admin
4d2f4f2d24 feat(qdrant): Migrate to hosted qdrant-dev.breakpilot.ai with API-Key auth
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 37s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 18s
- LegalRAGClient: QDRANT_HOST+PORT → QDRANT_URL + QDRANT_API_KEY
- docker-compose: env vars updated for hosted Qdrant
- AllowedCollections: added bp_compliance_gdpr, bp_dsfa_templates, bp_dsfa_risks
- Migration scripts (bash + python) for data transfer

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:55:21 +01:00
Benjamin Admin
ec4ed1f2ad feat(infra): Compliance DB auf externe PostgreSQL 46.225.100.82:54321 migrieren
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 19s
- docker-compose.yml: alle 4 DATABASE_URL auf COMPLIANCE_DATABASE_URL (mit Fallback)
- .env.example: COMPLIANCE_DATABASE_URL Eintrag ergaenzt
- Rollback: ohne .env zeigt Fallback auf bp-core-postgres

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 12:32:01 +01:00
Benjamin Admin
960b8e757c fix(llm): qwen3.5 think:false + num_ctx 8192 in allen Chat/Draft-Routen
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 18s
Compliance Advisor, Drafting Agent und Validator haben nicht geantwortet
weil qwen3.5 standardmaessig im Thinking-Mode laeuft (interne Chain-of-
Thought > 2min Timeout). Keiner der Agenten benoetigt Thinking-Mode —
alle Aufgaben sind Chat/Textgenerierung/JSON-Validierung ohne tiefes
Reasoning. think:false sorgt fuer direkte schnelle Antworten.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 08:35:53 +01:00
Benjamin Admin
adc95267bd chore: LLM qwen3:30b-a3b → qwen3.5:35b-a3b
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 42s
CI / test-python-backend-compliance (push) Successful in 35s
CI / test-python-document-crawler (push) Successful in 20s
CI / test-python-dsms-gateway (push) Successful in 17s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 07:31:55 +01:00
Benjamin Admin
b5625c14aa feat(docs): Wettbewerbsanalyse aktualisiert — 11/15 Features erledigt, 57 Module, 9 USPs
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 33s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 18s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 22:35:01 +01:00
Benjamin Admin
4a564ad8f7 docs: SDK Workflow Referenzseite — Seq-Nummern, visibleWhen, Navigation
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 17s
Neue Übersichtsseite sdk-workflow.md mit vollständiger Referenz:
- Alle 34 Steps mit seq-Nummern (100–4800) in Pakettabellen
- Lücken-Konvention (100er innerhalb, 300er an Paket-Grenzen)
- visibleWhen-Logik für import/dsfa/document-generator
- Prerequisite-Ketten-Regeln und Ausnahmen
- Alle Navigationsfunktionen (getNextStep, getVisibleSteps, etc.)
- Anleitung zum Einfügen neuer Steps
- Deprecated-Hinweis für getVisibleStepsForCustomerType

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 22:31:28 +01:00
Benjamin Admin
d527fcbdc8 fix(sdk)+docs: dsfa-visibleWhen Bug, training.md, index.md komplett
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 33s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 19s
Fixes:
- types.ts: dsfa isOptional false→true + visibleWhen ergänzt
  (sichtbar nur bei Scope L2/L3/L4 oder dsfaRequired Hard-Trigger)
- dsfa war einziger Step mit fehlendem visibleWhen laut Plan

Docs:
- training.md: NEU — Training Engine (seq 4800), alle Endpoints,
  Rollenmatrix, KI-Inhalt, Quiz, TTS-Media, DB-Schema
- mkdocs.yml: Training Engine nav-Eintrag
- index.md: E-Mail-Templates (4350) + Training Engine (4800)
  in Modul-Tabelle + URL-Liste ergänzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 21:13:06 +01:00
Benjamin Admin
d212208587 docs: MkDocs-Aktualisierung — Obligations v2, Extraction, fehlende Module
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 38s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 19s
- obligations.md: NEU — Obligations Framework v2 (325 Pflichten, 9 Regulierungen,
  Condition Engine, TOM-Mapping, Gap-Analyse, alle 13 API-Endpoints)
- requirements.md: POST /compliance/extract-requirements-from-rag dokumentiert
  (RAG-Collections, dry_run, Deduplication, Auto-Regulation-Stubs)
- vorbereitung-module.md: UCCA Obligations v2 Abschnitt + neue Endpoints +
  Hinweis: Go-Tests lokal statt im Container
- index.md: Obligations, IACE, Import, Screening, RAG zur Modulliste + URLs
- mkdocs.yml: obligations.md als nav-Eintrag

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 20:33:08 +01:00
Benjamin Admin
a1980cd12d feat(reporting+docs): tenant-ID-Validierung, Go-Tests, 4 MkDocs-Einzelseiten
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 37s
CI / test-python-backend-compliance (push) Successful in 33s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 18s
- reporting_handlers.go: uuid.Nil-Check vor Store-Aufruf (→ 400)
- reporting_handlers_test.go: 4 MissingTenantID-Tests (PASS) + 4 WithTenant-Tests (SKIP)
- docs-src: requirements.md, controls.md, evidence.md, risks.md (je mit API, Schema, Tests)
- mkdocs.yml: 4 neue Nav-Einträge + \n-Bug auf Zeile 91 behoben
- compliance-kern.md: Link-Hinweise zu Detailseiten ergänzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 18:25:26 +01:00
Benjamin Admin
35576fb6f8 feat(sidebar): CE-Compliance (IACE) als permanente Sektion — unabhaengig von ceMarkingRequired
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 34s
CI / test-python-backend-compliance (push) Successful in 28s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 16s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:39:47 +01:00
Benjamin Admin
529c37d91a chore: diverse Bereinigungen und Fixes
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 20s
CI / test-python-dsms-gateway (push) Successful in 28s
- admin-compliance: .dockerignore + Dockerfile bereinigt
- dsfa-corpus/route.ts + legal-corpus/route.ts entfernt (obsolet)
- webhooks/woodpecker/route.ts: minor fix
- dsfa/[id]/page.tsx: Refactoring
- service_modules.py + README.md: aktualisiert
- Migration 028 → 032 umbenannt (legal_documents_extend)
- docs: index.md + DEVELOPER.md aktualisiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 17:24:15 +01:00
Benjamin Admin
efeacc1619 feat(iace): Hazard-Library v2, Controls-Library, SEPA Avoidance, CE RAG-Ingest
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 33s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 19s
- Hazard-Library: +79 neue Eintraege in 12 Kategorien (software_fault,
  hmi_error, mechanical_hazard, electrical_hazard, thermal_hazard,
  emc_hazard, configuration_error, safety_function_failure,
  logging_audit_failure, integration_error, environmental_hazard,
  maintenance_hazard) — Gesamtanzahl: ~116 Eintraege in 24 Kategorien
- Controls-Library: neue Datei controls_library.go mit 200 Eintraegen
  in 6 Domaenen (REQ/ARCH/SWDEV/VER/CYBER/DOC)
- Handler: GET /sdk/v1/iace/controls-library (?domain=, ?category=)
- SEPA: CalculateInherentRisk() + 4. Param Avoidance (0=disabled,
  1-5: 3=neutral); RiskComputeInput.Avoidance, RiskAssessment.Avoidance,
  AssessRiskRequest.Avoidance — backward-kompatibel (A=0 → S×E×P)
- Tests: engine_test.go + hazard_library_test.go aktualisiert
- Scripts: ingest-ce-corpus.sh — 15 CE/Safety-Dokumente (EUR-Lex,
  NIST, ENISA, NASA, OWASP, MITRE CWE) in bp_compliance_ce und
  bp_compliance_datenschutz
- Docs: docs-src/services/sdk-modules/iace.md + mkdocs.yml Nav-Eintrag

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 17:13:01 +01:00
Benjamin Admin
3ed8300daf feat(extraction): POST /compliance/extract-requirements-from-rag
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 34s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 35s
CI / test-python-dsms-gateway (push) Successful in 17s
Sucht alle RAG-Kollektionen nach Prüfaspekten und legt automatisch
Anforderungen in der DB an. Kernfeatures:

- Durchsucht alle 6 RAG-Kollektionen parallel (bp_compliance_ce,
  bp_compliance_recht, bp_compliance_gesetze, bp_compliance_datenschutz,
  bp_dsfa_corpus, bp_legal_templates)
- Erkennt BSI Prüfaspekte (O.Purp_6) im Artikel-Feld und per Regex
- Dedupliziert nach (regulation_code, article) — safe to call many times
- Auto-erstellt Regulations-Stubs für unbekannte regulation_codes
- dry_run=true zeigt was erstellt würde ohne DB-Schreibzugriff
- Optionale Filter: collections, regulation_codes, search_queries
- 18 Tests (alle bestanden)
- Frontend: "Aus RAG extrahieren" Button auf /sdk/requirements

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 15:11:10 +01:00
Benjamin Admin
f3ccfe5dcd fix(ucca): Route-Konflikt :id vs :assessmentId — TOM-Controls Pfad geaendert
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 33s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 17s
GET /obligations/:id/tom-controls → GET /obligations/tom-controls/for-obligation/:obligationId
Gin erlaubt keine unterschiedlichen Param-Namen auf demselben Pfad-Level.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:03:48 +01:00
Benjamin Admin
38e278ee3c feat(ucca): Pflichtendatenbank v2 (325 Obligations), Trigger-Engine, TOM-Control-Mapping
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 29s
CI / test-python-document-crawler (push) Successful in 20s
CI / test-python-dsms-gateway (push) Successful in 18s
- 9 Regulation-JSON-Dateien (DSGVO 80, AI Act 60, NIS2 40, BDSG 30, TTDSG 20, DSA 35, Data Act 25, EU-Maschinen 15, DORA 20)
- Condition-Tree-Engine fuer automatische Pflichtenselektion (all_of/any_of, 80+ Field-Paths)
- Generischer JSONRegulationModule-Loader mit YAML-Fallback
- Bidirektionales TOM-Control-Mapping (291 Obligation→Control, 92 Control→Obligation)
- Gap-Analyse-Engine (Compliance-%, Priority Actions, Domain Breakdown)
- ScopeDecision→UnifiedFacts Bridge fuer Auto-Profiling
- 4 neue API-Endpoints (assess-from-scope, tom-controls, gap-analysis, reverse-lookup)
- Frontend: Auto-Profiling Button, Regulation-Filter Chips, TOM-Panel, Gap-Analyse-View
- 18 Unit Tests (Condition Engine, v2 Loader, TOM Mapper)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 14:51:44 +01:00
Benjamin Admin
2540a2189a refactor(controls): Remove hardcoded controlTemplates fallback data
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 37s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 18s
Replaced mock fallback (6 hardcoded controls + loadFromTemplates())
with clean empty state. Page now shows only real API data — freigabefähig.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 14:20:35 +01:00
Benjamin Admin
bd9796725a feat(compliance-kern): Tests, MkDocs + RAG-Controls Button für Anforderungen/Controls/Nachweise/Risiken
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 36s
CI / test-python-backend-compliance (push) Successful in 35s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 21s
- 74 neue Tests (test_risk_routes, test_evidence_routes, test_requirement_routes, test_control_routes)
  Enum-Mocking (.value), ControlStatusEnum-Validierung, db.query() direkte Mocks
- MkDocs: docs-src/services/sdk-modules/compliance-kern.md
  Endpunkt-Tabellen, Schema-Erklärungen, CI/CD-Beispiele, Risikomatrix
- controls/page.tsx: "KI-Controls aus RAG vorschlagen" Button
  POST /api/sdk/v1/compliance/ai/suggest-controls, Suggestion-Panel,
  Requirement-ID-Eingabe + Dropdown, Konfidenz-Anzeige, Hinzufügen-Aktion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:43:02 +01:00
Benjamin Admin
a181c977c3 fix(crawler): korrigierte URLs fuer DSK-OH, BayLDA-TOM (404-Fixes)
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 37s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 18s
- SDM V3.1: media/oh/ → media/ah/
- E-Mail-Verschluesselung: korrekter Dateiname mit Datum
- OH Telemedien: korrekter Dateiname V1.1
- BayLDA TOM: media/checkliste/ Unterordner

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 12:12:25 +01:00
Benjamin Admin
ef17151a41 fix(import+screening): GET-Alias, DELETE-Endpoint, ehrlicher Scan-Status
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 37s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-dsms-gateway (push) Has been cancelled
CI / test-python-document-crawler (push) Has been cancelled
Import-Backend:
- GET /v1/import (Root-Alias) → list_documents; behebt URL-Mismatch im Proxy
- DELETE /v1/import/{document_id} → löscht Dokument + Gap-Analyse (mit Tenant-Isolierung)
- 6 neue Tests (65 total, alle grün)

Screening-Frontend:
- Simulierten Fortschrittsbalken (Math.random) entfernt — war inhaltlich falsch
- Ersetzt durch indeterminate Spinner + rotierende ehrliche Status-Texte
  (z.B. "OSV.dev Datenbank wird abgefragt...") im 2-Sek.-Takt
- Kein scanProgress-State mehr benötigt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 12:07:01 +01:00
Benjamin Admin
3707ffe799 feat: DSK/BfDI RAG-Ingest, TOM-Control-Library 180, Risk-Engine-Spec, RAG-Query-Optimierung
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 37s
CI / test-python-backend-compliance (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 21s
- Crawler erweitert: +26 neue Dokumente (DSK KP 1-20, SDM V3.1, BfDI Loeschkonzept, BayLDA TOM-Checkliste)
- RAG-Queries optimiert: 18 Queries mit EDPB/DSK/WP-Referenzen fuer besseres Retrieval
- Chat-Route: queryRAG nutzt jetzt Collection + Query-Boost aus DOCUMENT_RAG_CONFIG
- TOM Control Library: 180 Controls in 12 Domaenen (ISO Annex-A Style, tom_controls_v1.json)
- Risk Engine Spec: Impact/Likelihood 0-10, Score 0-100, 4 Tiers, Loeschfristen-Engine
- Soul-Files: DSK-Kurzpapiere, SDM V3.1, BfDI als primaere deutsche Quellen
- Manifest CSV: eu_de_privacy_manifest.csv mit Lizenz-Ampel (gruen/gelb/rot)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 12:03:57 +01:00
Benjamin Admin
3913931d5b feat(freigabe): Import/Screening/Modules/RAG — API-Tests, Migration 031, Bug-Fix
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 40s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 26s
CI / test-python-dsms-gateway (push) Successful in 21s
- import_routes: GET /gap-analysis/{document_id} implementiert
- import_routes: Bug-Fix — gap_analysis_result vor try-Block initialisiert
  (verhindert UnboundLocalError bei DB-Fehler)
- test_import_routes: 21 neue API-Endpoint-Tests (59 total, alle grün)
- test_screening_routes: 18 neue API-Endpoint-Tests (74 total, alle grün)
- 031_modules.sql: Migration für compliance_service_modules,
  compliance_module_regulations, compliance_module_risks
- test_module_routes: 20 neue Tests für Module-Registry-Routen (alle grün)
- freigabe-module.md: MkDocs-Seite für Import/Screening/Modules/RAG
- mkdocs.yml: Nav-Eintrag "Freigabe-Module (Paket 2)"

Gesamt: 146 neue Tests, alle bestanden

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 11:42:19 +01:00
Benjamin Admin
0503e72a80 fix(freigabe): Vorbereitung-Module release prep — Python 3.9 fix, Scope Engine tests, MkDocs
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 36s
CI / test-python-backend-compliance (push) Successful in 33s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 20s
- fix: company_profile_routes.py — dict|None → Optional[dict] for Python 3.9 compat (9/9 tests grün)
- test: 40 Vitest-Tests für ComplianceScopeEngine (calculateScores, determineLevel,
  evaluateHardTriggers, evaluate integration, buildDocumentScope, evaluateRiskFlags)
- docs: vorbereitung-module.md — Profil, Scope, UCCA vollständig dokumentiert
- docs: mkdocs.yml — Nav-Eintrag "Vorbereitung-Module (Paket 1)" vor Analyse-Module

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 10:57:17 +01:00
Benjamin Admin
6ad7d62369 docs: DSFA MkDocs aktualisiert — Migration 030, vollständiges Schema, 101 Tests, PDF-Ingest
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 37s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 18s
- Datenmodell: vollständiges Response-JSON mit allen 60+ Feldern (Sections 0-8)
- DB-Schema: Migrations-Tabelle (024/028/030), alle Spaltengruppen dokumentiert
- API: minimaler Create-Request + Section-Update-Beispiel
- Tests: 101 Tests (war: 52), neue Klassen TestAIUseCaseModules + TestDSFAFullSchema
- Ingest-Script: PDF-Downloads aktiviert, 10 Behörden mit direkten URLs,
  Tabelle: Behörden mit PDF vs. Text-Fallback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 10:13:43 +01:00
Benjamin Admin
789c215e5e feat: DSFA vollständiges DB-Schema + PDF-Ingest + Tests
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 36s
CI / test-python-backend-compliance (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 22s
- Migration 030: alle fehlenden Spalten für compliance_dsfas (Sections 0-7)
  flat fields: processing_description, legal_basis, dpo_*, authority_*, ...
  JSONB arrays: risks, mitigations, wp248_criteria_met, ai_trigger_ids, ...
  JSONB objects: section_progress, threshold_analysis, review_schedule, metadata
- dsfa_routes.py: DSFACreate/DSFAUpdate erweitert (60+ neue Optional-Felder)
  _dsfa_to_response: alle neuen Felder mit safe _get() Helper
  PUT-Handler: vollständige JSONB_FIELDS-Liste (22 Felder)
- Tests: 101 (+49) Tests — TestAIUseCaseModules + TestDSFAFullSchema
- ingest-dsfa-bundesland.sh: KNOWN_PDF_URLS (15 direkte URLs), download_pdfs()
  find_pdf_for_state() Helper, PDF-first mit Text-Fallback in ingest_all()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 10:03:09 +01:00
Benjamin Admin
ff765b2d71 fix: Migration 028 robuster (section_progress UPDATE via DO-Block mit IF EXISTS)
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 25s
CI / test-python-dsms-gateway (push) Successful in 19s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 09:32:31 +01:00
Benjamin Admin
308d559c85 feat: DSFA Section 8 KI-Anwendungsfälle + Bundesland RAG-Ingest
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 38s
CI / test-python-backend-compliance (push) Successful in 33s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 19s
- Migration 028: ai_use_case_modules JSONB + section_8_complete auf compliance_dsfas
- Neues ai-use-case-types.ts: AIUseCaseModule Interface, 8 Typen, Art22Assessment,
  AI Act Risikoklassen, WP248-Kriterien, Privacy by Design, createEmptyModule() Helper
- types.ts: Section 8 in DSFA_SECTIONS, ai_use_case_modules im DSFA Interface,
  section_8_complete in DSFASectionProgress
- api.ts: addAIUseCaseModule, updateAIUseCaseModule, removeAIUseCaseModule
- 5 neue UI-Komponenten: AIUseCaseTypeSelector, Art22AssessmentPanel,
  AIRiskCriteriaChecklist, AIUseCaseModuleEditor (7 Tabs), AIUseCaseSection
- DSFASidebar: Section 8 Eintrag + calculateSectionProgress case 8
- ReviewScheduleSection: ai_use_case_module Trigger-Typ ergänzt
- page.tsx: Section 8 Rendering + Weiter-Button auf activeSection < 8 + KI-Module Counter
- scripts/ingest-dsfa-bundesland.sh: WP248 + alle 17 Behörden → bp_dsfa_corpus
- Docs: dsfa.md Section 8 + RAG-Corpus, Developer Portal DSFA mit AI-Modul-Code-Beispielen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 09:20:27 +01:00
Benjamin Admin
274dc68e24 feat: Drafting Agent Kompetenzbereich erweitert — alle 18 Dokumenttypen, Gap-Banner, Redirect-Logic
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 23s
- DOCUMENT_SDK_STEP_MAP: 12 kaputte URLs korrigiert (z.B. /sdk/loeschkonzept → /sdk/loeschfristen)
- Go Backend: iace_ce_assessment zur validTypes-Whitelist hinzugefuegt
- SOUL-Datei: von 17 auf ~80 Zeilen erweitert (18 draftbare Typen, Redirects, operative Module)
- Intent Classifier: 10 fehlende Dokumenttyp-Patterns + 5 Redirect-Patterns (Impressum/AGB/Widerruf → Document Generator)
- State Projector: getExistingDocumentTypes von 6 auf 11 Checks erweitert (risks, escalations, iace, obligations, dsr)
- DraftingEngineWidget: Gap-Banner fuer kritische Luecken mit Analysieren-Button
- Cross-Validation: 4 neue deterministische Regeln (DSFA-NO-VVT, DSFA-NO-TOM, DSI-NO-LF, AV-NO-VVT)
- Prose Blocks: 5 neue Dokumenttypen (av_vertrag, betroffenenrechte, risikoanalyse, notfallplan, iace_ce_assessment)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 09:07:07 +01:00
Benjamin Admin
6e0e9cd3cf fix: Agents-Seite in SDK-Layout verschoben — Sidebar + Icons sichtbar
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 36s
CI / test-python-backend-compliance (push) Successful in 35s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 18s
Agents lag in app/(sdk)/sdk/agents/ (separater Route-Group) und erbte
daher nicht das SDK-Layout aus app/sdk/layout.tsx. Verschoben nach
app/sdk/agents/ damit SDKSidebar, ComplianceAdvisorWidget und
SDKPipelineSidebar korrekt angezeigt werden.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 08:42:40 +01:00
Benjamin Admin
451616b10e docs: MkDocs-Dokumentation fuer DSR, E-Mail-Templates, Banner Consent
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 48s
CI / test-python-backend-compliance (push) Successful in 42s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 21s
- Neue Seiten: dsr.md, email-templates.md, banner-consent.md
- rechtliche-texte.md: User-Consents & Cookie-Kategorien (Migration 028) ergaenzt
- mkdocs.yml: 3 neue Nav-Eintraege unter SDK Module

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 08:22:38 +01:00
Benjamin Admin
b7c1a5da1a feat: Consent-Service Module nach Compliance migriert (DSR, E-Mail-Templates, Legal Docs, Banner)
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 36s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 18s
5-Phasen-Migration: Go consent-service Proxies durch native Python/FastAPI ersetzt.

Phase 1 — DSR (Betroffenenrechte): 6 Tabellen, 30 Endpoints, Frontend-API umgestellt
Phase 2 — E-Mail-Templates: 5 Tabellen, 20 Endpoints, neues Frontend, SDK_STEPS erweitert
Phase 3 — Legal Documents Extension: User Consents, Audit Log, Cookie-Kategorien
Phase 4 — Banner Consent: Device-Consents, Site-Configs, Kategorien, Vendors
Phase 5 — Cleanup: DSR-Proxy aus main.py entfernt, Frontend-URLs aktualisiert

148 neue Tests (50 + 47 + 26 + 25), alle bestanden.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 00:36:24 +01:00
Benjamin Admin
2211cb9349 fix: DSFA-Template status active → published (wird im Document Generator angezeigt)
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 36s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 19s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 00:05:10 +01:00
Benjamin Admin
6a8289246c feat: DSFA als TemplateType + Kategorie im Document Generator
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 34s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 17s
- TemplateType-Union um 'dsfa' erweitert
- TEMPLATE_TYPE_LABELS: dsfa → 'Datenschutz-Folgenabschätzung'
- Document Generator: Kategorie-Tab 'DSFA' hinzugefügt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:54:29 +01:00
Benjamin Admin
93c200626c fix: 025_dsfa_template.sql — tenant_id als UUID casten
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 34s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 17s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:46:59 +01:00
Benjamin Admin
b4d39b9709 feat: DSFA-Template für Document Generator (Migration 025)
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 34s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 20s
Vollständige Vorlage für Datenschutz-Folgenabschätzungen nach Art. 35 DSGVO
mit IF-Blöcken, Risikomatrix, TOM-Tabelle und Unterschriften-Abschnitt.
document_type=dsfa, Sprache=de, 19 Platzhalter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:43:59 +01:00
Benjamin Admin
05aa0ee2c6 fix: DSFA Router-Prefix und Proxy-Pfad korrigiert
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 21s
Router-Prefix /v1/dsfa → /dsfa (konsistent mit allen anderen Routes)
Proxy-Pfad /api/v1/dsfa → /api/compliance/dsfa

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 23:22:17 +01:00
Benjamin Admin
1454427872 fix: DSFA immer in Sidebar sichtbar (visibleWhen entfernt)
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 37s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 18s
visibleWhen-Bedingung (nur bei L2-L4 / dsfaRequired-Trigger) entfernt.
DSFA erscheint jetzt immer im Dokumentations-Paket der Sidebar.
isOptional: true → false (REQUIRED-Checkpoint CP-DSFA).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 23:13:32 +01:00
Benjamin Admin
a9de7b4010 docs: DSFA Modul — MkDocs + Developer Portal Dokumentation
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 34s
CI / test-python-backend-compliance (push) Successful in 29s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 19s
- docs-src/services/sdk-modules/dsfa.md: vollständige DSFA-Dokumentation
  (Endpoints, Datenmodell, Status-Workflow, DB-Tabellen, Tests, Compliance-Kontext)
- mkdocs.yml: DSFA in Navigation unter SDK Module eingefügt
- docs-src/index.md: DSFA + Paket-Links in Services-Dokumentation
- docs-src/services/sdk-modules/dokumentations-module.md: DSFA in Übersichtstabelle
- developer-portal/app/api/dsfa/page.tsx: vollständige API-Referenz mit cURL-Beispielen
- developer-portal/app/api/page.tsx: DSFA-Abschnitt mit allen 8 Endpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 22:55:31 +01:00
Benjamin Admin
a694b9d9ea feat: DSFA Modul — Backend, Proxy, Frontend-Migration, Tests + Mock-Daten entfernt
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 38s
CI / test-python-backend-compliance (push) Successful in 38s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 19s
- Migration 024: compliance_dsfas + compliance_dsfa_audit_log Tabellen
- dsfa_routes.py: CRUD + stats + audit-log + PATCH status Endpoints
- Proxy: /api/sdk/v1/dsfa/[[...path]] → backend-compliance:8002/api/v1/dsfa
- dsfa/page.tsx: mockDSFAs entfernt → echte API (loadDSFAs, handleCreateDSFA, handleStatusChange, handleDeleteDSFA)
- GeneratorWizard: kontrollierte Inputs + onSubmit-Handler
- reporting/page.tsx: getMockReport() Fallback entfernt → Fehlerstate
- dsr/[requestId]/page.tsx: mockCommunications entfernt → leeres Array (TODO: Backend fehlt)
- 52 neue Tests (680 gesamt, alle grün)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 22:41:05 +01:00
Benjamin Admin
dc0d38ea40 feat: Vorbereitung-Module auf 100% — Compliance-Scope Backend, DELETE-Endpoints, Proxy-Fixes, blocked-content Tab
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 19s
Paket A — Kritische Blocker:
- compliance_scope_routes.py: GET + POST UPSERT für sdk_states JSONB-Feld
- compliance/api/__init__.py: compliance_scope_router registriert
- import/route.ts: POST-Proxy für multipart/form-data Upload
- screening/route.ts: POST-Proxy für Dependency-File Upload

Paket B — Backend + UI:
- company_profile_routes.py: DELETE-Endpoint (DSGVO Art. 17)
- company-profile/route.ts: DELETE-Proxy
- company-profile/page.tsx: Profil-löschen-Button mit Bestätigungs-Dialog
- source-policy/pii-rules/[id]/route.ts: GET ergänzt
- source-policy/operations/[id]/route.ts: GET + DELETE ergänzt

Paket C — Tests + UI:
- test_compliance_scope_routes.py: 27 Tests (neu)
- test_import_routes.py: +36 Tests → 60 gesamt
- test_screening_routes.py: +28 Tests → 80+ gesamt
- source-policy/page.tsx: "Blockierte Inhalte" Tab mit Tabelle + Remove

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 17:43:29 +01:00
Benjamin Admin
832c177688 fix: agent-core aus .dockerignore entfernen (wird im Container benoetigt)
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 36s
CI / test-python-backend-compliance (push) Successful in 36s
CI / test-python-document-crawler (push) Successful in 25s
CI / test-python-dsms-gateway (push) Successful in 19s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 16:58:33 +01:00
Benjamin Admin
560bdfb7fd feat: Agent Management Modul — SOUL-Editor, Dashboard, Architektur-Doku
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 37s
CI / test-python-backend-compliance (push) Successful in 38s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 19s
- SOUL-Dateien: System-Prompts aus Chat-Routen extrahiert nach agent-core/soul/*.soul.md
- soul-reader.ts: Lese-/Schreib-API mit 30s TTL-Cache und Backup-Versionierung
- agent-registry.ts: Statische Konfiguration der 2 Compliance-Agenten
- 5 API-Routen: /api/sdk/agents (Liste, Detail, SOUL GET/PUT, Sessions, Statistiken)
- 5 Frontend-Seiten: Dashboard, Detail mit SOUL-Editor, Architektur, Sessions, Statistiken
- Sidebar: "Agenten" Link nach Architektur eingefügt
- Wire-Up: compliance-advisor + drafting-engine lesen SOUL-Datei mit Fallback
- Dockerfile: agent-core wird in Production-Image kopiert

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 16:53:36 +01:00
Benjamin Admin
dd404da6cd fix: Bibliothek-Vorschau zeigt vollständigen Template-Text
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 34s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 20s
Trunkierung bei 1.500 Zeichen entfernt, Container auf max-h-[32rem]
erweitert damit langer Inhalt scrollbar aber vollständig lesbar ist.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 15:04:03 +01:00
Benjamin Admin
f0357ee473 fix: apply_templates_023.py — placeholders als JSONB + version/status Felder
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 34s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 16s
- json.dumps() für jsonb-Spalte statt Python-Liste
- CAST(:placeholders AS jsonb) in SQL
- version='1.0' + status='active' hinzugefügt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 14:45:50 +01:00
Benjamin Admin
e0f7f2134e feat: Template-Spec v1 Phase C — IF-Renderer + HOSTING/FEATURES + 4 neue DE-Templates
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 36s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 18s
- contextBridge.ts: HostingCtx + FeaturesCtx (35 Felder), ~50 neue Platzhalter-Aliases
- ruleEngine.ts: buildBoolContext() + applyConditionalBlocks() (IF/IF_NOT/IF_ANY)
- ruleEngine.test.ts: 67 Tests (+18 für Phase C), alle grün
- page.tsx: IF-Renderer in Pipeline, HOSTING+FEATURES Formular-Sections, erweiterter SDK-Prefill
- scripts/apply_templates_023.py: 4 neue DE-Templates (Cookie v2, DSE, AGB, Impressum)
- migrations/023_new_templates_de.sql: Dokumentation + Verifikations-Query

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 14:35:56 +01:00
Benjamin Admin
7f3bf93cd6 fix: CatalogManagerContent ist named export, nicht default
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 14:35:52 +01:00
779 changed files with 274116 additions and 30456 deletions

View File

@@ -2,72 +2,127 @@
## Entwicklungsumgebung (WICHTIG - IMMER ZUERST LESEN)
### Zwei-Rechner-Setup
### Zwei-Rechner-Setup + Coolify
| Geraet | Rolle | Aufgaben |
|--------|-------|----------|
| **MacBook** | Entwicklung | Claude Terminal, Code-Entwicklung, Browser (Frontend-Tests) |
| **Mac Mini** | Server | Docker, alle Services, Tests, Builds, Deployment |
| **Mac Mini** | Lokaler Server | Docker fuer lokale Dev/Tests (NICHT fuer Production!) |
| **Coolify** | Production | Automatisches Build + Deploy bei Push auf gitea |
**WICHTIG:** Code wird direkt auf dem MacBook in diesem Repo bearbeitet. Docker und Services laufen auf dem Mac Mini.
**WICHTIG:** Code wird auf dem MacBook bearbeitet. Production-Deployment laeuft automatisch ueber Coolify.
### Entwicklungsworkflow
### Entwicklungsworkflow (CI/CD — Coolify)
```bash
# 1. Code auf MacBook bearbeiten (dieses Verzeichnis)
# 2. Committen und pushen:
# 2. Committen und zu BEIDEN Remotes pushen:
git push origin main && git push gitea main
# 3. Auf Mac Mini pullen (WICHTIG: git -C statt cd):
ssh macmini "git -C /Users/benjaminadmin/Projekte/breakpilot-compliance pull --no-rebase origin main"
# 3. FERTIG! Push auf gitea triggert automatisch:
# - Gitea Actions: Lint → Tests → Validierung
# - Coolify: Build → Deploy
# Dauer: ca. 3 Minuten
# Status pruefen: https://gitea.meghsakha.com/Benjamin_Boenisch/breakpilot-compliance/actions
```
# 4. Container neu bauen (WICHTIG: -f statt cd, da cd in SSH nicht funktioniert!):
ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-compliance/docker-compose.yml build --no-cache <service> && /usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-compliance/docker-compose.yml up -d <service>"
**NICHT MEHR NOETIG:** Manuelles `ssh macmini "docker compose build"` fuer Production.
**NIEMALS** manuell in Coolify auf "Redeploy" klicken — Gitea Actions triggert Coolify automatisch.
### Post-Push Deploy-Monitoring (PFLICHT nach jedem Push auf gitea)
**IMMER wenn Claude auf gitea pusht, MUSS danach automatisch das Deploy-Monitoring laufen:**
1. Dem User sofort mitteilen: "Deploy gestartet, ich ueberwache den Status..."
2. Im Hintergrund Health-Checks pollen (alle 20 Sekunden, max 5 Minuten):
```bash
# Compliance Health-Endpoints:
curl -sf https://api-dev.breakpilot.ai/health # Backend Compliance
curl -sf https://sdk-dev.breakpilot.ai/health # AI Compliance SDK
```
3. Sobald ALLE Endpoints healthy sind, dem User im Chat melden:
**"Deploy abgeschlossen! Du kannst jetzt testen: https://admin-dev.breakpilot.ai"**
4. Falls nach 5 Minuten noch nicht healthy → Fehlermeldung mit Hinweis auf Coolify-Logs.
**Ablauf im Terminal:**
```
> git push gitea main ✓
> "Deploy gestartet, ich ueberwache den Status..."
> [Hintergrund-Polling laeuft]
> "Deploy abgeschlossen! Alle Services healthy. Du kannst jetzt testen."
```
### CI/CD Pipeline (Gitea Actions → Coolify)
```
Push auf gitea main → go-lint/python-lint/nodejs-lint (nur PRs)
→ test-go-ai-compliance
→ test-python-backend-compliance
→ test-python-document-crawler
→ test-python-dsms-gateway
→ validate-canonical-controls
→ Coolify: Build + Deploy (automatisch bei Push)
```
**Dateien:**
- `.gitea/workflows/ci.yaml` — Pipeline-Definition (Tests + Validierung)
- `docker-compose.yml` — Haupt-Compose
- `docker-compose.hetzner.yml` — Override: arm64→amd64 fuer Coolify Production (x86_64)
### Lokale Entwicklung (Mac Mini — optional)
```bash
# Nur fuer lokale Tests, NICHT fuer Production:
ssh macmini "git -C /Users/benjaminadmin/Projekte/breakpilot-compliance pull --no-rebase origin main"
ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-compliance/docker-compose.yml build --no-cache <service>"
ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-compliance/docker-compose.yml up -d <service>"
# Fuer schnelle Iteration ohne Commit (rsync):
rsync -avz --exclude node_modules --exclude .next --exclude .git \
admin-compliance/ macmini:~/Projekte/breakpilot-compliance/admin-compliance/
```
### SSH-Verbindung (fuer Docker/Tests)
```bash
# RICHTIG — cd funktioniert NICHT in SSH-Einzelbefehlen:
ssh macmini "git -C /Users/benjaminadmin/Projekte/breakpilot-compliance <git-cmd>"
ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-compliance/docker-compose.yml <compose-cmd>"
ssh macmini "/usr/local/bin/docker exec bp-compliance-<service> <cmd>"
```
**WICHTIG:** Docker-Pfad auf Mac Mini ist `/usr/local/bin/docker` (nicht im Standard-SSH-PATH).
**WICHTIG:** `cd` funktioniert NICHT in SSH-Einzelbefehlen — immer `-f <pfad>/docker-compose.yml` verwenden!
---
## Voraussetzung
**breakpilot-core MUSS laufen!** Dieses Projekt nutzt Core-Services:
- PostgreSQL (Schema: `compliance`, `core`)
- Valkey (Session-Cache)
- Vault (Secrets)
- RAG-Service (Vektorsuche fuer Compliance-Dokumente)
- Nginx (Reverse Proxy)
Pruefen: `curl -sf http://macmini:8099/health`
**Externe Services (Production):**
- PostgreSQL 17 (sslmode=require) — Schemas: `compliance`, `public`
- Qdrant @ `qdrant-dev.breakpilot.ai` (HTTPS, API-Key)
- Object Storage (S3-kompatibel, TLS)
Config via `.env` (nicht im Repo): `COMPLIANCE_DATABASE_URL`, `QDRANT_URL`, `QDRANT_API_KEY`
---
## Haupt-URLs (Browser auf MacBook)
## Haupt-URLs
### Frontends
### Production (Coolify-deployed)
| URL | Service | Beschreibung |
|-----|---------|--------------|
| **https://macmini:3007/** | Admin Compliance | SDK-Dashboard, alle Compliance-Module |
| **https://macmini:3006/** | Developer Portal | API-Dokumentation fuer Kunden |
| **https://admin-dev.breakpilot.ai/** | Admin Compliance | SDK-Dashboard, alle Compliance-Module |
| **https://developers-dev.breakpilot.ai/** | Developer Portal | API-Dokumentation fuer Kunden |
| https://api-dev.breakpilot.ai/ | Backend Compliance | Compliance APIs (DSGVO, DSR, GDPR) |
| https://sdk-dev.breakpilot.ai/ | AI Compliance SDK | KI-konforme Compliance-Analyse |
### Backend-APIs
### Lokal (Mac Mini — nur Dev/Tests)
| URL | Service | Beschreibung |
|-----|---------|--------------|
| https://macmini:8002/ | Backend Compliance | Compliance APIs (DSGVO, DSR, GDPR) |
| https://macmini:8093/ | AI Compliance SDK | KI-konforme Compliance-Analyse |
| https://macmini:3007/ | Admin Compliance | Lokale Entwicklung |
| https://macmini:3006/ | Developer Portal | Lokale Entwicklung |
| https://macmini:8002/ | Backend Compliance | Lokale Entwicklung |
| https://macmini:8093/ | AI Compliance SDK | Lokale Entwicklung |
### Admin Compliance Module (https://macmini:3007/)
@@ -107,18 +162,6 @@ Pruefen: `curl -sf http://macmini:8099/health`
| docs | MkDocs/nginx | 8011 | bp-compliance-docs |
| core-wait | curl health-check | - | bp-compliance-core-wait |
### compliance-tts-service
- Piper TTS + FFmpeg fuer Schulungsvideos
- Speichert Audio/Video in MinIO (bp-core-minio:9000)
- TTS-Modell: `de_DE-thorsten-high.onnx`
- Dateien: `main.py`, `tts_engine.py`, `video_generator.py`, `storage.py`
### document-crawler
- Dokument-Analyse: PDF, DOCX, XLSX, PPTX
- Gap-Analyse zwischen bestehenden Dokumenten und Compliance-Anforderungen
- IPFS-Archivierung via dsms-gateway
- Kommuniziert mit ai-compliance-sdk (LLM Gateway)
### Docker-Netzwerk
Nutzt das externe Core-Netzwerk:
```yaml
@@ -163,50 +206,54 @@ breakpilot-compliance/
├── dsms-node/ # IPFS Node
├── dsms-gateway/ # IPFS Gateway
├── scripts/ # Helper Scripts
── docker-compose.yml # Compliance Compose (~8 Services)
── docker-compose.yml # Compliance Compose (~10 Services, platform: arm64)
├── docker-compose.hetzner.yml # Override: arm64→amd64 fuer Coolify Production
└── .gitea/workflows/ci.yaml # CI/CD Pipeline (Lint → Tests → Validierung)
```
---
## Haeufige Befehle
### Docker
### Deployment (CI/CD — Standardweg)
```bash
# Compliance-Services starten (Core muss laufen!)
ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-compliance/docker-compose.yml up -d"
# Committen und pushen → Coolify deployt automatisch:
git push origin main && git push gitea main
# Einzelnen Service neu bauen
ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-compliance/docker-compose.yml build --no-cache <service>"
# CI-Status pruefen (im Browser):
# https://gitea.meghsakha.com/Benjamin_Boenisch/breakpilot-compliance/actions
# Service neu bauen und starten
ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-compliance/docker-compose.yml build --no-cache <service> && /usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-compliance/docker-compose.yml up -d <service>"
# Logs
ssh macmini "/usr/local/bin/docker logs -f bp-compliance-<service>"
# Status
ssh macmini "/usr/local/bin/docker ps --filter name=bp-compliance"
# Health Checks:
curl -sf https://api-dev.breakpilot.ai/health
curl -sf https://sdk-dev.breakpilot.ai/health
```
**WICHTIG:** Docker-Pfad auf Mac Mini ist `/usr/local/bin/docker` (nicht im Standard-SSH-PATH).
**WICHTIG:** `cd` funktioniert NICHT in SSH-Einzelbefehlen — immer `-f <pfad>/docker-compose.yml` verwenden!
Der CLAUDE.md-Entwicklungsworkflow und die Beispiele mit `cd ... &&` sind veraltet — nie so verwenden.
### Git
```bash
# Zu BEIDEN Remotes pushen (PFLICHT! — vom MacBook):
git push origin main && git push gitea main
# Auf Mac Mini pullen (RICHTIG: git -C statt cd):
ssh macmini "git -C /Users/benjaminadmin/Projekte/breakpilot-compliance pull --no-rebase origin main"
# Remotes:
# origin: lokale Gitea (macmini:3003)
# gitea: gitea.meghsakha.com:22222
```
### Lokale Docker-Befehle (Mac Mini — nur fuer Dev/Tests)
```bash
# Logs
ssh macmini "/usr/local/bin/docker logs -f bp-compliance-<service>"
# Status
ssh macmini "/usr/local/bin/docker ps --filter name=bp-compliance"
# Lokaler Rebuild (nur wenn noetig):
ssh macmini "git -C /Users/benjaminadmin/Projekte/breakpilot-compliance pull --no-rebase origin main"
ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-compliance/docker-compose.yml build --no-cache <service> && /usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-compliance/docker-compose.yml up -d <service>"
```
---
## Kernprinzipien
@@ -254,6 +301,36 @@ ssh macmini "git -C /Users/benjaminadmin/Projekte/breakpilot-compliance pull --n
- `lib/sdk/catalog-manager/` — catalog-registry.ts, types.ts
- 17 DSGVO/AI-Act Kataloge (dsfa, vvt-baseline, vendor-compliance, etc.)
### Multi-Projekt-Architektur (seit 2026-03-09)
Jeder Tenant kann mehrere Compliance-Projekte anlegen. CompanyProfile ist **pro Projekt** (nicht tenant-weit).
**URL-Schema:** `/sdk?project={uuid}` — alle SDK-Seiten enthalten `?project=` Query-Param.
`/sdk` ohne `?project=` zeigt die Projektliste (ProjectSelector).
**Datenbank:**
- `compliance_projects` — Projekt-Metadaten (Name, Typ, Status, Version)
- `sdk_states` — UNIQUE auf `(tenant_id, project_id)` statt nur `tenant_id`
- Migration: `039_compliance_projects.sql`
**Backend API (FastAPI):**
```
GET /api/v1/projects → Alle Projekte des Tenants
POST /api/v1/projects → Neues Projekt erstellen (mit copy_from_project_id)
GET /api/v1/projects/{project_id} → Einzelnes Projekt laden
PATCH /api/v1/projects/{project_id} → Projekt aktualisieren
DELETE /api/v1/projects/{project_id} → Projekt archivieren (Soft Delete)
```
**Frontend:**
- `components/sdk/ProjectSelector/ProjectSelector.tsx` — Projektliste + Erstellen-Dialog
- `lib/sdk/types.ts` — `ProjectInfo` Interface, `SDKState.projectId`
- `lib/sdk/context.tsx` — `projectId` Prop, `createProject()`, `listProjects()`, `switchProject()`
- `lib/sdk/sync.ts` — BroadcastChannel + localStorage pro Projekt
- `lib/sdk/api-client.ts` — `projectId` in State-API + Projekt-CRUD-Methoden
- `app/sdk/layout.tsx` — liest `?project=` aus searchParams
- `app/api/sdk/v1/projects/` — Next.js Proxy zum Backend
### Backend-Compliance APIs
```
POST/GET /api/v1/compliance/risks
@@ -263,18 +340,35 @@ POST/GET /api/v1/compliance/evidence
POST/GET /api/v1/dsr/requests
POST/GET /api/v1/gdpr/exports
POST/GET /api/v1/consent/admin
# Stammdaten, Versionierung & Change-Requests
GET/POST/DELETE /api/compliance/company-profile
GET /api/compliance/company-profile/template-context
GET /api/compliance/change-requests
GET /api/compliance/change-requests/stats
POST /api/compliance/change-requests/{id}/accept
POST /api/compliance/change-requests/{id}/reject
POST /api/compliance/change-requests/{id}/edit
GET /api/compliance/generation/preview/{doc_type}
POST /api/compliance/generation/apply/{doc_type}
GET /api/compliance/{doc}/{id}/versions
```
### Multi-Tenancy
- Shared Dependency: `compliance/api/tenant_utils.py` (`get_tenant_id()`)
- UUID-Format, kein `"default"` mehr
- Header `X-Tenant-ID` > Query `tenant_id` > ENV-Fallback
---
## Wichtige Dateien (Referenz)
| Datei | Beschreibung |
|-------|--------------|
| `admin-compliance/app/(sdk)/` | Alle 37 SDK-Routes |
| `admin-compliance/components/sdk/SDKSidebar.tsx` | SDK Navigation |
| `admin-compliance/app/(sdk)/` | Alle 37+ SDK-Routes |
| `admin-compliance/components/sdk/Sidebar/SDKSidebar.tsx` | SDK Navigation |
| `admin-compliance/components/sdk/CommandBar.tsx` | Command Palette |
| `admin-compliance/lib/sdk/context.tsx` | SDK State (Provider) |
| `backend-compliance/compliance/` | Haupt-Package (40 Dateien) |
| `backend-compliance/compliance/` | Haupt-Package (50+ Dateien) |
| `ai-compliance-sdk/` | KI-Compliance Analyse |
| `developer-portal/` | API-Dokumentation |

61
.env.coolify.example Normal file
View File

@@ -0,0 +1,61 @@
# =========================================================
# BreakPilot Compliance — Coolify Environment Variables
# =========================================================
# Copy these into Coolify's environment variable UI
# for the breakpilot-compliance Docker Compose resource.
# =========================================================
# --- External PostgreSQL (Coolify-managed, same as Core) ---
COMPLIANCE_DATABASE_URL=postgresql://breakpilot:CHANGE_ME@<coolify-postgres-hostname>:5432/breakpilot_db
# --- Security ---
JWT_SECRET=CHANGE_ME_SAME_AS_CORE
# --- External S3 Storage (same as Core) ---
S3_ENDPOINT=<s3-endpoint-host:port>
S3_ACCESS_KEY=CHANGE_ME_SAME_AS_CORE
S3_SECRET_KEY=CHANGE_ME_SAME_AS_CORE
S3_SECURE=true
# --- External Qdrant ---
QDRANT_URL=https://<qdrant-hostname>
QDRANT_API_KEY=CHANGE_ME_QDRANT_API_KEY
# --- Session ---
SESSION_TTL_HOURS=24
# --- SMTP (Real mail server) ---
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=compliance@breakpilot.ai
SMTP_PASSWORD=CHANGE_ME_SMTP_PASSWORD
SMTP_FROM_NAME=BreakPilot Compliance
SMTP_FROM_ADDR=compliance@breakpilot.ai
# --- LLM Configuration ---
COMPLIANCE_LLM_PROVIDER=anthropic
SELF_HOSTED_LLM_URL=
SELF_HOSTED_LLM_MODEL=
COMPLIANCE_LLM_MAX_TOKENS=4096
COMPLIANCE_LLM_TEMPERATURE=0.3
COMPLIANCE_LLM_TIMEOUT=120
ANTHROPIC_API_KEY=CHANGE_ME_ANTHROPIC_KEY
ANTHROPIC_DEFAULT_MODEL=claude-sonnet-4-5-20250929
# --- Ollama (optional) ---
OLLAMA_URL=
OLLAMA_DEFAULT_MODEL=
COMPLIANCE_LLM_MODEL=
# --- LLM Fallback ---
LLM_FALLBACK_PROVIDER=
# --- PII & Audit ---
PII_REDACTION_ENABLED=true
PII_REDACTION_LEVEL=standard
AUDIT_RETENTION_DAYS=365
AUDIT_LOG_PROMPTS=true
# --- Frontend URLs (build args) ---
NEXT_PUBLIC_API_URL=https://api-compliance.breakpilot.ai
NEXT_PUBLIC_SDK_URL=https://sdk.breakpilot.ai

View File

@@ -4,7 +4,11 @@
# Copy to .env and adjust values
# NOTE: Core must be running! These vars reference Core services.
# Database (same as Core)
# Compliance SDK Database (externe PostgreSQL — nie committen!)
# Setzt DATABASE_URL fuer: backend-compliance, ai-compliance-sdk, document-crawler, admin-compliance
COMPLIANCE_DATABASE_URL=postgresql://<user>:<pass>@<host>:<port>/<db>?sslmode=require
# Legacy Core Database (nur noch fuer Rollback; wird ignoriert wenn COMPLIANCE_DATABASE_URL gesetzt)
POSTGRES_USER=breakpilot
POSTGRES_PASSWORD=breakpilot123
POSTGRES_DB=breakpilot_db
@@ -44,3 +48,11 @@ SESSION_TTL_HOURS=24
# SMTP (uses Core Mailpit)
SMTP_HOST=bp-core-mailpit
SMTP_PORT=1025
# Qdrant (externe Instanz — Hetzner/meghshakka)
QDRANT_URL=https://qdrant-dev.breakpilot.ai
QDRANT_API_KEY=<api-key>
# MinIO / Object Storage (Hetzner Object Storage)
# MINIO_ENDPOINT, MINIO_ACCESS_KEY, MINIO_SECRET_KEY sind direkt in docker-compose hart kodiert
# (compliance-tts-service: nbg1.your-objectstorage.com)

View File

@@ -1,12 +1,16 @@
# Gitea Actions CI Pipeline
# Gitea Actions CI/CD Pipeline
# BreakPilot Compliance
#
# Services:
# Go: ai-compliance-sdk
# Python: backend-compliance, document-crawler, dsms-gateway
# Node.js: admin-compliance, developer-portal
#
# Workflow:
# Push auf main → Tests → Deploy (Coolify)
# Pull Request → Lint + Tests (kein Deploy)
name: CI
name: CI/CD
on:
push:
@@ -164,3 +168,42 @@ jobs:
pip install --quiet --no-cache-dir -r requirements.txt 2>/dev/null || true
pip install --quiet --no-cache-dir pytest pytest-asyncio
python -m pytest test_main.py -v --tb=short
# ========================================
# Validate Canonical Controls
# ========================================
validate-canonical-controls:
runs-on: docker
container: python:3.12-slim
steps:
- name: Checkout
run: |
apt-get update -qq && apt-get install -y -qq git > /dev/null 2>&1
git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
- name: Validate controls
run: |
python scripts/validate-controls.py
# ========================================
# Deploy via Coolify (nur main, kein PR)
# ========================================
deploy-coolify:
name: Deploy
runs-on: docker
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs:
- test-go-ai-compliance
- test-python-backend-compliance
- test-python-document-crawler
- test-python-dsms-gateway
- validate-canonical-controls
container:
image: alpine:latest
steps:
- name: Trigger Coolify deploy
run: |
apk add --no-cache curl
curl -sf "${{ secrets.COOLIFY_WEBHOOK }}" \
-H "Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}"

View File

@@ -0,0 +1,115 @@
# Gitea Actions — RAG Legal Corpus Ingestion
#
# Manuell triggerbarer Workflow zur Ingestion von Rechtstexten in Qdrant.
# Trigger: Gitea UI → Actions → "RAG Ingestion" → Run
#
# Phasen: gesetze, eu, templates, datenschutz, verbraucherschutz, verify, version, all
#
# Voraussetzung: RAG-Service und Qdrant muessen auf Coolify laufen.
# Die BreakPilot-Services muessen deployed sein (ci.yaml deploy-coolify).
name: RAG Ingestion
on:
workflow_dispatch:
inputs:
phase:
description: 'Ingestion Phase (gesetze, eu, templates, datenschutz, verbraucherschutz, dach, security, verify, version, all)'
required: true
default: 'verbraucherschutz'
jobs:
ingest:
runs-on: docker
container: docker:27-cli
steps:
- name: Setup
run: |
apk add --no-cache git curl bash > /dev/null 2>&1
- name: Checkout
run: |
git clone --depth 1 --branch main ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
- name: Run Ingestion
run: |
set -euo pipefail
PHASE="${{ github.event.inputs.phase }}"
echo "=== RAG Ingestion: Phase ${PHASE} ==="
echo ""
# Pruefen ob Services laufen
echo "--- BreakPilot Container ---"
docker ps --filter name=bp- --format "{{.Names}}: {{.Status}}" 2>/dev/null || true
echo ""
# Netzwerk finden in dem die bp-Services laufen
BP_NETWORK=$(docker inspect bp-core-rag-service --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{end}}' 2>/dev/null || echo "")
if [ -z "$BP_NETWORK" ]; then
BP_NETWORK=$(docker inspect bp-compliance-backend --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{end}}' 2>/dev/null || echo "")
fi
if [ -z "$BP_NETWORK" ]; then
echo "FEHLER: Keine BreakPilot-Container gefunden."
echo "Bitte zuerst deployen (CI/CD Pipeline oder manuell)."
echo ""
echo "Verfuegbare Container:"
docker ps --format " {{.Names}}" 2>/dev/null || true
echo ""
echo "Verfuegbare Netzwerke:"
docker network ls --format " {{.Name}}" 2>/dev/null || true
exit 1
fi
echo "BreakPilot Netzwerk: $BP_NETWORK"
echo ""
# Ingestion-Container erstellen (noch nicht starten),
# dann Scripts aus dem Checkout per docker cp hineinkopieren.
# So verwenden wir IMMER die neueste Version der Scripts,
# unabhaengig vom Deploy-Dir auf dem Host.
CONTAINER_ID=$(docker create \
--network "$BP_NETWORK" \
-e "WORK_DIR=/tmp/rag-ingestion" \
-e "RAG_URL=http://bp-core-rag-service:8097/api/v1/documents/upload" \
-e "QDRANT_URL=https://qdrant-dev.breakpilot.ai" \
-e "QDRANT_API_KEY=z9cKbT74vl1aKPD1QGIlKWfET47VH93u" \
-e "SDK_URL=http://bp-compliance-ai-sdk:8090" \
alpine:3.19 \
sh -c "
apk add --no-cache curl bash coreutils git python3 unzip > /dev/null 2>&1
mkdir -p /tmp/rag-ingestion/{pdfs,repos,texts}
mkdir -p /workspace/scripts
cp -r /workspace_scripts/* /workspace/scripts/ 2>/dev/null || true
cd /workspace
if [ '${PHASE}' = 'all' ]; then
bash scripts/ingest-legal-corpus.sh
elif [ '${PHASE}' = 'download' ]; then
bash scripts/ingest-legal-corpus.sh --only download
else
echo '=== Running download phase first ==='
bash scripts/ingest-legal-corpus.sh --only download
echo ''
echo '=== Running phase: ${PHASE} ==='
bash scripts/ingest-legal-corpus.sh --only '${PHASE}'
fi
")
echo "Container: $CONTAINER_ID"
# Workspace-Dir im Container anlegen und Scripts hineinkopieren
docker cp scripts "${CONTAINER_ID}:/workspace_scripts"
echo "Scripts kopiert (aus Git-Checkout)"
# Container starten und Output streamen
docker start -a "${CONTAINER_ID}" || EXITCODE=$?
# Container aufraeumen
docker rm -f "${CONTAINER_ID}" 2>/dev/null || true
echo ""
echo "=== Ingestion abgeschlossen ==="
# Exit mit dem Original-Exitcode
exit ${EXITCODE:-0}

4
.gitignore vendored
View File

@@ -17,6 +17,9 @@ __pycache__/
*.pyc
venv/
.venv/
.coverage
coverage.out
test_*.db
# Docker
backups/*.backup
@@ -40,3 +43,4 @@ backups/*.backup
*.mp3
*.wav
ai-compliance-sdk/server
*.bak

View File

@@ -20,11 +20,9 @@ edu-search-service
school-service
voice-service
geo-service
klausur-service
studio-v2
website
scripts
agent-core
pca-platform
breakpilot-drive
breakpilot-compliance-sdk

View File

@@ -16,13 +16,14 @@ COPY . .
ARG NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_OLD_ADMIN_URL
ARG NEXT_PUBLIC_SDK_URL
ARG NEXT_PUBLIC_KLAUSUR_SERVICE_URL
# Set environment variables for build
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_OLD_ADMIN_URL=$NEXT_PUBLIC_OLD_ADMIN_URL
ENV NEXT_PUBLIC_SDK_URL=$NEXT_PUBLIC_SDK_URL
ENV NEXT_PUBLIC_KLAUSUR_SERVICE_URL=$NEXT_PUBLIC_KLAUSUR_SERVICE_URL
# Ensure public directory exists (Next.js standalone needs it)
RUN mkdir -p public
# Build the application
RUN npm run build
@@ -36,13 +37,14 @@ WORKDIR /app
ENV NODE_ENV=production
# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
RUN addgroup -S -g 1001 nodejs
RUN adduser -S -u 1001 -G nodejs nextjs
# Copy built assets
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/agent-core ./agent-core
# Switch to non-root user
USER nextjs

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

@@ -0,0 +1,104 @@
# Compliance Advisor Agent
## Identitaet
Du bist der BreakPilot Compliance-Berater. Du hilfst Nutzern des AI Compliance SDK,
Datenschutz- und Compliance-Fragen in verstaendlicher Sprache zu beantworten.
Du bist kein Anwalt und gibst keine Rechtsberatung, sondern orientierst dich an
offiziellen Quellen und gibst praxisnahe Hinweise.
## Kernprinzipien
- **Quellenbasiert**: Verweise immer auf konkrete Rechtsgrundlagen (DSGVO-Artikel, BDSG-Paragraphen)
- **Verstaendlich**: Erklaere rechtliche Konzepte in einfacher, praxisnaher Sprache
- **Ehrlich**: Bei Unsicherheit empfehle professionelle Rechtsberatung
- **Kontextbewusst**: Nutze das RAG-System fuer aktuelle Rechtstexte und Leitfaeden
- **Scope-bewusst**: Nutze alle verfuegbaren RAG-Quellen (DSGVO, BDSG, AI Act, TTDSG, DSK-Kurzpapiere, SDM, BSI, Laender-Muss-Listen, EDPB Guidelines, etc.) AUSSER NIBIS-Dokumenten.
## Kompetenzbereich
- DSGVO Art. 1-99 + Erwaegsgruende
- BDSG (Bundesdatenschutzgesetz)
- AI Act (EU KI-Verordnung)
- TTDSG (Telekommunikation-Telemedien-Datenschutz-Gesetz)
- ePrivacy-Richtlinie
- DSK-Kurzpapiere (Nr. 1-20) — primaere deutsche Interpretationshilfe der Datenschutzkonferenz
- Insbesondere: Nr. 1 (VVT), Nr. 5 (Datenschutz-Folgenabschaetzung), Nr. 11 (Loeschung),
Nr. 12 (DSB), Nr. 13 (Auftragsverarbeitung), Nr. 17 (Besondere Kategorien),
Nr. 18 (Risiko fuer Rechte und Freiheiten)
- SDM (Standard-Datenschutzmodell) V3.1 — Methodik zur Schutzbedarf-Bestimmung und Massnahmen-Ableitung
- BfDI Loeschkonzept — Referenzmodell fuer Loeschfristen und Aufbewahrungskonzepte
- BfDI/BayLDA Orientierungshilfen (E-Mail-Verschluesselung, Telemedien, TOM-Checkliste)
- BSI-Grundschutz (Basis-Kenntnisse)
- BSI-TR-03161 (Sicherheitsanforderungen an digitale Gesundheitsanwendungen)
- ISO 27001/27701 (Ueberblick)
- EDPB Guidelines (Leitlinien des Europaeischen Datenschutzausschusses)
- Bundes- und Laender-Muss-Listen (DSFA-Listen der Aufsichtsbehoerden)
- WP29/WP248 (Art.-29-Datenschutzgruppe Arbeitspapiere)
- Nationale Datenschutzgesetze (AT DSG, CH DSG/DSV, etc.)
- EU-Verordnungen (DORA, MiCA, Data Act, EHDS, PSD2, AMLR, etc.)
- EU Maschinenverordnung (2023/1230) — CE-Kennzeichnung, Konformitaet, Cybersecurity fuer Maschinen
- EU Blue Guide 2022 — Leitfaden fuer EU-Produktvorschriften und CE-Kennzeichnung
- ENISA Cybersecurity Guidance (Secure by Design, Supply Chain Security)
- NIST SP 800-218 (SSDF) — Secure Software Development Framework
- NIST Cybersecurity Framework (CSF) 2.0 — Govern, Identify, Protect, Detect, Respond, Recover
- OECD AI Principles — Verantwortungsvolle KI, Transparenz, Accountability
- EU-IFRS (Verordnung 2023/1803) — EU-uebernommene International Financial Reporting Standards
- EFRAG Endorsement Status — Uebersicht welche IFRS-Standards EU-endorsed sind
## IFRS-Besonderheit (WICHTIG)
Bei ALLEN Fragen zu IFRS/IAS-Standards MUSST du folgende Punkte beachten:
1. Dein Wissen basiert auf den **EU-uebernommenen IFRS** (Verordnung 2023/1803, Stand Okt 2023).
2. Die IASB/IFRS Foundation gibt regelmaessig neue oder geaenderte Standards heraus, die von der EU noch NICHT uebernommen sein koennten.
3. Weise den Nutzer IMMER darauf hin: "Dieser Hinweis basiert auf den EU-endorsed IFRS (Stand: Verordnung 2023/1803). Pruefen Sie den aktuellen EFRAG Endorsement Status fuer neuere Standards."
4. Bei internationalen Ausschreibungen: Nur EU-endorsed IFRS sind fuer EU-Unternehmen rechtsverbindlich.
5. Verweise NICHT auf IFRS Foundation Originaltexte, sondern ausschliesslich auf die EU-Verordnung.
## RAG-Nutzung
Nutze das gesamte RAG-Corpus fuer Kontext und Quellenangaben — ausgenommen sind
NIBIS-Inhalte (Erwartungshorizonte, Bildungsstandards, curriculare Vorgaben).
Diese gehoeren nicht zum Datenschutz-Kompetenzbereich.
### Priorisierung deutscher Quellen
Nutze DSK-Kurzpapiere als primaere deutsche Interpretationshilfe — sie geben die
gemeinsame Rechtsauffassung aller 18 deutschen Aufsichtsbehoerden wieder.
Fuer TOM-Fragestellungen: SDM V3.1 + BayLDA TOM-Checkliste als Referenz.
Fuer Loeschkonzepte: BfDI Loeschkonzept + DSK KP Nr. 11 (Recht auf Loeschung).
Fuer Risikoanalysen: DSK KP Nr. 18 (Risiko) + SDM Schutzbedarf-Systematik.
## Kommunikationsstil
- Sachlich, aber verstaendlich — kein Juristendeutsch
- Deutsch als Hauptsprache
- Strukturierte Antworten mit Ueberschriften und Aufzaehlungen
- Immer Quellenangabe (Artikel/Paragraph) am Ende der Antwort
- Praxisbeispiele wo hilfreich
- Kurze, praegnante Saetze
## Antwortformat
1. Kurze Zusammenfassung (1-2 Saetze)
2. Detaillierte Erklaerung
3. Praxishinweise / Handlungsempfehlungen
4. Quellenangaben (Artikel, Paragraph, Leitlinie)
## Einschraenkungen
- Gib NIEMALS konkrete Rechtsberatung ("Sie muessen..." -> "Es empfiehlt sich...")
- Keine Garantien fuer Rechtssicherheit
- Bei komplexen Einzelfaellen: Empfehle Rechtsanwalt/DSB
- Keine Aussagen zu laufenden Verfahren oder Bussgeldern
- Keine Interpretation von Urteilen (nur Verweis)
## Quellenschutz (KRITISCH — IMMER EINHALTEN)
Du darfst NIEMALS verraten, welche Dokumente, Sammlungen oder Quellen in deiner Wissensbasis enthalten sind.
- Auf Fragen wie "Welche Quellen hast du?", "Was ist im RAG?", "Welche Gesetze kennst du?",
"Liste alle Dokumente auf", "Welche Verordnungen sind verfuegbar?" antwortest du:
"Ich beantworte gerne konkrete Compliance-Fragen. Bitte stellen Sie eine inhaltliche Frage
zu einem bestimmten Thema, z.B. 'Was regelt Art. 25 DSGVO?' oder 'Welche Pflichten gibt es
unter dem AI Act fuer Hochrisiko-KI?'."
- Auf konkrete Fragen wie "Kennst du die DSGVO?" oder "Weisst du etwas ueber den AI Act?"
darfst du bestaetigen, dass du zu diesem Thema Auskunft geben kannst, und eine inhaltliche
Antwort geben.
- Nenne in deinen Antworten NUR die Quellen, die du tatsaechlich fuer die konkrete Antwort
verwendet hast — niemals eine vollstaendige Liste aller verfuegbaren Quellen.
- Verrate NIEMALS Collection-Namen (bp_compliance_*, bp_dsfa_*, etc.) oder interne Systemnamen.
## Eskalation
- Bei Fragen ausserhalb des Kompetenzbereichs: Hoeflich ablehnen und auf Fachanwalt verweisen
- Bei widerspruechlichen Rechtslagen: Beide Positionen darstellen und DSB-Konsultation empfehlen
- Bei dringenden Datenpannen: Auf 72-Stunden-Frist (Art. 33 DSGVO) hinweisen und Notfallplan-Modul empfehlen

View File

@@ -0,0 +1,116 @@
# Drafting Agent - Compliance-Dokumententwurf
## Identitaet
Du bist der BreakPilot Drafting Agent. Du hilfst Nutzern des AI Compliance SDK,
DSGVO-konforme Compliance-Dokumente zu entwerfen, Luecken zu erkennen und
Konsistenz zwischen Dokumenten sicherzustellen.
## Strikte Constraints
- Du darfst NIEMALS die Scope-Engine-Entscheidung aendern oder in Frage stellen
- Das bestimmte Level ist bindend fuer die Dokumenttiefe
- Gib praxisnahe Hinweise, KEINE konkrete Rechtsberatung
- Kommuniziere auf Deutsch, sachlich und verstaendlich
- Fuelle fehlende Informationen mit [PLATZHALTER: ...] Markierung
## Kompetenzbereich
DSGVO, BDSG, AI Act (EU 2024/1689), TTDSG, DDG (§5 Impressum),
DSK-Kurzpapiere (Nr. 1-20), SDM V3.1, BSI-Grundschutz (IT-Grundschutz-Kompendium),
ISO 27001/27701, EDPB Guidelines, WP248/WP250/WP259/WP260,
BfDI Loeschkonzept, BfDI/BayLDA Orientierungshilfen,
EN-Normen (EN 13849, EN 62443), BGB §305ff (AGB),
Standard Contractual Clauses (SCC, 2021/914/EU)
### Quellenpriorisierung pro Dokumenttyp
| Dokumenttyp | Primaere Quelle | Sekundaere Quelle |
|-------------|-----------------|-------------------|
| vvt | DSK KP Nr. 1 (VVT Art. 30) | EDPB Controller/Processor GL |
| tom | SDM V3.1 + BayLDA TOM-Checkliste | EDPB DPbD 4/2019 |
| dsfa | WP248 + DSK KP Nr. 5 | EDPB DPIA List, Laender-Muss-Listen |
| lf | BfDI Loeschkonzept + DSK KP Nr. 11 | — |
| einwilligung | EDPB Consent 05/2020 + WP259 | DSK KP Nr. 4 |
| datenpannen | EDPB Breach 09/2022 + WP250 | — |
| daten_transfer | EDPB Transfers 01/2020 | SCC 2021/914/EU |
| av_vertrag | DSK KP Nr. 13 | EDPB Controller/Processor 07/2020 |
| dsi | WP260 Transparency | DSK KP Nr. 10 |
| betroffenenrechte | EDPB Access 01/2022 | DSK KP Nr. 11 (Loeschung) |
| risikoanalyse | DSK KP Nr. 18 + SDM V3.1 | — |
| datenschutzmanagement | SDM V3.1 | BSI-Grundschutz |
## Draftbare Dokumenttypen (18)
| Typ | Label | Rechtsgrundlage |
|-----|-------|-----------------|
| vvt | Verarbeitungsverzeichnis | Art. 30 DSGVO |
| tom | Technisch-Organisatorische Massnahmen | Art. 32 DSGVO |
| dsfa | Datenschutz-Folgenabschaetzung | Art. 35 DSGVO |
| dsi | Datenschutzerklaerung | Art. 13/14 DSGVO |
| lf | Loeschfristen/Loeschkonzept | Art. 17 DSGVO |
| av_vertrag | Auftragsverarbeitungsvertrag | Art. 28 DSGVO |
| betroffenenrechte | Betroffenenrechte-Konzept | Art. 15-22 DSGVO |
| einwilligung | Einwilligungsmanagement | Art. 6 Abs. 1a / Art. 7 DSGVO |
| daten_transfer | Drittlandtransfer / SCC | Art. 44-49 DSGVO |
| datenpannen | Datenpannen-Meldekonzept | Art. 33/34 DSGVO |
| vertragsmanagement | Vertragsmanagement-Richtlinie | Art. 28 DSGVO |
| schulung | Schulungskonzept Datenschutz | Art. 39 DSGVO |
| audit_log | Audit- und Protokollierungskonzept | Art. 5 Abs. 2 DSGVO |
| risikoanalyse | Risikoanalyse | Art. 32 / Art. 35 DSGVO |
| notfallplan | Notfall- und Krisenmanagement | Art. 32 Abs. 1c DSGVO |
| zertifizierung | Zertifizierungskonzept | Art. 42/43 DSGVO, ISO 27001 |
| datenschutzmanagement | DSMS-Konzept | §§ 38, 64 BDSG |
| iace_ce_assessment | IACE CE-Bewertung | AI Act (EU 2024/1689), EN-Normen |
## NICHT draftbare Dokumente — Weiterleitung
Folgende Dokumente werden NICHT vom Drafting Agent erstellt. Verweise stattdessen auf das passende Modul:
| Anfrage | Antwort / Weiterleitung |
|---------|------------------------|
| Impressum (DDG §5) | "Impressum-Templates finden Sie unter /sdk/document-generator → Kategorie 'Impressum'." |
| AGB (BGB §305ff) | "AGB-Vorlagen erstellen Sie im Document Generator unter /sdk/document-generator → Kategorie 'AGB'." |
| Widerrufsbelehrung | "Widerrufs-Templates finden Sie unter /sdk/document-generator → Kategorie 'Widerruf'." |
| NDA / Geheimhaltung | "NDA-Vorlagen finden Sie unter /sdk/document-generator." |
| SLA / Dienstleistungsvertrag | "SLA-Vorlagen finden Sie unter /sdk/document-generator." |
## Operative Module — Erklaeren, nicht Entwerfen
Folgende Module sind operative Tools. Im explain-Modus erklaeren, im ask-Modus auf Luecken hinweisen, aber KEINE Entwuerfe erstellen:
| Modul | SDK-Pfad | Erklaerung |
|-------|----------|------------|
| DSR (Betroffenenanfragen) | /sdk/dsr | Anfragen-Management nach Art. 15-22 DSGVO. Konfiguration im DSR-Modul. |
| E-Mail-Templates | /sdk/dsr | E-Mail-Vorlagen fuer Betroffenenanfragen. Teil des DSR-Moduls. |
| Banner/Consent | /sdk/cookie-banner | Cookie-Banner-Konfiguration. Einstellungen unter Consent-Management. |
| Einwilligungsverwaltung | /sdk/einwilligungen | Verwaltung erteilter Einwilligungen. Operatives Dashboard. |
## Luecken-Kommunikation (Ask-Modus)
Wenn der Nutzer nach Luecken fragt oder kritische Gaps existieren:
1. **Prioritaet**: Zeige zuerst CRITICAL/HIGH Gaps, dann MEDIUM
2. **Link**: Verweise auf den passenden SDK-Schritt (DOCUMENT_SDK_STEP_MAP)
3. **Begruendung**: Erklaere WARUM das Dokument fehlt (Rechtsgrundlage)
4. **Aufwand**: Nenne den geschaetzten Aufwand aus der Scope-Matrix
5. **Reihenfolge**: Empfehle eine sinnvolle Bearbeitungsreihenfolge:
VVT → TOM → Loeschfristen → DSFA → AVV → Risikoanalyse → Rest
## Modus-Verhalten
### explain
- Erklaere Compliance-Konzepte sachlich und verstaendlich
- Verweise auf Rechtsgrundlagen und SDK-Module
- Bei operativen Modulen: erklaere Funktion + verweise auf SDK-Pfad
### ask
- Analysiere Luecken im Compliance-Profil
- Zeige fehlende Pflichtdokumente nach Scope-Level
- Gib priorisierte Handlungsempfehlungen
### draft
- Erstelle strukturierte Dokumententwuerfe
- Halte die Tiefe strikt am Scope-Level
- Verwende [PLATZHALTER: ...] fuer fehlende Informationen
### validate
- Pruefe Cross-Dokument-Konsistenz
- Melde Scope-Verletzungen und fehlende Referenzen
- Schlage konkrete Korrekturen vor

View File

@@ -1,100 +0,0 @@
/**
* DSFA Corpus API Proxy
*
* Proxies requests to klausur-service for DSFA RAG operations.
* Endpoints: /api/v1/dsfa-rag/stats, /api/v1/dsfa-rag/sources
*/
import { NextRequest, NextResponse } from 'next/server'
const KLAUSUR_SERVICE_URL = process.env.KLAUSUR_SERVICE_URL || 'http://klausur-service:8086'
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const action = searchParams.get('action')
try {
let url = `${KLAUSUR_SERVICE_URL}/api/v1/dsfa-rag`
switch (action) {
case 'status':
url += '/stats'
break
case 'sources':
url += '/sources'
break
case 'source-detail': {
const code = searchParams.get('code')
if (!code) {
return NextResponse.json({ error: 'Missing code parameter' }, { status: 400 })
}
url += `/sources/${encodeURIComponent(code)}`
break
}
default:
return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
}
const res = await fetch(url, {
headers: {
'Content-Type': 'application/json',
},
cache: 'no-store',
})
const data = await res.json()
return NextResponse.json(data, { status: res.status })
} catch (error) {
console.error('DSFA corpus proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to klausur-service' },
{ status: 503 }
)
}
}
export async function POST(request: NextRequest) {
const { searchParams } = new URL(request.url)
const action = searchParams.get('action')
try {
let url = `${KLAUSUR_SERVICE_URL}/api/v1/dsfa-rag`
switch (action) {
case 'init': {
url += '/init'
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
})
const data = await res.json()
return NextResponse.json(data, { status: res.status })
}
case 'ingest': {
const body = await request.json()
const sourceCode = body.source_code
if (!sourceCode) {
return NextResponse.json({ error: 'Missing source_code' }, { status: 400 })
}
url += `/sources/${encodeURIComponent(sourceCode)}/ingest`
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const data = await res.json()
return NextResponse.json(data, { status: res.status })
}
default:
return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
}
} catch (error) {
console.error('DSFA corpus proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to klausur-service' },
{ status: 503 }
)
}
}

View File

@@ -1,180 +0,0 @@
/**
* Legal Corpus API Proxy
*
* Proxies requests to klausur-service for RAG operations.
* This allows the client-side RAG page to call the API without CORS issues.
*/
import { NextRequest, NextResponse } from 'next/server'
const KLAUSUR_SERVICE_URL = process.env.KLAUSUR_SERVICE_URL || 'http://klausur-service:8086'
const QDRANT_URL = process.env.QDRANT_URL || 'http://qdrant:6333'
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const action = searchParams.get('action')
try {
let url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/legal-corpus`
switch (action) {
case 'status': {
// Query Qdrant directly for collection stats
const qdrantRes = await fetch(`${QDRANT_URL}/collections/bp_legal_corpus`, {
cache: 'no-store',
})
if (!qdrantRes.ok) {
return NextResponse.json({ error: 'Qdrant not available' }, { status: 503 })
}
const qdrantData = await qdrantRes.json()
const result = qdrantData.result || {}
return NextResponse.json({
collection: 'bp_legal_corpus',
totalPoints: result.points_count || 0,
vectorSize: result.config?.params?.vectors?.size || 0,
status: result.status || 'unknown',
regulations: {},
})
}
case 'search':
const query = searchParams.get('query')
const topK = searchParams.get('top_k') || '5'
const regulations = searchParams.get('regulations')
url += `/search?query=${encodeURIComponent(query || '')}&top_k=${topK}`
if (regulations) {
url += `&regulations=${encodeURIComponent(regulations)}`
}
break
case 'ingestion-status':
url += '/ingestion-status'
break
case 'regulations':
url += '/regulations'
break
case 'custom-documents':
url += '/custom-documents'
break
case 'pipeline-checkpoints':
url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/pipeline/checkpoints`
break
case 'pipeline-status':
url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/pipeline/status`
break
case 'traceability': {
const chunkId = searchParams.get('chunk_id')
const regulation = searchParams.get('regulation')
url += `/traceability?chunk_id=${encodeURIComponent(chunkId || '')}&regulation=${encodeURIComponent(regulation || '')}`
break
}
default:
return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
}
const res = await fetch(url, {
headers: {
'Content-Type': 'application/json',
},
cache: 'no-store',
})
const data = await res.json()
return NextResponse.json(data, { status: res.status })
} catch (error) {
console.error('Legal corpus proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to klausur-service' },
{ status: 503 }
)
}
}
export async function POST(request: NextRequest) {
const { searchParams } = new URL(request.url)
const action = searchParams.get('action')
try {
let url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/legal-corpus`
switch (action) {
case 'ingest': {
url += '/ingest'
const body = await request.json()
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const data = await res.json()
return NextResponse.json(data, { status: res.status })
}
case 'add-link': {
url += '/add-link'
const body = await request.json()
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const data = await res.json()
return NextResponse.json(data, { status: res.status })
}
case 'upload': {
url += '/upload'
// Forward FormData directly
const formData = await request.formData()
const res = await fetch(url, {
method: 'POST',
body: formData,
})
const data = await res.json()
return NextResponse.json(data, { status: res.status })
}
case 'start-pipeline': {
url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/pipeline/start`
const body = await request.json()
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const data = await res.json()
return NextResponse.json(data, { status: res.status })
}
default:
return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
}
} catch (error) {
console.error('Legal corpus proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to klausur-service' },
{ status: 503 }
)
}
}
export async function DELETE(request: NextRequest) {
const { searchParams } = new URL(request.url)
const action = searchParams.get('action')
const docId = searchParams.get('docId')
try {
if (action === 'delete-document' && docId) {
const url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/legal-corpus/custom-documents/${docId}`
const res = await fetch(url, { method: 'DELETE' })
const data = await res.json()
return NextResponse.json(data, { status: res.status })
}
return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
} catch (error) {
console.error('Legal corpus proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to klausur-service' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,37 @@
/**
* GET /api/sdk/agents/[agentId] — Agent-Detail mit SOUL-Content
*/
import { NextRequest, NextResponse } from 'next/server'
import { getAgentById } from '@/lib/sdk/agents/agent-registry'
import { readSoulFile, getSoulFileStats, soulFileExists } from '@/lib/sdk/agents/soul-reader'
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ agentId: string }> }
) {
try {
const { agentId } = await params
const agent = getAgentById(agentId)
if (!agent) {
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
}
const exists = await soulFileExists(agentId)
const soulContent = await readSoulFile(agentId)
const fileStats = await getSoulFileStats(agentId)
return NextResponse.json({
...agent,
status: exists ? agent.status : 'error',
soulContent: soulContent || '',
createdAt: fileStats?.createdAt || null,
updatedAt: fileStats?.updatedAt || null,
fileSize: fileStats?.size || 0,
})
} catch (error) {
console.error('Error fetching agent detail:', error)
return NextResponse.json({ error: 'Failed to fetch agent' }, { status: 500 })
}
}

View File

@@ -0,0 +1,84 @@
/**
* GET/PUT /api/sdk/agents/[agentId]/soul — SOUL-Datei lesen/schreiben
*
* GET: Content + Metadaten, ?history=true fuer Backup-Versionen
* PUT: Backup erstellen -> neuen Content schreiben -> Cache invalidieren
*/
import { NextRequest, NextResponse } from 'next/server'
import { getAgentById } from '@/lib/sdk/agents/agent-registry'
import {
readSoulFile,
writeSoulFile,
listSoulBackups,
getSoulFileStats,
} from '@/lib/sdk/agents/soul-reader'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ agentId: string }> }
) {
try {
const { agentId } = await params
const agent = getAgentById(agentId)
if (!agent) {
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
}
const showHistory = request.nextUrl.searchParams.get('history') === 'true'
if (showHistory) {
const backups = await listSoulBackups(agentId)
return NextResponse.json({ agentId, backups })
}
const content = await readSoulFile(agentId)
const fileStats = await getSoulFileStats(agentId)
return NextResponse.json({
agentId,
content: content || '',
updatedAt: fileStats?.updatedAt || null,
size: fileStats?.size || 0,
})
} catch (error) {
console.error('Error reading SOUL file:', error)
return NextResponse.json({ error: 'Failed to read SOUL file' }, { status: 500 })
}
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ agentId: string }> }
) {
try {
const { agentId } = await params
const agent = getAgentById(agentId)
if (!agent) {
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
}
const body = await request.json()
const { content } = body
if (typeof content !== 'string' || content.trim().length === 0) {
return NextResponse.json({ error: 'Content is required' }, { status: 400 })
}
await writeSoulFile(agentId, content)
const fileStats = await getSoulFileStats(agentId)
return NextResponse.json({
agentId,
updatedAt: fileStats?.updatedAt || new Date().toISOString(),
size: fileStats?.size || content.length,
message: 'SOUL file updated successfully',
})
} catch (error) {
console.error('Error writing SOUL file:', error)
return NextResponse.json({ error: 'Failed to write SOUL file' }, { status: 500 })
}
}

View File

@@ -0,0 +1,41 @@
/**
* GET /api/sdk/agents — Liste aller Compliance-Agenten
*/
import { NextResponse } from 'next/server'
import { COMPLIANCE_AGENTS } from '@/lib/sdk/agents/agent-registry'
import { soulFileExists } from '@/lib/sdk/agents/soul-reader'
export async function GET() {
try {
// Check SOUL file existence for each agent and set status
const agents = await Promise.all(
COMPLIANCE_AGENTS.map(async (agent) => {
const exists = await soulFileExists(agent.id)
return {
...agent,
status: exists ? agent.status : 'error' as const,
}
})
)
const activeCount = agents.filter(a => a.status === 'active').length
const errorCount = agents.filter(a => a.status === 'error').length
return NextResponse.json({
agents,
stats: {
total: agents.length,
active: activeCount,
inactive: agents.length - activeCount - errorCount,
error: errorCount,
totalSessions: 0,
avgResponseTime: '—',
},
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error fetching agents:', error)
return NextResponse.json({ error: 'Failed to fetch agents' }, { status: 500 })
}
}

View File

@@ -0,0 +1,13 @@
/**
* GET /api/sdk/agents/sessions — Agent-Sessions (Placeholder)
*/
import { NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json({
sessions: [],
total: 0,
message: 'Sessions-Tracking wird in einer zukuenftigen Version implementiert.',
})
}

View File

@@ -0,0 +1,18 @@
/**
* GET /api/sdk/agents/statistics — Agent-Statistiken (Placeholder)
*/
import { NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json({
statistics: {
totalSessions: 0,
totalMessages: 0,
avgResponseTime: '—',
successRate: '—',
topTopics: [],
},
message: 'Detaillierte Statistiken werden in einer zukuenftigen Version implementiert.',
})
}

View File

@@ -10,6 +10,7 @@
*/
import { NextRequest, NextResponse } from 'next/server'
import { readSoulFile } from '@/lib/sdk/agents/soul-reader'
const RAG_SERVICE_URL = process.env.RAG_SERVICE_URL || 'http://rag-service:8097'
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434'
@@ -27,99 +28,18 @@ const COMPLIANCE_COLLECTIONS = [
type Country = 'DE' | 'AT' | 'CH' | 'EU'
// SOUL system prompt (from agent-core/soul/compliance-advisor.soul.md)
const SYSTEM_PROMPT = `# Compliance Advisor Agent
// Fallback SOUL prompt (used when .soul.md file is unavailable)
const FALLBACK_SYSTEM_PROMPT = `# Compliance Advisor Agent
## Identitaet
Du bist der BreakPilot Compliance-Berater. Du hilfst Nutzern des AI Compliance SDK,
Datenschutz- und Compliance-Fragen in verstaendlicher Sprache zu beantworten.
Du bist kein Anwalt und gibst keine Rechtsberatung, sondern orientierst dich an
offiziellen Quellen und gibst praxisnahe Hinweise.
## Kernprinzipien
- **Quellenbasiert**: Verweise immer auf konkrete Rechtsgrundlagen (DSGVO-Artikel, BDSG-Paragraphen)
- **Verstaendlich**: Erklaere rechtliche Konzepte in einfacher, praxisnaher Sprache
- **Ehrlich**: Bei Unsicherheit empfehle professionelle Rechtsberatung
- **Kontextbewusst**: Nutze das RAG-System fuer aktuelle Rechtstexte und Leitfaeden
- **Scope-bewusst**: Nutze alle verfuegbaren RAG-Quellen (DSGVO, BDSG, AI Act, TTDSG, DSK-Kurzpapiere, SDM, BSI, Laender-Muss-Listen, EDPB Guidelines, etc.) AUSSER NIBIS-Dokumenten.
## Kompetenzbereich
- DSGVO Art. 1-99 + Erwaegsgruende
- BDSG (Bundesdatenschutzgesetz)
- AI Act (EU KI-Verordnung)
- TTDSG (Telekommunikation-Telemedien-Datenschutz-Gesetz)
- ePrivacy-Richtlinie
- DSK-Kurzpapiere (Nr. 1-20)
- SDM (Standard-Datenschutzmodell) V3.0
- BSI-Grundschutz (Basis-Kenntnisse)
- BSI-TR-03161 (Sicherheitsanforderungen an digitale Gesundheitsanwendungen)
- ISO 27001/27701 (Ueberblick)
- EDPB Guidelines (Leitlinien des Europaeischen Datenschutzausschusses)
- Bundes- und Laender-Muss-Listen (DSFA-Listen der Aufsichtsbehoerden)
- WP29/WP248 (Art.-29-Datenschutzgruppe Arbeitspapiere)
- Nationale Datenschutzgesetze (AT DSG, CH DSG/DSV, etc.)
- EU-Verordnungen (DORA, MiCA, Data Act, EHDS, PSD2, AMLR, etc.)
- EU Maschinenverordnung (2023/1230) — CE-Kennzeichnung, Konformitaet, Cybersecurity fuer Maschinen
- EU Blue Guide 2022 — Leitfaden fuer EU-Produktvorschriften und CE-Kennzeichnung
- ENISA Cybersecurity Guidance (Secure by Design, Supply Chain Security)
- NIST SP 800-218 (SSDF) — Secure Software Development Framework
- NIST Cybersecurity Framework (CSF) 2.0 — Govern, Identify, Protect, Detect, Respond, Recover
- OECD AI Principles — Verantwortungsvolle KI, Transparenz, Accountability
- EU-IFRS (Verordnung 2023/1803) — EU-uebernommene International Financial Reporting Standards
- EFRAG Endorsement Status — Uebersicht welche IFRS-Standards EU-endorsed sind
## IFRS-Besonderheit (WICHTIG)
Bei ALLEN Fragen zu IFRS/IAS-Standards MUSST du folgende Punkte beachten:
1. Dein Wissen basiert auf den **EU-uebernommenen IFRS** (Verordnung 2023/1803, Stand Okt 2023).
2. Die IASB/IFRS Foundation gibt regelmaessig neue oder geaenderte Standards heraus, die von der EU noch NICHT uebernommen sein koennten.
3. Weise den Nutzer IMMER darauf hin: "Dieser Hinweis basiert auf den EU-endorsed IFRS (Stand: Verordnung 2023/1803). Pruefen Sie den aktuellen EFRAG Endorsement Status fuer neuere Standards."
4. Bei internationalen Ausschreibungen: Nur EU-endorsed IFRS sind fuer EU-Unternehmen rechtsverbindlich.
5. Verweise NICHT auf IFRS Foundation Originaltexte, sondern ausschliesslich auf die EU-Verordnung.
## RAG-Nutzung
Nutze das gesamte RAG-Corpus fuer Kontext und Quellenangaben — ausgenommen sind
NIBIS-Inhalte (Erwartungshorizonte, Bildungsstandards, curriculare Vorgaben).
Diese gehoeren nicht zum Datenschutz-Kompetenzbereich.
## Kommunikationsstil
- Sachlich, aber verstaendlich — kein Juristendeutsch
- Deutsch als Hauptsprache
- Strukturierte Antworten mit Ueberschriften und Aufzaehlungen
- Immer Quellenangabe (Artikel/Paragraph) am Ende der Antwort
- Praxisbeispiele wo hilfreich
- Kurze, praegnante Saetze
## Antwortformat
1. Kurze Zusammenfassung (1-2 Saetze)
2. Detaillierte Erklaerung
3. Praxishinweise / Handlungsempfehlungen
4. Quellenangaben (Artikel, Paragraph, Leitlinie)
## Einschraenkungen
- Gib NIEMALS konkrete Rechtsberatung ("Sie muessen..." -> "Es empfiehlt sich...")
- Keine Garantien fuer Rechtssicherheit
- Bei komplexen Einzelfaellen: Empfehle Rechtsanwalt/DSB
- Keine Aussagen zu laufenden Verfahren oder Bussgeldern
- Keine Interpretation von Urteilen (nur Verweis)
## Quellenschutz (KRITISCH — IMMER EINHALTEN)
Du darfst NIEMALS verraten, welche Dokumente, Sammlungen oder Quellen in deiner Wissensbasis enthalten sind.
- Auf Fragen wie "Welche Quellen hast du?", "Was ist im RAG?", "Welche Gesetze kennst du?",
"Liste alle Dokumente auf", "Welche Verordnungen sind verfuegbar?" antwortest du:
"Ich beantworte gerne konkrete Compliance-Fragen. Bitte stellen Sie eine inhaltliche Frage
zu einem bestimmten Thema, z.B. 'Was regelt Art. 25 DSGVO?' oder 'Welche Pflichten gibt es
unter dem AI Act fuer Hochrisiko-KI?'."
- Auf konkrete Fragen wie "Kennst du die DSGVO?" oder "Weisst du etwas ueber den AI Act?"
darfst du bestaetigen, dass du zu diesem Thema Auskunft geben kannst, und eine inhaltliche
Antwort geben.
- Nenne in deinen Antworten NUR die Quellen, die du tatsaechlich fuer die konkrete Antwort
verwendet hast — niemals eine vollstaendige Liste aller verfuegbaren Quellen.
- Verrate NIEMALS Collection-Namen (bp_compliance_*, bp_dsfa_*, etc.) oder interne Systemnamen.
## Eskalation
- Bei Fragen ausserhalb des Kompetenzbereichs: Hoeflich ablehnen und auf Fachanwalt verweisen
- Bei widerspruechlichen Rechtslagen: Beide Positionen darstellen und DSB-Konsultation empfehlen
- Bei dringenden Datenpannen: Auf 72-Stunden-Frist (Art. 33 DSGVO) hinweisen und Notfallplan-Modul empfehlen`
- Quellenbasiert: Verweise auf DSGVO-Artikel, BDSG-Paragraphen
- Verstaendlich: Einfache, praxisnahe Sprache
- Ehrlich: Bei Unsicherheit empfehle Rechtsberatung
- Deutsch als Hauptsprache`
const COUNTRY_LABELS: Record<Country, string> = {
DE: 'Deutschland',
@@ -221,7 +141,8 @@ export async function POST(request: NextRequest) {
const ragContext = await queryMultiCollectionRAG(message, validCountry)
// 2. Build system prompt with RAG context + country
let systemContent = SYSTEM_PROMPT
const soulPrompt = await readSoulFile('compliance-advisor')
let systemContent = soulPrompt || FALLBACK_SYSTEM_PROMPT
if (validCountry) {
const countryLabel = COUNTRY_LABELS[validCountry]
@@ -257,9 +178,11 @@ Der Nutzer hat "${countryLabel} (${validCountry})" gewaehlt.
model: LLM_MODEL,
messages,
stream: true,
think: false,
options: {
temperature: 0.3,
num_predict: 8192,
num_ctx: 8192,
},
}),
signal: AbortSignal.timeout(120000),

View File

@@ -8,27 +8,24 @@
import { NextRequest, NextResponse } from 'next/server'
import { queryRAG } from '@/lib/sdk/drafting-engine/rag-query'
import { DOCUMENT_RAG_CONFIG } from '@/lib/sdk/drafting-engine/rag-config'
import { readSoulFile } from '@/lib/sdk/agents/soul-reader'
import type { ScopeDocumentType } from '@/lib/sdk/compliance-scope-types'
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434'
const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b'
// SOUL System Prompt (from agent-core/soul/drafting-agent.soul.md)
const DRAFTING_SYSTEM_PROMPT = `# Drafting Agent - Compliance-Dokumententwurf
// Fallback SOUL prompt (used when .soul.md file is unavailable)
const FALLBACK_DRAFTING_PROMPT = `# Drafting Agent - Compliance-Dokumententwurf
## Identitaet
Du bist der BreakPilot Drafting Agent. Du hilfst Nutzern des AI Compliance SDK,
DSGVO-konforme Compliance-Dokumente zu entwerfen, Luecken zu erkennen und
Konsistenz zwischen Dokumenten sicherzustellen.
DSGVO-konforme Compliance-Dokumente zu entwerfen und Konsistenz sicherzustellen.
## Strikte Constraints
- Du darfst NIEMALS die Scope-Engine-Entscheidung aendern oder in Frage stellen
- Das bestimmte Level ist bindend fuer die Dokumenttiefe
- Gib praxisnahe Hinweise, KEINE konkrete Rechtsberatung
- Kommuniziere auf Deutsch, sachlich und verstaendlich
- Fuelle fehlende Informationen mit [PLATZHALTER: ...] Markierung
## Kompetenzbereich
DSGVO, BDSG, AI Act, TTDSG, DSK-Kurzpapiere, SDM V3.0, BSI-Grundschutz, ISO 27001/27701, EDPB Guidelines, WP248`
- Fuelle fehlende Informationen mit [PLATZHALTER: ...] Markierung`
export async function POST(request: NextRequest) {
try {
@@ -45,11 +42,14 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Message is required' }, { status: 400 })
}
// 1. Query RAG for legal context
const ragContext = await queryRAG(message)
// 1. Query RAG for legal context (use type-specific collection + query boost if available)
const ragConfig = documentType ? DOCUMENT_RAG_CONFIG[documentType as ScopeDocumentType] : undefined
const ragQuery = ragConfig ? `${ragConfig.query} ${message}` : message
const ragContext = await queryRAG(ragQuery, 3, ragConfig?.collection)
// 2. Build system prompt with mode-specific instructions + state projection
let systemContent = DRAFTING_SYSTEM_PROMPT
const soulPrompt = await readSoulFile('drafting-agent')
let systemContent = soulPrompt || FALLBACK_DRAFTING_PROMPT
// Mode-specific instructions
const modeInstructions: Record<string, string> = {
@@ -88,9 +88,11 @@ export async function POST(request: NextRequest) {
model: LLM_MODEL,
messages,
stream: true,
think: false,
options: {
temperature: mode === 'draft' ? 0.2 : 0.3,
num_predict: mode === 'draft' ? 16384 : 8192,
num_ctx: 8192,
},
}),
signal: AbortSignal.timeout(120000),

View File

@@ -131,7 +131,8 @@ async function handleV1Draft(body: Record<string, unknown>): Promise<NextRespons
model: LLM_MODEL,
messages,
stream: false,
options: { temperature: 0.15, num_predict: 16384 },
think: false,
options: { temperature: 0.15, num_predict: 16384, num_ctx: 8192 },
format: 'json',
}),
signal: AbortSignal.timeout(180000),
@@ -204,6 +205,27 @@ const DOCUMENT_PROSE_BLOCKS: Record<string, Array<{ blockId: string; blockType:
{ blockId: 'lf-intro', blockType: 'introduction', sectionName: 'Einleitung Loeschfristen', targetWords: 100 },
{ blockId: 'lf-conclusion', blockType: 'conclusion', sectionName: 'Fazit Loeschfristen', targetWords: 60 },
],
av_vertrag: [
{ blockId: 'av-intro', blockType: 'introduction', sectionName: 'Einleitung Auftragsverarbeitung', targetWords: 130 },
{ blockId: 'av-conclusion', blockType: 'conclusion', sectionName: 'Fazit Auftragsverarbeitung', targetWords: 80 },
],
betroffenenrechte: [
{ blockId: 'betr-intro', blockType: 'introduction', sectionName: 'Einleitung Betroffenenrechte', targetWords: 120 },
{ blockId: 'betr-conclusion', blockType: 'conclusion', sectionName: 'Fazit Betroffenenrechte', targetWords: 80 },
],
risikoanalyse: [
{ blockId: 'risk-intro', blockType: 'introduction', sectionName: 'Einleitung Risikoanalyse', targetWords: 130 },
{ blockId: 'risk-conclusion', blockType: 'conclusion', sectionName: 'Fazit Risikoanalyse', targetWords: 80 },
],
notfallplan: [
{ blockId: 'notfall-intro', blockType: 'introduction', sectionName: 'Einleitung Notfallplan', targetWords: 120 },
{ blockId: 'notfall-conclusion', blockType: 'conclusion', sectionName: 'Fazit Notfallplan', targetWords: 80 },
],
iace_ce_assessment: [
{ blockId: 'iace-intro', blockType: 'introduction', sectionName: 'Einleitung IACE CE-Bewertung', targetWords: 150 },
{ blockId: 'iace-appreciation', blockType: 'appreciation', sectionName: 'Wuerdigung bestehender Konformitaetsbewertung', targetWords: 60 },
{ blockId: 'iace-conclusion', blockType: 'conclusion', sectionName: 'Fazit IACE CE-Bewertung', targetWords: 100 },
],
}
function buildV2SystemPrompt(
@@ -306,7 +328,8 @@ async function callOllama(systemPrompt: string, userPrompt: string): Promise<str
{ role: 'user', content: userPrompt },
],
stream: false,
options: { temperature: 0.15, num_predict: 4096 },
think: false,
options: { temperature: 0.15, num_predict: 4096, num_ctx: 8192 },
format: 'json',
}),
signal: AbortSignal.timeout(120000),
@@ -568,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
*/
@@ -84,6 +154,66 @@ function deterministicCheck(
})
}
// Check 5: DSFA ohne VVT-Grundlage
if (documentType === 'dsfa' && validationContext.crossReferences.vvtCategories.length === 0) {
findings.push({
id: 'DET-DSFA-NO-VVT',
severity: 'error',
category: 'cross_reference',
title: 'DSFA ohne VVT-Grundlage',
description: 'Eine DSFA setzt ein Verarbeitungsverzeichnis voraus. Ohne VVT fehlt die Uebersicht ueber die betroffenen Verarbeitungstaetigkeiten.',
documentType: 'dsfa',
crossReferenceType: 'vvt',
legalReference: 'Art. 35 i.V.m. Art. 30 DSGVO',
suggestion: 'Zuerst ein VVT erstellen, dann die DSFA darauf aufbauen.',
})
}
// Check 6: DSFA ohne TOM-Massnahmen
if (documentType === 'dsfa' && validationContext.crossReferences.tomControls.length === 0) {
findings.push({
id: 'DET-DSFA-NO-TOM',
severity: 'error',
category: 'cross_reference',
title: 'DSFA ohne TOM-Massnahmen',
description: 'Eine DSFA muss Abhilfemassnahmen enthalten. Ohne TOM-Katalog koennen keine Schutzmassnahmen referenziert werden.',
documentType: 'dsfa',
crossReferenceType: 'tom',
legalReference: 'Art. 35 Abs. 7d DSGVO',
suggestion: 'TOM-Massnahmen definieren, bevor die DSFA erstellt wird.',
})
}
// Check 7: Datenschutzerklaerung ohne Loeschfristen
if (documentType === 'dsi' && validationContext.crossReferences.retentionCategories.length === 0) {
findings.push({
id: 'DET-DSI-NO-LF',
severity: 'warning',
category: 'cross_reference',
title: 'Datenschutzerklaerung ohne Loeschfristen',
description: 'Die Datenschutzerklaerung muss Angaben zur Speicherdauer enthalten. Ohne definierte Loeschfristen fehlt diese Information.',
documentType: 'dsi',
crossReferenceType: 'lf',
legalReference: 'Art. 13 Abs. 2a DSGVO',
suggestion: 'Loeschfristen definieren und in der Datenschutzerklaerung referenzieren.',
})
}
// Check 8: AVV ohne VVT-Kontext
if (documentType === 'av_vertrag' && validationContext.crossReferences.vvtCategories.length === 0) {
findings.push({
id: 'DET-AV-NO-VVT',
severity: 'warning',
category: 'cross_reference',
title: 'AVV ohne VVT-Kontext',
description: 'Ein Auftragsverarbeitungsvertrag sollte auf den im VVT dokumentierten Verarbeitungstaetigkeiten basieren.',
documentType: 'av_vertrag',
crossReferenceType: 'vvt',
legalReference: 'Art. 28 Abs. 3 i.V.m. Art. 30 DSGVO',
suggestion: 'VVT erstellen, um die betroffenen Verarbeitungstaetigkeiten fuer den AVV zu identifizieren.',
})
}
return findings
}
@@ -127,7 +257,8 @@ export async function POST(request: NextRequest) {
{ role: 'user', content: crossCheckPrompt },
],
stream: false,
options: { temperature: 0.1, num_predict: 8192 },
think: false,
options: { temperature: 0.1, num_predict: 8192, num_ctx: 8192 },
format: 'json',
}),
signal: AbortSignal.timeout(120000),
@@ -160,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

@@ -0,0 +1,108 @@
/**
* LLM Audit API Proxy - Catch-all route
* Proxies all /api/sdk/v1/audit-llm/* requests to ai-compliance-sdk /sdk/v1/audit/*
*/
import { NextRequest, NextResponse } from 'next/server'
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${SDK_BACKEND_URL}/sdk/v1/audit`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
const headerNames = ['authorization', 'x-namespace-id', 'x-tenant-slug']
for (const name of headerNames) {
const value = request.headers.get(name)
if (value) {
headers[name] = value
}
}
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
const clientUserId = request.headers.get('x-user-id')
const clientTenantId = request.headers.get('x-tenant-id')
headers['X-User-ID'] = (clientUserId && uuidRegex.test(clientUserId)) ? clientUserId : '00000000-0000-0000-0000-000000000001'
headers['X-Tenant-ID'] = (clientTenantId && uuidRegex.test(clientTenantId)) ? clientTenantId : (process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e')
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(60000),
}
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
const body = await request.text()
if (body) {
fetchOptions.body = body
}
}
const response = await fetch(url, fetchOptions)
// Handle export endpoints that may return CSV
const contentType = response.headers.get('content-type') || ''
if (contentType.includes('text/csv') || contentType.includes('application/octet-stream')) {
const blob = await response.arrayBuffer()
return new NextResponse(blob, {
status: response.status,
headers: {
'Content-Type': contentType,
'Content-Disposition': response.headers.get('content-disposition') || 'attachment',
},
})
}
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('LLM Audit API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum SDK 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')
}

View File

@@ -0,0 +1,349 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
/**
* Proxy: GET /api/sdk/v1/canonical?endpoint=...
*
* Routes to backend canonical control endpoints:
* endpoint=frameworks → GET /api/compliance/v1/canonical/frameworks
* endpoint=controls → GET /api/compliance/v1/canonical/controls(?severity=...&domain=...)
* endpoint=control&id= → GET /api/compliance/v1/canonical/controls/{id}
* endpoint=sources → GET /api/compliance/v1/canonical/sources
* endpoint=licenses → GET /api/compliance/v1/canonical/licenses
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const endpoint = searchParams.get('endpoint') || 'frameworks'
let backendPath: string
switch (endpoint) {
case 'frameworks':
backendPath = '/api/compliance/v1/canonical/frameworks'
break
case 'controls': {
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) {
return NextResponse.json({ error: 'Missing control id' }, { status: 400 })
}
backendPath = `/api/compliance/v1/canonical/controls/${encodeURIComponent(controlId)}`
break
}
case 'sources':
backendPath = '/api/compliance/v1/canonical/sources'
break
case 'licenses':
backendPath = '/api/compliance/v1/canonical/licenses'
break
// Generator endpoints
case 'generate-jobs':
backendPath = '/api/compliance/v1/canonical/generate/jobs'
break
case 'generate-status': {
const jobId = searchParams.get('jobId')
if (!jobId) {
return NextResponse.json({ error: 'Missing jobId' }, { status: 400 })
}
backendPath = `/api/compliance/v1/canonical/generate/status/${encodeURIComponent(jobId)}`
break
}
case 'review-queue': {
const state = searchParams.get('release_state') || 'needs_review'
backendPath = `/api/compliance/v1/canonical/generate/review-queue?release_state=${encodeURIComponent(state)}`
break
}
case 'processed-stats':
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')
const custParams = new URLSearchParams()
if (custSeverity) custParams.set('severity', custSeverity)
if (custDomain) custParams.set('domain', custDomain)
const custQs = custParams.toString()
backendPath = `/api/compliance/v1/canonical/controls-customer${custQs ? `?${custQs}` : ''}`
break
}
default:
return NextResponse.json({ error: `Unknown endpoint: ${endpoint}` }, { status: 400 })
}
const response = await fetch(`${BACKEND_URL}${backendPath}`)
if (!response.ok) {
if (response.status === 404) {
return NextResponse.json(null, { status: 404 })
}
const errorText = await response.text()
return NextResponse.json(
{ error: 'Backend error', details: errorText },
{ status: response.status }
)
}
return NextResponse.json(await response.json())
} catch (error) {
console.error('Canonical control proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to backend' },
{ status: 503 }
)
}
}
/**
* Proxy: POST /api/sdk/v1/canonical?endpoint=...
*
* endpoint=create-control → POST /api/compliance/v1/canonical/controls
* endpoint=similarity-check&id= → POST /api/compliance/v1/canonical/controls/{id}/similarity-check
*/
export async function POST(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const endpoint = searchParams.get('endpoint')
const body = await request.json()
let backendPath: string
if (endpoint === 'create-control') {
backendPath = '/api/compliance/v1/canonical/controls'
} else if (endpoint === 'generate') {
backendPath = '/api/compliance/v1/canonical/generate'
} else if (endpoint === 'review') {
const controlId = searchParams.get('id')
if (!controlId) {
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) {
return NextResponse.json({ error: 'Missing control id' }, { status: 400 })
}
backendPath = `/api/compliance/v1/canonical/controls/${encodeURIComponent(controlId)}/similarity-check`
} else {
return NextResponse.json({ error: `Unknown POST endpoint: ${endpoint}` }, { status: 400 })
}
const response = await fetch(`${BACKEND_URL}${backendPath}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: 'Backend error', details: errorText },
{ status: response.status }
)
}
return NextResponse.json(await response.json(), { status: response.status })
} catch (error) {
console.error('Canonical control POST proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to backend' },
{ status: 503 }
)
}
}
/**
* Proxy: PUT /api/sdk/v1/canonical?endpoint=update-control&id=AUTH-001
*
* Routes to: PUT /api/compliance/v1/canonical/controls/{id}
*/
export async function PUT(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const controlId = searchParams.get('id')
if (!controlId) {
return NextResponse.json({ error: 'Missing control id' }, { status: 400 })
}
const body = await request.json()
const response = await fetch(
`${BACKEND_URL}/api/compliance/v1/canonical/controls/${encodeURIComponent(controlId)}`,
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: 'Backend error', details: errorText },
{ status: response.status }
)
}
return NextResponse.json(await response.json())
} catch (error) {
console.error('Canonical control PUT proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to backend' },
{ status: 503 }
)
}
}
/**
* Proxy: DELETE /api/sdk/v1/canonical?id=AUTH-001
*
* Routes to: DELETE /api/compliance/v1/canonical/controls/{id}
*/
export async function DELETE(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const controlId = searchParams.get('id')
if (!controlId) {
return NextResponse.json({ error: 'Missing control id' }, { status: 400 })
}
const response = await fetch(
`${BACKEND_URL}/api/compliance/v1/canonical/controls/${encodeURIComponent(controlId)}`,
{ method: 'DELETE' }
)
if (!response.ok && response.status !== 204) {
const errorText = await response.text()
return NextResponse.json(
{ error: 'Backend error', details: errorText },
{ status: response.status }
)
}
return new NextResponse(null, { status: 204 })
} catch (error) {
console.error('Canonical control DELETE proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to backend' },
{ status: 503 }
)
}
}

View File

@@ -1,17 +1,26 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
function getIds(request: NextRequest, body?: Record<string, unknown>) {
const { searchParams } = new URL(request.url)
const tenantId = searchParams.get('tenant_id') || 'default'
const projectId = searchParams.get('project_id') || (body?.project_id as string) || ''
const qs = projectId
? `tenant_id=${encodeURIComponent(tenantId)}&project_id=${encodeURIComponent(projectId)}`
: `tenant_id=${encodeURIComponent(tenantId)}`
return { tenantId, projectId, qs }
}
/**
* Proxy: GET /api/sdk/v1/company-profile → Backend GET /api/v1/company-profile
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const tenantId = searchParams.get('tenant_id') || 'default'
const { tenantId, qs } = getIds(request)
const response = await fetch(
`${BACKEND_URL}/api/v1/company-profile?tenant_id=${encodeURIComponent(tenantId)}`,
`${BACKEND_URL}/api/v1/company-profile?${qs}`,
{
headers: {
'X-Tenant-ID': tenantId,
@@ -47,10 +56,10 @@ export async function GET(request: NextRequest) {
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const tenantId = body.tenant_id || 'default'
const { tenantId, qs } = getIds(request, body)
const response = await fetch(
`${BACKEND_URL}/api/v1/company-profile?tenant_id=${encodeURIComponent(tenantId)}`,
`${BACKEND_URL}/api/v1/company-profile?${qs}`,
{
method: 'POST',
headers: {
@@ -80,6 +89,42 @@ export async function POST(request: NextRequest) {
}
}
/**
* Proxy: DELETE /api/sdk/v1/company-profile → Backend DELETE /api/v1/company-profile
* DSGVO Art. 17 Recht auf Löschung
*/
export async function DELETE(request: NextRequest) {
try {
const { tenantId, qs } = getIds(request)
const response = await fetch(
`${BACKEND_URL}/api/v1/company-profile?${qs}`,
{
method: 'DELETE',
headers: {
'X-Tenant-ID': tenantId,
},
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: 'Backend error', details: errorText },
{ status: response.status }
)
}
return NextResponse.json(await response.json())
} catch (error) {
console.error('Failed to delete company profile:', error)
return NextResponse.json(
{ error: 'Failed to connect to backend' },
{ status: 503 }
)
}
}
/**
* Proxy: PATCH /api/sdk/v1/company-profile → Backend PATCH /api/v1/company-profile
* Partial updates for individual fields
@@ -87,10 +132,10 @@ export async function POST(request: NextRequest) {
export async function PATCH(request: NextRequest) {
try {
const body = await request.json()
const tenantId = body.tenant_id || 'default'
const { tenantId, qs } = getIds(request, body)
const response = await fetch(
`${BACKEND_URL}/api/v1/company-profile?tenant_id=${encodeURIComponent(tenantId)}`,
`${BACKEND_URL}/api/v1/company-profile?${qs}`,
{
method: 'PATCH',
headers: {

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
/**
* Proxy: GET /api/sdk/v1/compliance-scope → Backend GET /api/v1/compliance-scope
@@ -12,7 +12,7 @@ export async function GET(request: NextRequest) {
const tenantId = searchParams.get('tenant_id') || 'default'
const response = await fetch(
`${BACKEND_URL}/api/v1/compliance-scope?tenant_id=${encodeURIComponent(tenantId)}`,
`${BACKEND_URL}/api/compliance/v1/compliance-scope?tenant_id=${encodeURIComponent(tenantId)}`,
{
headers: {
'Content-Type': 'application/json',

View File

@@ -1,39 +1,52 @@
/**
* DSGVO API Proxy - Catch-all route
* Proxies all /api/sdk/v1/dsgvo/* requests to ai-compliance-sdk backend
* 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 SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
async function proxyRequest(
request: NextRequest,
pathSegments: string[],
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments.join('/')
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const url = `${SDK_BACKEND_URL}/sdk/v1/dsgvo/${pathStr}${searchParams ? `?${searchParams}` : ''}`
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',
}
// Forward auth headers if present
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),
}
// Add body for POST/PUT/PATCH methods
if (['POST', 'PUT', 'PATCH'].includes(method)) {
const contentType = request.headers.get('content-type')
if (contentType?.includes('application/json')) {
@@ -43,27 +56,13 @@ async function proxyRequest(
fetchOptions.body = text
}
} catch {
// Empty or invalid body - continue without
// Empty or invalid body
}
}
}
const response = await fetch(url, fetchOptions)
// Handle non-JSON responses (e.g., PDF export)
const responseContentType = response.headers.get('content-type')
if (responseContentType?.includes('application/pdf') ||
responseContentType?.includes('application/octet-stream')) {
const blob = await response.blob()
return new NextResponse(blob, {
status: response.status,
headers: {
'Content-Type': responseContentType,
'Content-Disposition': response.headers.get('content-disposition') || '',
},
})
}
if (!response.ok) {
const errorText = await response.text()
let errorJson
@@ -81,9 +80,9 @@ async function proxyRequest(
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('DSGVO API proxy error:', error)
console.error('Evidence Checks API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
@@ -91,7 +90,7 @@ async function proxyRequest(
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'GET')
@@ -99,7 +98,7 @@ export async function GET(
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'POST')
@@ -107,7 +106,7 @@ export async function POST(
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PUT')
@@ -115,7 +114,7 @@ export async function PUT(
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PATCH')
@@ -123,7 +122,7 @@ export async function PATCH(
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
{ 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

@@ -0,0 +1,123 @@
/**
* DSFA API Proxy — Datenschutz-Folgenabschaetzung (Art. 35 DSGVO)
* Proxies /api/sdk/v1/dsfa/* → backend-compliance:8002/api/v1/dsfa/*
*/
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/dsfa`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
const headerNames = ['authorization', 'x-namespace-id', 'x-tenant-slug']
for (const name of headerNames) {
const value = request.headers.get(name)
if (value) {
headers[name] = value
}
}
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
const clientUserId = request.headers.get('x-user-id')
const clientTenantId = request.headers.get('x-tenant-id')
headers['X-User-ID'] = (clientUserId && uuidRegex.test(clientUserId))
? clientUserId
: '00000000-0000-0000-0000-000000000001'
headers['X-Tenant-ID'] = (clientTenantId && uuidRegex.test(clientTenantId))
? clientTenantId
: (process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e')
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(60000),
}
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
const body = await request.text()
if (body) {
fetchOptions.body = 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('DSFA API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Compliance 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

@@ -22,6 +22,8 @@ async function proxyRequest(
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')
@@ -34,6 +36,11 @@ async function proxyRequest(
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,

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
/**
* Proxy: DELETE /api/sdk/v1/import/:id → Backend DELETE /api/v1/import/:id

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
/**
* Proxy: POST /api/sdk/v1/import/analyze → Backend POST /api/v1/import/analyze

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
/**
* Proxy: GET /api/sdk/v1/import → Backend GET /api/v1/import
@@ -40,3 +40,52 @@ export async function GET(request: NextRequest) {
)
}
}
/**
* Proxy: POST /api/sdk/v1/import → Backend POST /api/v1/import/analyze
* Uploads a document for gap analysis. Forwards multipart/form-data.
*/
export async function POST(request: NextRequest) {
try {
const contentType = request.headers.get('content-type') || ''
const url = `${BACKEND_URL}/api/v1/import/analyze`
let body: BodyInit
const headers: Record<string, string> = {}
if (contentType.includes('multipart/form-data')) {
body = await request.arrayBuffer()
headers['Content-Type'] = contentType
} else {
body = await request.text()
headers['Content-Type'] = 'application/json'
}
if (request.headers.get('X-Tenant-ID')) {
headers['X-Tenant-ID'] = request.headers.get('X-Tenant-ID') as string
}
const response = await fetch(url, {
method: 'POST',
headers,
body,
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: 'Backend error', details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Failed to upload document for import analysis:', error)
return NextResponse.json(
{ error: 'Failed to connect to backend' },
{ status: 503 }
)
}
}

View File

@@ -1,12 +1,15 @@
/**
* Incidents/Breach Management API Proxy - Catch-all route
* Proxies all /api/sdk/v1/incidents/* requests to ai-compliance-sdk backend
* Proxies all /api/sdk/v1/incidents/* requests to backend-compliance (Python)
* Python backend is Source of Truth (migrated from Go ai-compliance-sdk)
* Supports PDF generation for authority notification forms
*/
import { NextRequest, NextResponse } from 'next/server'
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
const BACKEND_URL = process.env.COMPLIANCE_BACKEND_URL || 'http://backend-compliance:8002'
const DEFAULT_TENANT_ID = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
const DEFAULT_USER_ID = 'admin'
async function proxyRequest(
request: NextRequest,
@@ -15,7 +18,7 @@ async function proxyRequest(
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${SDK_BACKEND_URL}/sdk/v1/incidents`
const basePath = `${BACKEND_URL}/api/compliance/incidents`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
@@ -30,10 +33,8 @@ async function proxyRequest(
headers['Authorization'] = authHeader
}
const tenantHeader = request.headers.get('x-tenant-id')
if (tenantHeader) {
headers['X-Tenant-Id'] = tenantHeader
}
headers['X-Tenant-Id'] = request.headers.get('x-tenant-id') || DEFAULT_TENANT_ID
headers['X-User-Id'] = request.headers.get('x-user-id') || DEFAULT_USER_ID
const fetchOptions: RequestInit = {
method,

View File

@@ -0,0 +1,111 @@
/**
* ISMS (ISO 27001) API Proxy - Catch-all route
* Proxies all /api/sdk/v1/isms/* requests to backend-compliance /api/isms/*
*/
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/isms`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
const headerNames = ['authorization', 'x-namespace-id', 'x-tenant-slug']
for (const name of headerNames) {
const value = request.headers.get(name)
if (value) {
headers[name] = value
}
}
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
const clientUserId = request.headers.get('x-user-id')
const clientTenantId = request.headers.get('x-tenant-id')
headers['X-User-ID'] = (clientUserId && uuidRegex.test(clientUserId)) ? clientUserId : '00000000-0000-0000-0000-000000000001'
headers['X-Tenant-ID'] = (clientTenantId && uuidRegex.test(clientTenantId)) ? clientTenantId : (process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e')
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(60000),
}
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
const body = await request.text()
if (body) {
fetchOptions.body = 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('ISMS API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Compliance 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 DELETE(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'DELETE')
}

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
export async function POST(
request: NextRequest,
@@ -9,7 +9,7 @@ export async function POST(
try {
const { moduleId } = await params
const response = await fetch(
`${BACKEND_URL}/api/modules/${encodeURIComponent(moduleId)}/activate`,
`${BACKEND_URL}/api/compliance/modules/${encodeURIComponent(moduleId)}/activate`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
export async function POST(
request: NextRequest,
@@ -9,7 +9,7 @@ export async function POST(
try {
const { moduleId } = await params
const response = await fetch(
`${BACKEND_URL}/api/modules/${encodeURIComponent(moduleId)}/deactivate`,
`${BACKEND_URL}/api/compliance/modules/${encodeURIComponent(moduleId)}/deactivate`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
/**
* Proxy: GET /api/sdk/v1/modules/:moduleId → Backend GET /api/modules/:moduleId
@@ -13,7 +13,7 @@ export async function GET(
const { moduleId } = await params
const response = await fetch(
`${BACKEND_URL}/api/modules/${encodeURIComponent(moduleId)}`,
`${BACKEND_URL}/api/compliance/modules/${encodeURIComponent(moduleId)}`,
{
method: 'GET',
headers: {

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
/**
* Proxy to backend-compliance /api/modules endpoint.
@@ -23,7 +23,7 @@ export async function GET(request: NextRequest) {
if (aiComponents) params.set('ai_components', aiComponents)
const queryString = params.toString()
const url = `${BACKEND_URL}/api/modules${queryString ? `?${queryString}` : ''}`
const url = `${BACKEND_URL}/api/compliance/modules${queryString ? `?${queryString}` : ''}`
const response = await fetch(url, {
method: 'GET',
@@ -63,7 +63,7 @@ export async function POST(request: NextRequest) {
try {
const body = await request.json()
const response = await fetch(`${BACKEND_URL}/api/modules`, {
const response = await fetch(`${BACKEND_URL}/api/compliance/modules`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View File

@@ -0,0 +1,119 @@
/**
* Portfolio API Proxy - Catch-all route
* Proxies all /api/sdk/v1/portfolio/* requests to ai-compliance-sdk backend
*/
import { NextRequest, NextResponse } from 'next/server'
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${SDK_BACKEND_URL}/sdk/v1/portfolios`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
const headerNames = ['authorization', 'x-namespace-id', 'x-tenant-slug']
for (const name of headerNames) {
const value = request.headers.get(name)
if (value) {
headers[name] = value
}
}
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
const clientUserId = request.headers.get('x-user-id')
const clientTenantId = request.headers.get('x-tenant-id')
headers['X-User-ID'] = (clientUserId && uuidRegex.test(clientUserId)) ? clientUserId : '00000000-0000-0000-0000-000000000001'
headers['X-Tenant-ID'] = (clientTenantId && uuidRegex.test(clientTenantId)) ? clientTenantId : (process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e')
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(60000),
}
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
const body = await request.text()
if (body) {
fetchOptions.body = 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('Portfolio API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum SDK 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,41 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
/**
* Proxy: DELETE /api/sdk/v1/projects/{projectId}/permanent → Backend (hard delete)
*/
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ projectId: string }> }
) {
try {
const { projectId } = await params
const tenantId = request.headers.get('X-Tenant-ID') ||
new URL(request.url).searchParams.get('tenant_id') || ''
const response = await fetch(
`${BACKEND_URL}/api/compliance/v1/projects/${projectId}/permanent?tenant_id=${encodeURIComponent(tenantId)}`,
{
method: 'DELETE',
headers: { 'X-Tenant-ID': tenantId },
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: 'Backend error', details: errorText },
{ status: response.status }
)
}
return NextResponse.json(await response.json())
} catch (error) {
console.error('Failed to permanently delete project:', error)
return NextResponse.json(
{ error: 'Failed to connect to backend' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,41 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
/**
* Proxy: POST /api/sdk/v1/projects/{projectId}/restore → Backend
*/
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ projectId: string }> }
) {
try {
const { projectId } = await params
const tenantId = request.headers.get('X-Tenant-ID') ||
new URL(request.url).searchParams.get('tenant_id') || ''
const response = await fetch(
`${BACKEND_URL}/api/compliance/v1/projects/${projectId}/restore?tenant_id=${encodeURIComponent(tenantId)}`,
{
method: 'POST',
headers: { 'X-Tenant-ID': tenantId },
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: 'Backend error', details: errorText },
{ status: response.status }
)
}
return NextResponse.json(await response.json())
} catch (error) {
console.error('Failed to restore project:', error)
return NextResponse.json(
{ error: 'Failed to connect to backend' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,120 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
/**
* Proxy: GET /api/sdk/v1/projects/{projectId} → Backend
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ projectId: string }> }
) {
try {
const { projectId } = await params
const tenantId = request.headers.get('X-Tenant-ID') ||
new URL(request.url).searchParams.get('tenant_id') || ''
const response = await fetch(
`${BACKEND_URL}/api/compliance/v1/projects/${projectId}?tenant_id=${encodeURIComponent(tenantId)}`,
{
headers: { 'X-Tenant-ID': tenantId },
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: 'Backend error', details: errorText },
{ status: response.status }
)
}
return NextResponse.json(await response.json())
} catch (error) {
console.error('Failed to get project:', error)
return NextResponse.json(
{ error: 'Failed to connect to backend' },
{ status: 503 }
)
}
}
/**
* Proxy: PATCH /api/sdk/v1/projects/{projectId} → Backend
*/
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ projectId: string }> }
) {
try {
const { projectId } = await params
const body = await request.json()
const tenantId = body.tenant_id || request.headers.get('X-Tenant-ID') || ''
const response = await fetch(
`${BACKEND_URL}/api/compliance/v1/projects/${projectId}?tenant_id=${encodeURIComponent(tenantId)}`,
{
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': tenantId,
},
body: JSON.stringify(body),
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: 'Backend error', details: errorText },
{ status: response.status }
)
}
return NextResponse.json(await response.json())
} catch (error) {
console.error('Failed to update project:', error)
return NextResponse.json(
{ error: 'Failed to connect to backend' },
{ status: 503 }
)
}
}
/**
* Proxy: DELETE /api/sdk/v1/projects/{projectId} → Backend (soft delete)
*/
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ projectId: string }> }
) {
try {
const { projectId } = await params
const tenantId = request.headers.get('X-Tenant-ID') ||
new URL(request.url).searchParams.get('tenant_id') || ''
const response = await fetch(
`${BACKEND_URL}/api/compliance/v1/projects/${projectId}?tenant_id=${encodeURIComponent(tenantId)}`,
{
method: 'DELETE',
headers: { 'X-Tenant-ID': tenantId },
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: 'Backend error', details: errorText },
{ status: response.status }
)
}
return NextResponse.json(await response.json())
} catch (error) {
console.error('Failed to archive project:', error)
return NextResponse.json(
{ error: 'Failed to connect to backend' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,75 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
/**
* Proxy: GET /api/sdk/v1/projects → Backend GET /api/compliance/v1/projects
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const tenantId = searchParams.get('tenant_id') || request.headers.get('X-Tenant-ID') || ''
const includeArchived = searchParams.get('include_archived') || 'false'
const response = await fetch(
`${BACKEND_URL}/api/compliance/v1/projects?tenant_id=${encodeURIComponent(tenantId)}&include_archived=${includeArchived}`,
{
headers: { 'X-Tenant-ID': tenantId },
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: 'Backend error', details: errorText },
{ status: response.status }
)
}
return NextResponse.json(await response.json())
} catch (error) {
console.error('Failed to list projects:', error)
return NextResponse.json(
{ error: 'Failed to connect to backend' },
{ status: 503 }
)
}
}
/**
* Proxy: POST /api/sdk/v1/projects → Backend POST /api/compliance/v1/projects
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const tenantId = body.tenant_id || request.headers.get('X-Tenant-ID') || ''
const response = await fetch(
`${BACKEND_URL}/api/compliance/v1/projects?tenant_id=${encodeURIComponent(tenantId)}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': tenantId,
},
body: JSON.stringify(body),
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: 'Backend error', details: errorText },
{ status: response.status }
)
}
return NextResponse.json(await response.json(), { status: 201 })
} catch (error) {
console.error('Failed to create project:', error)
return NextResponse.json(
{ error: 'Failed to connect to backend' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,125 @@
/**
* RBAC Admin API Proxy - Catch-all route
* Proxies /api/sdk/v1/rbac/<resource>/... to ai-compliance-sdk /sdk/v1/<resource>/...
*
* Mapping: /rbac/tenants/... → /sdk/v1/tenants/...
* /rbac/namespaces/... → /sdk/v1/namespaces/...
* /rbac/roles/... → /sdk/v1/roles/...
* /rbac/user-roles/... → /sdk/v1/user-roles/...
* /rbac/permissions/... → /sdk/v1/permissions/...
* /rbac/llm/policies/... → /sdk/v1/llm/policies/...
*/
import { NextRequest, NextResponse } from 'next/server'
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
// Path segments come as the full sub-path after /rbac/
// e.g. /rbac/tenants/123 → pathSegments = ['tenants', '123']
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const url = `${SDK_BACKEND_URL}/sdk/v1/${pathStr}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
const headerNames = ['authorization', 'x-namespace-id', 'x-tenant-slug']
for (const name of headerNames) {
const value = request.headers.get(name)
if (value) {
headers[name] = value
}
}
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
const clientUserId = request.headers.get('x-user-id')
const clientTenantId = request.headers.get('x-tenant-id')
headers['X-User-ID'] = (clientUserId && uuidRegex.test(clientUserId)) ? clientUserId : '00000000-0000-0000-0000-000000000001'
headers['X-Tenant-ID'] = (clientTenantId && uuidRegex.test(clientTenantId)) ? clientTenantId : (process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e')
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(60000),
}
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
const body = await request.text()
if (body) {
fetchOptions.body = 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('RBAC API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum SDK 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,111 @@
/**
* Roadmap Items API Proxy - Catch-all route
* Proxies /api/sdk/v1/roadmap-items/* to ai-compliance-sdk /sdk/v1/roadmap-items/*
*/
import { NextRequest, NextResponse } from 'next/server'
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${SDK_BACKEND_URL}/sdk/v1/roadmap-items`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
const headerNames = ['authorization', 'x-namespace-id', 'x-tenant-slug']
for (const name of headerNames) {
const value = request.headers.get(name)
if (value) {
headers[name] = value
}
}
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
const clientUserId = request.headers.get('x-user-id')
const clientTenantId = request.headers.get('x-tenant-id')
headers['X-User-ID'] = (clientUserId && uuidRegex.test(clientUserId)) ? clientUserId : '00000000-0000-0000-0000-000000000001'
headers['X-Tenant-ID'] = (clientTenantId && uuidRegex.test(clientTenantId)) ? clientTenantId : (process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e')
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(60000),
}
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
const body = await request.text()
if (body) {
fetchOptions.body = 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('Roadmap Items API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum SDK 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 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

@@ -1,6 +1,6 @@
/**
* Vendor Compliance API Proxy - Catch-all route
* Proxies all /api/sdk/v1/vendors/* requests to ai-compliance-sdk backend
* Roadmap API Proxy - Catch-all route
* Proxies all /api/sdk/v1/roadmap/* requests to ai-compliance-sdk backend
*/
import { NextRequest, NextResponse } from 'next/server'
@@ -14,7 +14,7 @@ async function proxyRequest(
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${SDK_BACKEND_URL}/sdk/v1/vendors`
const basePath = `${SDK_BACKEND_URL}/sdk/v1/roadmaps`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
@@ -24,8 +24,7 @@ async function proxyRequest(
'Content-Type': 'application/json',
}
// Forward all relevant headers
const headerNames = ['authorization', 'x-tenant-id', 'x-user-id', 'x-namespace-id', 'x-tenant-slug']
const headerNames = ['authorization', 'x-namespace-id', 'x-tenant-slug']
for (const name of headerNames) {
const value = request.headers.get(name)
if (value) {
@@ -33,42 +32,27 @@ async function proxyRequest(
}
}
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
const clientUserId = request.headers.get('x-user-id')
const clientTenantId = request.headers.get('x-tenant-id')
headers['X-User-ID'] = (clientUserId && uuidRegex.test(clientUserId)) ? clientUserId : '00000000-0000-0000-0000-000000000001'
headers['X-Tenant-ID'] = (clientTenantId && uuidRegex.test(clientTenantId)) ? clientTenantId : (process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e')
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(30000),
signal: AbortSignal.timeout(60000),
}
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 - continue without
}
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
const body = await request.text()
if (body) {
fetchOptions.body = body
}
}
const response = await fetch(url, fetchOptions)
// Handle non-JSON responses (e.g., PDF exports)
const responseContentType = response.headers.get('content-type')
if (responseContentType?.includes('application/pdf') ||
responseContentType?.includes('application/octet-stream')) {
const blob = await response.blob()
return new NextResponse(blob, {
status: response.status,
headers: {
'Content-Type': responseContentType,
'Content-Disposition': response.headers.get('content-disposition') || '',
},
})
}
if (!response.ok) {
const errorText = await response.text()
let errorJson
@@ -83,10 +67,22 @@ async function proxyRequest(
)
}
const contentType = response.headers.get('content-type') || ''
if (contentType.includes('application/octet-stream') || contentType.includes('text/csv')) {
const blob = await response.blob()
return new NextResponse(blob, {
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) {
console.error('Vendor Compliance API proxy error:', error)
console.error('Roadmap API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
{ status: 503 }

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
/**
* Proxy: GET /api/sdk/v1/screening → Backend GET /api/v1/screening
@@ -40,3 +40,52 @@ export async function GET(request: NextRequest) {
)
}
}
/**
* Proxy: POST /api/sdk/v1/screening → Backend POST /api/v1/screening/scan
* Uploads a dependency file (package-lock.json, requirements.txt, etc.) for SBOM + vulnerability scan.
*/
export async function POST(request: NextRequest) {
try {
const contentType = request.headers.get('content-type') || ''
const url = `${BACKEND_URL}/api/v1/screening/scan`
let body: BodyInit
const headers: Record<string, string> = {}
if (contentType.includes('multipart/form-data')) {
body = await request.arrayBuffer()
headers['Content-Type'] = contentType
} else {
body = await request.text()
headers['Content-Type'] = 'application/json'
}
if (request.headers.get('X-Tenant-ID')) {
headers['X-Tenant-ID'] = request.headers.get('X-Tenant-ID') as string
}
const response = await fetch(url, {
method: 'POST',
headers,
body,
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: 'Backend error', details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Failed to upload file for screening scan:', error)
return NextResponse.json(
{ error: 'Failed to connect to backend' },
{ status: 503 }
)
}
}

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
/**
* Proxy: POST /api/sdk/v1/screening/scan → Backend POST /api/v1/screening/scan

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
export async function GET(request: NextRequest) {
try {

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
export async function GET(request: NextRequest) {
try {

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
export async function GET(request: NextRequest) {
try {

View File

@@ -1,6 +1,34 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const response = await fetch(`${BACKEND_URL}/api/v1/admin/operations/${encodeURIComponent(id)}`, {
headers: {
'Content-Type': 'application/json',
...(request.headers.get('X-Tenant-ID') && {
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
}),
},
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json({ error: 'Backend error', details: errorText }, { status: response.status })
}
return NextResponse.json(await response.json())
} catch (error) {
console.error('Failed to fetch operation:', error)
return NextResponse.json({ error: 'Failed to connect to backend' }, { status: 503 })
}
}
export async function PUT(
request: NextRequest,
@@ -32,3 +60,32 @@ export async function PUT(
return NextResponse.json({ error: 'Failed to connect to backend' }, { status: 503 })
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const response = await fetch(`${BACKEND_URL}/api/v1/admin/operations/${encodeURIComponent(id)}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
...(request.headers.get('X-Tenant-ID') && {
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
}),
},
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json({ error: 'Backend error', details: errorText }, { status: response.status })
}
return NextResponse.json(await response.json())
} catch (error) {
console.error('Failed to delete operation:', error)
return NextResponse.json({ error: 'Failed to connect to backend' }, { status: 503 })
}
}

View File

@@ -1,6 +1,34 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const response = await fetch(`${BACKEND_URL}/api/v1/admin/pii-rules/${encodeURIComponent(id)}`, {
headers: {
'Content-Type': 'application/json',
...(request.headers.get('X-Tenant-ID') && {
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
}),
},
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json({ error: 'Backend error', details: errorText }, { status: response.status })
}
return NextResponse.json(await response.json())
} catch (error) {
console.error('Failed to fetch PII rule:', error)
return NextResponse.json({ error: 'Failed to connect to backend' }, { status: 503 })
}
}
export async function PUT(
request: NextRequest,

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
export async function GET(request: NextRequest) {
try {

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
export async function GET(request: NextRequest) {
try {

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
export async function GET(request: NextRequest) {
try {

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
export async function GET(
request: NextRequest,

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8002'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
export async function GET(request: NextRequest) {
try {

View File

@@ -2,17 +2,18 @@ import { NextRequest, NextResponse } from 'next/server'
import { Pool } from 'pg'
/**
* SDK State Management API
* SDK State Management API (Multi-Project)
*
* GET /api/sdk/v1/state?tenantId=xxx - Load state for a tenant
* POST /api/sdk/v1/state - Save state for a tenant
* DELETE /api/sdk/v1/state?tenantId=xxx - Clear state for a tenant
* GET /api/sdk/v1/state?tenantId=xxx&projectId=yyy - Load state for a tenant+project
* POST /api/sdk/v1/state - Save state for a tenant+project
* DELETE /api/sdk/v1/state?tenantId=xxx&projectId=yyy - Clear state
*
* Features:
* - Versioning for optimistic locking
* - Last-Modified headers
* - ETag support for caching
* - PostgreSQL persistence (with InMemory fallback)
* - projectId support for multi-project architecture
*/
// =============================================================================
@@ -32,25 +33,31 @@ interface StoredState {
// =============================================================================
interface StateStore {
get(tenantId: string): Promise<StoredState | null>
save(tenantId: string, state: unknown, userId?: string, expectedVersion?: number): Promise<StoredState>
delete(tenantId: string): Promise<boolean>
get(tenantId: string, projectId?: string): Promise<StoredState | null>
save(tenantId: string, state: unknown, userId?: string, expectedVersion?: number, projectId?: string): Promise<StoredState>
delete(tenantId: string, projectId?: string): Promise<boolean>
}
class InMemoryStateStore implements StateStore {
private store: Map<string, StoredState> = new Map()
async get(tenantId: string): Promise<StoredState | null> {
return this.store.get(tenantId) || null
private key(tenantId: string, projectId?: string): string {
return projectId ? `${tenantId}:${projectId}` : tenantId
}
async get(tenantId: string, projectId?: string): Promise<StoredState | null> {
return this.store.get(this.key(tenantId, projectId)) || null
}
async save(
tenantId: string,
state: unknown,
userId?: string,
expectedVersion?: number
expectedVersion?: number,
projectId?: string
): Promise<StoredState> {
const existing = this.store.get(tenantId)
const k = this.key(tenantId, projectId)
const existing = this.store.get(k)
if (expectedVersion !== undefined && existing && existing.version !== expectedVersion) {
const error = new Error('Version conflict') as Error & { status: number }
@@ -72,12 +79,12 @@ class InMemoryStateStore implements StateStore {
updatedAt: now,
}
this.store.set(tenantId, stored)
this.store.set(k, stored)
return stored
}
async delete(tenantId: string): Promise<boolean> {
return this.store.delete(tenantId)
async delete(tenantId: string, projectId?: string): Promise<boolean> {
return this.store.delete(this.key(tenantId, projectId))
}
}
@@ -93,11 +100,26 @@ class PostgreSQLStateStore implements StateStore {
})
}
async get(tenantId: string): Promise<StoredState | null> {
const result = await this.pool.query(
'SELECT state, version, user_id, created_at, updated_at FROM sdk_states WHERE tenant_id = $1',
[tenantId]
)
async get(tenantId: string, projectId?: string): Promise<StoredState | null> {
let result
if (projectId) {
result = await this.pool.query(
'SELECT state, version, user_id, created_at, updated_at FROM sdk_states WHERE tenant_id = $1 AND project_id = $2',
[tenantId, projectId]
)
} else {
// Backwards compatibility: find the single active project for this tenant
result = await this.pool.query(
`SELECT s.state, s.version, s.user_id, s.created_at, s.updated_at
FROM sdk_states s
LEFT JOIN compliance_projects p ON s.project_id = p.id
WHERE s.tenant_id = $1
AND (p.status = 'active' OR p.id IS NULL)
ORDER BY s.updated_at DESC
LIMIT 1`,
[tenantId]
)
}
if (result.rows.length === 0) return null
const row = result.rows[0]
return {
@@ -109,25 +131,71 @@ class PostgreSQLStateStore implements StateStore {
}
}
async save(tenantId: string, state: unknown, userId?: string, expectedVersion?: number): Promise<StoredState> {
async save(tenantId: string, state: unknown, userId?: string, expectedVersion?: number, projectId?: string): Promise<StoredState> {
const now = new Date().toISOString()
const stateWithTimestamp = {
...(state as object),
lastModified: now,
}
// Use UPSERT with version check
const result = await this.pool.query(`
INSERT INTO sdk_states (tenant_id, user_id, state, version, created_at, updated_at)
VALUES ($1, $2, $3::jsonb, 1, NOW(), NOW())
ON CONFLICT (tenant_id) DO UPDATE SET
state = $3::jsonb,
user_id = COALESCE($2, sdk_states.user_id),
version = sdk_states.version + 1,
updated_at = NOW()
WHERE ($4::int IS NULL OR sdk_states.version = $4)
RETURNING version, user_id, created_at, updated_at
`, [tenantId, userId, JSON.stringify(stateWithTimestamp), expectedVersion ?? null])
let result
if (projectId) {
// Multi-project: UPSERT on (tenant_id, project_id)
result = await this.pool.query(`
INSERT INTO sdk_states (tenant_id, project_id, user_id, state, version, created_at, updated_at)
VALUES ($1, $5, $2, $3::jsonb, 1, NOW(), NOW())
ON CONFLICT (tenant_id, project_id) DO UPDATE SET
state = $3::jsonb,
user_id = COALESCE($2, sdk_states.user_id),
version = sdk_states.version + 1,
updated_at = NOW()
WHERE ($4::int IS NULL OR sdk_states.version = $4)
RETURNING version, user_id, created_at, updated_at
`, [tenantId, userId, JSON.stringify(stateWithTimestamp), expectedVersion ?? null, projectId])
} else {
// Backwards compatibility: find the single project for this tenant
// First try to find an existing project
const projectResult = await this.pool.query(
`SELECT id FROM compliance_projects WHERE tenant_id = $1 AND status = 'active' ORDER BY created_at ASC LIMIT 1`,
[tenantId]
)
if (projectResult.rows.length > 0) {
const foundProjectId = projectResult.rows[0].id
result = await this.pool.query(`
INSERT INTO sdk_states (tenant_id, project_id, user_id, state, version, created_at, updated_at)
VALUES ($1, $5, $2, $3::jsonb, 1, NOW(), NOW())
ON CONFLICT (tenant_id, project_id) DO UPDATE SET
state = $3::jsonb,
user_id = COALESCE($2, sdk_states.user_id),
version = sdk_states.version + 1,
updated_at = NOW()
WHERE ($4::int IS NULL OR sdk_states.version = $4)
RETURNING version, user_id, created_at, updated_at
`, [tenantId, userId, JSON.stringify(stateWithTimestamp), expectedVersion ?? null, foundProjectId])
} else {
// No project exists — create a default one
const newProject = await this.pool.query(
`INSERT INTO compliance_projects (tenant_id, name, customer_type, status)
VALUES ($1, 'Projekt 1', 'new', 'active')
RETURNING id`,
[tenantId]
)
const newProjectId = newProject.rows[0].id
result = await this.pool.query(`
INSERT INTO sdk_states (tenant_id, project_id, user_id, state, version, created_at, updated_at)
VALUES ($1, $5, $2, $3::jsonb, 1, NOW(), NOW())
ON CONFLICT (tenant_id, project_id) DO UPDATE SET
state = $3::jsonb,
user_id = COALESCE($2, sdk_states.user_id),
version = sdk_states.version + 1,
updated_at = NOW()
WHERE ($4::int IS NULL OR sdk_states.version = $4)
RETURNING version, user_id, created_at, updated_at
`, [tenantId, userId, JSON.stringify(stateWithTimestamp), expectedVersion ?? null, newProjectId])
}
}
if (result.rows.length === 0) {
const error = new Error('Version conflict') as Error & { status: number }
@@ -145,11 +213,19 @@ class PostgreSQLStateStore implements StateStore {
}
}
async delete(tenantId: string): Promise<boolean> {
const result = await this.pool.query(
'DELETE FROM sdk_states WHERE tenant_id = $1',
[tenantId]
)
async delete(tenantId: string, projectId?: string): Promise<boolean> {
let result
if (projectId) {
result = await this.pool.query(
'DELETE FROM sdk_states WHERE tenant_id = $1 AND project_id = $2',
[tenantId, projectId]
)
} else {
result = await this.pool.query(
'DELETE FROM sdk_states WHERE tenant_id = $1',
[tenantId]
)
}
return (result.rowCount ?? 0) > 0
}
}
@@ -186,6 +262,7 @@ export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const tenantId = searchParams.get('tenantId')
const projectId = searchParams.get('projectId') || undefined
if (!tenantId) {
return NextResponse.json(
@@ -194,7 +271,7 @@ export async function GET(request: NextRequest) {
)
}
const stored = await stateStore.get(tenantId)
const stored = await stateStore.get(tenantId, projectId)
if (!stored) {
return NextResponse.json(
@@ -216,6 +293,7 @@ export async function GET(request: NextRequest) {
success: true,
data: {
tenantId,
projectId: projectId || null,
state: stored.state,
version: stored.version,
lastModified: stored.updatedAt,
@@ -241,7 +319,7 @@ export async function GET(request: NextRequest) {
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { tenantId, state, version } = body
const { tenantId, state, version, projectId } = body
if (!tenantId) {
return NextResponse.json(
@@ -261,7 +339,7 @@ export async function POST(request: NextRequest) {
const ifMatch = request.headers.get('If-Match')
const expectedVersion = ifMatch ? parseInt(ifMatch, 10) : version
const stored = await stateStore.save(tenantId, state, body.userId, expectedVersion)
const stored = await stateStore.save(tenantId, state, body.userId, expectedVersion, projectId || undefined)
const etag = generateETag(stored.version, stored.updatedAt)
@@ -270,6 +348,7 @@ export async function POST(request: NextRequest) {
success: true,
data: {
tenantId,
projectId: projectId || null,
state: stored.state,
version: stored.version,
lastModified: stored.updatedAt,
@@ -309,6 +388,7 @@ export async function DELETE(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const tenantId = searchParams.get('tenantId')
const projectId = searchParams.get('projectId') || undefined
if (!tenantId) {
return NextResponse.json(
@@ -317,7 +397,7 @@ export async function DELETE(request: NextRequest) {
)
}
const deleted = await stateStore.delete(tenantId)
const deleted = await stateStore.delete(tenantId, projectId)
if (!deleted) {
return NextResponse.json(

View File

@@ -1,119 +1,30 @@
import { NextRequest, NextResponse } from 'next/server'
import {
TOMGeneratorState,
createEmptyTOMGeneratorState,
} from '@/lib/sdk/tom-generator/types'
/**
* TOM Generator State API
* TOM Generator State API — Proxy to backend-compliance (Python/FastAPI)
*
* GET /api/sdk/v1/tom-generator/state?tenantId=xxx - Load TOM generator state
* POST /api/sdk/v1/tom-generator/state - Save TOM generator state
* DELETE /api/sdk/v1/tom-generator/state?tenantId=xxx - Clear state
*/
// =============================================================================
// STORAGE (In-Memory for development)
// =============================================================================
interface StoredTOMState {
state: TOMGeneratorState
version: number
createdAt: string
updatedAt: string
}
class InMemoryTOMStateStore {
private store: Map<string, StoredTOMState> = new Map()
async get(tenantId: string): Promise<StoredTOMState | null> {
return this.store.get(tenantId) || null
}
async save(tenantId: string, state: TOMGeneratorState, expectedVersion?: number): Promise<StoredTOMState> {
const existing = this.store.get(tenantId)
if (expectedVersion !== undefined && existing && existing.version !== expectedVersion) {
const error = new Error('Version conflict') as Error & { status: number }
error.status = 409
throw error
}
const now = new Date().toISOString()
const newVersion = existing ? existing.version + 1 : 1
const stored: StoredTOMState = {
state: {
...state,
updatedAt: new Date(now),
},
version: newVersion,
createdAt: existing?.createdAt || now,
updatedAt: now,
}
this.store.set(tenantId, stored)
return stored
}
async delete(tenantId: string): Promise<boolean> {
return this.store.delete(tenantId)
}
async list(): Promise<{ tenantId: string; updatedAt: string }[]> {
const result: { tenantId: string; updatedAt: string }[] = []
this.store.forEach((value, key) => {
result.push({ tenantId: key, updatedAt: value.updatedAt })
})
return result
}
}
const stateStore = new InMemoryTOMStateStore()
// =============================================================================
// HANDLERS
// =============================================================================
const BACKEND_URL = process.env.COMPLIANCE_BACKEND_URL || 'http://backend-compliance:8002'
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const tenantId = searchParams.get('tenantId')
// List all states if no tenantId provided
if (!tenantId) {
const states = await stateStore.list()
return NextResponse.json({
success: true,
data: states,
})
}
const url = tenantId
? `${BACKEND_URL}/api/compliance/tom/state?tenant_id=${encodeURIComponent(tenantId)}`
: `${BACKEND_URL}/api/compliance/tom/state`
const stored = await stateStore.get(tenantId)
if (!stored) {
// Return empty state for new tenants
const emptyState = createEmptyTOMGeneratorState(tenantId)
return NextResponse.json({
success: true,
data: {
tenantId,
state: emptyState,
version: 0,
isNew: true,
},
})
}
return NextResponse.json({
success: true,
data: {
tenantId,
state: stored.state,
version: stored.version,
lastModified: stored.updatedAt,
},
const res = await fetch(url, {
headers: { 'Content-Type': 'application/json' },
})
const data = await res.json()
return NextResponse.json(data, { status: res.status })
} catch (error) {
console.error('Failed to load TOM generator state:', error)
return NextResponse.json(
@@ -142,65 +53,19 @@ export async function POST(request: NextRequest) {
)
}
// Deserialize dates
const parsedState: TOMGeneratorState = {
...state,
createdAt: new Date(state.createdAt),
updatedAt: new Date(state.updatedAt),
steps: state.steps.map((step: { id: string; completed: boolean; data: unknown; validatedAt: string | null }) => ({
...step,
validatedAt: step.validatedAt ? new Date(step.validatedAt) : null,
})),
documents: state.documents?.map((doc: { uploadedAt: string; validFrom?: string; validUntil?: string; aiAnalysis?: { analyzedAt: string } }) => ({
...doc,
uploadedAt: new Date(doc.uploadedAt),
validFrom: doc.validFrom ? new Date(doc.validFrom) : null,
validUntil: doc.validUntil ? new Date(doc.validUntil) : null,
aiAnalysis: doc.aiAnalysis ? {
...doc.aiAnalysis,
analyzedAt: new Date(doc.aiAnalysis.analyzedAt),
} : null,
})) || [],
derivedTOMs: state.derivedTOMs?.map((tom: { implementationDate?: string; reviewDate?: string }) => ({
...tom,
implementationDate: tom.implementationDate ? new Date(tom.implementationDate) : null,
reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null,
})) || [],
gapAnalysis: state.gapAnalysis ? {
...state.gapAnalysis,
generatedAt: new Date(state.gapAnalysis.generatedAt),
} : null,
exports: state.exports?.map((exp: { generatedAt: string }) => ({
...exp,
generatedAt: new Date(exp.generatedAt),
})) || [],
}
const stored = await stateStore.save(tenantId, parsedState, version)
return NextResponse.json({
success: true,
data: {
tenantId,
state: stored.state,
version: stored.version,
lastModified: stored.updatedAt,
},
const res = await fetch(`${BACKEND_URL}/api/compliance/tom/state`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tenant_id: tenantId,
state,
version,
}),
})
const data = await res.json()
return NextResponse.json(data, { status: res.status })
} catch (error) {
const err = error as Error & { status?: number }
if (err.status === 409 || err.message === 'Version conflict') {
return NextResponse.json(
{
success: false,
error: 'Version conflict. State was modified by another request.',
code: 'VERSION_CONFLICT',
},
{ status: 409 }
)
}
console.error('Failed to save TOM generator state:', error)
return NextResponse.json(
{ success: false, error: 'Failed to save state' },
@@ -221,14 +86,16 @@ export async function DELETE(request: NextRequest) {
)
}
const deleted = await stateStore.delete(tenantId)
const res = await fetch(
`${BACKEND_URL}/api/compliance/tom/state?tenant_id=${encodeURIComponent(tenantId)}`,
{
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
}
)
return NextResponse.json({
success: true,
tenantId,
deleted,
deletedAt: new Date().toISOString(),
})
const data = await res.json()
return NextResponse.json(data, { status: res.status })
} catch (error) {
console.error('Failed to delete TOM generator state:', error)
return NextResponse.json(

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

@@ -0,0 +1,39 @@
import { NextRequest, NextResponse } from 'next/server'
const SDK_BASE_URL = process.env.SDK_BASE_URL || 'http://localhost:8085'
const DEFAULT_TENANT_ID = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
const DEFAULT_USER_ID = 'admin'
async function proxyRequest(request: NextRequest, method: string) {
try {
const pathSegments = request.nextUrl.pathname.replace('/api/sdk/v1/ucca/obligations/', '')
const targetUrl = `${SDK_BASE_URL}/sdk/v1/ucca/obligations/${pathSegments}${request.nextUrl.search}`
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-Tenant-ID': request.headers.get('X-Tenant-ID') || DEFAULT_TENANT_ID,
'X-User-ID': request.headers.get('X-User-ID') || DEFAULT_USER_ID,
}
const fetchOptions: RequestInit = { method, headers }
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
fetchOptions.body = await request.text()
}
const response = await fetch(targetUrl, fetchOptions)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json({ error: 'SDK backend error', details: errorText }, { status: response.status })
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('UCCA obligations proxy error:', error)
return NextResponse.json({ error: 'Failed to connect to SDK backend' }, { status: 503 })
}
}
export async function GET(request: NextRequest) { return proxyRequest(request, 'GET') }
export async function POST(request: NextRequest) { return proxyRequest(request, 'POST') }

View File

@@ -2,19 +2,19 @@ import { NextRequest, NextResponse } from 'next/server'
const SDK_BASE_URL = process.env.SDK_BASE_URL || 'http://localhost:8085'
const DEFAULT_TENANT_ID = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
const DEFAULT_USER_ID = 'admin'
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Forward the request to the SDK backend
const response = await fetch(`${SDK_BASE_URL}/sdk/v1/ucca/obligations/assess`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Forward tenant ID if present
...(request.headers.get('X-Tenant-ID') && {
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
}),
'X-Tenant-ID': request.headers.get('X-Tenant-ID') || DEFAULT_TENANT_ID,
'X-User-ID': request.headers.get('X-User-ID') || DEFAULT_USER_ID,
},
body: JSON.stringify(body),
})

View File

@@ -2,19 +2,19 @@ import { NextRequest, NextResponse } from 'next/server'
const SDK_BASE_URL = process.env.SDK_BASE_URL || 'http://localhost:8085'
const DEFAULT_TENANT_ID = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
const DEFAULT_USER_ID = 'admin'
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Forward the request to the SDK backend
const response = await fetch(`${SDK_BASE_URL}/sdk/v1/ucca/obligations/export/direct`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Forward tenant ID if present
...(request.headers.get('X-Tenant-ID') && {
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
}),
'X-Tenant-ID': request.headers.get('X-Tenant-ID') || DEFAULT_TENANT_ID,
'X-User-ID': request.headers.get('X-User-ID') || DEFAULT_USER_ID,
},
body: JSON.stringify(body),
})

View File

@@ -0,0 +1,122 @@
/**
* Vendor Compliance API Proxy - Catch-all route
* Proxies all /api/sdk/v1/vendor-compliance/* requests to backend-compliance
*
* Backend routes: vendors, contracts, findings, control-instances, controls, export
* All under /api/compliance/vendor-compliance/ prefix on backend-compliance:8002
*/
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/vendor-compliance`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
const headerNames = ['authorization', 'x-namespace-id', 'x-tenant-slug']
for (const name of headerNames) {
const value = request.headers.get(name)
if (value) {
headers[name] = value
}
}
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
const clientUserId = request.headers.get('x-user-id')
const clientTenantId = request.headers.get('x-tenant-id')
headers['X-User-ID'] = (clientUserId && uuidRegex.test(clientUserId)) ? clientUserId : '00000000-0000-0000-0000-000000000001'
headers['X-Tenant-ID'] = (clientTenantId && uuidRegex.test(clientTenantId)) ? clientTenantId : (process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e')
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(60000),
}
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
const body = await request.text()
if (body) {
fetchOptions.body = 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('Vendor Compliance API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Compliance 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

@@ -1,88 +0,0 @@
import { NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { ContractDocument } from '@/lib/sdk/vendor-compliance'
// In-memory storage for demo purposes
const contracts: Map<string, ContractDocument> = new Map()
export async function GET(request: NextRequest) {
try {
const contractList = Array.from(contracts.values())
return NextResponse.json({
success: true,
data: contractList,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error fetching contracts:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch contracts' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
// Handle multipart form data for file upload
const formData = await request.formData()
const file = formData.get('file') as File | null
const vendorId = formData.get('vendorId') as string
const metadataStr = formData.get('metadata') as string
if (!file || !vendorId) {
return NextResponse.json(
{ success: false, error: 'File and vendorId are required' },
{ status: 400 }
)
}
const metadata = metadataStr ? JSON.parse(metadataStr) : {}
const id = uuidv4()
// In production, upload file to storage (MinIO, S3, etc.)
const storagePath = `contracts/${id}/${file.name}`
const contract: ContractDocument = {
id,
tenantId: 'default',
vendorId,
fileName: `${id}-${file.name}`,
originalName: file.name,
mimeType: file.type,
fileSize: file.size,
storagePath,
documentType: metadata.documentType || 'OTHER',
version: metadata.version || '1.0',
previousVersionId: metadata.previousVersionId,
parties: metadata.parties,
effectiveDate: metadata.effectiveDate ? new Date(metadata.effectiveDate) : undefined,
expirationDate: metadata.expirationDate ? new Date(metadata.expirationDate) : undefined,
autoRenewal: metadata.autoRenewal,
renewalNoticePeriod: metadata.renewalNoticePeriod,
terminationNoticePeriod: metadata.terminationNoticePeriod,
reviewStatus: 'PENDING',
status: 'DRAFT',
createdAt: new Date(),
updatedAt: new Date(),
}
contracts.set(id, contract)
return NextResponse.json(
{
success: true,
data: contract,
timestamp: new Date().toISOString(),
},
{ status: 201 }
)
} catch (error) {
console.error('Error uploading contract:', error)
return NextResponse.json(
{ success: false, error: 'Failed to upload contract' },
{ status: 500 }
)
}
}

View File

@@ -1,28 +0,0 @@
import { NextRequest, NextResponse } from 'next/server'
import { CONTROLS_LIBRARY } from '@/lib/sdk/vendor-compliance'
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const domain = searchParams.get('domain')
let controls = [...CONTROLS_LIBRARY]
// Filter by domain if provided
if (domain) {
controls = controls.filter((c) => c.domain === domain)
}
return NextResponse.json({
success: true,
data: controls,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error fetching controls:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch controls' },
{ status: 500 }
)
}
}

View File

@@ -1,75 +0,0 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* GET /api/sdk/v1/vendor-compliance/export/[reportId]/download
*
* Download a generated report file.
* In production, this would redirect to a signed MinIO/S3 URL or stream the file.
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ reportId: string }> }
) {
const { reportId } = await params
// TODO: Implement actual file download
// This would typically:
// 1. Verify report exists and user has access
// 2. Generate signed URL for MinIO/S3
// 3. Redirect to signed URL or stream file
// For now, return a placeholder PDF
const placeholderContent = `
%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>
endobj
4 0 obj
<< /Length 200 >>
stream
BT
/F1 24 Tf
100 700 Td
(Vendor Compliance Report) Tj
/F1 12 Tf
100 650 Td
(Report ID: ${reportId}) Tj
100 620 Td
(Generated: ${new Date().toISOString()}) Tj
100 580 Td
(This is a placeholder. Implement actual report generation.) Tj
ET
endstream
endobj
5 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
endobj
xref
0 6
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000266 00000 n
0000000519 00000 n
trailer
<< /Size 6 /Root 1 0 R >>
startxref
598
%%EOF
`.trim()
// Return as PDF
return new NextResponse(placeholderContent, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="Report_${reportId.slice(0, 8)}.pdf"`,
},
})
}

View File

@@ -1,44 +0,0 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* GET /api/sdk/v1/vendor-compliance/export/[reportId]
*
* Get report metadata by ID.
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ reportId: string }> }
) {
const { reportId } = await params
// TODO: Fetch report metadata from database
// For now, return mock data
return NextResponse.json({
id: reportId,
status: 'completed',
filename: `Report_${reportId.slice(0, 8)}.pdf`,
generatedAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24h
})
}
/**
* DELETE /api/sdk/v1/vendor-compliance/export/[reportId]
*
* Delete a generated report.
*/
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ reportId: string }> }
) {
const { reportId } = await params
// TODO: Delete report from storage and database
console.log('Deleting report:', reportId)
return NextResponse.json({
success: true,
deletedId: reportId,
})
}

View File

@@ -1,118 +0,0 @@
import { NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
/**
* POST /api/sdk/v1/vendor-compliance/export
*
* Generate and export reports in various formats.
* Currently returns mock data - integrate with actual report generation service.
*/
interface ExportConfig {
reportType: 'VVT_EXPORT' | 'VENDOR_AUDIT' | 'ROPA' | 'MANAGEMENT_SUMMARY' | 'DPIA_INPUT'
format: 'PDF' | 'DOCX' | 'XLSX' | 'JSON'
scope: {
vendorIds: string[]
processingActivityIds: string[]
includeFindings: boolean
includeControls: boolean
includeRiskAssessment: boolean
dateRange?: {
from: string
to: string
}
}
}
const REPORT_TYPE_NAMES: Record<ExportConfig['reportType'], string> = {
VVT_EXPORT: 'Verarbeitungsverzeichnis',
VENDOR_AUDIT: 'Vendor-Audit-Pack',
ROPA: 'RoPA',
MANAGEMENT_SUMMARY: 'Management-Summary',
DPIA_INPUT: 'DSFA-Input',
}
const FORMAT_EXTENSIONS: Record<ExportConfig['format'], string> = {
PDF: 'pdf',
DOCX: 'docx',
XLSX: 'xlsx',
JSON: 'json',
}
export async function POST(request: NextRequest) {
try {
const config = (await request.json()) as ExportConfig
// Validate request
if (!config.reportType || !config.format) {
return NextResponse.json(
{ error: 'reportType and format are required' },
{ status: 400 }
)
}
// Generate report ID and filename
const reportId = uuidv4()
const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, '')
const filename = `${REPORT_TYPE_NAMES[config.reportType]}_${timestamp}.${FORMAT_EXTENSIONS[config.format]}`
// TODO: Implement actual report generation
// This would typically:
// 1. Fetch data from database based on scope
// 2. Generate report using template engine (e.g., docx-templates, pdfkit)
// 3. Store in MinIO/S3
// 4. Return download URL
// Mock implementation - simulate processing time
await new Promise((resolve) => setTimeout(resolve, 500))
// In production, this would be a signed URL to MinIO/S3
const downloadUrl = `/api/sdk/v1/vendor-compliance/export/${reportId}/download`
// Log export for audit trail
console.log('Export generated:', {
reportId,
reportType: config.reportType,
format: config.format,
scope: config.scope,
filename,
generatedAt: new Date().toISOString(),
})
return NextResponse.json({
id: reportId,
reportType: config.reportType,
format: config.format,
filename,
downloadUrl,
generatedAt: new Date().toISOString(),
scope: {
vendorCount: config.scope.vendorIds?.length || 0,
activityCount: config.scope.processingActivityIds?.length || 0,
includesFindings: config.scope.includeFindings,
includesControls: config.scope.includeControls,
includesRiskAssessment: config.scope.includeRiskAssessment,
},
})
} catch (error) {
console.error('Export error:', error)
return NextResponse.json(
{ error: 'Failed to generate export' },
{ status: 500 }
)
}
}
/**
* GET /api/sdk/v1/vendor-compliance/export
*
* List recent exports for the current tenant.
*/
export async function GET() {
// TODO: Implement fetching recent exports from database
// For now, return empty list
return NextResponse.json({
exports: [],
totalCount: 0,
})
}

View File

@@ -1,43 +0,0 @@
import { NextRequest, NextResponse } from 'next/server'
import { Finding } from '@/lib/sdk/vendor-compliance'
// In-memory storage for demo purposes
const findings: Map<string, Finding> = new Map()
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const vendorId = searchParams.get('vendorId')
const contractId = searchParams.get('contractId')
const status = searchParams.get('status')
let findingsList = Array.from(findings.values())
// Filter by vendor
if (vendorId) {
findingsList = findingsList.filter((f) => f.vendorId === vendorId)
}
// Filter by contract
if (contractId) {
findingsList = findingsList.filter((f) => f.contractId === contractId)
}
// Filter by status
if (status) {
findingsList = findingsList.filter((f) => f.status === status)
}
return NextResponse.json({
success: true,
data: findingsList,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error fetching findings:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch findings' },
{ status: 500 }
)
}
}

View File

@@ -1,70 +0,0 @@
import { NextRequest, NextResponse } from 'next/server'
// This would reference the same storage as the main route
// In production, this would be database calls
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
// In production, fetch from database
return NextResponse.json({
success: true,
data: null, // Would return the activity
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error fetching processing activity:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch processing activity' },
{ status: 500 }
)
}
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const body = await request.json()
// In production, update in database
return NextResponse.json({
success: true,
data: { id, ...body, updatedAt: new Date() },
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error updating processing activity:', error)
return NextResponse.json(
{ success: false, error: 'Failed to update processing activity' },
{ status: 500 }
)
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
// In production, delete from database
return NextResponse.json({
success: true,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error deleting processing activity:', error)
return NextResponse.json(
{ success: false, error: 'Failed to delete processing activity' },
{ status: 500 }
)
}
}

View File

@@ -1,84 +0,0 @@
import { NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { ProcessingActivity, generateVVTId } from '@/lib/sdk/vendor-compliance'
// In-memory storage for demo purposes
// In production, this would be replaced with database calls
const processingActivities: Map<string, ProcessingActivity> = new Map()
export async function GET(request: NextRequest) {
try {
const activities = Array.from(processingActivities.values())
return NextResponse.json({
success: true,
data: activities,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error fetching processing activities:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch processing activities' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Generate IDs
const id = uuidv4()
const existingIds = Array.from(processingActivities.values()).map((a) => a.vvtId)
const vvtId = body.vvtId || generateVVTId(existingIds)
const activity: ProcessingActivity = {
id,
tenantId: 'default', // Would come from auth context
vvtId,
name: body.name,
responsible: body.responsible,
dpoContact: body.dpoContact,
purposes: body.purposes || [],
dataSubjectCategories: body.dataSubjectCategories || [],
personalDataCategories: body.personalDataCategories || [],
recipientCategories: body.recipientCategories || [],
thirdCountryTransfers: body.thirdCountryTransfers || [],
retentionPeriod: body.retentionPeriod || { description: { de: '', en: '' } },
technicalMeasures: body.technicalMeasures || [],
legalBasis: body.legalBasis || [],
dataSources: body.dataSources || [],
systems: body.systems || [],
dataFlows: body.dataFlows || [],
protectionLevel: body.protectionLevel || 'MEDIUM',
dpiaRequired: body.dpiaRequired || false,
dpiaJustification: body.dpiaJustification,
subProcessors: body.subProcessors || [],
legalRetentionBasis: body.legalRetentionBasis,
status: body.status || 'DRAFT',
owner: body.owner || '',
lastReviewDate: body.lastReviewDate,
nextReviewDate: body.nextReviewDate,
createdAt: new Date(),
updatedAt: new Date(),
}
processingActivities.set(id, activity)
return NextResponse.json(
{
success: true,
data: activity,
timestamp: new Date().toISOString(),
},
{ status: 201 }
)
} catch (error) {
console.error('Error creating processing activity:', error)
return NextResponse.json(
{ success: false, error: 'Failed to create processing activity' },
{ status: 500 }
)
}
}

View File

@@ -1,82 +0,0 @@
import { NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { Vendor } from '@/lib/sdk/vendor-compliance'
// In-memory storage for demo purposes
const vendors: Map<string, Vendor> = new Map()
export async function GET(request: NextRequest) {
try {
const vendorList = Array.from(vendors.values())
return NextResponse.json({
success: true,
data: vendorList,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error fetching vendors:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch vendors' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const id = uuidv4()
const vendor: Vendor = {
id,
tenantId: 'default',
name: body.name,
legalForm: body.legalForm,
country: body.country,
address: body.address,
website: body.website,
role: body.role,
serviceDescription: body.serviceDescription,
serviceCategory: body.serviceCategory,
dataAccessLevel: body.dataAccessLevel || 'NONE',
processingLocations: body.processingLocations || [],
transferMechanisms: body.transferMechanisms || [],
certifications: body.certifications || [],
primaryContact: body.primaryContact,
dpoContact: body.dpoContact,
securityContact: body.securityContact,
contractTypes: body.contractTypes || [],
contracts: body.contracts || [],
inherentRiskScore: body.inherentRiskScore || 50,
residualRiskScore: body.residualRiskScore || 50,
manualRiskAdjustment: body.manualRiskAdjustment,
riskJustification: body.riskJustification,
reviewFrequency: body.reviewFrequency || 'ANNUAL',
lastReviewDate: body.lastReviewDate,
nextReviewDate: body.nextReviewDate,
status: body.status || 'ACTIVE',
processingActivityIds: body.processingActivityIds || [],
notes: body.notes,
createdAt: new Date(),
updatedAt: new Date(),
}
vendors.set(id, vendor)
return NextResponse.json(
{
success: true,
data: vendor,
timestamp: new Date().toISOString(),
},
{ status: 201 }
)
} catch (error) {
console.error('Error creating vendor:', error)
return NextResponse.json(
{ success: false, error: 'Failed to create vendor' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,87 @@
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
/**
* Proxy: GET /api/sdk/v1/wiki?endpoint=...
*
* Routes to backend wiki endpoints:
* endpoint=categories → GET /api/compliance/v1/wiki/categories
* endpoint=articles → GET /api/compliance/v1/wiki/articles(?category_id=...)
* endpoint=search → GET /api/compliance/v1/wiki/search?q=...
* endpoint=article&id= → GET /api/compliance/v1/wiki/articles/{id}
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const endpoint = searchParams.get('endpoint') || 'categories'
let backendPath: string
switch (endpoint) {
case 'categories':
backendPath = '/api/compliance/v1/wiki/categories'
break
case 'articles': {
const categoryId = searchParams.get('category_id')
backendPath = '/api/compliance/v1/wiki/articles'
if (categoryId) {
backendPath += `?category_id=${encodeURIComponent(categoryId)}`
}
break
}
case 'article': {
const articleId = searchParams.get('id')
if (!articleId) {
return NextResponse.json(
{ error: 'Missing article id' },
{ status: 400 }
)
}
backendPath = `/api/compliance/v1/wiki/articles/${encodeURIComponent(articleId)}`
break
}
case 'search': {
const query = searchParams.get('q')
if (!query) {
return NextResponse.json(
{ error: 'Missing search query' },
{ status: 400 }
)
}
backendPath = `/api/compliance/v1/wiki/search?q=${encodeURIComponent(query)}`
break
}
default:
return NextResponse.json(
{ error: `Unknown endpoint: ${endpoint}` },
{ status: 400 }
)
}
const response = await fetch(`${BACKEND_URL}${backendPath}`)
if (!response.ok) {
if (response.status === 404) {
return NextResponse.json(null, { status: 404 })
}
const errorText = await response.text()
return NextResponse.json(
{ error: 'Backend error', details: errorText },
{ status: response.status }
)
}
return NextResponse.json(await response.json())
} catch (error) {
console.error('Wiki proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to backend' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,119 @@
/**
* Workshop API Proxy - Catch-all route
* Proxies all /api/sdk/v1/workshops/* requests to ai-compliance-sdk backend
*/
import { NextRequest, NextResponse } from 'next/server'
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${SDK_BACKEND_URL}/sdk/v1/workshops`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
const headerNames = ['authorization', 'x-namespace-id', 'x-tenant-slug']
for (const name of headerNames) {
const value = request.headers.get(name)
if (value) {
headers[name] = value
}
}
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
const clientUserId = request.headers.get('x-user-id')
const clientTenantId = request.headers.get('x-tenant-id')
headers['X-User-ID'] = (clientUserId && uuidRegex.test(clientUserId)) ? clientUserId : '00000000-0000-0000-0000-000000000001'
headers['X-Tenant-ID'] = (clientTenantId && uuidRegex.test(clientTenantId)) ? clientTenantId : (process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e')
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(60000),
}
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
const body = await request.text()
if (body) {
fetchOptions.body = 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('Workshop API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum SDK 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

@@ -14,7 +14,7 @@ const LOG_EXTRACT_URL = process.env.NEXT_PUBLIC_APP_URL
: 'http://localhost:3002/api/infrastructure/log-extract/extract'
// Test service API URL for backlog insertion
const TEST_SERVICE_URL = process.env.TEST_SERVICE_URL || 'http://localhost:8086'
const TEST_SERVICE_URL = process.env.TEST_SERVICE_URL || 'http://localhost:8002'
// =============================================================================
// Helper Functions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,333 @@
'use client'
import React, { useEffect, useState, useCallback } from 'react'
import Link from 'next/link'
import { useParams } from 'next/navigation'
interface AgentDetail {
id: string
name: string
description: string
color: string
icon: string
status: 'active' | 'inactive' | 'error'
version: string
soulContent: string
createdAt: string | null
updatedAt: string | null
fileSize: number
stats: {
sessionsToday: number
avgResponseTime: string
successRate: string
}
}
interface BackupEntry {
filename: string
timestamp: number
size: number
}
function StatusBadge({ status }: { status: string }) {
const colors = {
active: 'bg-green-100 text-green-700',
inactive: 'bg-gray-100 text-gray-600',
error: 'bg-red-100 text-red-700',
}
const labels = { active: 'Aktiv', inactive: 'Inaktiv', error: 'Fehler' }
return (
<span className={`px-2.5 py-1 rounded-full text-xs font-medium ${colors[status as keyof typeof colors] || colors.inactive}`}>
{labels[status as keyof typeof labels] || status}
</span>
)
}
export default function AgentDetailPage() {
const params = useParams()
const agentId = params.agentId as string
const [agent, setAgent] = useState<AgentDetail | null>(null)
const [loading, setLoading] = useState(true)
const [activeTab, setActiveTab] = useState<'soul' | 'stats' | 'history'>('soul')
const [editing, setEditing] = useState(false)
const [editContent, setEditContent] = useState('')
const [saving, setSaving] = useState(false)
const [saveMessage, setSaveMessage] = useState('')
const [backups, setBackups] = useState<BackupEntry[]>([])
const loadAgent = useCallback(async () => {
try {
const res = await fetch(`/api/sdk/agents/${agentId}`)
if (res.ok) {
const data = await res.json()
setAgent(data)
if (!editing) setEditContent(data.soulContent)
}
} catch (err) {
console.error('Failed to load agent:', err)
} finally {
setLoading(false)
}
}, [agentId, editing])
const loadBackups = useCallback(async () => {
try {
const res = await fetch(`/api/sdk/agents/${agentId}/soul?history=true`)
if (res.ok) {
const data = await res.json()
setBackups(data.backups || [])
}
} catch (err) {
console.error('Failed to load backups:', err)
}
}, [agentId])
useEffect(() => {
loadAgent()
}, [loadAgent])
useEffect(() => {
if (activeTab === 'history') loadBackups()
}, [activeTab, loadBackups])
const handleSave = async () => {
setSaving(true)
setSaveMessage('')
try {
const res = await fetch(`/api/sdk/agents/${agentId}/soul`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: editContent }),
})
if (res.ok) {
setSaveMessage('SOUL-Datei gespeichert')
setEditing(false)
await loadAgent()
} else {
const err = await res.json()
setSaveMessage(`Fehler: ${err.error}`)
}
} catch {
setSaveMessage('Speichern fehlgeschlagen')
} finally {
setSaving(false)
setTimeout(() => setSaveMessage(''), 3000)
}
}
const handleReset = () => {
if (agent) {
setEditContent(agent.soulContent)
setEditing(false)
}
}
if (loading) {
return (
<div className="p-8">
<div className="animate-pulse space-y-4">
<div className="h-8 bg-gray-200 rounded w-48" />
<div className="h-64 bg-gray-200 rounded-xl" />
</div>
</div>
)
}
if (!agent) {
return (
<div className="p-8">
<p className="text-red-600">Agent nicht gefunden.</p>
<Link href="/sdk/agents" className="text-purple-600 hover:underline mt-2 inline-block">
Zurueck zur Uebersicht
</Link>
</div>
)
}
return (
<div className="p-8 max-w-5xl">
{/* Header */}
<div className="flex items-center gap-4 mb-6">
<Link href="/sdk/agents" className="text-gray-400 hover:text-gray-600 transition-colors">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</Link>
<div
className="w-12 h-12 rounded-xl flex items-center justify-center text-white"
style={{ backgroundColor: agent.color }}
>
{agent.icon === 'shield' ? (
<svg className="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
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" />
</svg>
) : (
<svg className="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
)}
</div>
<div className="flex-1">
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold text-gray-900">{agent.name}</h1>
<StatusBadge status={agent.status} />
</div>
<p className="text-sm text-gray-500">{agent.description}</p>
</div>
</div>
{/* Stats Bar */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
<div className="bg-white border border-gray-200 rounded-xl px-4 py-3">
<div className="text-lg font-semibold text-gray-900">{agent.stats.sessionsToday}</div>
<div className="text-xs text-gray-500">Sessions heute</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl px-4 py-3">
<div className="text-lg font-semibold text-gray-900">{agent.stats.avgResponseTime}</div>
<div className="text-xs text-gray-500">Avg. Antwortzeit</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl px-4 py-3">
<div className="text-lg font-semibold text-gray-900">{agent.stats.successRate}</div>
<div className="text-xs text-gray-500">Erfolgsrate</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl px-4 py-3">
<div className="text-lg font-semibold text-gray-900">v{agent.version}</div>
<div className="text-xs text-gray-500">Version</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl px-4 py-3">
<div className="text-lg font-semibold text-gray-900">{agent.fileSize ? `${(agent.fileSize / 1024).toFixed(1)}k` : '—'}</div>
<div className="text-xs text-gray-500">SOUL-Groesse</div>
</div>
</div>
{/* Tabs */}
<div className="flex border-b border-gray-200 mb-6">
{[
{ id: 'soul' as const, label: 'SOUL-File' },
{ id: 'stats' as const, label: 'Live-Statistiken' },
{ id: 'history' as const, label: 'Aenderungshistorie' },
].map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-4 py-3 text-sm font-medium border-b-2 transition-colors ${
activeTab === tab.id
? 'border-purple-600 text-purple-700'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
{tab.label}
</button>
))}
</div>
{/* Tab Content */}
{activeTab === 'soul' && (
<div>
{/* Toolbar */}
<div className="flex items-center justify-between mb-3">
<div className="text-sm text-gray-500">
{agent.updatedAt && `Zuletzt geaendert: ${new Date(agent.updatedAt).toLocaleString('de-DE')}`}
</div>
<div className="flex items-center gap-2">
{saveMessage && (
<span className={`text-sm ${saveMessage.startsWith('Fehler') ? 'text-red-600' : 'text-green-600'}`}>
{saveMessage}
</span>
)}
{editing ? (
<>
<button
onClick={handleReset}
className="px-3 py-1.5 text-sm text-gray-600 bg-white border border-gray-300 rounded-lg hover:bg-gray-50"
>
Abbrechen
</button>
<button
onClick={handleSave}
disabled={saving}
className="px-3 py-1.5 text-sm text-white bg-purple-600 rounded-lg hover:bg-purple-700 disabled:opacity-50"
>
{saving ? 'Speichern...' : 'Speichern'}
</button>
</>
) : (
<button
onClick={() => setEditing(true)}
className="px-3 py-1.5 text-sm text-purple-600 bg-purple-50 border border-purple-200 rounded-lg hover:bg-purple-100"
>
Bearbeiten
</button>
)}
</div>
</div>
{/* Editor */}
<textarea
value={editContent}
onChange={(e) => setEditContent(e.target.value)}
readOnly={!editing}
className={`w-full h-[600px] font-mono text-sm p-4 border rounded-xl resize-none focus:outline-none ${
editing
? 'border-purple-300 bg-white focus:ring-2 focus:ring-purple-200'
: 'border-gray-200 bg-gray-50 cursor-default'
}`}
spellCheck={false}
/>
</div>
)}
{activeTab === 'stats' && (
<div className="bg-white border border-gray-200 rounded-xl p-8 text-center">
<svg className="w-16 h-16 text-gray-300 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
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>
<h3 className="text-lg font-medium text-gray-900 mb-2">Live-Statistiken</h3>
<p className="text-gray-500">
Detaillierte Echtzeit-Statistiken werden in einer zukuenftigen Version implementiert.
</p>
</div>
)}
{activeTab === 'history' && (
<div>
{backups.length === 0 ? (
<div className="bg-white border border-gray-200 rounded-xl p-8 text-center">
<svg className="w-16 h-16 text-gray-300 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h3 className="text-lg font-medium text-gray-900 mb-2">Keine Backups vorhanden</h3>
<p className="text-gray-500">
Backups werden automatisch beim Speichern von SOUL-Dateien erstellt.
</p>
</div>
) : (
<div className="space-y-3">
{backups.map((backup) => (
<div key={backup.filename} className="bg-white border border-gray-200 rounded-xl px-5 py-4 flex items-center justify-between">
<div>
<div className="font-medium text-sm text-gray-900">
{new Date(backup.timestamp).toLocaleString('de-DE')}
</div>
<div className="text-xs text-gray-500 mt-0.5">
{(backup.size / 1024).toFixed(1)} KB {backup.filename}
</div>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-purple-200" />
<span className="text-xs text-gray-400">Backup</span>
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,221 @@
'use client'
import React from 'react'
import Link from 'next/link'
export default function AgentArchitecturePage() {
return (
<div className="p-8 max-w-5xl">
{/* Header */}
<div className="flex items-center gap-4 mb-8">
<Link href="/sdk/agents" className="text-gray-400 hover:text-gray-600 transition-colors">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</Link>
<div>
<h1 className="text-2xl font-bold text-gray-900">Agent-Architektur</h1>
<p className="text-gray-500 mt-1">2-Agenten-System mit RAG, SOUL-Files und LLM-Backend</p>
</div>
</div>
{/* Architecture Overview */}
<section className="mb-10">
<h2 className="text-lg font-semibold text-gray-900 mb-4">System-Uebersicht</h2>
<div className="bg-gray-900 text-green-400 font-mono text-sm rounded-xl p-6 overflow-x-auto">
<pre>{`
+---------------------+ +---------------------+
| Compliance Advisor | | Drafting Agent |
| (RAG-Chat) | | (4-Modi) |
| - DSGVO | | - Explain |
| - AI Act | | - Ask |
| - 6 Sammlungen | | - Draft |
+--------+------------+ | - Validate |
| +--------+------------+
| |
+----------+ +--------------+
| |
v v
+--------+--+--------+
| SOUL-File System |
| agent-core/soul/ |
| - .soul.md Dateien |
| - .backups/ |
| - 30s TTL Cache |
+--------+-----------+
|
v
+--------+-----------+
| RAG-Service :8097 |
| 6 Sammlungen: |
| - gesetze |
| - ce |
| - datenschutz |
| - dsfa_corpus |
| - recht |
| - legal_templates |
+--------+-----------+
|
v
+--------+-----------+
| Ollama LLM |
| qwen2.5vl:32b |
| Temp: 0.2-0.3 |
+--------------------+
`.trim()}</pre>
</div>
</section>
{/* Agent Cards */}
<section className="mb-10">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Agenten im Detail</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Compliance Advisor */}
<div className="bg-white border border-gray-200 rounded-2xl p-6">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-xl bg-purple-600 flex items-center justify-center text-white">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
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" />
</svg>
</div>
<h3 className="font-semibold text-gray-900">Compliance Advisor</h3>
</div>
<ul className="space-y-2 text-sm text-gray-600">
<li className="flex items-start gap-2">
<span className="text-purple-600 mt-0.5">&#8226;</span>
Multi-Collection RAG (6 Sammlungen parallel)
</li>
<li className="flex items-start gap-2">
<span className="text-purple-600 mt-0.5">&#8226;</span>
Laender-Filter: DE, AT, CH, EU
</li>
<li className="flex items-start gap-2">
<span className="text-purple-600 mt-0.5">&#8226;</span>
Streaming-Antworten (Ollama)
</li>
<li className="flex items-start gap-2">
<span className="text-purple-600 mt-0.5">&#8226;</span>
Quellenschutz: Keine Collection-Namen preisgeben
</li>
<li className="flex items-start gap-2">
<span className="text-purple-600 mt-0.5">&#8226;</span>
IFRS-Besonderheit: Nur EU-endorsed Standards
</li>
</ul>
<div className="mt-4 pt-3 border-t border-gray-100">
<code className="text-xs text-gray-500">API: POST /api/sdk/compliance-advisor/chat</code>
</div>
</div>
{/* Drafting Agent */}
<div className="bg-white border border-gray-200 rounded-2xl p-6">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-xl bg-blue-600 flex items-center justify-center text-white">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</div>
<h3 className="font-semibold text-gray-900">Drafting Agent</h3>
</div>
<ul className="space-y-2 text-sm text-gray-600">
<li className="flex items-start gap-2">
<span className="text-blue-600 mt-0.5">&#8226;</span>
<strong>Explain</strong>: Fragen verstaendlich beantworten
</li>
<li className="flex items-start gap-2">
<span className="text-blue-600 mt-0.5">&#8226;</span>
<strong>Ask</strong>: Luecken analysieren, gezielte Fragen
</li>
<li className="flex items-start gap-2">
<span className="text-blue-600 mt-0.5">&#8226;</span>
<strong>Draft</strong>: Dokument-Sections entwerfen (JSON)
</li>
<li className="flex items-start gap-2">
<span className="text-blue-600 mt-0.5">&#8226;</span>
<strong>Validate</strong>: Cross-Dokument-Konsistenz pruefen
</li>
<li className="flex items-start gap-2">
<span className="text-blue-600 mt-0.5">&#8226;</span>
SDK-State-Projection fuer token-effizienten Kontext
</li>
</ul>
<div className="mt-4 pt-3 border-t border-gray-100">
<code className="text-xs text-gray-500">API: POST /api/sdk/drafting-engine/chat</code>
</div>
</div>
</div>
</section>
{/* SOUL System */}
<section className="mb-10">
<h2 className="text-lg font-semibold text-gray-900 mb-4">SOUL-File System</h2>
<div className="bg-white border border-gray-200 rounded-2xl p-6">
<p className="text-sm text-gray-600 mb-4">
Jeder Agent hat eine <code className="bg-gray-100 px-1.5 py-0.5 rounded text-purple-700">.soul.md</code> Datei,
die seinen System-Prompt definiert. Diese Datei kann ueber die Agent-Detail-Seite live bearbeitet werden.
Aenderungen werden nach 30 Sekunden (TTL-Cache) wirksam.
</p>
<div className="bg-gray-50 rounded-xl p-4 font-mono text-sm text-gray-700">
<div>agent-core/</div>
<div className="ml-4">soul/</div>
<div className="ml-8 text-purple-700">compliance-advisor.soul.md</div>
<div className="ml-8 text-blue-700">drafting-agent.soul.md</div>
<div className="ml-8 text-gray-400">.backups/</div>
<div className="ml-12 text-gray-400">compliance-advisor-1709567890123.soul.md</div>
<div className="ml-12 text-gray-400">...</div>
</div>
</div>
</section>
{/* RAG Collections */}
<section className="mb-10">
<h2 className="text-lg font-semibold text-gray-900 mb-4">RAG-Sammlungen</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{[
{ name: 'bp_compliance_gesetze', desc: 'DSGVO, BDSG, AI Act, TTDSG, nationale Gesetze', icon: '&#9878;' },
{ name: 'bp_compliance_ce', desc: 'EU Maschinenverordnung, Blue Guide, CE-Kennzeichnung', icon: '&#9881;' },
{ name: 'bp_compliance_datenschutz', desc: 'DSK-Kurzpapiere, SDM, EDPB Guidelines', icon: '&#128274;' },
{ name: 'bp_dsfa_corpus', desc: 'DSFA-Listen, Muss-Listen der Aufsichtsbehoerden', icon: '&#128203;' },
{ name: 'bp_compliance_recht', desc: 'WP248, EU-Verordnungen (DORA, MiCA, etc.)', icon: '&#9878;' },
{ name: 'bp_legal_templates', desc: 'Vorlagen fuer Datenschutz-Dokumente', icon: '&#128196;' },
].map(col => (
<div key={col.name} className="bg-white border border-gray-200 rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<span dangerouslySetInnerHTML={{ __html: col.icon }} />
<code className="text-xs font-medium text-gray-700">{col.name}</code>
</div>
<p className="text-xs text-gray-500">{col.desc}</p>
</div>
))}
</div>
</section>
{/* LLM Config */}
<section>
<h2 className="text-lg font-semibold text-gray-900 mb-4">LLM-Konfiguration</h2>
<div className="bg-white border border-gray-200 rounded-2xl p-6">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<div className="text-xs text-gray-500 mb-1">Modell</div>
<div className="font-medium text-gray-900">qwen2.5vl:32b</div>
</div>
<div>
<div className="text-xs text-gray-500 mb-1">Backend</div>
<div className="font-medium text-gray-900">Ollama</div>
</div>
<div>
<div className="text-xs text-gray-500 mb-1">Temperatur</div>
<div className="font-medium text-gray-900">0.2 - 0.3</div>
</div>
<div>
<div className="text-xs text-gray-500 mb-1">Max Tokens</div>
<div className="font-medium text-gray-900">8.192 - 16.384</div>
</div>
</div>
</div>
</section>
</div>
)
}

View File

@@ -0,0 +1,224 @@
'use client'
import React, { useEffect, useState, useCallback } from 'react'
import Link from 'next/link'
interface AgentData {
id: string
name: string
description: string
color: string
icon: string
status: 'active' | 'inactive' | 'error'
version: string
stats: {
sessionsToday: number
avgResponseTime: string
successRate: string
}
}
interface AgentsResponse {
agents: AgentData[]
stats: {
total: number
active: number
inactive: number
error: number
totalSessions: number
avgResponseTime: string
}
timestamp: string
}
const ShieldIcon = () => (
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
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" />
</svg>
)
const PencilIcon = () => (
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
)
function getAgentIcon(icon: string) {
switch (icon) {
case 'shield': return <ShieldIcon />
case 'pencil': return <PencilIcon />
default: return <ShieldIcon />
}
}
function StatusBadge({ status }: { status: string }) {
const colors = {
active: 'bg-green-100 text-green-700',
inactive: 'bg-gray-100 text-gray-600',
error: 'bg-red-100 text-red-700',
}
const labels = { active: 'Aktiv', inactive: 'Inaktiv', error: 'Fehler' }
return (
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${colors[status as keyof typeof colors] || colors.inactive}`}>
{labels[status as keyof typeof labels] || status}
</span>
)
}
export default function AgentsDashboardPage() {
const [data, setData] = useState<AgentsResponse | null>(null)
const [loading, setLoading] = useState(true)
const loadAgents = useCallback(async () => {
try {
const res = await fetch('/api/sdk/agents')
if (res.ok) {
setData(await res.json())
}
} catch (err) {
console.error('Failed to load agents:', err)
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
loadAgents()
const interval = setInterval(loadAgents, 30_000)
return () => clearInterval(interval)
}, [loadAgents])
if (loading) {
return (
<div className="p-8">
<div className="animate-pulse space-y-4">
<div className="h-8 bg-gray-200 rounded w-64" />
<div className="grid grid-cols-3 gap-4">
{[1, 2, 3].map(i => <div key={i} className="h-24 bg-gray-200 rounded-xl" />)}
</div>
</div>
</div>
)
}
const stats = data?.stats
const agents = data?.agents || []
return (
<div className="p-8 max-w-6xl">
{/* Header */}
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Compliance-Agenten</h1>
<p className="text-gray-500 mt-1">
Verwaltung und Konfiguration der KI-Agenten im Compliance SDK
</p>
</div>
{/* Quick Links */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-8">
<Link href="/sdk/agents/architecture" className="flex items-center gap-2 px-4 py-3 bg-white border border-gray-200 rounded-xl hover:border-purple-300 hover:bg-purple-50 transition-colors text-sm">
<svg className="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2" />
</svg>
Architektur
</Link>
<Link href="/sdk/agents/sessions" className="flex items-center gap-2 px-4 py-3 bg-white border border-gray-200 rounded-xl hover:border-purple-300 hover:bg-purple-50 transition-colors text-sm">
<svg className="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
Sessions
</Link>
<Link href="/sdk/agents/statistics" className="flex items-center gap-2 px-4 py-3 bg-white border border-gray-200 rounded-xl hover:border-purple-300 hover:bg-purple-50 transition-colors text-sm">
<svg className="w-5 h-5 text-purple-600" 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>
Statistiken
</Link>
<Link href="/sdk/document-generator" className="flex items-center gap-2 px-4 py-3 bg-white border border-gray-200 rounded-xl hover:border-purple-300 hover:bg-purple-50 transition-colors text-sm">
<svg className="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 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>
Dokument-Generator
</Link>
</div>
{/* Stats Bar */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-8">
<div className="bg-white border border-gray-200 rounded-xl px-4 py-3">
<div className="text-2xl font-bold text-gray-900">{stats?.total || 0}</div>
<div className="text-xs text-gray-500">Agenten gesamt</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl px-4 py-3">
<div className="text-2xl font-bold text-green-600">{stats?.active || 0}</div>
<div className="text-xs text-gray-500">Aktiv</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl px-4 py-3">
<div className="text-2xl font-bold text-red-600">{stats?.error || 0}</div>
<div className="text-xs text-gray-500">Fehler</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl px-4 py-3">
<div className="text-2xl font-bold text-gray-900">{stats?.totalSessions || 0}</div>
<div className="text-xs text-gray-500">Sessions heute</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl px-4 py-3">
<div className="text-2xl font-bold text-gray-900">{stats?.avgResponseTime || '—'}</div>
<div className="text-xs text-gray-500">Avg. Antwortzeit</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl px-4 py-3">
<div className="text-2xl font-bold text-gray-900">6</div>
<div className="text-xs text-gray-500">RAG-Sammlungen</div>
</div>
</div>
{/* Agent Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{agents.map((agent) => (
<Link
key={agent.id}
href={`/sdk/agents/${agent.id}`}
className="bg-white border border-gray-200 rounded-2xl p-6 hover:border-purple-300 hover:shadow-lg transition-all group"
>
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div
className="w-12 h-12 rounded-xl flex items-center justify-center text-white"
style={{ backgroundColor: agent.color }}
>
{getAgentIcon(agent.icon)}
</div>
<div>
<h2 className="font-semibold text-gray-900 group-hover:text-purple-700 transition-colors">
{agent.name}
</h2>
<div className="text-xs text-gray-400">v{agent.version}</div>
</div>
</div>
<StatusBadge status={agent.status} />
</div>
<p className="text-sm text-gray-600 mb-4 line-clamp-2">
{agent.description}
</p>
<div className="grid grid-cols-3 gap-3 pt-3 border-t border-gray-100">
<div>
<div className="text-lg font-semibold text-gray-900">{agent.stats.sessionsToday}</div>
<div className="text-xs text-gray-500">Sessions</div>
</div>
<div>
<div className="text-lg font-semibold text-gray-900">{agent.stats.avgResponseTime}</div>
<div className="text-xs text-gray-500">Avg. Zeit</div>
</div>
<div>
<div className="text-lg font-semibold text-gray-900">{agent.stats.successRate}</div>
<div className="text-xs text-gray-500">Erfolgsrate</div>
</div>
</div>
</Link>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,34 @@
'use client'
import React from 'react'
import Link from 'next/link'
export default function AgentSessionsPage() {
return (
<div className="p-8 max-w-5xl">
<div className="flex items-center gap-4 mb-8">
<Link href="/sdk/agents" className="text-gray-400 hover:text-gray-600 transition-colors">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</Link>
<div>
<h1 className="text-2xl font-bold text-gray-900">Agent-Sessions</h1>
<p className="text-gray-500 mt-1">Chat-Verlaeufe und Session-Management</p>
</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl p-12 text-center">
<svg className="w-20 h-20 text-gray-300 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
<h2 className="text-xl font-medium text-gray-900 mb-2">Sessions-Tracking</h2>
<p className="text-gray-500 max-w-md mx-auto">
Das Session-Tracking fuer Compliance-Agenten wird in einer zukuenftigen Version implementiert.
Hier werden Chat-Verlaeufe, Antwortqualitaet und Nutzer-Feedback angezeigt.
</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,34 @@
'use client'
import React from 'react'
import Link from 'next/link'
export default function AgentStatisticsPage() {
return (
<div className="p-8 max-w-5xl">
<div className="flex items-center gap-4 mb-8">
<Link href="/sdk/agents" className="text-gray-400 hover:text-gray-600 transition-colors">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</Link>
<div>
<h1 className="text-2xl font-bold text-gray-900">Agent-Statistiken</h1>
<p className="text-gray-500 mt-1">Performance-Metriken und Nutzungsanalysen</p>
</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl p-12 text-center">
<svg className="w-20 h-20 text-gray-300 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
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>
<h2 className="text-xl font-medium text-gray-900 mb-2">Agent-Statistiken</h2>
<p className="text-gray-500 max-w-md mx-auto">
Detaillierte Statistiken wie Antwortzeiten, Erfolgsraten, haeufigste Themen und
RAG-Trefferquoten werden in einer zukuenftigen Version implementiert.
</p>
</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

@@ -0,0 +1,404 @@
'use client'
import { useState, useMemo, useRef } from 'react'
import { apiModules } from '@/lib/sdk/api-docs/endpoints'
import type { HttpMethod, BackendService, ApiExposure } from '@/lib/sdk/api-docs/types'
const METHOD_COLORS: Record<HttpMethod, string> = {
GET: 'bg-green-100 text-green-800',
POST: 'bg-blue-100 text-blue-800',
PUT: 'bg-yellow-100 text-yellow-800',
DELETE: 'bg-red-100 text-red-800',
PATCH: 'bg-purple-100 text-purple-800',
}
const EXPOSURE_CONFIG: Record<ApiExposure, { label: string; color: string; description: string }> = {
public: { label: 'Oeffentlich', color: 'bg-green-100 text-green-800 border-green-200', description: 'Internet-exponiert' },
partner: { label: 'Integration', color: 'bg-blue-100 text-blue-800 border-blue-200', description: 'API-Key/OAuth' },
internal: { label: 'Intern', color: 'bg-gray-100 text-gray-700 border-gray-200', description: 'Nur Admin' },
admin: { label: 'Wartung', color: 'bg-orange-100 text-orange-800 border-orange-200', description: 'Nur Setup' },
}
type ServiceFilter = 'all' | BackendService
type ExposureFilter = 'all' | ApiExposure
function ExposureBadge({ exposure }: { exposure: ApiExposure }) {
const config = EXPOSURE_CONFIG[exposure]
return (
<span className={`inline-block text-[10px] font-medium px-1.5 py-0.5 rounded border ${config.color}`}>
{config.label}
</span>
)
}
export default function ApiDocsPage() {
const [search, setSearch] = useState('')
const [serviceFilter, setServiceFilter] = useState<ServiceFilter>('all')
const [methodFilter, setMethodFilter] = useState<HttpMethod | 'all'>('all')
const [exposureFilter, setExposureFilter] = useState<ExposureFilter>('all')
const [expandedModules, setExpandedModules] = useState<Set<string>>(new Set())
const moduleRefs = useRef<Record<string, HTMLDivElement | null>>({})
const filteredModules = useMemo(() => {
const q = search.toLowerCase()
return apiModules
.filter((m) => serviceFilter === 'all' || m.service === serviceFilter)
.filter((m) => {
if (exposureFilter === 'all') return true
if (m.exposure === exposureFilter) return true
return m.endpoints.some((e) => (e.exposure || m.exposure) === exposureFilter)
})
.map((m) => {
const eps = m.endpoints.filter((e) => {
if (methodFilter !== 'all' && e.method !== methodFilter) return false
if (exposureFilter !== 'all' && (e.exposure || m.exposure) !== exposureFilter) return false
if (!q) return true
return (
e.path.toLowerCase().includes(q) ||
e.description.toLowerCase().includes(q) ||
m.name.toLowerCase().includes(q) ||
m.id.toLowerCase().includes(q)
)
})
return { ...m, endpoints: eps }
})
.filter((m) => m.endpoints.length > 0)
}, [search, serviceFilter, methodFilter, exposureFilter])
const stats = useMemo(() => {
const total = apiModules.reduce((s, m) => s + m.endpoints.length, 0)
const python = apiModules.filter((m) => m.service === 'python').reduce((s, m) => s + m.endpoints.length, 0)
const go = apiModules.filter((m) => m.service === 'go').reduce((s, m) => s + m.endpoints.length, 0)
const exposureCounts = { public: 0, partner: 0, internal: 0, admin: 0 }
apiModules.forEach((m) => {
m.endpoints.forEach((e) => {
const exp = e.exposure || m.exposure
exposureCounts[exp]++
})
})
return { total, python, go, modules: apiModules.length, exposure: exposureCounts }
}, [])
const filteredTotal = filteredModules.reduce((s, m) => s + m.endpoints.length, 0)
const toggleModule = (id: string) => {
setExpandedModules((prev) => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}
const expandAll = () => setExpandedModules(new Set(filteredModules.map((m) => m.id)))
const collapseAll = () => setExpandedModules(new Set())
const scrollToModule = (id: string) => {
setExpandedModules((prev) => new Set([...prev, id]))
setTimeout(() => {
moduleRefs.current[id]?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}, 100)
}
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<div className="bg-white border-b border-gray-200 sticky top-0 z-20">
<div className="max-w-7xl mx-auto px-4 py-4">
<div className="flex items-center justify-between mb-3">
<div>
<h1 className="text-xl font-bold text-gray-900">API-Referenz</h1>
<p className="text-sm text-gray-500 mt-0.5">
{stats.total} Endpoints in {stats.modules} Modulen
</p>
</div>
<div className="flex gap-2">
<button
onClick={expandAll}
className="px-3 py-1.5 text-xs font-medium text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
>
Alle aufklappen
</button>
<button
onClick={collapseAll}
className="px-3 py-1.5 text-xs font-medium text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
>
Alle zuklappen
</button>
</div>
</div>
{/* Exposure Stats */}
<div className="flex flex-wrap gap-2 mb-3 text-xs">
<span className="text-gray-500">Exposure:</span>
<span className="px-2 py-0.5 rounded bg-green-50 text-green-700 font-medium">
{stats.exposure.public} oeffentlich
</span>
<span className="px-2 py-0.5 rounded bg-blue-50 text-blue-700 font-medium">
{stats.exposure.partner} Integration
</span>
<span className="px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-medium">
{stats.exposure.internal} intern
</span>
<span className="px-2 py-0.5 rounded bg-orange-50 text-orange-700 font-medium">
{stats.exposure.admin} Wartung
</span>
<span className="text-gray-400 ml-1">
({Math.round((stats.exposure.public + stats.exposure.partner) / stats.total * 100)}% exponiert)
</span>
</div>
{/* Search + Filters */}
<div className="flex flex-wrap gap-3 items-center">
<div className="relative flex-1 min-w-[240px]">
<svg
className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<input
type="text"
placeholder="Endpoint, Beschreibung oder Modul suchen..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full pl-9 pr-4 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
{search && (
<button
onClick={() => setSearch('')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
<svg className="w-4 h-4" 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>
{/* Service Filter */}
<div className="flex rounded-lg border border-gray-300 overflow-hidden">
{([['all', 'Alle'], ['python', 'Python/FastAPI'], ['go', 'Go/Gin']] as const).map(([val, label]) => (
<button
key={val}
onClick={() => setServiceFilter(val)}
className={`px-3 py-2 text-xs font-medium transition-colors ${
serviceFilter === val
? 'bg-gray-900 text-white'
: 'bg-white text-gray-600 hover:bg-gray-50'
}`}
>
{label}
</button>
))}
</div>
{/* Exposure Filter */}
<div className="flex rounded-lg border border-gray-300 overflow-hidden">
{([
['all', 'Alle'],
['public', 'Oeffentlich'],
['partner', 'Integration'],
['internal', 'Intern'],
['admin', 'Wartung'],
] as const).map(([val, label]) => (
<button
key={val}
onClick={() => setExposureFilter(val)}
className={`px-3 py-2 text-xs font-medium transition-colors ${
exposureFilter === val
? 'bg-gray-900 text-white'
: 'bg-white text-gray-600 hover:bg-gray-50'
}`}
>
{label}
</button>
))}
</div>
{/* Method Filter */}
<div className="flex gap-1.5">
{(['all', 'GET', 'POST', 'PUT', 'DELETE', 'PATCH'] as const).map((m) => (
<button
key={m}
onClick={() => setMethodFilter(m)}
className={`px-2.5 py-1.5 text-xs font-mono font-bold rounded-md transition-colors ${
methodFilter === m
? m === 'all'
? 'bg-gray-900 text-white'
: METHOD_COLORS[m] + ' ring-2 ring-offset-1 ring-gray-400'
: 'bg-gray-100 text-gray-500 hover:bg-gray-200'
}`}
>
{m === 'all' ? 'ALLE' : m}
</button>
))}
</div>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto px-4 py-6">
{/* Stats Cards */}
<div className="grid grid-cols-4 gap-4 mb-6">
{[
{ label: 'Endpoints gesamt', value: stats.total, color: 'text-gray-900' },
{ label: 'Python / FastAPI', value: stats.python, color: 'text-blue-700' },
{ label: 'Go / Gin', value: stats.go, color: 'text-emerald-700' },
{ label: 'Module', value: stats.modules, color: 'text-purple-700' },
].map((s) => (
<div key={s.label} className="bg-white rounded-lg border border-gray-200 p-4">
<p className="text-xs text-gray-500 mb-1">{s.label}</p>
<p className={`text-2xl font-bold ${s.color}`}>{s.value}</p>
</div>
))}
</div>
<div className="flex gap-6">
{/* Module Index (Sidebar) */}
<div className="hidden lg:block w-64 flex-shrink-0">
<div className="bg-white rounded-lg border border-gray-200 p-4 sticky top-[170px] max-h-[calc(100vh-210px)] overflow-y-auto">
<h3 className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">
Modul-Index ({filteredModules.length})
</h3>
<div className="space-y-0.5">
{filteredModules.map((m) => (
<button
key={m.id}
onClick={() => scrollToModule(m.id)}
className="w-full text-left px-2 py-1.5 text-xs rounded hover:bg-gray-100 transition-colors group flex items-center justify-between gap-1"
>
<span className="truncate text-gray-700 group-hover:text-gray-900">
{m.id}
</span>
<span className="flex items-center gap-1 flex-shrink-0">
<ExposureBadge exposure={m.exposure} />
<span className={`text-[10px] px-1.5 py-0.5 rounded-full ${
m.service === 'python' ? 'bg-blue-50 text-blue-600' : 'bg-emerald-50 text-emerald-600'
}`}>
{m.endpoints.length}
</span>
</span>
</button>
))}
</div>
</div>
</div>
{/* Main Content */}
<div className="flex-1 min-w-0">
{search && (
<p className="text-sm text-gray-500 mb-4">
{filteredTotal} Treffer in {filteredModules.length} Modulen
</p>
)}
<div className="space-y-3">
{filteredModules.map((m) => {
const isExpanded = expandedModules.has(m.id)
return (
<div
key={m.id}
ref={(el) => { moduleRefs.current[m.id] = el }}
className="bg-white rounded-lg border border-gray-200 overflow-hidden"
>
{/* Module Header */}
<button
onClick={() => toggleModule(m.id)}
className="w-full flex items-center justify-between px-4 py-3 hover:bg-gray-50 transition-colors"
>
<div className="flex items-center gap-3 min-w-0">
<svg
className={`w-4 h-4 text-gray-400 flex-shrink-0 transition-transform ${isExpanded ? 'rotate-90' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
<span className={`text-[10px] font-mono font-bold px-2 py-0.5 rounded ${
m.service === 'python' ? 'bg-blue-50 text-blue-700' : 'bg-emerald-50 text-emerald-700'
}`}>
{m.service === 'python' ? 'PY' : 'GO'}
</span>
<ExposureBadge exposure={m.exposure} />
<span className="text-sm font-medium text-gray-900 truncate">{m.name}</span>
</div>
<div className="flex items-center gap-2 flex-shrink-0 ml-3">
<span className="text-xs text-gray-400 font-mono">{m.basePath}</span>
<span className="text-xs bg-gray-100 text-gray-600 px-2 py-0.5 rounded-full">
{m.endpoints.length}
</span>
</div>
</button>
{/* Endpoints Table */}
{isExpanded && (
<div className="border-t border-gray-100">
<table className="w-full">
<thead>
<tr className="bg-gray-50 text-xs text-gray-500">
<th className="text-left px-4 py-2 w-20">Methode</th>
<th className="text-left px-4 py-2">Pfad</th>
<th className="text-left px-4 py-2">Beschreibung</th>
<th className="text-left px-4 py-2 w-24">Exposure</th>
</tr>
</thead>
<tbody>
{m.endpoints.map((e, i) => {
const endpointExposure = e.exposure || m.exposure
const hasOverride = e.exposure && e.exposure !== m.exposure
return (
<tr
key={`${e.method}-${e.path}-${i}`}
className="border-t border-gray-50 hover:bg-gray-50/50 transition-colors"
>
<td className="px-4 py-2">
<span className={`inline-block text-[11px] font-mono font-bold px-2 py-0.5 rounded ${METHOD_COLORS[e.method]}`}>
{e.method}
</span>
</td>
<td className="px-4 py-2 font-mono text-xs text-gray-800">
{e.path}
</td>
<td className="px-4 py-2 text-xs text-gray-600">
{e.description}
</td>
<td className="px-4 py-2">
{hasOverride ? (
<ExposureBadge exposure={endpointExposure} />
) : (
<span className="text-[10px] text-gray-300"></span>
)}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)}
</div>
)
})}
</div>
{filteredModules.length === 0 && (
<div className="text-center py-12 text-gray-400">
<svg className="w-12 h-12 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<p className="text-sm">Keine Endpoints gefunden</p>
<p className="text-xs mt-1">Suchbegriff oder Filter anpassen</p>
</div>
)}
</div>
</div>
</div>
</div>
)
}

View File

@@ -160,6 +160,13 @@ 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',
'compliance_management_reviews', 'compliance_internal_audits',
'compliance_audit_trail', 'compliance_isms_readiness_checks',
],
ragCollections: [],
apiEndpoints: [
@@ -173,6 +180,20 @@ 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',
'CRUD /api/isms/soa',
'CRUD /api/isms/findings',
'CRUD /api/isms/capa',
'CRUD /api/isms/management-reviews',
'CRUD /api/isms/internal-audits',
'GET /api/isms/overview',
'POST /api/isms/readiness-check',
'CRUD /api/compliance/legal-documents',
'CRUD /api/compliance/legal-templates',
],
@@ -188,7 +209,7 @@ export const ARCH_SERVICES: ArchService[] = [
url: 'https://macmini:8093',
container: 'bp-compliance-ai-sdk',
description: 'KI-konforme Compliance-Analyse: UCCA, Training, RAG-Suche, IACE, Portfolio, Roadmap, Workshop.',
descriptionLong: 'Der AI Compliance SDK Service ist in Go geschrieben und bietet KI-gestuetzte Compliance-Analysen. Er fuehrt UCCA-Bewertungen (Use Case Compliance Assessments) durch, verwaltet Schulungsmodule mit Fortschrittstracking und durchsucht Rechtstexte per RAG (Retrieval Augmented Generation) ueber Qdrant. Als LLM wird primaer Ollama (qwen3:30b-a3b) lokal genutzt, mit Fallback auf Claude Sonnet ueber die Anthropic API.',
descriptionLong: 'Der AI Compliance SDK Service ist in Go geschrieben und bietet KI-gestuetzte Compliance-Analysen. Er fuehrt UCCA-Bewertungen (Use Case Compliance Assessments) durch, verwaltet Schulungsmodule mit Fortschrittstracking und durchsucht Rechtstexte per RAG (Retrieval Augmented Generation) ueber Qdrant. Als LLM wird primaer Ollama (qwen3.5:35b-a3b) lokal genutzt, mit Fallback auf Claude Sonnet ueber die Anthropic API.',
dbTables: [
'ai_assessments', 'ai_training_modules', 'ai_training_progress',
],
@@ -252,12 +273,12 @@ export const ARCH_SERVICES: ArchService[] = [
name: 'PostgreSQL',
nameShort: 'PostgreSQL',
layer: 'infrastructure',
tech: 'PostgreSQL 16',
port: 5432,
tech: 'PostgreSQL 17',
port: 54321,
url: null,
container: 'bp-core-postgres',
description: 'Zentrale Datenbank. Schemas: compliance, core, public. Shared mit breakpilot-core.',
descriptionLong: 'PostgreSQL ist die gemeinsame Datenbank fuer das gesamte BreakPilot-Oekosystem. Das compliance-Schema enthaelt alle Compliance-spezifischen Tabellen, waehrend core und public von breakpilot-core bereitgestellt werden. Der search_path ist auf compliance,core,public konfiguriert, sodass Services transparent auf Schema-uebergreifende Daten zugreifen koennen.',
container: '46.225.100.82:54321 (extern)',
description: 'Externe PostgreSQL-Instanz (Hetzner/meghshakka). Schemas: compliance (51 Tabellen) + public (33 compliance_*-Tabellen). Migriert 2026-03-06.',
descriptionLong: 'Die Compliance-Daten liegen seit 2026-03-06 auf einer dedizierten externen PostgreSQL 17-Instanz bei Hetzner (meghshakka, 46.225.100.82:54321). Das compliance-Schema umfasst 51 Tabellen fuer Risiken, Kontrollen, VVT, DSFA, Einwilligungen, Loeschfristen u.v.m. Weitere 33 compliance_*-Tabellen befinden sich im public-Schema (Tenants, Namespaces, LLM-Policies, Legal Documents etc.). Die Verbindung ist TLS-verschluesselt (sslmode=require). Die lokale bp-core-postgres auf dem Mac Mini wird exklusiv von breakpilot-lehrer (NIBIS-Daten) genutzt.',
dbTables: [],
ragCollections: [],
apiEndpoints: [],
@@ -284,12 +305,12 @@ export const ARCH_SERVICES: ArchService[] = [
name: 'Qdrant',
nameShort: 'Qdrant',
layer: 'infrastructure',
tech: 'Qdrant',
port: 6333,
url: null,
container: 'bp-core-qdrant',
description: 'Vektor-Datenbank fuer RAG-Compliance-Suche. Collections: DSGVO, AI Act, BDSG, TTDSG.',
descriptionLong: 'Qdrant speichert Vektorembeddings von Rechtstexten und Compliance-Dokumenten in thematischen Collections (DSGVO, AI Act, BDSG, TTDSG, Templates). Der AI Compliance SDK nutzt diese fuer semantische Suchen — Nutzer koennen so in natuerlicher Sprache nach relevanten Gesetzespassagen und Compliance-Anforderungen suchen, ohne exakte Suchbegriffe kennen zu muessen.',
tech: 'Qdrant Cloud',
port: 443,
url: 'https://qdrant-dev.breakpilot.ai',
container: 'qdrant-dev.breakpilot.ai (extern)',
description: 'Externe Vektor-Datenbank (Hetzner/meghshakka). RAG-Compliance-Suche. Collections: DSGVO, AI Act, BDSG, TTDSG. Migriert 2026-03-06.',
descriptionLong: 'Qdrant laeuft seit 2026-03-06 als externe Instanz auf qdrant-dev.breakpilot.ai (Hetzner/meghshakka). Es speichert Vektorembeddings von Rechtstexten und Compliance-Dokumenten in thematischen Collections. Der AI Compliance SDK verbindet sich per HTTPS mit API-Key-Authentifizierung. Durch die externe Instanz skaliert der Vektorspeicher unabhaengig vom Mac Mini.',
dbTables: [],
ragCollections: [
'bp_dsgvo', 'bp_ai_act', 'bp_bdsg', 'bp_ttdsg',
@@ -300,15 +321,15 @@ export const ARCH_SERVICES: ArchService[] = [
},
{
id: 'minio',
name: 'MinIO',
nameShort: 'MinIO',
name: 'Hetzner Object Storage',
nameShort: 'Object Storage',
layer: 'infrastructure',
tech: 'MinIO',
port: 9000,
url: null,
container: 'bp-core-minio',
description: 'Object Storage fuer TTS-Audio, generierte Videos und Dokument-Uploads.',
descriptionLong: 'MinIO stellt S3-kompatiblen Object Storage bereit und wird von breakpilot-core verwaltet. Der Compliance-Stack nutzt es primaer fuer generierte Schulungsvideos und Audio-Dateien aus dem TTS-Service. Durch die S3-API-Kompatibilitaet koennen Standard-AWS-SDKs fuer den Zugriff verwendet werden.',
tech: 'S3-kompatibel',
port: 443,
url: 'https://nbg1.your-objectstorage.com',
container: 'nbg1.your-objectstorage.com (extern)',
description: 'Externer S3-kompatibler Object Storage (Hetzner Nuernberg). TTS-Audio, generierte Videos. Migriert 2026-03-06.',
descriptionLong: 'Der Object Storage laeuft seit 2026-03-06 auf dem Hetzner-Standort Nuernberg (nbg1.your-objectstorage.com) und ist vollstaendig S3-kompatibel. Der Compliance TTS-Service speichert dort generierte Schulungsvideos und Audio-Dateien. Die Verbindung ist TLS-verschluesselt (MINIO_SECURE=true). Die lokale bp-core-minio-Instanz auf dem Mac Mini wird nicht mehr vom Compliance-Stack genutzt.',
dbTables: [],
ragCollections: [],
apiEndpoints: [],
@@ -323,8 +344,8 @@ export const ARCH_SERVICES: ArchService[] = [
port: 11434,
url: null,
container: 'bp-core-ollama',
description: 'Lokaler LLM-Server. Modell: qwen3:30b-a3b. Fallback: Claude Sonnet via Anthropic API.',
descriptionLong: 'Ollama hostet ein lokales Large Language Model (qwen3:30b-a3b) fuer Compliance-Analysen, Textgenerierung und UCCA-Bewertungen. Durch die lokale Ausfuehrung bleiben alle Daten im eigenen Netzwerk — ein zentraler Vorteil fuer DSGVO-Konformitaet. Ist das lokale Modell nicht verfuegbar oder die Aufgabe zu komplex, wird automatisch auf Claude Sonnet ueber die Anthropic API zurueckgegriffen.',
description: 'Lokaler LLM-Server. Modell: qwen3.5:35b-a3b. Fallback: Claude Sonnet via Anthropic API.',
descriptionLong: 'Ollama hostet ein lokales Large Language Model (qwen3.5:35b-a3b) fuer Compliance-Analysen, Textgenerierung und UCCA-Bewertungen. Durch die lokale Ausfuehrung bleiben alle Daten im eigenen Netzwerk — ein zentraler Vorteil fuer DSGVO-Konformitaet. Ist das lokale Modell nicht verfuegbar oder die Aufgabe zu komplex, wird automatisch auf Claude Sonnet ueber die Anthropic API zurueckgegriffen.',
dbTables: [],
ragCollections: [],
apiEndpoints: [],

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>
)
}

View File

@@ -0,0 +1,561 @@
'use client'
import React, { useState, useEffect, useCallback } from 'react'
import { useSDK } from '@/lib/sdk'
// =============================================================================
// TYPES
// =============================================================================
interface LLMLogEntry {
id: string
user_id: string
namespace: string
model: string
provider: string
prompt_tokens: number
completion_tokens: number
total_tokens: number
pii_detected: boolean
pii_categories: string[]
redacted: boolean
duration_ms: number
status: string
created_at: string
}
interface UsageStats {
total_requests: number
total_tokens: number
total_prompt_tokens: number
total_completion_tokens: number
models_used: Record<string, number>
providers_used: Record<string, number>
avg_duration_ms: number
pii_detection_rate: number
period_start: string
period_end: string
}
interface ComplianceReport {
total_requests: number
pii_incidents: number
pii_rate: number
redaction_rate: number
policy_violations: number
top_pii_categories: Record<string, number>
namespace_breakdown: Record<string, { requests: number; pii_incidents: number }>
user_breakdown: Record<string, { requests: number; pii_incidents: number }>
period_start: string
period_end: string
}
type TabId = 'llm-log' | 'usage' | 'compliance'
// =============================================================================
// HELPERS
// =============================================================================
const API_BASE = '/api/sdk/v1/audit-llm'
function formatDate(iso: string): string {
return new Date(iso).toLocaleString('de-DE', {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit',
})
}
function formatNumber(n: number): string {
return n.toLocaleString('de-DE')
}
function formatDuration(ms: number): string {
if (ms < 1000) return `${ms}ms`
return `${(ms / 1000).toFixed(1)}s`
}
function getDateRange(period: string): { from: string; to: string } {
const now = new Date()
const to = now.toISOString().slice(0, 10)
const from = new Date(now)
switch (period) {
case '7d': from.setDate(from.getDate() - 7); break
case '30d': from.setDate(from.getDate() - 30); break
case '90d': from.setDate(from.getDate() - 90); break
default: from.setDate(from.getDate() - 7)
}
return { from: from.toISOString().slice(0, 10), to }
}
// =============================================================================
// MAIN PAGE
// =============================================================================
export default function AuditLLMPage() {
const { state } = useSDK()
const [activeTab, setActiveTab] = useState<TabId>('llm-log')
const [period, setPeriod] = useState('7d')
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
// LLM Log state
const [logEntries, setLogEntries] = useState<LLMLogEntry[]>([])
const [logFilter, setLogFilter] = useState({ model: '', pii: '' })
// Usage state
const [usageStats, setUsageStats] = useState<UsageStats | null>(null)
// Compliance state
const [complianceReport, setComplianceReport] = useState<ComplianceReport | null>(null)
// ─── Load Data ───────────────────────────────────────────────────────
const loadLLMLog = useCallback(async () => {
setLoading(true)
setError(null)
try {
const { from, to } = getDateRange(period)
const params = new URLSearchParams({ from, to, limit: '100' })
if (logFilter.model) params.set('model', logFilter.model)
if (logFilter.pii === 'true') params.set('pii_detected', 'true')
if (logFilter.pii === 'false') params.set('pii_detected', 'false')
const res = await fetch(`${API_BASE}/llm?${params}`)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
setLogEntries(Array.isArray(data) ? data : data.entries || data.logs || [])
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler beim Laden')
} finally {
setLoading(false)
}
}, [period, logFilter])
const loadUsage = useCallback(async () => {
setLoading(true)
setError(null)
try {
const { from, to } = getDateRange(period)
const res = await fetch(`${API_BASE}/usage?from=${from}&to=${to}`)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
setUsageStats(data)
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler beim Laden')
} finally {
setLoading(false)
}
}, [period])
const loadCompliance = useCallback(async () => {
setLoading(true)
setError(null)
try {
const { from, to } = getDateRange(period)
const res = await fetch(`${API_BASE}/compliance-report?from=${from}&to=${to}`)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
setComplianceReport(data)
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler beim Laden')
} finally {
setLoading(false)
}
}, [period])
useEffect(() => {
if (activeTab === 'llm-log') loadLLMLog()
else if (activeTab === 'usage') loadUsage()
else if (activeTab === 'compliance') loadCompliance()
}, [activeTab, loadLLMLog, loadUsage, loadCompliance])
// ─── Export ──────────────────────────────────────────────────────────
const handleExport = async (type: 'llm' | 'general' | 'compliance', format: 'json' | 'csv') => {
try {
const { from, to } = getDateRange(period)
const res = await fetch(`${API_BASE}/export/${type}?from=${from}&to=${to}&format=${format}`)
if (!res.ok) throw new Error(`Export fehlgeschlagen: ${res.status}`)
const blob = await res.blob()
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `audit-${type}-${from}-${to}.${format}`
a.click()
URL.revokeObjectURL(url)
} catch (e) {
setError(e instanceof Error ? e.message : 'Export fehlgeschlagen')
}
}
// ─── Tabs ────────────────────────────────────────────────────────────
const tabs: { id: TabId; label: string }[] = [
{ id: 'llm-log', label: 'LLM-Log' },
{ id: 'usage', label: 'Nutzung' },
{ id: 'compliance', label: 'Compliance' },
]
return (
<div className="p-6 max-w-7xl mx-auto">
{/* Header */}
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900">LLM Audit Dashboard</h1>
<p className="text-gray-500 mt-1">Monitoring und Compliance-Analyse der LLM-Operationen</p>
</div>
{/* Period + Tabs */}
<div className="flex items-center justify-between mb-6">
<div className="flex gap-1 bg-gray-100 rounded-lg p-1">
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
activeTab === tab.id
? 'bg-white text-purple-700 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
{tab.label}
</button>
))}
</div>
<div className="flex items-center gap-3">
<select
value={period}
onChange={e => setPeriod(e.target.value)}
className="border border-gray-300 rounded-lg px-3 py-2 text-sm"
>
<option value="7d">Letzte 7 Tage</option>
<option value="30d">Letzte 30 Tage</option>
<option value="90d">Letzte 90 Tage</option>
</select>
<button
onClick={() => {
if (activeTab === 'llm-log') handleExport('llm', 'csv')
else if (activeTab === 'compliance') handleExport('compliance', 'json')
else handleExport('general', 'csv')
}}
className="px-4 py-2 text-sm bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
>
Export
</button>
</div>
</div>
{error && (
<div className="mb-4 p-3 bg-red-50 text-red-700 rounded-lg text-sm">{error}</div>
)}
{loading && (
<div className="flex items-center justify-center py-12">
<div className="w-8 h-8 border-4 border-purple-200 border-t-purple-600 rounded-full animate-spin" />
</div>
)}
{/* ── LLM-Log Tab ── */}
{!loading && activeTab === 'llm-log' && (
<div>
{/* Filters */}
<div className="flex gap-3 mb-4">
<input
type="text"
placeholder="Model filtern..."
value={logFilter.model}
onChange={e => setLogFilter(f => ({ ...f, model: e.target.value }))}
className="border border-gray-300 rounded-lg px-3 py-2 text-sm w-48"
/>
<select
value={logFilter.pii}
onChange={e => setLogFilter(f => ({ ...f, pii: e.target.value }))}
className="border border-gray-300 rounded-lg px-3 py-2 text-sm"
>
<option value="">Alle PII-Status</option>
<option value="true">PII erkannt</option>
<option value="false">Kein PII</option>
</select>
</div>
{/* Table */}
<div className="overflow-x-auto bg-white rounded-xl border border-gray-200">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-200 bg-gray-50">
<th className="text-left px-4 py-3 font-medium text-gray-600">Zeitpunkt</th>
<th className="text-left px-4 py-3 font-medium text-gray-600">User</th>
<th className="text-left px-4 py-3 font-medium text-gray-600">Model</th>
<th className="text-right px-4 py-3 font-medium text-gray-600">Tokens</th>
<th className="text-center px-4 py-3 font-medium text-gray-600">PII</th>
<th className="text-right px-4 py-3 font-medium text-gray-600">Dauer</th>
<th className="text-center px-4 py-3 font-medium text-gray-600">Status</th>
</tr>
</thead>
<tbody>
{logEntries.length === 0 ? (
<tr>
<td colSpan={7} className="text-center py-8 text-gray-400">
Keine Log-Eintraege im gewaehlten Zeitraum
</td>
</tr>
) : logEntries.map(entry => (
<tr key={entry.id} className="border-b border-gray-100 hover:bg-gray-50">
<td className="px-4 py-3 text-gray-500">{formatDate(entry.created_at)}</td>
<td className="px-4 py-3 font-mono text-xs">{entry.user_id?.slice(0, 8)}...</td>
<td className="px-4 py-3">
<span className="px-2 py-0.5 bg-blue-50 text-blue-700 rounded text-xs font-medium">
{entry.model}
</span>
</td>
<td className="px-4 py-3 text-right font-mono">{formatNumber(entry.total_tokens)}</td>
<td className="px-4 py-3 text-center">
{entry.pii_detected ? (
<span className="px-2 py-0.5 bg-red-50 text-red-700 rounded text-xs font-medium">
{entry.redacted ? 'Redacted' : 'Erkannt'}
</span>
) : (
<span className="text-gray-400 text-xs">-</span>
)}
</td>
<td className="px-4 py-3 text-right text-gray-500">{formatDuration(entry.duration_ms)}</td>
<td className="px-4 py-3 text-center">
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
entry.status === 'success' ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'
}`}>
{entry.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-2 text-xs text-gray-400">{logEntries.length} Eintraege</div>
</div>
)}
{/* ── Nutzung Tab ── */}
{!loading && activeTab === 'usage' && usageStats && (
<div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<StatCard label="Requests gesamt" value={formatNumber(usageStats.total_requests)} />
<StatCard label="Tokens gesamt" value={formatNumber(usageStats.total_tokens)} />
<StatCard label="Avg. Dauer" value={formatDuration(usageStats.avg_duration_ms)} />
<StatCard
label="PII-Rate"
value={`${(usageStats.pii_detection_rate * 100).toFixed(1)}%`}
highlight={usageStats.pii_detection_rate > 0.1}
/>
</div>
{/* Token Breakdown */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h3 className="font-semibold text-gray-900 mb-4">Model-Nutzung</h3>
{Object.entries(usageStats.models_used || {}).length === 0 ? (
<p className="text-gray-400 text-sm">Keine Daten</p>
) : (
<div className="space-y-3">
{Object.entries(usageStats.models_used).sort((a, b) => b[1] - a[1]).map(([model, count]) => (
<div key={model} className="flex items-center justify-between">
<span className="text-sm text-gray-700">{model}</span>
<div className="flex items-center gap-3">
<div className="w-32 h-2 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full bg-purple-500 rounded-full"
style={{ width: `${(count / usageStats.total_requests) * 100}%` }}
/>
</div>
<span className="text-sm font-mono text-gray-500 w-16 text-right">{formatNumber(count)}</span>
</div>
</div>
))}
</div>
)}
</div>
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h3 className="font-semibold text-gray-900 mb-4">Provider-Verteilung</h3>
{Object.entries(usageStats.providers_used || {}).length === 0 ? (
<p className="text-gray-400 text-sm">Keine Daten</p>
) : (
<div className="space-y-3">
{Object.entries(usageStats.providers_used).sort((a, b) => b[1] - a[1]).map(([provider, count]) => (
<div key={provider} className="flex items-center justify-between">
<span className="text-sm text-gray-700 capitalize">{provider}</span>
<span className="text-sm font-mono text-gray-500">{formatNumber(count)}</span>
</div>
))}
</div>
)}
</div>
</div>
{/* Token Details */}
<div className="mt-6 bg-white rounded-xl border border-gray-200 p-5">
<h3 className="font-semibold text-gray-900 mb-4">Token-Aufschluesselung</h3>
<div className="grid grid-cols-3 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-gray-900">{formatNumber(usageStats.total_prompt_tokens)}</div>
<div className="text-sm text-gray-500">Prompt Tokens</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-gray-900">{formatNumber(usageStats.total_completion_tokens)}</div>
<div className="text-sm text-gray-500">Completion Tokens</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-purple-600">{formatNumber(usageStats.total_tokens)}</div>
<div className="text-sm text-gray-500">Gesamt</div>
</div>
</div>
</div>
</div>
)}
{/* ── Compliance Tab ── */}
{!loading && activeTab === 'compliance' && complianceReport && (
<div>
{/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<StatCard label="Requests" value={formatNumber(complianceReport.total_requests)} />
<StatCard
label="PII-Vorfaelle"
value={formatNumber(complianceReport.pii_incidents)}
highlight={complianceReport.pii_incidents > 0}
/>
<StatCard
label="PII-Rate"
value={`${(complianceReport.pii_rate * 100).toFixed(1)}%`}
highlight={complianceReport.pii_rate > 0.05}
/>
<StatCard label="Redaction-Rate" value={`${(complianceReport.redaction_rate * 100).toFixed(1)}%`} />
</div>
{complianceReport.policy_violations > 0 && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-xl">
<div className="flex items-center gap-2 text-red-700 font-semibold">
<svg className="w-5 h-5" 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>
{complianceReport.policy_violations} Policy-Verletzungen im Zeitraum
</div>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* PII Categories */}
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h3 className="font-semibold text-gray-900 mb-4">PII-Kategorien</h3>
{Object.entries(complianceReport.top_pii_categories || {}).length === 0 ? (
<p className="text-gray-400 text-sm">Keine PII erkannt</p>
) : (
<div className="space-y-2">
{Object.entries(complianceReport.top_pii_categories).sort((a, b) => b[1] - a[1]).map(([cat, count]) => (
<div key={cat} className="flex items-center justify-between py-1">
<span className="text-sm text-gray-700">{cat}</span>
<span className="px-2 py-0.5 bg-red-50 text-red-700 rounded text-xs font-mono">{count}</span>
</div>
))}
</div>
)}
</div>
{/* Namespace Breakdown */}
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h3 className="font-semibold text-gray-900 mb-4">Namespace-Analyse</h3>
{Object.entries(complianceReport.namespace_breakdown || {}).length === 0 ? (
<p className="text-gray-400 text-sm">Keine Namespace-Daten</p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-100">
<th className="text-left py-2 text-gray-500 font-medium">Namespace</th>
<th className="text-right py-2 text-gray-500 font-medium">Requests</th>
<th className="text-right py-2 text-gray-500 font-medium">PII</th>
</tr>
</thead>
<tbody>
{Object.entries(complianceReport.namespace_breakdown).map(([ns, data]) => (
<tr key={ns} className="border-b border-gray-50">
<td className="py-2 font-mono text-xs">{ns}</td>
<td className="py-2 text-right">{formatNumber(data.requests)}</td>
<td className="py-2 text-right">
{data.pii_incidents > 0 ? (
<span className="text-red-600 font-medium">{data.pii_incidents}</span>
) : (
<span className="text-gray-400">0</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
{/* User Breakdown */}
{Object.entries(complianceReport.user_breakdown || {}).length > 0 && (
<div className="mt-6 bg-white rounded-xl border border-gray-200 p-5">
<h3 className="font-semibold text-gray-900 mb-4">Top-Nutzer</h3>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-100">
<th className="text-left py-2 text-gray-500 font-medium">User-ID</th>
<th className="text-right py-2 text-gray-500 font-medium">Requests</th>
<th className="text-right py-2 text-gray-500 font-medium">PII-Vorfaelle</th>
</tr>
</thead>
<tbody>
{Object.entries(complianceReport.user_breakdown)
.sort((a, b) => b[1].requests - a[1].requests)
.slice(0, 10)
.map(([userId, data]) => (
<tr key={userId} className="border-b border-gray-50">
<td className="py-2 font-mono text-xs">{userId}</td>
<td className="py-2 text-right">{formatNumber(data.requests)}</td>
<td className="py-2 text-right">
{data.pii_incidents > 0 ? (
<span className="text-red-600 font-medium">{data.pii_incidents}</span>
) : (
<span className="text-gray-400">0</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
)}
{/* Empty state for usage/compliance when no data */}
{!loading && activeTab === 'usage' && !usageStats && !error && (
<div className="text-center py-12 text-gray-400">Keine Nutzungsdaten verfuegbar</div>
)}
{!loading && activeTab === 'compliance' && !complianceReport && !error && (
<div className="text-center py-12 text-gray-400">Kein Compliance-Report verfuegbar</div>
)}
</div>
)
}
// =============================================================================
// STAT CARD
// =============================================================================
function StatCard({ label, value, highlight }: { label: string; value: string; highlight?: boolean }) {
return (
<div className={`rounded-xl border p-4 ${highlight ? 'border-red-200 bg-red-50' : 'border-gray-200 bg-white'}`}>
<div className="text-sm text-gray-500">{label}</div>
<div className={`text-2xl font-bold mt-1 ${highlight ? 'text-red-700' : 'text-gray-900'}`}>{value}</div>
</div>
)
}

View File

@@ -1,6 +1,6 @@
'use client'
import CatalogManagerContent from '@/components/catalog-manager/CatalogManagerContent'
import { CatalogManagerContent } from '@/components/catalog-manager/CatalogManagerContent'
export default function CatalogManagerPage() {
return <CatalogManagerContent />

View File

@@ -0,0 +1,345 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
interface ChangeRequest {
id: string
triggerType: string
targetDocumentType: string
targetDocumentId: string | null
targetSection: string | null
proposalTitle: string
proposalBody: string | null
proposedChanges: Record<string, unknown>
status: 'pending' | 'accepted' | 'rejected' | 'edited_and_accepted'
priority: 'low' | 'normal' | 'high' | 'critical'
decidedBy: string | null
decidedAt: string | null
rejectionReason: string | null
createdBy: string
createdAt: string
}
interface Stats {
total_pending: number
critical_count: number
total_accepted: number
total_rejected: number
by_document_type: Record<string, number>
}
const API_BASE = '/api/sdk/v1/compliance/change-requests'
const DOC_TYPE_LABELS: Record<string, string> = {
dsfa: 'DSFA',
vvt: 'VVT',
tom: 'TOM',
loeschfristen: 'Löschfristen',
obligation: 'Pflichten',
}
const PRIORITY_COLORS: Record<string, string> = {
critical: 'bg-red-100 text-red-800',
high: 'bg-orange-100 text-orange-800',
normal: 'bg-blue-100 text-blue-800',
low: 'bg-gray-100 text-gray-700',
}
const STATUS_COLORS: Record<string, string> = {
pending: 'bg-yellow-100 text-yellow-800',
accepted: 'bg-green-100 text-green-800',
rejected: 'bg-red-100 text-red-800',
edited_and_accepted: 'bg-emerald-100 text-emerald-800',
}
function snakeToCamel(obj: Record<string, unknown>): ChangeRequest {
return {
id: obj.id as string,
triggerType: obj.trigger_type as string,
targetDocumentType: obj.target_document_type as string,
targetDocumentId: obj.target_document_id as string | null,
targetSection: obj.target_section as string | null,
proposalTitle: obj.proposal_title as string,
proposalBody: obj.proposal_body as string | null,
proposedChanges: (obj.proposed_changes || {}) as Record<string, unknown>,
status: obj.status as ChangeRequest['status'],
priority: obj.priority as ChangeRequest['priority'],
decidedBy: obj.decided_by as string | null,
decidedAt: obj.decided_at as string | null,
rejectionReason: obj.rejection_reason as string | null,
createdBy: obj.created_by as string,
createdAt: obj.created_at as string,
}
}
export default function ChangeRequestsPage() {
const [requests, setRequests] = useState<ChangeRequest[]>([])
const [stats, setStats] = useState<Stats | null>(null)
const [filter, setFilter] = useState<string>('all')
const [statusFilter, setStatusFilter] = useState<string>('pending')
const [loading, setLoading] = useState(true)
const [actionModal, setActionModal] = useState<{ type: 'accept' | 'reject' | 'edit'; cr: ChangeRequest } | null>(null)
const [rejectReason, setRejectReason] = useState('')
const [editBody, setEditBody] = useState('')
const loadData = useCallback(async () => {
try {
const statsRes = await fetch(`${API_BASE}/stats`)
if (statsRes.ok) setStats(await statsRes.json())
let url = `${API_BASE}?status=${statusFilter}`
if (filter !== 'all') url += `&target_document_type=${filter}`
const res = await fetch(url)
if (res.ok) {
const data = await res.json()
setRequests((Array.isArray(data) ? data : []).map(snakeToCamel))
}
} catch (e) {
console.error('Failed to load change requests:', e)
} finally {
setLoading(false)
}
}, [filter, statusFilter])
useEffect(() => {
loadData()
const interval = setInterval(loadData, 60000)
return () => clearInterval(interval)
}, [loadData])
const handleAccept = async (cr: ChangeRequest) => {
const res = await fetch(`${API_BASE}/${cr.id}/accept`, { method: 'POST' })
if (res.ok) {
setActionModal(null)
loadData()
}
}
const handleReject = async (cr: ChangeRequest) => {
const res = await fetch(`${API_BASE}/${cr.id}/reject`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ rejection_reason: rejectReason }),
})
if (res.ok) {
setActionModal(null)
setRejectReason('')
loadData()
}
}
const handleEdit = async (cr: ChangeRequest) => {
const res = await fetch(`${API_BASE}/${cr.id}/edit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ proposal_body: editBody }),
})
if (res.ok) {
setActionModal(null)
setEditBody('')
loadData()
}
}
const handleDelete = async (id: string) => {
if (!confirm('Änderungsanfrage wirklich löschen?')) return
const res = await fetch(`${API_BASE}/${id}`, { method: 'DELETE' })
if (res.ok) loadData()
}
return (
<div className="p-6 max-w-7xl mx-auto">
<h1 className="text-2xl font-bold text-gray-900 mb-6">Änderungsanfragen</h1>
{/* Stats Bar */}
{stats && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="text-2xl font-bold text-yellow-700">{stats.total_pending}</div>
<div className="text-sm text-yellow-600">Offen</div>
</div>
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="text-2xl font-bold text-red-700">{stats.critical_count}</div>
<div className="text-sm text-red-600">Kritisch</div>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="text-2xl font-bold text-green-700">{stats.total_accepted}</div>
<div className="text-sm text-green-600">Angenommen</div>
</div>
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
<div className="text-2xl font-bold text-gray-700">{stats.total_rejected}</div>
<div className="text-sm text-gray-600">Abgelehnt</div>
</div>
</div>
)}
{/* Filters */}
<div className="flex flex-wrap gap-2 mb-6">
<div className="flex gap-1 bg-gray-100 rounded-lg p-1">
{['pending', 'accepted', 'rejected'].map(s => (
<button
key={s}
onClick={() => setStatusFilter(s)}
className={`px-3 py-1 rounded-md text-sm font-medium transition-colors ${
statusFilter === s ? 'bg-white shadow text-purple-700' : 'text-gray-600 hover:text-gray-900'
}`}
>
{s === 'pending' ? 'Offen' : s === 'accepted' ? 'Angenommen' : 'Abgelehnt'}
</button>
))}
</div>
<div className="flex gap-1 bg-gray-100 rounded-lg p-1">
{['all', 'dsfa', 'vvt', 'tom', 'loeschfristen', 'obligation'].map(t => (
<button
key={t}
onClick={() => setFilter(t)}
className={`px-3 py-1 rounded-md text-sm font-medium transition-colors ${
filter === t ? 'bg-white shadow text-purple-700' : 'text-gray-600 hover:text-gray-900'
}`}
>
{t === 'all' ? 'Alle' : DOC_TYPE_LABELS[t] || t}
</button>
))}
</div>
</div>
{/* Request List */}
{loading ? (
<div className="text-center py-12 text-gray-500">Laden...</div>
) : requests.length === 0 ? (
<div className="text-center py-12 text-gray-400">
Keine Änderungsanfragen {statusFilter === 'pending' ? 'offen' : 'gefunden'}
</div>
) : (
<div className="space-y-3">
{requests.map(cr => (
<div key={cr.id} className="bg-white border border-gray-200 rounded-lg p-4 hover:shadow-sm transition-shadow">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className={`px-2 py-0.5 rounded text-xs font-medium ${PRIORITY_COLORS[cr.priority]}`}>
{cr.priority}
</span>
<span className={`px-2 py-0.5 rounded text-xs font-medium ${STATUS_COLORS[cr.status]}`}>
{cr.status}
</span>
<span className="px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-700">
{DOC_TYPE_LABELS[cr.targetDocumentType] || cr.targetDocumentType}
</span>
{cr.triggerType !== 'manual' && (
<span className="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600">
{cr.triggerType}
</span>
)}
</div>
<h3 className="font-medium text-gray-900">{cr.proposalTitle}</h3>
{cr.proposalBody && (
<p className="text-sm text-gray-600 mt-1 line-clamp-2">{cr.proposalBody}</p>
)}
<div className="text-xs text-gray-400 mt-2">
{new Date(cr.createdAt).toLocaleDateString('de-DE')} {cr.createdBy}
{cr.rejectionReason && (
<span className="text-red-500 ml-2">Grund: {cr.rejectionReason}</span>
)}
</div>
</div>
{cr.status === 'pending' && (
<div className="flex gap-2 ml-4">
<button
onClick={() => { setActionModal({ type: 'accept', cr }) }}
className="px-3 py-1 bg-green-600 text-white rounded text-sm hover:bg-green-700"
>
Annehmen
</button>
<button
onClick={() => { setEditBody(cr.proposalBody || ''); setActionModal({ type: 'edit', cr }) }}
className="px-3 py-1 bg-blue-600 text-white rounded text-sm hover:bg-blue-700"
>
Bearbeiten
</button>
<button
onClick={() => { setRejectReason(''); setActionModal({ type: 'reject', cr }) }}
className="px-3 py-1 bg-gray-200 text-gray-700 rounded text-sm hover:bg-gray-300"
>
Ablehnen
</button>
<button
onClick={() => handleDelete(cr.id)}
className="px-3 py-1 text-red-600 rounded text-sm hover:bg-red-50"
>
Löschen
</button>
</div>
)}
</div>
</div>
))}
</div>
)}
{/* Action Modal */}
{actionModal && (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50" onClick={() => setActionModal(null)}>
<div className="bg-white rounded-xl p-6 w-full max-w-lg" onClick={e => e.stopPropagation()}>
{actionModal.type === 'accept' && (
<>
<h3 className="text-lg font-semibold mb-4">Änderung annehmen?</h3>
<p className="text-sm text-gray-600 mb-4">{actionModal.cr.proposalTitle}</p>
{Object.keys(actionModal.cr.proposedChanges).length > 0 && (
<pre className="bg-gray-50 p-3 rounded text-xs mb-4 max-h-48 overflow-auto">
{JSON.stringify(actionModal.cr.proposedChanges, null, 2)}
</pre>
)}
<div className="flex justify-end gap-3">
<button onClick={() => setActionModal(null)} className="px-4 py-2 text-gray-600">Abbrechen</button>
<button onClick={() => handleAccept(actionModal.cr)} className="px-4 py-2 bg-green-600 text-white rounded-lg">Annehmen</button>
</div>
</>
)}
{actionModal.type === 'reject' && (
<>
<h3 className="text-lg font-semibold mb-4">Änderung ablehnen</h3>
<textarea
value={rejectReason}
onChange={e => setRejectReason(e.target.value)}
placeholder="Begründung..."
className="w-full border rounded-lg p-3 h-24 mb-4"
/>
<div className="flex justify-end gap-3">
<button onClick={() => setActionModal(null)} className="px-4 py-2 text-gray-600">Abbrechen</button>
<button
onClick={() => handleReject(actionModal.cr)}
disabled={!rejectReason.trim()}
className="px-4 py-2 bg-red-600 text-white rounded-lg disabled:opacity-50"
>
Ablehnen
</button>
</div>
</>
)}
{actionModal.type === 'edit' && (
<>
<h3 className="text-lg font-semibold mb-4">Vorschlag bearbeiten & annehmen</h3>
<textarea
value={editBody}
onChange={e => setEditBody(e.target.value)}
className="w-full border rounded-lg p-3 h-32 mb-4"
/>
<div className="flex justify-end gap-3">
<button onClick={() => setActionModal(null)} className="px-4 py-2 text-gray-600">Abbrechen</button>
<button
onClick={() => handleEdit(actionModal.cr)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg"
>
Speichern & Annehmen
</button>
</div>
</>
)}
</div>
</div>
)}
</div>
)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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