diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 3594332..f13d08c 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -1,5 +1,44 @@ # BreakPilot Compliance - DSGVO/AI-Act SDK Platform +> **NON-NEGOTIABLE STRUCTURE RULES** (enforced by `.claude/settings.json` hook, git pre-commit, and CI): +> 1. **File-size budget:** soft target **300** lines, **hard cap 500** lines for any non-test, non-generated source file. Anything larger → split it. Exceptions are listed in `.claude/rules/loc-exceptions.txt` and require a written rationale. +> 2. **Clean architecture per service.** Routers/handlers stay thin (≤30 lines per handler) and delegate to services; services use repositories; repositories own DB I/O. See `AGENTS.python.md` / `AGENTS.go.md` / `AGENTS.typescript.md`. +> 3. **Do not touch the database schema.** No new Alembic migrations, no `ALTER TABLE`, no model field renames without an explicit migration plan reviewed by the DB owner. SQLAlchemy `__tablename__` and column names are frozen. +> 4. **Public endpoints are a contract.** Any change to a path, method, status code, request schema, or response schema in `backend-compliance/`, `ai-compliance-sdk/`, `dsms-gateway/`, `document-crawler/`, or `compliance-tts-service/` must be accompanied by a matching update in **every** consumer (`admin-compliance/`, `developer-portal/`, `breakpilot-compliance-sdk/`, `consent-sdk/`). Use the OpenAPI snapshot tests in `tests/contracts/` as the gate. +> 5. **Tests are not optional.** New code without tests fails CI. Refactors must preserve coverage and add a characterization test before splitting an oversized file. +> 6. **Do not bypass the guardrails.** Do not edit `.claude/settings.json`, `scripts/check-loc.sh`, or the loc-exceptions list to silence violations. If a rule is wrong, raise it in a PR description. +> +> These rules apply to **every** Claude Code session opened inside this repository, regardless of who launched it. They are loaded automatically via this `CLAUDE.md`. + + + +## First-Time Setup & Claude Code Onboarding + +**For humans:** Read this CLAUDE.md top to bottom before your first commit. Then read `AGENTS..md` for the service you are working on (`AGENTS.python.md`, `AGENTS.go.md`, or `AGENTS.typescript.md`). + +**For Claude Code sessions — things that cause first-commit failures:** + +1. **Wrong branch.** Run `git branch --show-current` before touching any file. The answer must be `coolify`. If it is `main`, run `git checkout coolify` before proceeding. + +2. **PreToolUse hook blocks your write.** The `PreToolUse` hooks in `.claude/settings.json` will reject Write/Edit operations on any file that would push its line count past 500. This is intentional — split the file into smaller modules instead of trying to bypass the hook. + +3. **Missing `[guardrail-change]` marker.** The `guardrail-integrity` CI job fails if you modify a guardrail file without the marker in the commit message body. See the table below. + +4. **Never `git add -A` or `git add .`.** Stage files individually by path. `git add -A` risks committing `.env`, `node_modules/`, `.next/`, compiled binaries, and other artifacts that must never enter the repo. + +5. **LOC check before push.** After any session, run `bash scripts/check-loc.sh`. It must exit 0 before you push. The git pre-commit hook runs this automatically, but run it manually first to catch issues early. + +### Commit message quick reference + +| Marker | Required when touching | +|--------|----------------------| +| `[guardrail-change]` | `.claude/settings.json`, `scripts/check-loc.sh`, `scripts/githooks/pre-commit`, `.claude/rules/loc-exceptions.txt`, any `AGENTS.*.md` | +| `[migration-approved]` | Anything under `migrations/` or `alembic/versions/` | + +Add the marker anywhere in the commit message body or footer — the CI job does a plain-text grep for it. + +--- + ## Entwicklungsumgebung (WICHTIG - IMMER ZUERST LESEN) ### Zwei-Rechner-Setup + Orca diff --git a/.claude/rules/architecture.md b/.claude/rules/architecture.md new file mode 100644 index 0000000..26c6c36 --- /dev/null +++ b/.claude/rules/architecture.md @@ -0,0 +1,43 @@ +# Architecture Rules (auto-loaded) + +These rules apply to **every** Claude Code session in this repository, regardless of who launched it. They are non-negotiable. + +## File-size budget + +- **Soft target:** 300 lines per non-test, non-generated source file. +- **Hard cap:** 500 lines. The PreToolUse hook in `.claude/settings.json` blocks Write/Edit operations that would create or push a file past 500. The git pre-commit hook re-checks. CI is the final gate. +- Exceptions live in `.claude/rules/loc-exceptions.txt` and require a written rationale plus `[guardrail-change]` in the commit message. The exceptions list should shrink over time, not grow. + +## Clean architecture + +- Python (FastAPI): see `AGENTS.python.md`. Layering: `api → services → repositories → db.models`. Routers ≤30 LOC per handler. Schemas split per domain. +- Go (Gin): see `AGENTS.go.md`. Standard Go Project Layout + hexagonal. `cmd/` thin, wiring in `internal/app`. +- TypeScript (Next.js): see `AGENTS.typescript.md`. Server-by-default, push the client boundary deep, colocate `_components/` and `_hooks/` per route. + +## Database is frozen + +- No new Alembic migrations. No `ALTER TABLE`. No `__tablename__` or column renames. +- The pre-commit hook blocks any change under `migrations/` or `alembic/versions/` unless the commit message contains `[migration-approved]`. + +## Public endpoints are a contract + +- Any change to a path/method/status/request schema/response schema in a backend service must update every consumer in the same change set. +- Each backend service has an OpenAPI baseline at `tests/contracts/openapi.baseline.json`. Contract tests fail on drift. + +## Tests + +- New code without tests fails CI. +- Refactors must preserve coverage. Before splitting an oversized file, add a characterization test that pins current behavior. +- Layout: `tests/unit/`, `tests/integration/`, `tests/contracts/`, `tests/e2e/`. + +## Guardrails are themselves protected + +- Edits to `.claude/settings.json`, `scripts/check-loc.sh`, `scripts/githooks/pre-commit`, `.claude/rules/loc-exceptions.txt`, or any `AGENTS.*.md` require `[guardrail-change]` in the commit message. The pre-commit hook enforces this. +- If you (Claude) think a rule is wrong, surface it to the user. Do not silently weaken it. + +## Tooling baseline + +- Python: `ruff`, `mypy --strict` on new modules, `pytest --cov`. +- Go: `golangci-lint` strict config, `go vet`, table-driven tests. +- TS: `tsc --noEmit` strict, ESLint type-aware, Vitest, Playwright. +- All three: dependency caching in CI, license/SBOM scan via `syft`+`grype`. diff --git a/.claude/rules/loc-exceptions.txt b/.claude/rules/loc-exceptions.txt new file mode 100644 index 0000000..9653f85 --- /dev/null +++ b/.claude/rules/loc-exceptions.txt @@ -0,0 +1,103 @@ +# loc-exceptions.txt — files allowed to exceed the 500-line hard cap. +# +# Format: one repo-relative path per line. Comments start with '#' and are ignored. +# Each exception MUST be preceded by a comment explaining why splitting is not viable. +# +# Phase 0 baseline: this list is initially empty. Phases 1-4 will add grandfathered +# entries as we encounter legitimate exceptions (e.g. large generated data tables). +# The goal is for this list to SHRINK over time, never grow. + +# --- admin-compliance: static data catalogs (Phase 3) --- +# Splitting these would fragment lookup tables without improving readability. +admin-compliance/lib/sdk/tom-generator/controls/loader.ts +admin-compliance/lib/sdk/vendor-compliance/risk/controls-library.ts +admin-compliance/lib/sdk/compliance-scope-triggers.ts +admin-compliance/lib/sdk/vendor-compliance/catalog/processing-activities.ts +admin-compliance/lib/sdk/catalog-manager/catalog-registry.ts +admin-compliance/lib/sdk/dsfa/mitigation-library.ts +admin-compliance/lib/sdk/vvt-baseline-catalog.ts +admin-compliance/lib/sdk/dsfa/eu-legal-frameworks.ts +admin-compliance/lib/sdk/dsfa/risk-catalog.ts +admin-compliance/lib/sdk/loeschfristen-baseline-catalog.ts +admin-compliance/lib/sdk/vendor-compliance/catalog/vendor-templates.ts +admin-compliance/lib/sdk/vendor-compliance/catalog/legal-basis.ts +admin-compliance/lib/sdk/vendor-compliance/contract-review/findings.ts +admin-compliance/lib/sdk/vendor-compliance/contract-review/checklists.ts +admin-compliance/lib/sdk/compliance-scope-types/document-scope-matrix-core.ts +admin-compliance/lib/sdk/compliance-scope-types/document-scope-matrix-extended.ts +admin-compliance/lib/sdk/demo-data/index.ts +admin-compliance/lib/sdk/tom-generator/demo-data/index.ts + +# --- admin-compliance: self-contained export generators (Phase 3) --- +# Each file generates a complete document format. Splitting mid-generation +# logic would create artificial module boundaries without benefit. +admin-compliance/lib/sdk/tom-generator/export/zip.ts +admin-compliance/lib/sdk/tom-generator/export/docx.ts +admin-compliance/lib/sdk/tom-generator/export/pdf.ts +admin-compliance/lib/sdk/einwilligungen/export/pdf.ts +admin-compliance/lib/sdk/einwilligungen/generator/privacy-policy-sections.ts + +# --- backend-compliance: legacy utility services (Phase 1) --- +# Pre-refactor utility modules not yet split. Phase 5 targets. +backend-compliance/compliance/services/control_generator.py +backend-compliance/compliance/services/audit_pdf_generator.py +backend-compliance/compliance/services/regulation_scraper.py +backend-compliance/compliance/services/llm_provider.py +backend-compliance/compliance/services/export_generator.py +backend-compliance/compliance/services/pdf_extractor.py +backend-compliance/compliance/services/ai_compliance_assistant.py + +# --- backend-compliance: Phase 1 code refactor backlog --- +# These are the remaining oversized route/service/data/auth files that Phase 1 +# did not reach. Each entry is a tracked refactor debt item — the list must shrink. +backend-compliance/compliance/services/decomposition_pass.py +backend-compliance/compliance/api/schemas.py +backend-compliance/compliance/api/canonical_control_routes.py +backend-compliance/compliance/db/repository.py +backend-compliance/compliance/db/models.py +backend-compliance/compliance/api/evidence_check_routes.py +backend-compliance/compliance/api/control_generator_routes.py +backend-compliance/compliance/api/process_task_routes.py +backend-compliance/compliance/api/evidence_routes.py +backend-compliance/compliance/api/crosswalk_routes.py +backend-compliance/compliance/api/dashboard_routes.py +backend-compliance/compliance/api/dsfa_routes.py +backend-compliance/compliance/api/routes.py +backend-compliance/compliance/api/tom_mapping_routes.py +backend-compliance/compliance/services/control_dedup.py +backend-compliance/compliance/services/framework_decomposition.py +backend-compliance/compliance/services/pipeline_adapter.py +backend-compliance/compliance/services/batch_dedup_runner.py +backend-compliance/compliance/services/obligation_extractor.py +backend-compliance/compliance/services/control_composer.py +backend-compliance/compliance/services/pattern_matcher.py +backend-compliance/compliance/data/iso27001_annex_a.py +backend-compliance/compliance/data/service_modules.py +backend-compliance/compliance/data/controls.py +backend-compliance/services/pdf_service.py +backend-compliance/services/file_processor.py +backend-compliance/auth/keycloak_auth.py + +# --- scripts: one-off ingestion, QA, and migration scripts --- +# These are operational scripts, not production application code. +# LOC rules don't apply in the same way to single-purpose scripts. +scripts/ingest-legal-corpus.sh +scripts/ingest-ce-corpus.sh +scripts/ingest-dsfa-bundesland.sh +scripts/edpb-crawler.py +scripts/apply_templates_023.py +scripts/qa/phase74_generate_gap_controls.py +scripts/qa/pdf_qa_all.py +scripts/qa/benchmark_llm_controls.py +backend-compliance/scripts/seed_policy_templates.py + +# --- docs-src: copies of backend source for documentation rendering --- +# These are not production code; they are rendered into the static docs site. +docs-src/control_generator.py +docs-src/control_generator_routes.py + +# --- consent-sdk: platform-native mobile SDKs (Swift / Dart) --- +# Flutter and iOS SDKs follow platform conventions (verbose verbose) that make +# splitting into multiple files awkward without sacrificing single-import ergonomics. +consent-sdk/src/mobile/flutter/consent_sdk.dart +consent-sdk/src/mobile/ios/ConsentManager.swift diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..0d899bc --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,28 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Write", + "hooks": [ + { + "type": "command", + "command": "f=$(jq -r '.tool_input.file_path // empty'); [ -z \"$f\" ] && exit 0; lines=$(printf '%s' \"$(jq -r '.tool_input.content // empty')\" | awk 'END{print NR}'); if [ \"${lines:-0}\" -gt 500 ]; then echo '{\"decision\":\"block\",\"reason\":\"breakpilot guardrail: file exceeds the 500-line hard cap. Split it into smaller modules per the layering rules in AGENTS..md. If this is generated/data code, add an entry to .claude/rules/loc-exceptions.txt with rationale and reference [guardrail-change].\"}'; exit 0; fi", + "shell": "bash", + "timeout": 5 + } + ] + }, + { + "matcher": "Edit", + "hooks": [ + { + "type": "command", + "command": "f=$(jq -r '.tool_input.file_path // empty'); [ -z \"$f\" ] || [ ! -f \"$f\" ] && exit 0; case \"$f\" in *.md|*.json|*.yaml|*.yml|*test*|*tests/*|*node_modules/*|*.next/*|*migrations/*) exit 0 ;; esac; new_str=$(jq -r '.tool_input.new_string // empty'); old_str=$(jq -r '.tool_input.old_string // empty'); old_lines=$(printf '%s' \"$old_str\" | awk 'END{print NR}'); new_lines=$(printf '%s' \"$new_str\" | awk 'END{print NR}'); cur=$(wc -l < \"$f\" | tr -d ' '); proj=$((cur - old_lines + new_lines)); if [ \"$proj\" -gt 500 ]; then echo \"{\\\"decision\\\":\\\"block\\\",\\\"reason\\\":\\\"breakpilot guardrail: this edit would push $f to ~$proj lines (hard cap is 500). Split the file before continuing. See AGENTS..md for the layering rules.\\\"}\"; fi; exit 0", + "shell": "bash", + "timeout": 5 + } + ] + } + ] + } +} diff --git a/.gitea/workflows/build-push-deploy.yml b/.gitea/workflows/build-push-deploy.yml index f6d1e2d..802fbce 100644 --- a/.gitea/workflows/build-push-deploy.yml +++ b/.gitea/workflows/build-push-deploy.yml @@ -184,12 +184,11 @@ jobs: docker push registry.meghsakha.com/breakpilot/compliance-dsms-gateway:latest docker push registry.meghsakha.com/breakpilot/compliance-dsms-gateway:${SHORT_SHA} - # ── orca redeploy (runs if at least one build succeeded) ───────────────── + # ── orca redeploy (only after all builds succeed) ───────────────────────── trigger-orca: runs-on: docker container: docker:27-cli - if: always() && (needs.build-admin-compliance.result == 'success' || needs.build-backend-compliance.result == 'success' || needs.build-ai-sdk.result == 'success' || needs.build-developer-portal.result == 'success' || needs.build-tts.result == 'success' || needs.build-document-crawler.result == 'success' || needs.build-dsms-gateway.result == 'success') needs: - build-admin-compliance - build-backend-compliance diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index d1532fb..478b1af 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -19,6 +19,47 @@ on: branches: [main, develop] jobs: + # ======================================== + # Guardrails — LOC budget + architecture gates + # Runs on every push/PR. Fails fast and cheap. + # ======================================== + + loc-budget: + runs-on: docker + container: alpine:3.20 + steps: + - name: Checkout + run: | + apk add --no-cache git bash + git clone --depth 50 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git . + - name: Enforce 500-line hard cap (whole repo) + run: | + chmod +x scripts/check-loc.sh + scripts/check-loc.sh + # Phase 5: whole-repo blocking gate. Phases 1-4 have drained the legacy + # baseline; any remaining oversized files must be listed in + # .claude/rules/loc-exceptions.txt with a written rationale. + + guardrail-integrity: + runs-on: docker + container: alpine:3.20 + if: github.event_name == 'pull_request' + steps: + - name: Checkout + run: | + apk add --no-cache git bash + git clone --depth 20 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git . + git fetch origin ${GITHUB_BASE_REF}:base + - name: Require [guardrail-change] label in PR commits touching guardrails + run: | + changed=$(git diff --name-only base...HEAD) + echo "$changed" | grep -E '^(\.claude/settings\.json|\.claude/rules/loc-exceptions\.txt|scripts/check-loc\.sh|scripts/githooks/pre-commit|AGENTS\.(python|go|typescript)\.md)$' || exit 0 + if ! git log base..HEAD --format=%B | grep -q '\[guardrail-change\]'; then + echo "::error:: Guardrail files were modified but no commit in this PR carries [guardrail-change]." + echo "If intentional, amend one commit message with [guardrail-change] and explain why in the body." + exit 1 + fi + # ======================================== # Lint (nur bei PRs) # ======================================== @@ -47,15 +88,28 @@ jobs: 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: Lint Python services + - name: Lint Python services (ruff) run: | pip install --quiet ruff - for svc in backend-compliance document-crawler dsms-gateway; do + fail=0 + for svc in backend-compliance document-crawler dsms-gateway compliance-tts-service; do if [ -d "$svc" ]; then - echo "=== Linting $svc ===" - ruff check "$svc/" --output-format=github || true + echo "=== ruff: $svc ===" + ruff check "$svc/" --output-format=github || fail=1 fi done + exit $fail + - name: Type-check (mypy via backend-compliance/mypy.ini) + # Policy is declared in backend-compliance/mypy.ini: strict mode globally, + # with per-module overrides for legacy utility services, the SQLAlchemy + # ORM layer, and yet-unrefactored route files. Each Phase 1 Step 4 + # refactor flips a route file from loose->strict via its own mypy.ini + # override block. + run: | + pip install --quiet mypy + if [ -f "backend-compliance/mypy.ini" ]; then + cd backend-compliance && mypy compliance/ + fi nodejs-lint: runs-on: docker @@ -66,17 +120,20 @@ jobs: run: | apk add --no-cache git git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git . - - name: Lint Node.js services + - name: Lint + type-check Node.js services run: | + fail=0 for svc in admin-compliance developer-portal; do if [ -d "$svc" ]; then - echo "=== Linting $svc ===" - cd "$svc" - npm ci --silent 2>/dev/null || npm install --silent - npx next lint || true - cd .. + echo "=== $svc: install ===" + (cd "$svc" && (npm ci --silent 2>/dev/null || npm install --silent)) + echo "=== $svc: next lint ===" + (cd "$svc" && npx next lint) || fail=1 + echo "=== $svc: tsc --noEmit ===" + (cd "$svc" && npx tsc --noEmit) || fail=1 fi done + exit $fail # ======================================== # Unit Tests @@ -169,6 +226,32 @@ jobs: pip install --quiet --no-cache-dir pytest pytest-asyncio python -m pytest test_main.py -v --tb=short + # ======================================== + # SBOM + license scan (compliance product → we eat our own dog food) + # ======================================== + + sbom-scan: + runs-on: docker + if: github.event_name == 'pull_request' + container: alpine:3.20 + steps: + - name: Checkout + run: | + apk add --no-cache git curl bash + git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git . + - name: Install syft + grype + run: | + curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin + curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin + - name: Generate SBOM + run: | + mkdir -p sbom-out + syft dir:. -o cyclonedx-json=sbom-out/sbom.cdx.json -q + - name: Vulnerability scan (fail on high+) + run: | + grype sbom:sbom-out/sbom.cdx.json --fail-on high -q + # Phase 5: blocking. Any high+ CVE in the dependency graph fails the PR. + # ======================================== # Validate Canonical Controls # ======================================== diff --git a/.gitignore b/.gitignore index a8dffe4..4f44fe2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,10 @@ secrets/ # Node node_modules/ .next/ +dist/ +.turbo/ +pnpm-lock.yaml +.pnpm-store/ # Python __pycache__/ diff --git a/AGENTS.go.md b/AGENTS.go.md new file mode 100644 index 0000000..e4e98f1 --- /dev/null +++ b/AGENTS.go.md @@ -0,0 +1,156 @@ +# AGENTS.go.md — Go Service Conventions + +Applies to: `ai-compliance-sdk/`. + +## Layered architecture (Gin) + +Follows [Standard Go Project Layout](https://github.com/golang-standards/project-layout) + hexagonal/clean-arch. + +``` +ai-compliance-sdk/ +├── cmd/server/main.go # Thin: parse flags → app.New → app.Run. <50 LOC. +├── internal/ +│ ├── app/ # Wiring: config + DI graph + lifecycle. +│ ├── domain/ # Pure types, interfaces, errors. No I/O imports. +│ │ └── / +│ ├── service/ # Business logic. Depends on domain interfaces only. +│ │ └── / +│ ├── repository/postgres/ # Concrete repo implementations. +│ │ └── / +│ ├── transport/http/ # Gin handlers. Thin. One handler per file group. +│ │ ├── handler// +│ │ ├── middleware/ +│ │ └── router.go +│ └── platform/ # DB pool, logger, config, tracing. +└── pkg/ # Importable by other repos. Empty unless needed. +``` + +**Dependency direction:** `transport → service → domain ← repository`. `domain` imports nothing from siblings. + +## Handlers + +- One handler = one Gin function. ≤40 LOC. +- Bind → call service → map domain error to HTTP via `httperr.Write(c, err)` → respond. +- Return early on errors. No business logic, no SQL. + +```go +func (h *IACEHandler) Create(c *gin.Context) { + var req CreateIACERequest + if err := c.ShouldBindJSON(&req); err != nil { + httperr.Write(c, httperr.BadRequest(err)) + return + } + out, err := h.svc.Create(c.Request.Context(), req.ToInput()) + if err != nil { + httperr.Write(c, err) + return + } + c.JSON(http.StatusCreated, out) +} +``` + +## Services + +- Struct + constructor + interface methods. No package-level state. +- Take `context.Context` as first arg always. Propagate to repos. +- Return `(value, error)`. Wrap with `fmt.Errorf("create iace: %w", err)`. +- Domain errors implemented as sentinel vars or typed errors; matched with `errors.Is` / `errors.As`. + +## Repositories + +- Interface lives in `domain//repository.go`. Implementation in `repository/postgres//`. +- One file per query group; no file >500 LOC. +- Use `pgx`/`sqlc` over hand-rolled string SQL when feasible. No ORM globals. +- All queries take `ctx`. No background goroutines without explicit lifecycle. + +## Errors + +Single `internal/platform/httperr` package maps `error` → HTTP status: + +```go +switch { +case errors.Is(err, domain.ErrNotFound): return 404 +case errors.Is(err, domain.ErrConflict): return 409 +case errors.As(err, &validationErr): return 422 +default: return 500 +} +``` + +Never `panic` in request handling. `recover` middleware logs and returns 500. + +## Tests + +- Co-located `*_test.go`. +- **Table-driven** tests for service logic; use `t.Run(tt.name, ...)`. +- Handlers tested with `httptest.NewRecorder`. +- Repos tested with `testcontainers-go` (or the existing compose Postgres) — never mocks at the SQL boundary. +- Coverage target: 80% on `service/`. CI fails on regression. + +```go +func TestIACEService_Create(t *testing.T) { + tests := []struct { + name string + input service.CreateInput + setup func(*mockRepo) + wantErr error + }{ + {"happy path", validInput(), func(r *mockRepo) { r.createReturns(nil) }, nil}, + {"conflict", validInput(), func(r *mockRepo) { r.createReturns(domain.ErrConflict) }, domain.ErrConflict}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { /* ... */ }) + } +} +``` + +## Tooling + +Run lint before pushing: + +```bash +golangci-lint run --timeout 5m ./... +``` + +The `.golangci.yml` at the service root (`ai-compliance-sdk/.golangci.yml`) enables: `errcheck, govet, staticcheck, gosec, gocyclo (≤20), gocritic, revive, goimports, unused, ineffassign`. Fix lint violations in new code; legacy violations are tracked but not required to fix immediately. + +- `gofumpt` formatting. +- `go vet ./...` clean. +- `go mod tidy` clean — no unused deps. + +## File splitting pattern + +When a Go file exceeds the 500-line hard cap, split it in place — no new packages needed: + +- All split files stay in **the same package directory** with the **same `package ` declaration**. +- No import changes are needed anywhere because Go packages are directory-scoped. +- Naming: `store_projects.go`, `store_components.go` (noun + underscore + sub-resource). +- For handlers: `iace_handler_projects.go`, `iace_handler_hazards.go`, etc. +- Before splitting, add a characterization test that pins current behaviour. + +## Error handling + +Domain errors are defined in `internal/domain//errors.go` as sentinel vars or typed errors. The mapping from domain error to HTTP status lives exclusively in `internal/platform/httperr/httperr.go` via `errors.Is` / `errors.As`. Handlers call `httperr.Write(c, err)` — **never** directly call `c.JSON` with a status code derived from business logic. + +## Context propagation + +- Always pass `ctx context.Context` as the **first parameter** in every service and repository method. +- Never store a context in a struct field — pass it per call. +- Cancellation must be respected: check `ctx.Err()` in loops; propagate to all I/O calls. + +## Concurrency + +- Goroutines must have a clear lifecycle owner (struct method that started them must stop them). +- Pass `ctx` everywhere. Cancellation respected. +- No global mutexes for request data. Use per-request context. + +## What you may NOT do + +- Touch DB schema/migrations. +- Add a new top-level package directly under `internal/` without architectural review. +- `import "C"`, unsafe, reflection-heavy code. +- Use `init()` for non-trivial setup. Wire it in `internal/app`. +- Use `interface{}` / `any` in new code without an explicit comment justifying it. +- Call `log.Fatal` outside of `main.go`; panicking in request handling is also forbidden. +- Shadow `err` with `:=` inside an `if`-block when the outer scope already declares `err` — use `=` or rename. +- Create a file >500 lines. +- Change a public route's contract without updating consumers. diff --git a/AGENTS.python.md b/AGENTS.python.md new file mode 100644 index 0000000..9fe715d --- /dev/null +++ b/AGENTS.python.md @@ -0,0 +1,148 @@ +# AGENTS.python.md — Python Service Conventions + +Applies to: `backend-compliance/`, `document-crawler/`, `dsms-gateway/`, `compliance-tts-service/`. + +## Layered architecture (FastAPI) + +``` +compliance/ +├── api/ # HTTP layer — routers only. Thin (≤30 LOC per handler). +│ └── _routes.py +├── services/ # Business logic. Pure-ish; no FastAPI imports. +│ └── _service.py +├── repositories/ # DB access. Owns SQLAlchemy session usage. +│ └── _repository.py +├── domain/ # Value objects, enums, domain exceptions. +├── schemas/ # Pydantic models, split per domain. NEVER one giant schemas.py. +│ └── .py +└── db/ + └── models/ # SQLAlchemy ORM, one module per aggregate. __tablename__ frozen. +``` + +**Dependency direction:** `api → services → repositories → db.models`. Lower layers must not import upper layers. + +## Routers + +- One `APIRouter` per domain file. +- Handlers do exactly: parse request → call service → map domain errors to HTTPException → return response model. +- Inject services via `Depends`. No globals. +- Tag routes; document with summary + response_model. + +```python +@router.post("/dsr/requests", response_model=DSRRequestRead, status_code=201) +async def create_dsr_request( + payload: DSRRequestCreate, + service: DSRService = Depends(get_dsr_service), + tenant_id: UUID = Depends(get_tenant_id), +) -> DSRRequestRead: + try: + return await service.create(tenant_id, payload) + except DSRConflict as exc: + raise HTTPException(409, str(exc)) from exc +``` + +## Services + +- Constructor takes the repository (interface, not concrete). +- No `Request`, `Response`, or HTTP knowledge. +- Raise domain exceptions (e.g. `DSRConflict`, `DSRNotFound`), never `HTTPException`. +- Return domain objects or Pydantic schemas — pick one and stay consistent inside a service. + +## Repositories + +- Methods are intent-named (`get_pending_for_tenant`), not CRUD-named (`select_where`). +- Sessions injected, not constructed inside. +- No business logic. No cross-aggregate joins for unrelated workflows — that belongs in a service. +- Return ORM models or domain VOs; never `Row`. + +## Schemas (Pydantic v2) + +- One module per domain. Module ≤300 lines. +- Use `model_config = ConfigDict(from_attributes=True, frozen=True)` for read models. +- Separate `*Create`, `*Update`, `*Read`. No giant union schemas. + +## Tests (`pytest`) + +- Layout: `tests/unit/`, `tests/integration/`, `tests/contracts/`. +- Unit tests mock the repository. Use `pytest.fixture` + `unittest.mock.AsyncMock`. +- Integration tests run against the real Postgres from `docker-compose.yml` via a transactional fixture (rollback after each test). +- Contract tests diff `/openapi.json` against `tests/contracts/openapi.baseline.json`. +- Naming: `test___.py::TestClass::test_method`. +- `pytest-asyncio` mode = `auto`. Mark slow tests with `@pytest.mark.slow`. +- Coverage target: 80% for new code; never decrease the service baseline. + +## Tooling + +- `ruff check` + `ruff format` (line length 100). +- `mypy --strict` on `services/`, `repositories/`, `domain/`. Expand outward. +- `pip-audit` in CI. +- Async-first: prefer `httpx.AsyncClient`, `asyncpg`/`SQLAlchemy 2.x async`. + +## mypy configuration + +`backend-compliance/mypy.ini` is the mypy config. Strict mode is on globally; per-module overrides exist only for legacy files that have not been cleaned up yet. + +- New modules added to `compliance/services/` or `compliance/repositories/` **must** pass `mypy --strict`. +- To type-check a new module: `cd backend-compliance && mypy compliance/your_new_module.py` +- When you fully type a legacy file, **remove its loose-override block** from `mypy.ini` as part of the same PR. + +## Dependency injection + +Services and repositories are wired via FastAPI `Depends`. Never instantiate a service or repository directly inside a handler. + +```python +# dependencies.py +def get_my_service(db: AsyncSession = Depends(get_db)) -> MyService: + return MyService(MyRepository(db)) + +# router +@router.get("/items", response_model=list[ItemRead]) +async def list_items(svc: MyService = Depends(get_my_service)) -> list[ItemRead]: + return await svc.list() +``` + +- Services take repositories in `__init__`; repositories take `Session` or `AsyncSession`. + +## Structured logging + +```python +import structlog +logger = structlog.get_logger() + +# Always bind context before logging: +logger.bind(tenant_id=str(tid), action="create_dsfa").info("dsfa created") +``` + +- Audit-relevant actions must use the audit logger with a `legal_basis` field. +- Never log secrets, PII, or full request bodies. + +## Barrel re-export pattern + +When an oversized file (e.g. `schemas.py`, `models.py`) is split into a sub-package, the original stays as a **thin re-exporter** so existing consumer imports keep working: + +```python +# compliance/schemas.py (barrel — DO NOT ADD NEW CODE HERE) +from .schemas.ai import * # noqa: F401, F403 +from .schemas.consent import * # noqa: F401, F403 +``` + +- New code imports from the specific module (e.g. `from compliance.schemas.ai import AIRiskRead`), not the barrel. +- `from module import *` is only permitted in barrel files. + +## Errors & logging + +- Domain errors inherit from a single `DomainError` base per service. +- Log via `structlog` with bound context (`tenant_id`, `request_id`). Never log secrets, PII, or full request bodies. +- Audit-relevant actions go through the audit logger, not the application logger. + +## What you may NOT do + +- Add a new Alembic migration. +- Rename a `__tablename__`, column, or enum value. +- Change a public route's path/method/status/schema without simultaneous dashboard fix. +- Catch `Exception` broadly — catch the specific domain or library error. +- Put business logic in a router or in a Pydantic validator. +- `from module import *` in new code — only in barrel re-exporters. +- `raise HTTPException` inside the service layer — raise domain exceptions; map them in the router. +- Use `model_validate` on untrusted external data without an explicit schema boundary. +- Create a new file >500 lines. Period. diff --git a/AGENTS.typescript.md b/AGENTS.typescript.md new file mode 100644 index 0000000..c020c5f --- /dev/null +++ b/AGENTS.typescript.md @@ -0,0 +1,125 @@ +# AGENTS.typescript.md — TypeScript / Next.js Conventions + +Applies to: `admin-compliance/`, `developer-portal/`, `breakpilot-compliance-sdk/`, `consent-sdk/`, `dsms-node/` (where applicable). + +## Layered architecture (Next.js 15 App Router) + +``` +app/ +├── / +│ ├── page.tsx # Server Component by default. ≤200 LOC. +│ ├── layout.tsx +│ ├── _components/ # Private folder; not routable. Colocated UI. +│ │ └── .tsx # Each file ≤300 LOC. +│ ├── _hooks/ # Client hooks for this route. +│ ├── _server/ # Server actions, data loaders for this route. +│ └── loading.tsx / error.tsx +├── api/ +│ └── /route.ts # Thin handler. Delegates to lib/server//. +lib/ +├── / # Pure helpers, types, schemas (zod). Reusable. +└── server// # Server-only logic; uses "server-only" import. +components/ # Truly shared, app-wide components. +``` + +**Server vs Client:** Default is Server Component. Add `"use client"` only when you need state, effects, or browser APIs. Push the boundary as deep as possible. + +## API routes (route.ts) + +- One handler per HTTP method, ≤40 LOC. +- Validate input with zod `safeParse` — never `parse` (throws and bypasses error handling). +- Delegate to `lib/server//`. No business logic in `route.ts`. +- Always return `NextResponse.json(..., { status })`. Let the framework's error boundary handle unexpected errors — don't wrap the entire handler in `try/catch`. + +```typescript +// app/api//route.ts (≤40 LOC) +import { NextRequest, NextResponse } from 'next/server'; +import { mySchema } from '@/lib/schemas/'; +import { myService } from '@/lib/server/'; + +export async function POST(req: NextRequest) { + const body = mySchema.safeParse(await req.json()); + if (!body.success) return NextResponse.json({ error: body.error }, { status: 400 }); + const result = await myService.create(body.data); + return NextResponse.json(result, { status: 201 }); +} +``` + +## Page components + +- Pages >300 lines must be split into colocated `_components/`. +- Server Components fetch data; pass plain objects to Client Components. +- No data fetching in `useEffect` for server-renderable data. +- State management: prefer URL state (`searchParams`) and Server Components over global stores. + +## Types + +- `lib/sdk/types.ts` is being split into `lib/sdk/types/.ts`. Mirror backend domain boundaries. +- All API DTOs are zod schemas; infer types via `z.infer`. +- No `any`. No `as unknown as`. If you reach for it, the type is wrong. +- Always use `import type { Foo }` for type-only imports. +- Never use `as` type assertions except when bridging external data at a boundary (add a comment explaining why). +- No `@ts-ignore`. `@ts-expect-error` only with a comment explaining the suppression. + +## Barrel re-export pattern + +`lib/sdk/types.ts` is a barrel — it re-exports from domain-specific files. **Do not add new types directly to it.** + +```typescript +// lib/sdk/types.ts (barrel — DO NOT ADD NEW TYPES HERE) +export * from './types/enums'; +export * from './types/company-profile'; +// ... etc. + +// New types go in lib/sdk/types/.ts +``` + +- When splitting an oversized file, keep the original as a thin barrel so existing imports don't break. +- New code imports directly from the specific module (e.g. `import type { CompanyProfile } from '@/lib/sdk/types/company-profile'`), not the barrel. + +## Server vs Client components + +Default is Server Component. Add `"use client"` only when required: + +| Need | Pattern | +|------|---------| +| Data fetching only | Server Component (no directive) | +| `useState` / `useEffect` | Client Component (`"use client"`) | +| Browser API | Client Component | +| Event handlers | Client Component | + +- Pass only serializable props from Server → Client Components (no functions, no class instances). +- Never add `"use client"` to a layout or page just because one child needs it — extract the client part into a `_components/` file. + +## Tests + +- Unit: **Vitest** (`*.test.ts`/`*.test.tsx`), colocated. +- Hooks: `@testing-library/react` `renderHook`. +- E2E: **Playwright** (`tests/e2e/`), one spec per top-level page, smoke happy path minimum. +- Snapshot tests sparingly — only for stable output (CSV, JSON-LD). +- Coverage target: 70% on `lib/`, smoke coverage on `app/`. + +## Tooling + +- `tsc --noEmit` clean (strict mode, `noUncheckedIndexedAccess: true`). +- ESLint with `@typescript-eslint`, `eslint-config-next`, type-aware rules on. +- `prettier`. +- `next build` clean. No `// @ts-ignore`. `// @ts-expect-error` only with a comment explaining why. + +## Performance + +- Use `next/dynamic` for heavy client-only components. +- Image: `next/image` with explicit width/height. +- Avoid waterfalls — `Promise.all` for parallel data fetches in Server Components. + +## What you may NOT do + +- Put business logic in a `page.tsx` or `route.ts`. +- Reach across module boundaries (e.g. `admin-compliance` importing from `developer-portal`). +- Use `dangerouslySetInnerHTML` without DOMPurify sanitization. +- Call internal backend APIs directly from Client Components — use Server Components or API routes as a proxy. +- Add `"use client"` to a layout or page just because one child needs it — extract the client part. +- Spread `...props` onto a DOM element without filtering the props first (type error risk). +- Change a public API route's path/method/schema without updating SDK consumers in the same change. +- Create a file >500 lines. +- Disable a lint or type rule globally to silence a finding — fix the root cause. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..92edbfd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,203 @@ +# Contributing to breakpilot-compliance + +--- + +## 1. Getting Started + +```bash +git clone https://gitea.meghsakha.com/Benjamin_Boenisch/breakpilot-compliance.git +cd breakpilot-compliance +git checkout coolify # always base work off coolify, NOT main +``` + +**Branch conventions** (branch from `coolify`): + +| Prefix | Use for | +|--------|---------| +| `feature/` | New functionality | +| `fix/` | Bug fixes | +| `chore/` | Tooling, deps, CI, docs | + +Example: `git checkout -b feature/ai-sdk-risk-scoring` + +--- + +## 2. Dev Environment + +Each service runs independently. Start only what you need. + +**Go — ai-compliance-sdk** +```bash +cd ai-compliance-sdk +go run ./cmd/server +``` + +**Python — backend-compliance** +```bash +cd backend-compliance +pip install -r requirements.txt +uvicorn main:app --reload +``` + +**Python — dsms-gateway / document-crawler / compliance-tts-service** +```bash +cd +pip install -r requirements.txt +uvicorn main:app --reload --port +``` + +**Node.js — admin-compliance** +```bash +cd admin-compliance +npm install +npm run dev # http://localhost:3007 +``` + +**Node.js — developer-portal** +```bash +cd developer-portal +npm install +npm run dev # http://localhost:3006 +``` + +**All services together (local Docker)** +```bash +docker compose up -d +``` + +Config lives in `.env` (not committed). Copy `.env.example` and fill in `COMPLIANCE_DATABASE_URL`, `QDRANT_URL`, `QDRANT_API_KEY`, and Vault tokens. + +--- + +## 3. Before Your First Commit + +Run all of these locally. CI will run the same checks and fail if they don't pass. + +**LOC budget (mandatory)** +```bash +bash scripts/check-loc.sh # must exit 0 +``` + +**Go lint** +```bash +cd ai-compliance-sdk +golangci-lint run --timeout 5m ./... +``` + +**Python lint** +```bash +cd backend-compliance +ruff check . +mypy compliance/ # only if mypy.ini exists +``` + +**TypeScript type-check** +```bash +cd admin-compliance +npx tsc --noEmit +``` + +**Tests** +```bash +# Go +cd ai-compliance-sdk && go test ./... + +# Python backend +cd backend-compliance && pytest + +# DSMS gateway +cd dsms-gateway && pytest test_main.py +``` + +If any step fails, fix it before committing. The git pre-commit hook re-runs `check-loc.sh` automatically. + +--- + +## 4. Commit Message Rules + +Use [Conventional Commits](https://www.conventionalcommits.org/) style: + +``` +(): + +[optional body] +[optional footer] +``` + +Types: `feat`, `fix`, `chore`, `refactor`, `test`, `docs`, `ci`. + +### `[guardrail-change]` marker — REQUIRED + +Add `[guardrail-change]` anywhere in the commit message body (or footer) when your changeset touches **any** of these files: + +| File / path | Reason protected | +|-------------|-----------------| +| `.claude/settings.json` | PreToolUse/PostToolUse hooks | +| `scripts/check-loc.sh` | LOC enforcement script | +| `scripts/githooks/pre-commit` | Git hook | +| `.claude/rules/loc-exceptions.txt` | Exception registry | +| `AGENTS.*.md` (any) | Per-language architecture rules | + +The `guardrail-integrity` CI job checks for this marker and **fails the build** if it is missing. + +**Valid guardrail commit example:** +``` +chore(guardrail): add exception for generated protobuf file + +proto/generated/compliance.pb.go exceeds 500 LOC because it is +machine-generated and cannot be split. Added to loc-exceptions.txt +with rationale. + +[guardrail-change] +``` + +--- + +## 5. Architecture Rules (Non-Negotiable) + +### File budget +- **500 LOC hard cap** on every non-test, non-generated source file. +- The `PreToolUse` hook in `.claude/settings.json` blocks Claude Code from creating or editing files that would breach this limit. +- Exceptions require a written rationale in `.claude/rules/loc-exceptions.txt` plus `[guardrail-change]` in the commit. + +### Clean architecture per service +- Python (FastAPI): `api → services → repositories → db.models`. Handlers ≤ 30 LOC. See `AGENTS.python.md`. +- Go (Gin): Standard Go Project Layout + hexagonal. `cmd/` is thin wiring. See `AGENTS.go.md`. +- TypeScript (Next.js 15): server-first, push client boundary deep, colocate `_components/` + `_hooks/` per route. See `AGENTS.typescript.md`. + +### Database is frozen +- No new Alembic migrations, no `ALTER TABLE`, no `__tablename__` or column renames. +- The pre-commit hook blocks any change under `migrations/` or `alembic/versions/` unless the commit message contains `[migration-approved]`. + +### Public endpoints are a contract +- Any change to a route path, HTTP method, status code, request schema, or response schema in `backend-compliance/`, `ai-compliance-sdk/`, `dsms-gateway/`, `document-crawler/`, or `compliance-tts-service/` **must** be accompanied by a matching update in every consumer (`admin-compliance/`, `developer-portal/`, `breakpilot-compliance-sdk/`, `consent-sdk/`) in the **same changeset**. +- OpenAPI baseline snapshots live in `tests/contracts/`. Contract tests fail on any drift. + +--- + +## 6. Pull Requests + +- **Target branch: `coolify`** — never open a PR directly against `main`. +- Keep PRs focused; one logical change per PR. + +**PR checklist before requesting review:** + +- [ ] `bash scripts/check-loc.sh` exits 0 +- [ ] All lint checks pass (go, python, tsc) +- [ ] All tests pass locally +- [ ] No endpoint drift without consumer updates in the same PR +- [ ] `[guardrail-change]` present in commit message if guardrail files were touched +- [ ] Docs updated if new endpoints, config vars, or architecture changed + +--- + +## 7. Claude Code Users + +This section is for AI-assisted development sessions using Claude Code. + +- **Always verify your branch first:** `git branch --show-current` must return `coolify`. If it returns `main`, switch before doing anything. +- The `.claude/settings.json` `PreToolUse` hooks will automatically block Write/Edit operations on files that would exceed 500 lines. This is intentional — split the file instead. +- If the `guardrail-integrity` CI job fails, check that your commit message body includes `[guardrail-change]`. Add it and amend or create a fixup commit. +- **Never use `git add -A` or `git add .`** — always stage specific files by path to avoid accidentally committing `.env`, `node_modules/`, `.next/`, or compiled binaries. +- After every session: `bash scripts/check-loc.sh` must exit 0 before pushing. +- Read `CLAUDE.md` and the relevant `AGENTS..md` before starting work on a service. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9b58e25 --- /dev/null +++ b/README.md @@ -0,0 +1,132 @@ +# breakpilot-compliance + +**DSGVO/AI-Act compliance platform — 10 services, Go · Python · TypeScript** + +[![CI](https://gitea.meghsakha.com/Benjamin_Boenisch/breakpilot-compliance/actions/workflows/ci.yaml/badge.svg)](https://gitea.meghsakha.com/Benjamin_Boenisch/breakpilot-compliance/actions) +![Go](https://img.shields.io/badge/Go-1.24-00ADD8?logo=go&logoColor=white) +![Python](https://img.shields.io/badge/Python-3.12-3776AB?logo=python&logoColor=white) +![Node.js](https://img.shields.io/badge/Node.js-20-339933?logo=node.js&logoColor=white) +![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178C6?logo=typescript&logoColor=white) +![FastAPI](https://img.shields.io/badge/FastAPI-0.123-009688?logo=fastapi&logoColor=white) +![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg) +![DSGVO](https://img.shields.io/badge/DSGVO-compliant-green) +![AI Act](https://img.shields.io/badge/EU%20AI%20Act-compliant-green) +![LOC guard](https://img.shields.io/badge/LOC%20guard-500%20hard%20cap-orange) +![Services](https://img.shields.io/badge/services-10-blueviolet) + +--- + +## Overview + +breakpilot-compliance is a multi-tenant DSGVO/EU AI Act compliance platform that provides an SDK for consent management, data subject requests (DSR), audit logging, iACE impact assessments, and document archival. It ships as 10 containerised services covering an admin dashboard, a developer portal, a Python/FastAPI backend, a Go AI compliance engine, TTS, and a decentralised document store on IPFS. Every service is deployed automatically via Gitea Actions → Coolify on the `coolify` branch. + +--- + +## Architecture + +| Service | Tech | Port | Container | +|---------|------|------|-----------| +| admin-compliance | Next.js 15 | 3007 | bp-compliance-admin | +| backend-compliance | Python / FastAPI 0.123 | 8002 | bp-compliance-backend | +| ai-compliance-sdk | Go 1.24 / Gin | 8093 | bp-compliance-ai-sdk | +| developer-portal | Next.js 15 | 3006 | bp-compliance-developer-portal | +| breakpilot-compliance-sdk | TypeScript SDK (React/Vue/Angular/vanilla) | — | — | +| consent-sdk | JS/TS Consent SDK | — | — | +| compliance-tts-service | Python / Piper TTS | 8095 | bp-compliance-tts | +| document-crawler | Python / FastAPI | 8098 | bp-compliance-document-crawler | +| dsms-gateway | Python / FastAPI / IPFS | 8082 | bp-compliance-dsms-gateway | +| dsms-node | IPFS Kubo v0.24.0 | — | bp-compliance-dsms-node | + +All containers share the external `breakpilot-network` Docker network and depend on `breakpilot-core` (Valkey, Vault, RAG service, Nginx reverse proxy). + +--- + +## Quick Start + +**Prerequisites:** Docker, Go 1.24+, Python 3.12+, Node.js 20+ + +```bash +git clone https://gitea.meghsakha.com/Benjamin_Boenisch/breakpilot-compliance.git +cd breakpilot-compliance + +# Copy and populate secrets (never commit .env) +cp .env.example .env + +# Start all services +docker compose up -d +``` + +For the Coolify/Hetzner production target (x86_64), use the override: + +```bash +docker compose -f docker-compose.yml -f docker-compose.hetzner.yml up -d +``` + +--- + +## Development Workflow + +Work on the `coolify` branch. Push to **both** remotes to trigger CI and deploy: + +```bash +git checkout coolify +# ... make changes ... +git push origin coolify && git push gitea coolify +``` + +Push to `gitea` triggers: +1. **Gitea Actions** — lint → test → validate (see CI Pipeline below) +2. **Coolify** — automatic build + deploy (~3 min total) + +Monitor status: + +--- + +## CI Pipeline + +Defined in `.gitea/workflows/ci.yaml`. + +| Job | What it checks | +|-----|----------------| +| `loc-budget` | All source files ≤ 500 LOC; soft target 300 | +| `guardrail-integrity` | Commits touching guardrail files carry `[guardrail-change]` | +| `go-lint` | `golangci-lint` on `ai-compliance-sdk/` | +| `python-lint` | `ruff` + `mypy` on Python services | +| `nodejs-lint` | `tsc --noEmit` + ESLint on Next.js services | +| `test-go-ai-compliance` | `go test ./...` in `ai-compliance-sdk/` | +| `test-python-backend-compliance` | `pytest` in `backend-compliance/` | +| `test-python-document-crawler` | `pytest` in `document-crawler/` | +| `test-python-dsms-gateway` | `pytest test_main.py` in `dsms-gateway/` | +| `sbom-scan` | License + vulnerability scan via `syft` + `grype` | +| `validate-canonical-controls` | OpenAPI contract baseline diff | + +--- + +## File Budget + +| Limit | Value | How to check | +|-------|-------|--------------| +| Soft target | 300 LOC | `bash scripts/check-loc.sh` | +| Hard cap | 500 LOC | Same; also enforced by `PreToolUse` hook + git pre-commit + CI | +| Exceptions | `.claude/rules/loc-exceptions.txt` | Require written rationale + `[guardrail-change]` commit marker | + +The `.claude/settings.json` `PreToolUse` hook blocks Claude Code from writing or editing files that would exceed the hard cap. The git pre-commit hook re-checks. CI is the final gate. + +--- + +## Links + +| | URL | +|-|-----| +| Admin dashboard | | +| Developer portal | | +| Backend API | | +| AI SDK API | | +| Gitea repo | | +| Gitea Actions | | + +--- + +## License + +Apache-2.0. See [LICENSE](LICENSE). diff --git a/REFACTOR_PLAYBOOK.md b/REFACTOR_PLAYBOOK.md new file mode 100644 index 0000000..a4d4a79 --- /dev/null +++ b/REFACTOR_PLAYBOOK.md @@ -0,0 +1,915 @@ + +--- + +## 1.9 `AGENTS.python.md` — Python / FastAPI conventions + +```markdown +# AGENTS.python.md — Python Service Conventions + +## Layered architecture (FastAPI) + + +## 1. Guardrail files (drop these in first) + +These artifacts enforce the rules without you or Claude having to remember them. Install them as **Phase 0**, before touching any real code. + +### 1.1 `.claude/CLAUDE.md` — loaded into every Claude session + +```markdown +# + +> **NON-NEGOTIABLE STRUCTURE RULES** (enforced by `.claude/settings.json` hook, git pre-commit, and CI): +> 1. **File-size budget:** soft target **300** lines, **hard cap 500** lines for any non-test, non-generated source file. Anything larger → split it. Exceptions are listed in `.claude/rules/loc-exceptions.txt` and require a written rationale. +> 2. **Clean architecture per service.** Routers/handlers stay thin (≤30 lines per handler) and delegate to services; services use repositories; repositories own DB I/O. See `AGENTS.python.md` / `AGENTS.go.md` / `AGENTS.typescript.md`. +> 3. **Do not touch the database schema.** No new migrations, no `ALTER TABLE`, no model field renames without an explicit migration plan reviewed by the DB owner. +> 4. **Public endpoints are a contract.** Any change to a path/method/status/schema in a backend must be accompanied by a matching update in **every** consumer. OpenAPI snapshot tests in `tests/contracts/` are the gate. +> 5. **Tests are not optional.** New code without tests fails CI. Refactors must preserve coverage and add a characterization test before splitting an oversized file. +> 6. **Do not bypass the guardrails.** Do not edit `.claude/settings.json`, `scripts/check-loc.sh`, or the loc-exceptions list to silence violations. If a rule is wrong, raise it in a PR description. +> +> These rules apply to every Claude Code session opened inside this repository, regardless of who launched it. They are loaded automatically via this CLAUDE.md. +``` + +Keep project-specific notes (dev environment, URLs, tech stack) under this header. + +### 1.2 `.claude/settings.json` — PreToolUse LOC hook + +First line of defense. Blocks Write/Edit operations that would create or push a file past 500 lines. This stops Claude from ever producing oversized files. + +```json +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Write", + "hooks": [ + { + "type": "command", + "command": "f=$(jq -r '.tool_input.file_path // empty'); [ -z \"$f\" ] && exit 0; lines=$(printf '%s' \"$(jq -r '.tool_input.content // empty')\" | awk 'END{print NR}'); if [ \"${lines:-0}\" -gt 500 ]; then echo '{\"decision\":\"block\",\"reason\":\"guardrail: file exceeds the 500-line hard cap. Split it into smaller modules per the layering rules in AGENTS..md.\"}'; exit 0; fi", + "shell": "bash", + "timeout": 5 + } + ] + }, + { + "matcher": "Edit", + "hooks": [ + { + "type": "command", + "command": "f=$(jq -r '.tool_input.file_path // empty'); [ -z \"$f\" ] || [ ! -f \"$f\" ] && exit 0; case \"$f\" in *.md|*.json|*.yaml|*.yml|*test*|*tests/*|*node_modules/*|*.next/*|*migrations/*) exit 0 ;; esac; new_str=$(jq -r '.tool_input.new_string // empty'); old_str=$(jq -r '.tool_input.old_string // empty'); old_lines=$(printf '%s' \"$old_str\" | awk 'END{print NR}'); new_lines=$(printf '%s' \"$new_str\" | awk 'END{print NR}'); cur=$(wc -l < \"$f\" | tr -d ' '); proj=$((cur - old_lines + new_lines)); if [ \"$proj\" -gt 500 ]; then echo \"{\\\"decision\\\":\\\"block\\\",\\\"reason\\\":\\\"guardrail: this edit would push $f to ~$proj lines (hard cap is 500). Split the file before continuing.\\\"}\"; fi; exit 0", + "shell": "bash", + "timeout": 5 + } + ] + } + ] + } +} +``` + +### 1.3 `.claude/rules/architecture.md` — auto-loaded architecture rule + +```markdown +# Architecture Rules (auto-loaded) + +Non-negotiable. Applied to every Claude Code session in this repo. + +## File-size budget +- **Soft target:** 300 lines. **Hard cap:** 500 lines. +- Enforced by PreToolUse hook, pre-commit hook, and CI. +- Exceptions live in `.claude/rules/loc-exceptions.txt` and require `[guardrail-change]` in the commit message. This list should SHRINK over time. + +## Clean architecture +- Python: see `AGENTS.python.md`. Layering: api → services → repositories → db.models. +- Go: see `AGENTS.go.md`. Standard Go Project Layout + hexagonal. +- TypeScript: see `AGENTS.typescript.md`. Server-by-default, push client boundary deep, colocate `_components/` and `_hooks/` per route. + +## Database is frozen +- No new migrations. No `ALTER TABLE`. No column renames. +- Pre-commit hook blocks any change under `migrations/` unless commit message contains `[migration-approved]`. + +## Public endpoints are a contract +- Any change to path/method/status/schema must update every consumer in the same change set. +- OpenAPI baseline at `tests/contracts/openapi.baseline.json`. Contract tests fail on drift. + +## Tests +- New code without tests fails CI. +- Refactors preserve coverage. Before splitting an oversized file, add a characterization test pinning current behavior. +- Layout: `tests/unit/`, `tests/integration/`, `tests/contracts/`, `tests/e2e/`. + +## Guardrails are protected +- Edits to `.claude/settings.json`, `scripts/check-loc.sh`, `scripts/githooks/pre-commit`, `.claude/rules/loc-exceptions.txt`, or any `AGENTS.*.md` require `[guardrail-change]` in the commit message. +- If Claude thinks a rule is wrong, surface it to the user. Do not silently weaken. + +## Tooling baseline +- Python: `ruff`, `mypy --strict` on new modules, `pytest --cov`. +- Go: `golangci-lint` strict, `go vet`, table-driven tests. +- TS: `tsc --noEmit` strict, ESLint type-aware, Vitest, Playwright. +- All: dependency caching in CI, license/SBOM scan via `syft`+`grype`. +``` + +### 1.4 `.claude/rules/loc-exceptions.txt` + +``` +# loc-exceptions.txt — files allowed to exceed the 500-line hard cap. +# +# Format: one repo-relative path per line. Comments start with '#'. +# Each exception MUST be preceded by a comment explaining why splitting is not viable. +# Goal: this list SHRINKS over time. + +# --- Example entries --- +# Static data catalogs — splitting fragments lookup tables without improving readability. +# src/catalogs/country-data.ts +# src/catalogs/industry-taxonomy.ts + +# Generated files — regenerated from schemas. +# api/generated/types.ts +``` + + +### 1.5 `scripts/check-loc.sh` + +```bash +#!/usr/bin/env bash +# check-loc.sh — File-size budget enforcer. Soft: 300. Hard: 500. +# +# Usage: +# scripts/check-loc.sh # scan whole repo +# scripts/check-loc.sh --changed # only files changed vs origin/main +# scripts/check-loc.sh path/to/file.py # check specific files +# scripts/check-loc.sh --json # machine-readable output +# Exit codes: 0 clean, 1 hard violation, 2 bad invocation. + +set -euo pipefail +SOFT=300 +HARD=500 +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +EXCEPTIONS_FILE="$REPO_ROOT/.claude/rules/loc-exceptions.txt" + +CHANGED_ONLY=0; JSON=0; TARGETS=() +for arg in "$@"; do + case "$arg" in + --changed) CHANGED_ONLY=1 ;; + --json) JSON=1 ;; + -h|--help) sed -n '2,10p' "$0"; exit 0 ;; + -*) echo "unknown flag: $arg" >&2; exit 2 ;; + *) TARGETS+=("$arg") ;; + esac +done + +is_excluded() { + local f="$1" + case "$f" in + */node_modules/*|*/.next/*|*/.git/*|*/dist/*|*/build/*|*/__pycache__/*|*/vendor/*) return 0 ;; + */migrations/*|*/alembic/versions/*) return 0 ;; + *_test.go|*.test.ts|*.test.tsx|*.spec.ts|*.spec.tsx) return 0 ;; + */tests/*|*/test/*) return 0 ;; + *.md|*.json|*.yaml|*.yml|*.lock|*.sum|*.mod|*.toml|*.cfg|*.ini) return 0 ;; + *.svg|*.png|*.jpg|*.jpeg|*.gif|*.ico|*.pdf|*.woff|*.woff2|*.ttf) return 0 ;; + *.generated.*|*.gen.*|*_pb.go|*_pb2.py|*.pb.go) return 0 ;; + esac + return 1 +} +is_in_exceptions() { + [[ -f "$EXCEPTIONS_FILE" ]] || return 1 + local rel="${1#$REPO_ROOT/}" + grep -Fxq "$rel" "$EXCEPTIONS_FILE" +} +collect_targets() { + if (( ${#TARGETS[@]} > 0 )); then printf '%s\n' "${TARGETS[@]}" + elif (( CHANGED_ONLY )); then + git -C "$REPO_ROOT" diff --name-only --diff-filter=AM origin/main...HEAD 2>/dev/null \ + || git -C "$REPO_ROOT" diff --name-only --diff-filter=AM HEAD + else git -C "$REPO_ROOT" ls-files; fi +} + +violations_hard=(); violations_soft=() +while IFS= read -r f; do + [[ -z "$f" ]] && continue + abs="$f"; [[ "$abs" != /* ]] && abs="$REPO_ROOT/$f" + [[ -f "$abs" ]] || continue + is_excluded "$abs" && continue + is_in_exceptions "$abs" && continue + loc=$(wc -l < "$abs" | tr -d ' ') + if (( loc > HARD )); then violations_hard+=("$loc $f") + elif (( loc > SOFT )); then violations_soft+=("$loc $f"); fi +done < <(collect_targets) + +if (( JSON )); then + printf '{"hard":[' + first=1; for v in "${violations_hard[@]}"; do + loc="${v%% *}"; path="${v#* }" + (( first )) || printf ','; first=0 + printf '{"loc":%s,"path":"%s"}' "$loc" "$path" + done + printf '],"soft":[' + first=1; for v in "${violations_soft[@]}"; do + loc="${v%% *}"; path="${v#* }" + (( first )) || printf ','; first=0 + printf '{"loc":%s,"path":"%s"}' "$loc" "$path" + done + printf ']}\n' +else + if (( ${#violations_soft[@]} > 0 )); then + echo "::warning:: $((${#violations_soft[@]})) file(s) exceed soft target ($SOFT lines):" + printf ' %s\n' "${violations_soft[@]}" | sort -rn + fi + if (( ${#violations_hard[@]} > 0 )); then + echo "::error:: $((${#violations_hard[@]})) file(s) exceed HARD CAP ($HARD lines) — split required:" + printf ' %s\n' "${violations_hard[@]}" | sort -rn + fi +fi +(( ${#violations_hard[@]} == 0 )) +``` + +Make executable: `chmod +x scripts/check-loc.sh`. + +### 1.6 `scripts/githooks/pre-commit` + +```bash +#!/usr/bin/env bash +# pre-commit — enforces structural guardrails. +# +# 1. Blocks commits that introduce a non-test, non-generated source file > 500 LOC. +# 2. Blocks commits touching migrations/ unless commit message contains [migration-approved]. +# 3. Blocks edits to guardrail files unless [guardrail-change] is in the commit message. + +set -euo pipefail +REPO_ROOT="$(git rev-parse --show-toplevel)" + +mapfile -t staged < <(git diff --cached --name-only --diff-filter=ACM) +[[ ${#staged[@]} -eq 0 ]] && exit 0 + +# 1. LOC budget on staged files. +loc_targets=() +for f in "${staged[@]}"; do + [[ -f "$REPO_ROOT/$f" ]] && loc_targets+=("$REPO_ROOT/$f") +done +if [[ ${#loc_targets[@]} -gt 0 ]]; then + if ! "$REPO_ROOT/scripts/check-loc.sh" "${loc_targets[@]}"; then + echo; echo "Commit blocked: file-size budget violated." + echo "Split the file (preferred) or add to .claude/rules/loc-exceptions.txt." + exit 1 + fi +fi + +# 2. Migrations frozen unless approved. +if printf '%s\n' "${staged[@]}" | grep -qE '(^|/)(migrations|alembic/versions)/'; then + if ! grep -q '\[migration-approved\]' "$(git rev-parse --git-dir)/COMMIT_EDITMSG" 2>/dev/null; then + echo "Commit blocked: this change touches a migrations directory." + echo "Add '[migration-approved]' to your commit message if approved." + exit 1 + fi +fi + +# 3. Guardrail files protected. +guarded='^(\.claude/settings\.json|\.claude/rules/loc-exceptions\.txt|scripts/check-loc\.sh|scripts/githooks/pre-commit|AGENTS\.(python|go|typescript)\.md)$' +if printf '%s\n' "${staged[@]}" | grep -qE "$guarded"; then + if ! grep -q '\[guardrail-change\]' "$(git rev-parse --git-dir)/COMMIT_EDITMSG" 2>/dev/null; then + echo "Commit blocked: this change modifies guardrail files." + echo "Add '[guardrail-change]' to your commit message and explain why in the body." + exit 1 + fi +fi +exit 0 +``` + +### 1.7 `scripts/install-hooks.sh` + +```bash +#!/usr/bin/env bash +# install-hooks.sh — installs git hooks that enforce repo guardrails locally. +# Idempotent. Safe to re-run. Run once per clone: bash scripts/install-hooks.sh +set -euo pipefail +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +HOOKS_DIR="$REPO_ROOT/.git/hooks" +SRC_DIR="$REPO_ROOT/scripts/githooks" + +[[ -d "$REPO_ROOT/.git" ]] || { echo "Not a git repository: $REPO_ROOT" >&2; exit 1; } +mkdir -p "$HOOKS_DIR" +for hook in pre-commit; do + src="$SRC_DIR/$hook"; dst="$HOOKS_DIR/$hook" + if [[ -f "$src" ]]; then cp "$src" "$dst"; chmod +x "$dst"; echo "installed: $dst"; fi +done +echo "Done. Hooks active for this clone." +``` + +### 1.8 CI additions (`.github/workflows/ci.yaml` or `.gitea/workflows/ci.yaml`) + +Add a `loc-budget` job that fails on hard violations: + +```yaml +jobs: + loc-budget: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check file-size budget + run: bash scripts/check-loc.sh --changed + + python-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: ruff + run: pip install ruff && ruff check . + - name: mypy on new modules + run: pip install mypy && mypy --strict services/ repositories/ domain/ + + go-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: golangci-lint + uses: golangci/golangci-lint-action@v4 + with: { version: latest } + + ts-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: npm ci && npx tsc --noEmit && npx next build + + contract-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: pytest tests/contracts/ -v + + license-sbom-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: anchore/sbom-action@v0 + - uses: anchore/scan-action@v3 +``` + +--- + + +### 1.9 `AGENTS.python.md` (Python / FastAPI) + +````markdown +# AGENTS.python.md — Python Service Conventions + +## Layered architecture + +``` +/ +├── api/ # HTTP layer — routers only. Thin (≤30 LOC per handler). +│ └── _routes.py +├── services/ # Business logic. Pure-ish; no FastAPI imports. +├── repositories/ # DB access. Owns SQLAlchemy session usage. +├── domain/ # Value objects, enums, domain exceptions. +├── schemas/ # Pydantic models, split per domain. Never one giant schemas.py. +└── db/models/ # SQLAlchemy ORM, one module per aggregate. __tablename__ frozen. +``` + +Dependency direction: `api → services → repositories → db.models`. Lower layers must not import upper. + +## Routers +- One `APIRouter` per domain file. Handlers ≤30 LOC. +- Parse request → call service → map domain errors → return response model. +- Inject services via `Depends`. No globals. + +```python +@router.post("/items", response_model=ItemRead, status_code=201) +async def create_item( + payload: ItemCreate, + service: ItemService = Depends(get_item_service), + tenant_id: UUID = Depends(get_tenant_id), +) -> ItemRead: + with translate_domain_errors(): + return await service.create(tenant_id, payload) +``` + +## Domain errors + translator + +```python +# domain/errors.py +class DomainError(Exception): ... +class NotFoundError(DomainError): ... +class ConflictError(DomainError): ... +class ValidationError(DomainError): ... +class PermissionError(DomainError): ... + +# api/_http_errors.py +from contextlib import contextmanager +from fastapi import HTTPException + +@contextmanager +def translate_domain_errors(): + try: yield + except NotFoundError as e: raise HTTPException(404, str(e)) from e + except ConflictError as e: raise HTTPException(409, str(e)) from e + except ValidationError as e: raise HTTPException(400, str(e)) from e + except PermissionError as e: raise HTTPException(403, str(e)) from e +``` + +## Services +- Constructor takes repository interface, not concrete. +- No FastAPI / HTTP knowledge. +- Raise domain exceptions, never HTTPException. + +## Repositories +- Intent-named methods (`get_pending_for_tenant`), not CRUD-named (`select_where`). +- Session injected. No business logic. +- Return ORM models or domain VOs; never `Row`. + +## Schemas (Pydantic v2) +- One module per domain. ≤300 lines. +- `model_config = ConfigDict(from_attributes=True, frozen=True)` for reads. +- Separate `*Create`, `*Update`, `*Read`. + +## Tests +- `tests/unit/`, `tests/integration/`, `tests/contracts/`. +- Unit tests mock repository via `AsyncMock`. +- Integration tests use real Postgres from compose via transactional fixture (rollback per test). +- Contract tests diff `/openapi.json` against `tests/contracts/openapi.baseline.json`. +- Naming: `test___.py::TestX::test_method`. +- `pytest-asyncio` mode = `auto`. Coverage target: 80% new code. + +## Tooling +- `ruff check` + `ruff format` (line length 100). +- `mypy --strict` on `services/`, `repositories/`, `domain/` first. Expand outward via per-module overrides in mypy.ini: + +```ini +[mypy] +strict = True + +[mypy-.services.*] +strict = True + +[mypy-.legacy.*] +# Legacy modules not yet refactored — expand strictness over time. +ignore_errors = True +``` + +## What you may NOT do +- Add a new migration. +- Rename `__tablename__`, column, or enum value. +- Change route contract without simultaneous consumer update. +- Catch `Exception` broadly. +- Put business logic in a router or a Pydantic validator. +- Create a file > 500 lines. +```` + +### 1.10 `AGENTS.go.md` (Go / Gin or chi) + +````markdown +# AGENTS.go.md — Go Service Conventions + +## Layered architecture (Standard Go Project Layout + hexagonal) + +``` +/ +├── cmd/server/main.go # Thin: parse flags → app.New → app.Run. < 50 LOC. +├── internal/ +│ ├── app/ # Wiring: config + DI + lifecycle. +│ ├── domain// # Pure types, interfaces, errors. No I/O. +│ ├── service// # Business logic. Depends on domain interfaces. +│ ├── repository/postgres// # Concrete repos. +│ ├── transport/http/ +│ │ ├── handler// +│ │ ├── middleware/ +│ │ └── router.go +│ └── platform/ # DB pool, logger, config, tracing. +└── pkg/ # Importable by other repos. Empty unless needed. +``` + +Direction: `transport → service → domain ← repository`. `domain` imports no siblings. + +## Handlers +- ≤40 LOC. Bind → call service → map error via `httperr.Write(c, err)` → respond. + +```go +func (h *ItemHandler) Create(c *gin.Context) { + var req CreateItemRequest + if err := c.ShouldBindJSON(&req); err != nil { + httperr.Write(c, httperr.BadRequest(err)); return + } + out, err := h.svc.Create(c.Request.Context(), req.ToInput()) + if err != nil { httperr.Write(c, err); return } + c.JSON(http.StatusCreated, out) +} +``` + +## Errors — single `httperr` package +```go +switch { +case errors.Is(err, domain.ErrNotFound): return 404 +case errors.Is(err, domain.ErrConflict): return 409 +case errors.As(err, &validationErr): return 422 +default: return 500 +} +``` +Never `panic` in request handling. Recovery middleware logs and returns 500. + +## Services +- Struct + constructor + interface methods. No package-level state. +- `context.Context` first arg always. +- Return `(value, error)`. Wrap with `fmt.Errorf("create item: %w", err)`. +- Domain errors as sentinel vars or typed; match with `errors.Is` / `errors.As`. + +## Repositories +- Interface in `domain//repository.go`. Impl in `repository/postgres//`. +- One file per query group; no file > 500 LOC. +- `pgx`/`sqlc` over hand-rolled SQL. No ORM globals. Everything takes `ctx`. + +## Tests +- Co-located `*_test.go`. Table-driven for service logic. +- Handlers via `httptest.NewRecorder`. +- Repos via `testcontainers-go` (or the compose Postgres). Never mocks at SQL boundary. +- Coverage target: 80% on `service/`. + +## Tooling (`golangci-lint` strict config) +- Linters: `errcheck, govet, staticcheck, revive, gosec, gocyclo(max 15), gocognit(max 20), unused, ineffassign, errorlint, nilerr, nolintlint, contextcheck`. +- `gofumpt` formatting. `go vet ./...` clean. `go mod tidy` clean. + +## What you may NOT do +- Touch DB schema/migrations. +- Add a new top-level package under `internal/` without review. +- `import "C"`, unsafe, reflection-heavy code. +- Non-trivial setup in `init()`. Wire in `internal/app`. +- File > 500 lines. +- Change route contract without updating consumers. +```` + +### 1.11 `AGENTS.typescript.md` (TypeScript / Next.js) + +````markdown +# AGENTS.typescript.md — TypeScript / Next.js Conventions + +## Layered architecture (Next.js 15 App Router) + +``` +app/ +├── / +│ ├── page.tsx # Server Component by default. ≤200 LOC. +│ ├── layout.tsx +│ ├── _components/ # Private folder; colocated UI. Each file ≤300 LOC. +│ ├── _hooks/ # Client hooks for this route. +│ ├── _server/ # Server actions, data loaders for this route. +│ └── loading.tsx / error.tsx +├── api//route.ts # Thin handler. Delegates to lib/server//. +lib/ +├── / # Pure helpers, types, zod schemas. Reusable. +└── server// # Server-only logic; uses "server-only". +components/ # Truly shared, app-wide components. +``` + +Server vs Client: default is Server Component. Add `"use client"` only when state/effects/browser APIs needed. Push client boundary as deep as possible. + +## API routes (route.ts) +- One handler per HTTP method, ≤40 LOC. +- Validate with `zod`. Reject invalid → 400. +- Delegate to `lib/server//`. + +```ts +export async function POST(req: Request) { + const parsed = CreateItemSchema.safeParse(await req.json()); + if (!parsed.success) + return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }); + const result = await itemService.create(parsed.data); + return NextResponse.json(result, { status: 201 }); +} +``` + +## Page components +- Pages > 300 lines → split into colocated `_components/`. +- Server Components fetch data; pass plain objects to Client Components. +- No data fetching in `useEffect` for server-renderable data. +- State: prefer URL state (`searchParams`) + Server Components over global stores. + +## Types — barrel re-export pattern for splitting monolithic type files + +```ts +// lib/sdk/types/index.ts +export * from './enums' +export * from './vendor' +export * from './dsfa' +// consumers still `import { Foo } from '@/lib/sdk/types'` +``` + +Rules: no `any`. No `as unknown as`. All DTOs are zod schemas; infer via `z.infer`. + +## Tests +- Unit: **Vitest** (`*.test.ts`/`*.test.tsx`), colocated. +- Hooks: `@testing-library/react` `renderHook`. +- E2E: **Playwright** (`tests/e2e/`), one spec per top-level page minimum. +- Coverage: 70% on `lib/`, smoke on `app/`. + +## Tooling +- `tsc --noEmit` clean (strict, `noUncheckedIndexedAccess: true`). +- ESLint with `@typescript-eslint`, type-aware rules on. +- `next build` clean. No `@ts-ignore`. `@ts-expect-error` only with a reason comment. + +## What you may NOT do +- Business logic in `page.tsx` or `route.ts`. +- Cross-app module imports. +- `dangerouslySetInnerHTML` without explicit sanitization. +- Backend API calls from Client Components when a Server Component/Action would do. +- Change route contract without updating consumers in the same change. +- File > 500 lines. +- Globally disable lint/type rules — fix the root cause. +```` + +--- + + +## 2. Phase plan — behavior-preserving refactor + +Work in phases. Each phase ends green (tests pass, build clean, contract baseline unchanged). Do **not** skip ahead. + +### Phase 0 — Foundation (single PR, low risk) + +**Goal:** Set up rails. No code refactors yet. + +1. Drop in all files from Section 1. Install hooks: `bash scripts/install-hooks.sh`. +2. Populate `.claude/rules/loc-exceptions.txt` with grandfathered entries (one line each, with a comment rationale) so CI doesn't fail day 1. +3. Append the non-negotiable rules block to root `CLAUDE.md`. +4. Add per-language `AGENTS.*.md` at repo root. +5. Add the CI jobs from §1.8. +6. Per-service `README.md` + `CLAUDE.md` stubs: what it does, run/test commands, layered architecture diagram, env vars, API surface link. + +**Verification:** CI green; loc-budget job passes with allowlist; next Claude session loads the rules automatically. + +### Phase 1 — Backend service (Python/FastAPI) + +**Critical targets:** any `routes.py` / `schemas.py` / `repository.py` / `models.py` over 500 LOC. + +**Steps:** + +1. **Snapshot the API contract:** `curl /openapi.json > tests/contracts/openapi.baseline.json`. Add a contract test that diffs current vs baseline and fails on any path/method/param drift. +2. **Characterization tests first.** For each oversized route file, add `TestClient` tests exercising every endpoint (happy path + one error path). Use `httpx.AsyncClient` + factory fixtures. +3. **Split models.py per aggregate.** Keep a shim: `from .db.models import *` re-exports so existing imports keep working. One module per aggregate; `__tablename__` unchanged (no migration). +4. **Split schemas.py** similarly with a re-export shim. +5. **Extract service layer.** Each route handler delegates to a `*Service` class injected via `Depends`. Handlers shrink to ≤30 LOC. +6. **Repository extraction** from the giant repository file; one class per aggregate. +7. **`mypy --strict` scoped to new packages first.** Expand outward via `mypy.ini` per-module overrides. +8. **Tests:** unit tests per service (mocked repo), repo tests against a transactional fixture (real Postgres), integration tests at API layer. + +**Gotchas we hit:** +- Tests that patch module-level symbols (e.g. `SessionLocal`, `scan_X`) break when you move logic behind `Depends`. Fix: re-export the symbol from the route module, or have the service lookup use the module-level symbol directly so the patch still takes effect. +- `from __future__ import annotations` can break Pydantic TypeAdapter forward refs. Remove it where it conflicts. +- Sibling test file status codes drift when you introduce the domain-error translator (e.g. 422 → 400). Update assertions in the same commit. + +**Verification:** all pytest files green. Characterization tests green. Contract test green (no drift). `mypy` clean on new packages. Coverage ≥ baseline + 10%. + +### Phase 2 — Go backend + +**Critical targets:** any handler / store / rules file over 500 LOC. + +**Steps:** +1. OpenAPI/Swagger snapshot (or generate via `swag`) → contract tests. +2. Generate handler-level tests with `httptest` for every endpoint pre-refactor. +3. Define hexagonal layout (see AGENTS.go.md). Move incrementally with type aliases for back-compat where needed. +4. Replace ad-hoc error handling with `errors.Is/As` + a single `httperr` package. +5. Add `golangci-lint` strict config; fix new findings only (don't chase legacy lint). +6. Table-driven service tests. `testcontainers-go` for repo layer. + +**Verification:** `go test ./...` passes; `golangci-lint run` clean; contract tests green; no DB schema diff. + +### Phase 3 — Frontend (Next.js) + +**Biggest beast — expect this to dominate.** Critical targets: `page.tsx` / monolithic types / API routes over 500 LOC. + +**Per oversized page:** +1. Extract presentational components into `app//_components/` (private folder, Next.js convention). +2. Move data fetching into Server Components / Server Actions; Client Components become small. +3. Hooks → `app//_hooks/`. +4. Pure helpers → `lib//`. +5. Add Vitest unit tests for hooks and pure helpers; Playwright smoke tests for each top-level page. + +**Monolithic types file:** use barrel re-export pattern. +- Create `types/` directory with domain files. +- Create `types/index.ts` with `export * from './'` lines. +- **Critical:** TypeScript won't allow both `types.ts` AND `types/index.ts` — delete the file, atomic swap to directory. + +**API routes (`route.ts`):** same router→service split as backend. Each `route.ts` becomes a thin handler delegating to `lib/server//`. + +**Endpoint preservation:** if any internal route URL changes, grep every consumer (SDK packages, developer portal, sibling apps) and update in the same change. + +**Gotchas:** +- Pre-existing type bugs often surface when you try to build. Fix them as drive-by if they block your refactor; otherwise document in a separate follow-up. +- `useClient` component imports from `'../provider'` that rely on re-exports: preserve the re-export or update importers in the same commit. +- Next.js build can fail at page-manifest stage with unrelated prerender errors. Run `next build` fresh (not from cache) to see real status. + +**Verification:** `next build` clean; `tsc --noEmit` clean; Playwright smoke tests pass; visual diff check on key pages (manual + screenshots in PR). + +### Phase 4 — SDKs & smaller services + +Apply the same patterns at smaller scale: +- **SDK packages (0 tests):** add Vitest unit tests for public surface before/while splitting. +- **Manager/Client classes:** extract config defaults, side-effect helpers (e.g. Google Consent Mode wiring), framework adapters into sibling files. Keep the main class as orchestration. +- **Framework adapters (React/Vue/Angular):** each component/composable/service/module goes in its own sibling file; the entry `index.ts` is a thin barrel of re-exports. +- **Doc monoliths (`index.md` thousands of lines):** split per topic with mkdocs nav. + +### Phase 5 — CI hardening & governance + +1. Promote `loc-budget` from warning → blocking once the allowlist has drained to legitimate exceptions only. +2. Add mutation testing in nightly (`mutmut` for Python, `gomutesting` for Go). +3. Add `dependabot`/`renovate` for npm + pip + go mod. +4. Add release tagging workflow. +5. Write ADRs (`docs/adr/`) capturing the architecture decisions from phases 1–3. +6. Distill recurring patterns into `.claude/rules/` updates. + +--- + +## 3. Agent prompt templates + +When the work volume is big, parallelize with subagents. These prompts were battle-tested in practice. + +### 3.1 Backend route file split (Python) + +> You are working in `` on branch ``. Every source file must be under 500 LOC (hard cap enforced by a PreToolUse hook); soft target 300. +> +> **Task:** split `_routes.py` (NNN LOC) following the router → service → repository layering described in `AGENTS.python.md`. +> +> **Steps:** +> 1. Snapshot the relevant slice of `/openapi.json` and add a contract test that pins current behavior. +> 2. Add characterization tests for every endpoint in this file (happy path + one error path) using `httpx.AsyncClient`. +> 3. Extract each route handler's business logic into a `Service` class in `/services/_service.py`. Inject via `Depends(get__service)`. +> 4. Raise domain errors (`NotFoundError`, `ConflictError`, `ValidationError`), never `HTTPException`. Use the `translate_domain_errors()` context manager in handlers. +> 5. Move DB access to `/repositories/_repository.py`. Session injected. +> 6. Split Pydantic schemas from the giant `schemas.py` into `/schemas/.py` if >300 lines. +> +> **Constraints:** +> - Behavior preservation. No route rename/method/status/schema changes. +> - Tests that patch module-level symbols must keep working — re-export the symbol or refactor the lookup so the patch still takes effect. +> - Run `pytest` after each step. Commit each file as its own commit. +> - Push at end: `git push origin `. +> +> When done, report: (a) new LOC counts, (b) test results, (c) mypy status, (d) commit SHAs. Under 300 words. + +### 3.2 Go handler file split + +> You are working in `` on branch ``. Hard cap 500 LOC. +> +> **Task:** split `/handlers/_handler.go` (NNN LOC) into a hexagonal layout per `AGENTS.go.md`. +> +> **Steps:** +> 1. Add `httptest` tests for every endpoint pre-refactor. +> 2. Define `internal/domain//` with types + interfaces + sentinel errors. +> 3. Create `internal/service//` with business logic implementing domain interfaces. +> 4. Create `internal/repository/postgres//` splitting queries by group. +> 5. Thin handlers under `internal/transport/http/handler//`. Each handler ≤40 LOC. Error mapping via `internal/platform/httperr`. +> 6. Use `errors.Is` / `errors.As` for domain error matching. +> +> **Constraints:** +> - No DB schema change. +> - Table-driven service tests. `testcontainers-go` (or compose Postgres) for repo tests. +> - `golangci-lint run` clean. +> +> Report new LOC, test status, lint status, commit SHAs. Under 300 words. + +### 3.3 Next.js page split (the one we parallelized heavily) + +> You are working in `` on branch ``. Every source file must be under 500 LOC (hard cap enforced by a PreToolUse hook); soft target 300. Other agents are working on OTHER pages in parallel — stay in your lane. +> +> **Task:** split the following Next.js 15 App Router client pages into colocated components so each `page.tsx` drops below 500 LOC. +> +> 1. `admin-compliance/app/sdk//page.tsx` (NNNN LOC) +> 2. `admin-compliance/app/sdk//page.tsx` (NNNN LOC) +> +> **Pattern** (reference `admin-compliance/app/sdk//` for "done"): +> - Create `_components/` subdirectory (Next.js private folder, won't create routes). +> - Extract each logically-grouped section (forms, tables, modals, tabs, headers, cards) into its own component file. Name files after the component. +> - Create `_hooks/` for custom hooks that were inline. +> - Create `_types.ts` or `_data.ts` for hoisted types or data arrays. +> - Remaining `page.tsx` wires extracted pieces — aim for under 300 LOC, hard cap 500. +> - Preserve `'use client'` when present on original. +> - DO NOT rename any exports that other files import. Grep first before moving. +> +> **Constraints:** +> - Behavior preservation. No logic changes, no improvements. +> - Imports must resolve (relative `./_components/Foo`). +> - Run `cd admin-compliance && npx next build` after each file is done. Don't commit broken builds. +> - DO NOT edit `.claude/settings.json`, `scripts/check-loc.sh`, `loc-exceptions.txt`, or any `AGENTS.*.md`. +> - Commit each page as its own commit: `refactor(admin): split page.tsx into colocated components`. HEREDOC body, include `Co-Authored-By:` trailer. +> - Pull before push: `git pull --rebase origin `, then `git push origin `. +> +> **Coordination:** DO NOT touch ``. You own only ``. +> +> When done, report: (a) each file's new LOC count, (b) how many `_components` were created, (c) whether `next build` is clean, (d) commit SHAs. Under 300 words. +> +> If the LOC hook blocks a Write, split further. If you hit rate limits partway, commit what's done and report progress honestly. + +### 3.4 Monolithic types file split (TypeScript) + +> ``, branch ``. Hard cap 500 LOC. +> +> **Task:** split `/types.ts` (NNNN LOC) into per-domain modules under `/types/`. +> +> **Steps:** +> 1. Identify domain groupings (enums, API DTOs, one group per business aggregate). +> 2. Create `/types/` directory with `.ts` files. +> 3. Create `/types/index.ts` barrel: `export * from './'` per file. +> 4. **Atomic swap:** delete the old `types.ts` in the same commit as the new `types/` directory. TypeScript won't resolve both a file and a directory with the same stem. +> 5. Grep every consumer — imports from `'/types'` should still work via the barrel. No consumer file changes needed unless there's a name collision. +> 6. Resolve collisions by renaming the less-canonical export (e.g. if two modules both export `LegalDocument`, rename the RAG one to `RagLegalDocument`). +> +> **Verification:** `tsc --noEmit` clean, `next build` clean. +> +> Report new LOC per file, collisions resolved, consumer updates, commit SHAs. + +### 3.5 Agent orchestration rules (from hard-won experience) + +When you spawn multiple agents in parallel: + +1. **Own disjoint paths.** Give each agent a bounded list of files under specific directories. Spell out the "do NOT touch" list explicitly. +2. **Always instruct `git pull --rebase origin ` before push.** Agents running in parallel will push and cause non-fast-forward rejects without this. +3. **Instruct `commit each file as its own commit`** — not a single mega-commit. Makes revert surgical. +4. **Ask for concise reports (≤300 words):** new LOC counts, component counts, build status, commit SHAs. +5. **Tell them to commit partial progress on rate-limit.** If they don't, their partial work lives in the working tree and you have to chase it with `git status` after. (We hit this — 4 agents silently left uncommitted work.) +6. **Don't give an agent more than 2 big files at once.** Each page-split in practice took ~10–20 minutes + ~150k tokens. Two is a comfortable batch. +7. **Reference a prior "done" example.** Commit SHAs are gold — the agent can inspect exactly the style you want. +8. **Run one final `next build` / `pytest` / `go test` yourself after all agents finish.** Agent reports of "build clean" can be scoped (e.g. only their files); you want the whole-repo gate. + +--- + +## 4. Workflow loop (per file) + +``` +1. Read the oversized file end to end. Identify 3–6 extraction sections. +2. Write characterization test (if backend) — pin behavior. +3. Create the sibling files one at a time. + - If the PreToolUse hook blocks (file still > 500), split further. +4. Edit the root file: replace extracted bodies with imports + delegations. +5. Run the full verification: pytest / next build / go test. +6. Run LOC check: scripts/check-loc.sh +7. Commit with a scoped message and a 1–2 line body explaining why. +8. Push. +``` + +## 5. Commit message conventions + +``` +refactor(): + + + + + +Co-Authored-By: Claude Opus 4.6 (1M context) +``` + +Markers that unlock pre-commit guards: +- `[migration-approved]` — allows changes under `migrations/` / `alembic/versions/`. +- `[guardrail-change]` — allows changes to `.claude/settings.json`, `.claude/rules/loc-exceptions.txt`, `scripts/check-loc.sh`, `scripts/githooks/pre-commit`, or any `AGENTS.*.md`. + +Good examples from our session: +- `refactor(consent-sdk): split ConsentManager + framework adapters under 500 LOC` +- `refactor(compliance-sdk): split client/provider/embed/state under 500 LOC` +- `refactor(admin): split whistleblower page.tsx + restore scope helpers` +- `chore: document data-catalog + legacy-service LOC exceptions` (with `[guardrail-change]` body) + +## 6. Verification commands cheatsheet + +```bash +# LOC budget +scripts/check-loc.sh --changed # only changed files +scripts/check-loc.sh # whole repo +scripts/check-loc.sh --json # for CI parsing + +# Python +pytest --cov= --cov-report=term-missing +ruff check . +mypy --strict /services /repositories + +# Go +go test ./... -cover +golangci-lint run +go vet ./... + +# TypeScript +npx tsc --noEmit +npx next build # from the Next.js app dir +npm test -- --run # vitest one-shot +npx playwright test tests/e2e # e2e smoke + +# Contracts +pytest tests/contracts/ # OpenAPI snapshot diff +``` + +## 7. Out of scope (don't drift) + +- DB schema / migrations — unless separate green-lit plan. +- New features. This is a refactor. +- Public endpoint renames without simultaneous consumer fix-up (exception: intra-monorepo URLs when you do the grep sweep). +- Unrelated dead code cleanup — do it in a separate PR. +- Bundling refactors across services in one commit — one service = one commit. + +## 8. Memory / session handoff + +If using Claude Code with persistent memory, save a `project_refactor_status.md` in your memory store after each phase: +- What's done (files split, LOC before → after). +- What's in progress (current file, blocker if any). +- What's deferred (pre-existing bugs surfaced but left for follow-up). +- Key patterns established (so next session doesn't rediscover them). + +This lets you resume after context compacts or after rate-limit windows without losing the thread. + +--- + +That's the whole methodology. Install Section 1, follow Section 2 phase-by-phase, use Section 3 to parallelize the grind. The guardrails do the policing so you don't have to remember anything. + diff --git a/admin-compliance/README.md b/admin-compliance/README.md new file mode 100644 index 0000000..e378bfe --- /dev/null +++ b/admin-compliance/README.md @@ -0,0 +1,53 @@ +# admin-compliance + +Next.js 15 dashboard for BreakPilot Compliance — SDK module UI, company profile, DSR, DSFA, VVT, TOM, consent, AI Act, training, audit, change requests, etc. Also hosts 96+ API routes that proxy/orchestrate backend services. + +**Port:** `3007` (container: `bp-compliance-admin`) +**Stack:** Next.js 15 App Router, React 18, TailwindCSS, TypeScript strict. + +## Architecture (Phase 3 — in progress) + +``` +app/ +├── / +│ ├── page.tsx # Server Component (≤200 LOC) +│ ├── _components/ # Colocated UI, each ≤300 LOC +│ ├── _hooks/ # Client hooks +│ └── _server/ # Server actions +├── api//route.ts # Thin handlers → lib/server// +lib/ +├── / # Pure helpers, zod schemas +└── server// # "server-only" logic +components/ # App-wide shared UI +``` + +See `../AGENTS.typescript.md`. + +## Run locally + +```bash +cd admin-compliance +npm install +npm run dev # http://localhost:3007 +``` + +## Tests + +```bash +npm test # Vitest unit + component tests +npx playwright test # E2E +npx tsc --noEmit # Type-check +npx next lint +``` + +## Known debt + +- `lib/sdk/types.ts` has been split: it is now a barrel re-export to `lib/sdk/types/` (12 domain files: enums, company-profile, sdk-steps, and others). +- `lib/sdk/tom-generator/controls/loader.ts` has been split: it is now a barrel re-export to `categories/` (8 category files). +- Phase 3 refactoring is ongoing — several large page files remain and are being addressed incrementally. +- **0 test files** for the page layer. Adding Playwright smoke + Vitest unit coverage is ongoing Phase 3 work. + +## Don't touch + +- Backend API paths without updating `backend-compliance/` in the same change. +- `lib/sdk/types/` barrel re-exports — add new types to the appropriate domain file, not back into the root. diff --git a/admin-compliance/app/api/sdk/drafting-engine/draft/draft-helpers-v2.ts b/admin-compliance/app/api/sdk/drafting-engine/draft/draft-helpers-v2.ts new file mode 100644 index 0000000..6f3baec --- /dev/null +++ b/admin-compliance/app/api/sdk/drafting-engine/draft/draft-helpers-v2.ts @@ -0,0 +1,364 @@ +/** + * Drafting Engine - v2 Pipeline Helpers + * + * DOCUMENT_PROSE_BLOCKS, buildV2SystemPrompt, buildBlockSpecificPrompt, + * callOllama, handleV2Draft — split from draft-helpers.ts for the 500 LOC hard cap. + */ + +import { NextResponse } from 'next/server' +import type { DraftContext, DraftRevision, DraftSection } from '@/lib/sdk/drafting-engine/types' +import type { ScopeDocumentType } from '@/lib/sdk/compliance-scope-types' +import { deriveNarrativeTags, extractScoresFromDraftContext, narrativeTagsToPromptString } from '@/lib/sdk/drafting-engine/narrative-tags' +import type { NarrativeTags } from '@/lib/sdk/drafting-engine/narrative-tags' +import { buildAllowedFactsFromDraftContext, allowedFactsToPromptString, disallowedTopicsToPromptString } from '@/lib/sdk/drafting-engine/allowed-facts-v2' +import { sanitizeAllowedFacts, validateNoRemainingPII, SanitizationError } from '@/lib/sdk/drafting-engine/sanitizer' +import { terminologyToPromptString, styleContractToPromptString } from '@/lib/sdk/drafting-engine/terminology' +import { executeRepairLoop, type ProseBlockOutput, type RepairAudit } from '@/lib/sdk/drafting-engine/prose-validator' +import { computeChecksumSync, type CacheKeyParams } from '@/lib/sdk/drafting-engine/cache' +import { queryRAG } from '@/lib/sdk/drafting-engine/rag-query' +import { DOCUMENT_RAG_CONFIG } from '@/lib/sdk/drafting-engine/rag-config' +import { + constraintEnforcer, + proseCache, + TEMPLATE_VERSION, + TERMINOLOGY_VERSION, + VALIDATOR_VERSION, + V1_SYSTEM_PROMPT, + buildPromptForDocumentType, +} from './draft-helpers' + +const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434' +const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b' + +// ============================================================================ +// v2 Personalisierte Pipeline +// ============================================================================ + +export const DOCUMENT_PROSE_BLOCKS: Record> = { + tom: [ + { blockId: 'tom-intro', blockType: 'introduction', sectionName: 'Einleitung TOM', targetWords: 120 }, + { blockId: 'tom-transition', blockType: 'transition', sectionName: 'Ueberleitung Massnahmen', targetWords: 40 }, + { blockId: 'tom-conclusion', blockType: 'conclusion', sectionName: 'Fazit TOM', targetWords: 80 }, + ], + dsfa: [ + { blockId: 'dsfa-intro', blockType: 'introduction', sectionName: 'Einleitung DSFA', targetWords: 150 }, + { blockId: 'dsfa-transition', blockType: 'transition', sectionName: 'Ueberleitung Risikobewertung', targetWords: 40 }, + { blockId: 'dsfa-appreciation', blockType: 'appreciation', sectionName: 'Wuerdigung bestehender Massnahmen', targetWords: 60 }, + { blockId: 'dsfa-conclusion', blockType: 'conclusion', sectionName: 'Fazit DSFA', targetWords: 100 }, + ], + vvt: [ + { blockId: 'vvt-intro', blockType: 'introduction', sectionName: 'Einleitung VVT', targetWords: 120 }, + { blockId: 'vvt-conclusion', blockType: 'conclusion', sectionName: 'Fazit VVT', targetWords: 80 }, + ], + dsi: [ + { blockId: 'dsi-intro', blockType: 'introduction', sectionName: 'Einleitung Datenschutzerklaerung', targetWords: 130 }, + { blockId: 'dsi-conclusion', blockType: 'conclusion', sectionName: 'Fazit Datenschutzerklaerung', targetWords: 80 }, + ], + lf: [ + { 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 }, + ], +} + +export function buildV2SystemPrompt( + sanitizedFactsString: string, + narrativeTagsString: string, + terminologyString: string, + styleString: string, + disallowedString: string, + companyName: string, + blockId: string, + blockType: string, + sectionName: string, + documentType: string, + targetWords: number +): string { + return `Du bist ein Compliance-Dokumenten-Redakteur. +Du schreibst einzelne Textabschnitte fuer offizielle Compliance-Dokumente. + +KUNDENPROFIL (ERLAUBTE FAKTEN — nur diese darfst du verwenden): +${sanitizedFactsString} + +BEWERTUNGSERGEBNIS (sprachliche Tags — verwende nur diese Begriffe): +${narrativeTagsString} + +TERMINOLOGIE (verwende ausschliesslich diese Fachbegriffe): +${terminologyString} + +STIL: +${styleString} + +VERBOTENE INHALTE: +${disallowedString} +- Keine konkreten Prozentwerte, Scores oder Zahlen +- Keine Compliance-Level-Bezeichnungen (L1, L2, L3, L4) +- Keine direkte Ansprache ("Sie", "Ihr") +- Kein Denglisch, keine Marketing-Sprache, keine Superlative + +STRIKTE REGELN: +1. Verwende den Firmennamen "${companyName}" — nie "Ihr Unternehmen" +2. Schreibe in der dritten Person ("Die ${companyName}...") +3. Beziehe dich auf die Branche und organisatorische Merkmale +4. Verwende NUR Fakten aus dem Kundenprofil oben +5. Verwende NUR die sprachlichen Tags aus dem Bewertungsergebnis +6. Erfinde KEINE zusaetzlichen Fakten oder Bewertungen +7. Halte dich an die Terminologie-Vorgaben +8. Dein Text wird ZWISCHEN feste Datentabellen eingefuegt + +OUTPUT-FORMAT: Antworte ausschliesslich als JSON: +{ + "blockId": "${blockId}", + "blockType": "${blockType}", + "language": "de", + "text": "...", + "assertions": { + "companyNameUsed": true/false, + "industryReferenced": true/false, + "structureReferenced": true/false, + "itLandscapeReferenced": true/false, + "narrativeTagsUsed": ["riskSummary", ...] + }, + "forbiddenContentDetected": [] +} + +DOKUMENTENTYP: ${documentType} +SEKTION: ${sectionName} +BLOCK-TYP: ${blockType} +ZIEL-LAENGE: ${targetWords} Woerter` +} + +export function buildBlockSpecificPrompt(blockType: string, sectionName: string, documentType: string): string { + switch (blockType) { + case 'introduction': + return `Schreibe eine Einleitung fuer das Dokument "${documentType}" (Sektion: ${sectionName}). +Erklaere, warum dieses Dokument fuer das Unternehmen erstellt wurde. +Gehe auf die spezifische Situation des Unternehmens ein. +Erwaehne die Branche, die Organisationsform und die IT-Strategie.` + case 'transition': + return `Schreibe eine kurze Ueberleitung zur naechsten Sektion "${sectionName}". +Verknuepfe den vorherigen Abschnitt logisch mit dem folgenden.` + case 'conclusion': + return `Schreibe einen abschliessenden Absatz fuer die Sektion "${sectionName}". +Fasse die wesentlichen Punkte zusammen und verweise auf die fortlaufende Pflege.` + case 'appreciation': + return `Schreibe einen wertschaetzenden Satz ueber die bestehenden Massnahmen. +Verwende dabei die sprachlichen Tags aus dem Bewertungsergebnis. +Keine neuen Fakten erfinden — nur das Profil wuerdigen.` + default: + return `Schreibe einen Textabschnitt fuer "${sectionName}".` + } +} + +export async function callOllama(systemPrompt: string, userPrompt: string): Promise { + const response = await fetch(`${OLLAMA_URL}/api/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: LLM_MODEL, + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: userPrompt }, + ], + stream: false, + think: false, + options: { temperature: 0.15, num_predict: 4096, num_ctx: 8192 }, + format: 'json', + }), + signal: AbortSignal.timeout(120000), + }) + + if (!response.ok) { + throw new Error(`Ollama error: ${response.status}`) + } + + const result = await response.json() + return result.message?.content || '' +} + +export async function handleV2Draft(body: Record): Promise { + const { documentType, draftContext, instructions } = body as { + documentType: ScopeDocumentType + draftContext: DraftContext + instructions?: string + } + + const constraintCheck = constraintEnforcer.checkFromContext(documentType, draftContext) + if (!constraintCheck.allowed) { + return NextResponse.json({ + draft: null, constraintCheck, tokensUsed: 0, + error: 'Constraint-Verletzung: ' + constraintCheck.violations.join('; '), + }, { status: 403 }) + } + + const scores = extractScoresFromDraftContext(draftContext) + const narrativeTags: NarrativeTags = deriveNarrativeTags(scores) + const allowedFacts = buildAllowedFactsFromDraftContext(draftContext, narrativeTags) + + let sanitizationResult + try { + sanitizationResult = sanitizeAllowedFacts(allowedFacts) + } catch (error) { + if (error instanceof SanitizationError) { + return NextResponse.json({ + error: `Sanitization Hard Abort: ${error.message} (Feld: ${error.field})`, + draft: null, constraintCheck, tokensUsed: 0, + }, { status: 422 }) + } + throw error + } + + const sanitizedFacts = sanitizationResult.facts + const piiWarnings = validateNoRemainingPII(sanitizedFacts) + if (piiWarnings.length > 0) console.warn('PII-Warnungen nach Sanitization:', piiWarnings) + + const factsString = allowedFactsToPromptString(sanitizedFacts) + const tagsString = narrativeTagsToPromptString(narrativeTags) + const termsString = terminologyToPromptString() + const styleString = styleContractToPromptString() + const disallowedString = disallowedTopicsToPromptString() + const promptHash = computeChecksumSync({ factsString, tagsString, termsString, styleString, disallowedString }) + + const v2RagCfg = DOCUMENT_RAG_CONFIG[documentType] + const v2RagContext = await queryRAG(v2RagCfg.query, 3, v2RagCfg.collection) + + const proseBlocks = DOCUMENT_PROSE_BLOCKS[documentType] || DOCUMENT_PROSE_BLOCKS.tom + const generatedBlocks: ProseBlockOutput[] = [] + const repairAudits: RepairAudit[] = [] + let totalTokens = 0 + + for (const blockDef of proseBlocks) { + const cacheParams: CacheKeyParams = { + allowedFacts: sanitizedFacts, templateVersion: TEMPLATE_VERSION, + terminologyVersion: TERMINOLOGY_VERSION, narrativeTags, + promptHash, blockType: blockDef.blockType, sectionName: blockDef.sectionName, + } + + const cached = proseCache.getSync(cacheParams) + if (cached) { + generatedBlocks.push(cached) + repairAudits.push({ repairAttempts: 0, validatorFailures: [], repairSuccessful: true, fallbackUsed: false }) + continue + } + + let systemPrompt = buildV2SystemPrompt( + factsString, tagsString, termsString, styleString, disallowedString, + sanitizedFacts.companyName, blockDef.blockId, blockDef.blockType, + blockDef.sectionName, documentType, blockDef.targetWords + ) + if (v2RagContext) systemPrompt += `\n\nRECHTSKONTEXT (als Referenz, nicht woertlich uebernehmen):\n${v2RagContext}` + const userPrompt = buildBlockSpecificPrompt(blockDef.blockType, blockDef.sectionName, documentType) + + (instructions ? `\n\nZusaetzliche Anweisungen: ${instructions}` : '') + + try { + const rawOutput = await callOllama(systemPrompt, userPrompt) + totalTokens += rawOutput.length / 4 + const { block, audit } = await executeRepairLoop( + rawOutput, sanitizedFacts, narrativeTags, blockDef.blockId, blockDef.blockType, + async (repairPrompt) => callOllama(systemPrompt, repairPrompt), documentType + ) + generatedBlocks.push(block) + repairAudits.push(audit) + if (!audit.fallbackUsed) proseCache.setSync(cacheParams, block) + } catch (error) { + const { buildFallbackBlock } = await import('@/lib/sdk/drafting-engine/prose-validator') + generatedBlocks.push(buildFallbackBlock(blockDef.blockId, blockDef.blockType, sanitizedFacts, documentType)) + repairAudits.push({ + repairAttempts: 0, validatorFailures: [[(error as Error).message]], + repairSuccessful: false, fallbackUsed: true, + fallbackReason: `LLM-Fehler: ${(error as Error).message}`, + }) + } + } + + const draftPrompt = buildPromptForDocumentType(documentType, draftContext, instructions) + + let dataSections: DraftSection[] = [] + try { + const dataResponse = await callOllama(V1_SYSTEM_PROMPT, draftPrompt) + const parsed = JSON.parse(dataResponse) + dataSections = (parsed.sections || []).map((s: Record, i: number) => ({ + id: String(s.id || `section-${i}`), title: String(s.title || ''), + content: String(s.content || ''), schemaField: s.schemaField ? String(s.schemaField) : undefined, + })) + totalTokens += dataResponse.length / 4 + } catch { dataSections = [] } + + const introBlock = generatedBlocks.find(b => b.blockType === 'introduction') + const transitionBlocks = generatedBlocks.filter(b => b.blockType === 'transition') + const appreciationBlocks = generatedBlocks.filter(b => b.blockType === 'appreciation') + const conclusionBlock = generatedBlocks.find(b => b.blockType === 'conclusion') + + const mergedSections: DraftSection[] = [] + if (introBlock) mergedSections.push({ id: introBlock.blockId, title: 'Einleitung', content: introBlock.text }) + for (let i = 0; i < dataSections.length; i++) { + if (i > 0 && transitionBlocks[i - 1]) mergedSections.push({ id: transitionBlocks[i - 1].blockId, title: '', content: transitionBlocks[i - 1].text }) + mergedSections.push(dataSections[i]) + } + for (const block of appreciationBlocks) mergedSections.push({ id: block.blockId, title: 'Wuerdigung', content: block.text }) + if (conclusionBlock) mergedSections.push({ id: conclusionBlock.blockId, title: 'Fazit', content: conclusionBlock.text }) + + const finalSections = mergedSections.length > 0 ? mergedSections : generatedBlocks.map(b => ({ + id: b.blockId, + title: b.blockType === 'introduction' ? 'Einleitung' : b.blockType === 'conclusion' ? 'Fazit' : b.blockType === 'appreciation' ? 'Wuerdigung' : 'Ueberleitung', + content: b.text, + })) + + const draft: DraftRevision = { + id: `draft-v2-${Date.now()}`, + content: finalSections.map(s => s.title ? `## ${s.title}\n\n${s.content}` : s.content).join('\n\n'), + sections: finalSections, createdAt: new Date().toISOString(), instruction: instructions, + } + + const auditTrail = { + documentType, templateVersion: TEMPLATE_VERSION, terminologyVersion: TERMINOLOGY_VERSION, + validatorVersion: VALIDATOR_VERSION, promptHash, llmModel: LLM_MODEL, + llmTemperature: 0.15, llmProvider: 'ollama', narrativeTags, + sanitization: sanitizationResult.audit, repairAudits, + proseBlocks: generatedBlocks.map((b, i) => ({ + blockId: b.blockId, blockType: b.blockType, + wordCount: b.text.split(/\s+/).filter(Boolean).length, + fallbackUsed: repairAudits[i]?.fallbackUsed ?? false, + repairAttempts: repairAudits[i]?.repairAttempts ?? 0, + })), + cacheStats: proseCache.getStats(), + } + + const truthLabel = { generation_mode: 'draft_assistance', truth_status: 'generated', may_be_used_as_evidence: false, generated_by: 'system' } + + 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 }) +} diff --git a/admin-compliance/app/api/sdk/drafting-engine/draft/draft-helpers.ts b/admin-compliance/app/api/sdk/drafting-engine/draft/draft-helpers.ts new file mode 100644 index 0000000..60d88a7 --- /dev/null +++ b/admin-compliance/app/api/sdk/drafting-engine/draft/draft-helpers.ts @@ -0,0 +1,161 @@ +/** + * Drafting Engine - Draft Helper Functions (v1 pipeline + shared constants) + * + * Shared state, v1 legacy pipeline helpers. + * v2 pipeline lives in draft-helpers-v2.ts. + */ + +import { NextResponse } from 'next/server' +import { buildVVTDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-vvt' +import { buildTOMDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-tom' +import { buildDSFADraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-dsfa' +import { buildPrivacyPolicyDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-privacy-policy' +import { buildLoeschfristenDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-loeschfristen' +import type { DraftContext, DraftResponse, DraftRevision, DraftSection } from '@/lib/sdk/drafting-engine/types' +import type { ScopeDocumentType } from '@/lib/sdk/compliance-scope-types' +import { ConstraintEnforcer } from '@/lib/sdk/drafting-engine/constraint-enforcer' +import { ProseCacheManager } from '@/lib/sdk/drafting-engine/cache' +import { queryRAG } from '@/lib/sdk/drafting-engine/rag-query' +import { DOCUMENT_RAG_CONFIG } from '@/lib/sdk/drafting-engine/rag-config' + +const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434' +const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b' + +export const constraintEnforcer = new ConstraintEnforcer() +export const proseCache = new ProseCacheManager({ maxEntries: 200, ttlHours: 24 }) + +export const TEMPLATE_VERSION = '2.0.0' +export const TERMINOLOGY_VERSION = '1.0.0' +export const VALIDATOR_VERSION = '1.0.0' + +// ============================================================================ +// v1 Legacy Pipeline +// ============================================================================ + +export const V1_SYSTEM_PROMPT = `Du bist ein DSGVO-Compliance-Experte und erstellst strukturierte Dokument-Entwuerfe. +Du MUSST immer im JSON-Format antworten mit einem "sections" Array. +Jede Section hat: id, title, content, schemaField. +Halte die Tiefe strikt am vorgegebenen Level. +Markiere fehlende Informationen mit [PLATZHALTER: Beschreibung]. +Sprache: Deutsch.` + +export function buildPromptForDocumentType( + documentType: ScopeDocumentType, + context: DraftContext, + instructions?: string +): string { + switch (documentType) { + case 'vvt': + return buildVVTDraftPrompt({ context, instructions }) + case 'tom': + return buildTOMDraftPrompt({ context, instructions }) + case 'dsfa': + return buildDSFADraftPrompt({ context, instructions }) + case 'dsi': + return buildPrivacyPolicyDraftPrompt({ context, instructions }) + case 'lf': + return buildLoeschfristenDraftPrompt({ context, instructions }) + default: + return `## Aufgabe: Entwurf fuer ${documentType} + +### Level: ${context.decisions.level} +### Tiefe: ${context.constraints.depthRequirements.depth} +### Erforderliche Inhalte: +${context.constraints.depthRequirements.detailItems.map((item, i) => `${i + 1}. ${item}`).join('\n')} + +${instructions ? `### Anweisungen: ${instructions}` : ''} + +Antworte als JSON mit "sections" Array.` + } +} + +export async function handleV1Draft(body: Record): Promise { + const { documentType, draftContext, instructions, existingDraft } = body as { + documentType: ScopeDocumentType + draftContext: DraftContext + instructions?: string + existingDraft?: DraftRevision + } + + const constraintCheck = constraintEnforcer.checkFromContext(documentType, draftContext) + if (!constraintCheck.allowed) { + return NextResponse.json({ + draft: null, + constraintCheck, + tokensUsed: 0, + error: 'Constraint-Verletzung: ' + constraintCheck.violations.join('; '), + }, { status: 403 }) + } + + const ragCfg = DOCUMENT_RAG_CONFIG[documentType] + const ragContext = await queryRAG(ragCfg.query, 3, ragCfg.collection) + + let v1SystemPrompt = V1_SYSTEM_PROMPT + if (ragContext) { + v1SystemPrompt += `\n\n## Relevanter Rechtskontext\n${ragContext}` + } + + const draftPrompt = buildPromptForDocumentType(documentType, draftContext, instructions) + const messages = [ + { role: 'system', content: v1SystemPrompt }, + ...(existingDraft ? [{ + role: 'assistant', + content: `Bisheriger Entwurf:\n${JSON.stringify(existingDraft.sections, null, 2)}`, + }] : []), + { role: 'user', content: draftPrompt }, + ] + + const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: LLM_MODEL, + messages, + stream: false, + think: false, + options: { temperature: 0.15, num_predict: 16384, num_ctx: 8192 }, + format: 'json', + }), + signal: AbortSignal.timeout(180000), + }) + + if (!ollamaResponse.ok) { + return NextResponse.json( + { error: `LLM nicht erreichbar (Status ${ollamaResponse.status})` }, + { status: 502 } + ) + } + + const result = await ollamaResponse.json() + const content = result.message?.content || '' + + let sections: DraftSection[] = [] + try { + const parsed = JSON.parse(content) + sections = (parsed.sections || []).map((s: Record, i: number) => ({ + id: String(s.id || `section-${i}`), + title: String(s.title || ''), + content: String(s.content || ''), + schemaField: s.schemaField ? String(s.schemaField) : undefined, + })) + } catch { + sections = [{ id: 'raw', title: 'Entwurf', content }] + } + + const draft: DraftRevision = { + id: `draft-${Date.now()}`, + content: sections.map(s => `## ${s.title}\n\n${s.content}`).join('\n\n'), + sections, + createdAt: new Date().toISOString(), + instruction: instructions as string | undefined, + } + + return NextResponse.json({ + draft, + constraintCheck, + tokensUsed: result.eval_count || 0, + } satisfies DraftResponse) +} + +// Re-export v2 handler for route.ts (backward compat — single import point) +export { handleV2Draft } from './draft-helpers-v2' diff --git a/admin-compliance/app/api/sdk/drafting-engine/draft/route.ts b/admin-compliance/app/api/sdk/drafting-engine/draft/route.ts index 272a015..3b221c0 100644 --- a/admin-compliance/app/api/sdk/drafting-engine/draft/route.ts +++ b/admin-compliance/app/api/sdk/drafting-engine/draft/route.ts @@ -9,627 +9,7 @@ */ import { NextRequest, NextResponse } from 'next/server' - -const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434' -const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b' - -// v1 imports (Legacy) -import { buildVVTDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-vvt' -import { buildTOMDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-tom' -import { buildDSFADraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-dsfa' -import { buildPrivacyPolicyDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-privacy-policy' -import { buildLoeschfristenDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-loeschfristen' -import type { DraftContext, DraftResponse, DraftRevision, DraftSection } from '@/lib/sdk/drafting-engine/types' -import type { ScopeDocumentType } from '@/lib/sdk/compliance-scope-types' -import { ConstraintEnforcer } from '@/lib/sdk/drafting-engine/constraint-enforcer' - -// v2 imports (Personalisierte Pipeline) -import { deriveNarrativeTags, extractScoresFromDraftContext, narrativeTagsToPromptString } from '@/lib/sdk/drafting-engine/narrative-tags' -import type { NarrativeTags } from '@/lib/sdk/drafting-engine/narrative-tags' -import { buildAllowedFactsFromDraftContext, allowedFactsToPromptString, disallowedTopicsToPromptString } from '@/lib/sdk/drafting-engine/allowed-facts-v2' -import { sanitizeAllowedFacts, validateNoRemainingPII, SanitizationError } from '@/lib/sdk/drafting-engine/sanitizer' -import { terminologyToPromptString, styleContractToPromptString } from '@/lib/sdk/drafting-engine/terminology' -import { executeRepairLoop, type ProseBlockOutput, type RepairAudit } from '@/lib/sdk/drafting-engine/prose-validator' -import { ProseCacheManager, computeChecksumSync, type CacheKeyParams } from '@/lib/sdk/drafting-engine/cache' -import { queryRAG } from '@/lib/sdk/drafting-engine/rag-query' -import { DOCUMENT_RAG_CONFIG } from '@/lib/sdk/drafting-engine/rag-config' - -// ============================================================================ -// Shared State -// ============================================================================ - -const constraintEnforcer = new ConstraintEnforcer() -const proseCache = new ProseCacheManager({ maxEntries: 200, ttlHours: 24 }) - -// Template/Terminology Versionen (fuer Cache-Key) -const TEMPLATE_VERSION = '2.0.0' -const TERMINOLOGY_VERSION = '1.0.0' -const VALIDATOR_VERSION = '1.0.0' - -// ============================================================================ -// v1 Legacy Pipeline -// ============================================================================ - -const V1_SYSTEM_PROMPT = `Du bist ein DSGVO-Compliance-Experte und erstellst strukturierte Dokument-Entwuerfe. -Du MUSST immer im JSON-Format antworten mit einem "sections" Array. -Jede Section hat: id, title, content, schemaField. -Halte die Tiefe strikt am vorgegebenen Level. -Markiere fehlende Informationen mit [PLATZHALTER: Beschreibung]. -Sprache: Deutsch.` - -function buildPromptForDocumentType( - documentType: ScopeDocumentType, - context: DraftContext, - instructions?: string -): string { - switch (documentType) { - case 'vvt': - return buildVVTDraftPrompt({ context, instructions }) - case 'tom': - return buildTOMDraftPrompt({ context, instructions }) - case 'dsfa': - return buildDSFADraftPrompt({ context, instructions }) - case 'dsi': - return buildPrivacyPolicyDraftPrompt({ context, instructions }) - case 'lf': - return buildLoeschfristenDraftPrompt({ context, instructions }) - default: - return `## Aufgabe: Entwurf fuer ${documentType} - -### Level: ${context.decisions.level} -### Tiefe: ${context.constraints.depthRequirements.depth} -### Erforderliche Inhalte: -${context.constraints.depthRequirements.detailItems.map((item, i) => `${i + 1}. ${item}`).join('\n')} - -${instructions ? `### Anweisungen: ${instructions}` : ''} - -Antworte als JSON mit "sections" Array.` - } -} - -async function handleV1Draft(body: Record): Promise { - const { documentType, draftContext, instructions, existingDraft } = body as { - documentType: ScopeDocumentType - draftContext: DraftContext - instructions?: string - existingDraft?: DraftRevision - } - - const constraintCheck = constraintEnforcer.checkFromContext(documentType, draftContext) - if (!constraintCheck.allowed) { - return NextResponse.json({ - draft: null, - constraintCheck, - tokensUsed: 0, - error: 'Constraint-Verletzung: ' + constraintCheck.violations.join('; '), - }, { status: 403 }) - } - - // RAG: Fetch relevant legal context (config-based) - const ragCfg = DOCUMENT_RAG_CONFIG[documentType] - const ragContext = await queryRAG(ragCfg.query, 3, ragCfg.collection) - - let v1SystemPrompt = V1_SYSTEM_PROMPT - if (ragContext) { - v1SystemPrompt += `\n\n## Relevanter Rechtskontext\n${ragContext}` - } - - const draftPrompt = buildPromptForDocumentType(documentType, draftContext, instructions) - const messages = [ - { role: 'system', content: v1SystemPrompt }, - ...(existingDraft ? [{ - role: 'assistant', - content: `Bisheriger Entwurf:\n${JSON.stringify(existingDraft.sections, null, 2)}`, - }] : []), - { role: 'user', content: draftPrompt }, - ] - - const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model: LLM_MODEL, - messages, - stream: false, - think: false, - options: { temperature: 0.15, num_predict: 16384, num_ctx: 8192 }, - format: 'json', - }), - signal: AbortSignal.timeout(180000), - }) - - if (!ollamaResponse.ok) { - return NextResponse.json( - { error: `LLM nicht erreichbar (Status ${ollamaResponse.status})` }, - { status: 502 } - ) - } - - const result = await ollamaResponse.json() - const content = result.message?.content || '' - - let sections: DraftSection[] = [] - try { - const parsed = JSON.parse(content) - sections = (parsed.sections || []).map((s: Record, i: number) => ({ - id: String(s.id || `section-${i}`), - title: String(s.title || ''), - content: String(s.content || ''), - schemaField: s.schemaField ? String(s.schemaField) : undefined, - })) - } catch { - sections = [{ id: 'raw', title: 'Entwurf', content }] - } - - const draft: DraftRevision = { - id: `draft-${Date.now()}`, - content: sections.map(s => `## ${s.title}\n\n${s.content}`).join('\n\n'), - sections, - createdAt: new Date().toISOString(), - instruction: instructions as string | undefined, - } - - return NextResponse.json({ - draft, - constraintCheck, - tokensUsed: result.eval_count || 0, - } satisfies DraftResponse) -} - -// ============================================================================ -// v2 Personalisierte Pipeline -// ============================================================================ - -/** Prose block definitions per document type */ -const DOCUMENT_PROSE_BLOCKS: Record> = { - tom: [ - { blockId: 'tom-intro', blockType: 'introduction', sectionName: 'Einleitung TOM', targetWords: 120 }, - { blockId: 'tom-transition', blockType: 'transition', sectionName: 'Ueberleitung Massnahmen', targetWords: 40 }, - { blockId: 'tom-conclusion', blockType: 'conclusion', sectionName: 'Fazit TOM', targetWords: 80 }, - ], - dsfa: [ - { blockId: 'dsfa-intro', blockType: 'introduction', sectionName: 'Einleitung DSFA', targetWords: 150 }, - { blockId: 'dsfa-transition', blockType: 'transition', sectionName: 'Ueberleitung Risikobewertung', targetWords: 40 }, - { blockId: 'dsfa-appreciation', blockType: 'appreciation', sectionName: 'Wuerdigung bestehender Massnahmen', targetWords: 60 }, - { blockId: 'dsfa-conclusion', blockType: 'conclusion', sectionName: 'Fazit DSFA', targetWords: 100 }, - ], - vvt: [ - { blockId: 'vvt-intro', blockType: 'introduction', sectionName: 'Einleitung VVT', targetWords: 120 }, - { blockId: 'vvt-conclusion', blockType: 'conclusion', sectionName: 'Fazit VVT', targetWords: 80 }, - ], - dsi: [ - { blockId: 'dsi-intro', blockType: 'introduction', sectionName: 'Einleitung Datenschutzerklaerung', targetWords: 130 }, - { blockId: 'dsi-conclusion', blockType: 'conclusion', sectionName: 'Fazit Datenschutzerklaerung', targetWords: 80 }, - ], - lf: [ - { 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( - sanitizedFactsString: string, - narrativeTagsString: string, - terminologyString: string, - styleString: string, - disallowedString: string, - companyName: string, - blockId: string, - blockType: string, - sectionName: string, - documentType: string, - targetWords: number -): string { - return `Du bist ein Compliance-Dokumenten-Redakteur. -Du schreibst einzelne Textabschnitte fuer offizielle Compliance-Dokumente. - -KUNDENPROFIL (ERLAUBTE FAKTEN — nur diese darfst du verwenden): -${sanitizedFactsString} - -BEWERTUNGSERGEBNIS (sprachliche Tags — verwende nur diese Begriffe): -${narrativeTagsString} - -TERMINOLOGIE (verwende ausschliesslich diese Fachbegriffe): -${terminologyString} - -STIL: -${styleString} - -VERBOTENE INHALTE: -${disallowedString} -- Keine konkreten Prozentwerte, Scores oder Zahlen -- Keine Compliance-Level-Bezeichnungen (L1, L2, L3, L4) -- Keine direkte Ansprache ("Sie", "Ihr") -- Kein Denglisch, keine Marketing-Sprache, keine Superlative - -STRIKTE REGELN: -1. Verwende den Firmennamen "${companyName}" — nie "Ihr Unternehmen" -2. Schreibe in der dritten Person ("Die ${companyName}...") -3. Beziehe dich auf die Branche und organisatorische Merkmale -4. Verwende NUR Fakten aus dem Kundenprofil oben -5. Verwende NUR die sprachlichen Tags aus dem Bewertungsergebnis -6. Erfinde KEINE zusaetzlichen Fakten oder Bewertungen -7. Halte dich an die Terminologie-Vorgaben -8. Dein Text wird ZWISCHEN feste Datentabellen eingefuegt - -OUTPUT-FORMAT: Antworte ausschliesslich als JSON: -{ - "blockId": "${blockId}", - "blockType": "${blockType}", - "language": "de", - "text": "...", - "assertions": { - "companyNameUsed": true/false, - "industryReferenced": true/false, - "structureReferenced": true/false, - "itLandscapeReferenced": true/false, - "narrativeTagsUsed": ["riskSummary", ...] - }, - "forbiddenContentDetected": [] -} - -DOKUMENTENTYP: ${documentType} -SEKTION: ${sectionName} -BLOCK-TYP: ${blockType} -ZIEL-LAENGE: ${targetWords} Woerter` -} - -function buildBlockSpecificPrompt(blockType: string, sectionName: string, documentType: string): string { - switch (blockType) { - case 'introduction': - return `Schreibe eine Einleitung fuer das Dokument "${documentType}" (Sektion: ${sectionName}). -Erklaere, warum dieses Dokument fuer das Unternehmen erstellt wurde. -Gehe auf die spezifische Situation des Unternehmens ein. -Erwaehne die Branche, die Organisationsform und die IT-Strategie.` - case 'transition': - return `Schreibe eine kurze Ueberleitung zur naechsten Sektion "${sectionName}". -Verknuepfe den vorherigen Abschnitt logisch mit dem folgenden.` - case 'conclusion': - return `Schreibe einen abschliessenden Absatz fuer die Sektion "${sectionName}". -Fasse die wesentlichen Punkte zusammen und verweise auf die fortlaufende Pflege.` - case 'appreciation': - return `Schreibe einen wertschaetzenden Satz ueber die bestehenden Massnahmen. -Verwende dabei die sprachlichen Tags aus dem Bewertungsergebnis. -Keine neuen Fakten erfinden — nur das Profil wuerdigen.` - default: - return `Schreibe einen Textabschnitt fuer "${sectionName}".` - } -} - -async function callOllama(systemPrompt: string, userPrompt: string): Promise { - const response = await fetch(`${OLLAMA_URL}/api/chat`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model: LLM_MODEL, - messages: [ - { role: 'system', content: systemPrompt }, - { role: 'user', content: userPrompt }, - ], - stream: false, - think: false, - options: { temperature: 0.15, num_predict: 4096, num_ctx: 8192 }, - format: 'json', - }), - signal: AbortSignal.timeout(120000), - }) - - if (!response.ok) { - throw new Error(`Ollama error: ${response.status}`) - } - - const result = await response.json() - return result.message?.content || '' -} - -async function handleV2Draft(body: Record): Promise { - const { documentType, draftContext, instructions } = body as { - documentType: ScopeDocumentType - draftContext: DraftContext - instructions?: string - } - - // Step 1: Constraint Check (Hard Gate) - const constraintCheck = constraintEnforcer.checkFromContext(documentType, draftContext) - if (!constraintCheck.allowed) { - return NextResponse.json({ - draft: null, - constraintCheck, - tokensUsed: 0, - error: 'Constraint-Verletzung: ' + constraintCheck.violations.join('; '), - }, { status: 403 }) - } - - // Step 2: Derive Narrative Tags (deterministisch) - const scores = extractScoresFromDraftContext(draftContext) - const narrativeTags: NarrativeTags = deriveNarrativeTags(scores) - - // Step 3: Build Allowed Facts - const allowedFacts = buildAllowedFactsFromDraftContext(draftContext, narrativeTags) - - // Step 4: PII Sanitization - let sanitizationResult - try { - sanitizationResult = sanitizeAllowedFacts(allowedFacts) - } catch (error) { - if (error instanceof SanitizationError) { - return NextResponse.json({ - error: `Sanitization Hard Abort: ${error.message} (Feld: ${error.field})`, - draft: null, - constraintCheck, - tokensUsed: 0, - }, { status: 422 }) - } - throw error - } - - const sanitizedFacts = sanitizationResult.facts - - // Verify no remaining PII - const piiWarnings = validateNoRemainingPII(sanitizedFacts) - if (piiWarnings.length > 0) { - console.warn('PII-Warnungen nach Sanitization:', piiWarnings) - } - - // Step 5: Build prompt components - const factsString = allowedFactsToPromptString(sanitizedFacts) - const tagsString = narrativeTagsToPromptString(narrativeTags) - const termsString = terminologyToPromptString() - const styleString = styleContractToPromptString() - const disallowedString = disallowedTopicsToPromptString() - - // Compute prompt hash for audit - const promptHash = computeChecksumSync({ factsString, tagsString, termsString, styleString, disallowedString }) - - // Step 5b: RAG Legal Context (config-based) - const v2RagCfg = DOCUMENT_RAG_CONFIG[documentType] - const v2RagContext = await queryRAG(v2RagCfg.query, 3, v2RagCfg.collection) - - // Step 6: Generate Prose Blocks (with cache + repair loop) - const proseBlocks = DOCUMENT_PROSE_BLOCKS[documentType] || DOCUMENT_PROSE_BLOCKS.tom - const generatedBlocks: ProseBlockOutput[] = [] - const repairAudits: RepairAudit[] = [] - let totalTokens = 0 - - for (const blockDef of proseBlocks) { - // Check cache - const cacheParams: CacheKeyParams = { - allowedFacts: sanitizedFacts, - templateVersion: TEMPLATE_VERSION, - terminologyVersion: TERMINOLOGY_VERSION, - narrativeTags, - promptHash, - blockType: blockDef.blockType, - sectionName: blockDef.sectionName, - } - - const cached = proseCache.getSync(cacheParams) - if (cached) { - generatedBlocks.push(cached) - repairAudits.push({ - repairAttempts: 0, - validatorFailures: [], - repairSuccessful: true, - fallbackUsed: false, - }) - continue - } - - // Build prompts - let systemPrompt = buildV2SystemPrompt( - factsString, tagsString, termsString, styleString, disallowedString, - sanitizedFacts.companyName, - blockDef.blockId, blockDef.blockType, blockDef.sectionName, - documentType, blockDef.targetWords - ) - if (v2RagContext) { - systemPrompt += `\n\nRECHTSKONTEXT (als Referenz, nicht woertlich uebernehmen):\n${v2RagContext}` - } - const userPrompt = buildBlockSpecificPrompt( - blockDef.blockType, blockDef.sectionName, documentType - ) + (instructions ? `\n\nZusaetzliche Anweisungen: ${instructions}` : '') - - // Call LLM + Repair Loop - try { - const rawOutput = await callOllama(systemPrompt, userPrompt) - totalTokens += rawOutput.length / 4 // Rough token estimate - - const { block, audit } = await executeRepairLoop( - rawOutput, - sanitizedFacts, - narrativeTags, - blockDef.blockId, - blockDef.blockType, - async (repairPrompt) => callOllama(systemPrompt, repairPrompt), - documentType - ) - - generatedBlocks.push(block) - repairAudits.push(audit) - - // Cache successful blocks (not fallbacks) - if (!audit.fallbackUsed) { - proseCache.setSync(cacheParams, block) - } - } catch (error) { - // LLM unreachable → Fallback - const { buildFallbackBlock } = await import('@/lib/sdk/drafting-engine/prose-validator') - generatedBlocks.push( - buildFallbackBlock(blockDef.blockId, blockDef.blockType, sanitizedFacts, documentType) - ) - repairAudits.push({ - repairAttempts: 0, - validatorFailures: [[(error as Error).message]], - repairSuccessful: false, - fallbackUsed: true, - fallbackReason: `LLM-Fehler: ${(error as Error).message}`, - }) - } - } - - // Step 7: Build v1-compatible draft sections from prose blocks + original prompt - const draftPrompt = buildPromptForDocumentType(documentType, draftContext, instructions) - - // Also generate data sections via legacy pipeline - let dataSections: DraftSection[] = [] - try { - const dataResponse = await callOllama(V1_SYSTEM_PROMPT, draftPrompt) - const parsed = JSON.parse(dataResponse) - dataSections = (parsed.sections || []).map((s: Record, i: number) => ({ - id: String(s.id || `section-${i}`), - title: String(s.title || ''), - content: String(s.content || ''), - schemaField: s.schemaField ? String(s.schemaField) : undefined, - })) - totalTokens += dataResponse.length / 4 - } catch { - dataSections = [] - } - - // Merge: Prose intro → Data sections → Prose transitions/conclusion - const introBlock = generatedBlocks.find(b => b.blockType === 'introduction') - const transitionBlocks = generatedBlocks.filter(b => b.blockType === 'transition') - const appreciationBlocks = generatedBlocks.filter(b => b.blockType === 'appreciation') - const conclusionBlock = generatedBlocks.find(b => b.blockType === 'conclusion') - - const mergedSections: DraftSection[] = [] - - if (introBlock) { - mergedSections.push({ - id: introBlock.blockId, - title: 'Einleitung', - content: introBlock.text, - }) - } - - for (let i = 0; i < dataSections.length; i++) { - // Insert transition before data section (if available) - if (i > 0 && transitionBlocks[i - 1]) { - mergedSections.push({ - id: transitionBlocks[i - 1].blockId, - title: '', - content: transitionBlocks[i - 1].text, - }) - } - mergedSections.push(dataSections[i]) - } - - for (const block of appreciationBlocks) { - mergedSections.push({ - id: block.blockId, - title: 'Wuerdigung', - content: block.text, - }) - } - - if (conclusionBlock) { - mergedSections.push({ - id: conclusionBlock.blockId, - title: 'Fazit', - content: conclusionBlock.text, - }) - } - - // If no data sections generated, use prose blocks as sections - const finalSections = mergedSections.length > 0 ? mergedSections : generatedBlocks.map(b => ({ - id: b.blockId, - title: b.blockType === 'introduction' ? 'Einleitung' : - b.blockType === 'conclusion' ? 'Fazit' : - b.blockType === 'appreciation' ? 'Wuerdigung' : 'Ueberleitung', - content: b.text, - })) - - const draft: DraftRevision = { - id: `draft-v2-${Date.now()}`, - content: finalSections.map(s => s.title ? `## ${s.title}\n\n${s.content}` : s.content).join('\n\n'), - sections: finalSections, - createdAt: new Date().toISOString(), - instruction: instructions, - } - - // Step 8: Build Audit Trail - const auditTrail = { - documentType, - templateVersion: TEMPLATE_VERSION, - terminologyVersion: TERMINOLOGY_VERSION, - validatorVersion: VALIDATOR_VERSION, - promptHash, - llmModel: LLM_MODEL, - llmTemperature: 0.15, - llmProvider: 'ollama', - narrativeTags, - sanitization: sanitizationResult.audit, - repairAudits, - proseBlocks: generatedBlocks.map((b, i) => ({ - blockId: b.blockId, - blockType: b.blockType, - wordCount: b.text.split(/\s+/).filter(Boolean).length, - fallbackUsed: repairAudits[i]?.fallbackUsed ?? false, - repairAttempts: repairAudits[i]?.repairAttempts ?? 0, - })), - 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, - }) -} +import { handleV1Draft, handleV2Draft } from './draft-helpers' // ============================================================================ // Route Handler diff --git a/admin-compliance/app/sdk/academy/[id]/_components/CourseHeader.tsx b/admin-compliance/app/sdk/academy/[id]/_components/CourseHeader.tsx new file mode 100644 index 0000000..2a7d230 --- /dev/null +++ b/admin-compliance/app/sdk/academy/[id]/_components/CourseHeader.tsx @@ -0,0 +1,50 @@ +'use client' + +import Link from 'next/link' +import { Course, COURSE_CATEGORY_INFO } from '@/lib/sdk/academy/types' + +interface CourseHeaderProps { + course: Course + onDelete: () => void +} + +export function CourseHeader({ course, onDelete }: CourseHeaderProps) { + const categoryInfo = COURSE_CATEGORY_INFO[course.category] || COURSE_CATEGORY_INFO['custom'] + + return ( +
+
+ + + + + +
+
+ + {categoryInfo.label} + + + {course.status === 'published' ? 'Veroeffentlicht' : 'Entwurf'} + +
+

{course.title}

+

{course.description}

+
+
+
+ +
+
+ ) +} diff --git a/admin-compliance/app/sdk/academy/[id]/_components/CourseStats.tsx b/admin-compliance/app/sdk/academy/[id]/_components/CourseStats.tsx new file mode 100644 index 0000000..3fd4bd0 --- /dev/null +++ b/admin-compliance/app/sdk/academy/[id]/_components/CourseStats.tsx @@ -0,0 +1,33 @@ +'use client' + +import { Course, Lesson, Enrollment } from '@/lib/sdk/academy/types' + +interface CourseStatsProps { + course: Course + sortedLessons: Lesson[] + enrollments: Enrollment[] + completedEnrollments: number +} + +export function CourseStats({ course, sortedLessons, enrollments, completedEnrollments }: CourseStatsProps) { + return ( +
+
+
Lektionen
+
{sortedLessons.length}
+
+
+
Dauer
+
{course.durationMinutes} Min.
+
+
+
Teilnehmer
+
{enrollments.length}
+
+
+
Abgeschlossen
+
{completedEnrollments}
+
+
+ ) +} diff --git a/admin-compliance/app/sdk/academy/[id]/_components/CourseTabs.tsx b/admin-compliance/app/sdk/academy/[id]/_components/CourseTabs.tsx new file mode 100644 index 0000000..d2ede1f --- /dev/null +++ b/admin-compliance/app/sdk/academy/[id]/_components/CourseTabs.tsx @@ -0,0 +1,37 @@ +'use client' + +type TabId = 'overview' | 'lessons' | 'enrollments' | 'videos' + +interface CourseTabsProps { + activeTab: TabId + onTabChange: (tab: TabId) => void +} + +const TAB_LABELS: Record = { + overview: 'Uebersicht', + lessons: 'Lektionen', + enrollments: 'Einschreibungen', + videos: 'Videos', +} + +export function CourseTabs({ activeTab, onTabChange }: CourseTabsProps) { + return ( +
+ +
+ ) +} diff --git a/admin-compliance/app/sdk/academy/[id]/_components/EnrollmentsTab.tsx b/admin-compliance/app/sdk/academy/[id]/_components/EnrollmentsTab.tsx new file mode 100644 index 0000000..85383db --- /dev/null +++ b/admin-compliance/app/sdk/academy/[id]/_components/EnrollmentsTab.tsx @@ -0,0 +1,59 @@ +'use client' + +import { Enrollment, ENROLLMENT_STATUS_INFO, isEnrollmentOverdue, getDaysUntilDeadline } from '@/lib/sdk/academy/types' + +interface EnrollmentsTabProps { + enrollments: Enrollment[] + overdueEnrollments: number +} + +export function EnrollmentsTab({ enrollments, overdueEnrollments }: EnrollmentsTabProps) { + return ( +
+ {overdueEnrollments > 0 && ( +
+ {overdueEnrollments} ueberfaellige Einschreibung(en) +
+ )} + {enrollments.length === 0 ? ( +
+

Noch keine Einschreibungen fuer diesen Kurs.

+
+ ) : ( + enrollments.map(enrollment => { + const statusInfo = ENROLLMENT_STATUS_INFO[enrollment.status] + const overdue = isEnrollmentOverdue(enrollment) + const daysUntil = getDaysUntilDeadline(enrollment.deadline) + return ( +
+
+
+
+ + {statusInfo?.label} + + {overdue && Ueberfaellig} +
+
{enrollment.userName}
+
{enrollment.userEmail}
+
+
+
{enrollment.progress}%
+
+ {enrollment.status === 'completed' ? 'Abgeschlossen' : `${daysUntil > 0 ? daysUntil + ' Tage verbleibend' : Math.abs(daysUntil) + ' Tage ueberfaellig'}`} +
+
+
+
+
+
+
+ ) + }) + )} +
+ ) +} diff --git a/admin-compliance/app/sdk/academy/[id]/_components/LessonsTab.tsx b/admin-compliance/app/sdk/academy/[id]/_components/LessonsTab.tsx new file mode 100644 index 0000000..c511b87 --- /dev/null +++ b/admin-compliance/app/sdk/academy/[id]/_components/LessonsTab.tsx @@ -0,0 +1,239 @@ +'use client' + +import { Lesson, QuizQuestion } from '@/lib/sdk/academy/types' + +interface LessonsTabProps { + sortedLessons: Lesson[] + selectedLesson: Lesson | null + onSelectLesson: (lesson: Lesson) => void + quizAnswers: Record + onQuizAnswer: (answers: Record) => void + quizResult: any + isSubmittingQuiz: boolean + onSubmitQuiz: () => void + onResetQuiz: () => void + isEditing: boolean + editTitle: string + editContent: string + onEditTitle: (v: string) => void + onEditContent: (v: string) => void + isSaving: boolean + saveMessage: { type: 'success' | 'error'; text: string } | null + onStartEdit: () => void + onCancelEdit: () => void + onSaveLesson: () => void + onApproveLesson: () => void +} + +export function LessonsTab({ + sortedLessons, + selectedLesson, + onSelectLesson, + quizAnswers, + onQuizAnswer, + quizResult, + isSubmittingQuiz, + onSubmitQuiz, + onResetQuiz, + isEditing, + editTitle, + editContent, + onEditTitle, + onEditContent, + isSaving, + saveMessage, + onStartEdit, + onCancelEdit, + onSaveLesson, + onApproveLesson, +}: LessonsTabProps) { + return ( +
+ {/* Lesson Navigation */} +
+

Lektionen

+
+ {sortedLessons.map((lesson, i) => ( + + ))} +
+
+ + {/* Lesson Content */} +
+ {selectedLesson ? ( +
+
+ {isEditing ? ( + onEditTitle(e.target.value)} + className="text-xl font-semibold text-gray-900 border border-gray-300 rounded-lg px-3 py-1 flex-1 mr-3" + /> + ) : ( +

{selectedLesson.title}

+ )} +
+ + {selectedLesson.type === 'quiz' ? 'Quiz' : selectedLesson.type === 'video' ? 'Video' : 'Text'} + + {selectedLesson.type !== 'quiz' && !isEditing && ( + <> + + + + )} + {isEditing && ( + <> + + + + )} +
+
+ + {saveMessage && ( +
+ {saveMessage.text} +
+ )} + + {selectedLesson.type === 'video' && selectedLesson.videoUrl && ( +
+
+ )} + + {isEditing && (selectedLesson.type === 'text' || selectedLesson.type === 'video') && ( +
+ + +
+ + ${error ? `
${error}
` : ''} + + + + +

${t.disclaimer}

+
+ ` +} + +export function buildSuccessHtml(styles: string, t: DSRTranslations, email: string): string { + return ` + +
+
+
+

${t.successTitle}

+

+ ${t.successMessage} ${email}. +

+
+
+ ` +} diff --git a/breakpilot-compliance-sdk/packages/vanilla/src/web-components/dsr-portal-translations.ts b/breakpilot-compliance-sdk/packages/vanilla/src/web-components/dsr-portal-translations.ts new file mode 100644 index 0000000..bee3c61 --- /dev/null +++ b/breakpilot-compliance-sdk/packages/vanilla/src/web-components/dsr-portal-translations.ts @@ -0,0 +1,101 @@ +/** + * Translations for + * + * Supported languages: 'de' | 'en' + */ + +export const DSR_TRANSLATIONS = { + de: { + title: 'Betroffenenrechte-Portal', + subtitle: + 'Hier können Sie Ihre Rechte gemäß DSGVO wahrnehmen. Wählen Sie die gewünschte Anfrage und füllen Sie das Formular aus.', + requestType: 'Art der Anfrage', + name: 'Ihr Name', + namePlaceholder: 'Max Mustermann', + email: 'E-Mail-Adresse', + emailPlaceholder: 'max@example.com', + additionalInfo: 'Zusätzliche Informationen (optional)', + additionalInfoPlaceholder: 'Weitere Details zu Ihrer Anfrage...', + submit: 'Anfrage einreichen', + submitting: 'Wird gesendet...', + successTitle: 'Anfrage eingereicht', + successMessage: + 'Wir werden Ihre Anfrage innerhalb von 30 Tagen bearbeiten. Sie erhalten eine Bestätigung per E-Mail an', + disclaimer: + 'Ihre Anfrage wird gemäß Art. 12 DSGVO innerhalb von einem Monat bearbeitet. In komplexen Fällen kann diese Frist um weitere zwei Monate verlängert werden.', + types: { + ACCESS: { + name: 'Auskunft (Art. 15)', + description: 'Welche Daten haben Sie über mich gespeichert?', + }, + RECTIFICATION: { + name: 'Berichtigung (Art. 16)', + description: 'Korrigieren Sie falsche Daten über mich.', + }, + ERASURE: { + name: 'Löschung (Art. 17)', + description: 'Löschen Sie alle meine personenbezogenen Daten.', + }, + PORTABILITY: { + name: 'Datenübertragbarkeit (Art. 20)', + description: 'Exportieren Sie meine Daten in einem maschinenlesbaren Format.', + }, + RESTRICTION: { + name: 'Einschränkung (Art. 18)', + description: 'Schränken Sie die Verarbeitung meiner Daten ein.', + }, + OBJECTION: { + name: 'Widerspruch (Art. 21)', + description: 'Ich widerspreche der Verarbeitung meiner Daten.', + }, + }, + }, + en: { + title: 'Data Subject Rights Portal', + subtitle: + 'Here you can exercise your rights under GDPR. Select the type of request and fill out the form.', + requestType: 'Request Type', + name: 'Your Name', + namePlaceholder: 'John Doe', + email: 'Email Address', + emailPlaceholder: 'john@example.com', + additionalInfo: 'Additional Information (optional)', + additionalInfoPlaceholder: 'Any additional details about your request...', + submit: 'Submit Request', + submitting: 'Submitting...', + successTitle: 'Request Submitted', + successMessage: + 'We will process your request within 30 days. You will receive a confirmation email at', + disclaimer: + 'Your request will be processed in accordance with Article 12 GDPR within one month. In complex cases, this period may be extended by up to two additional months.', + types: { + ACCESS: { + name: 'Access (Art. 15)', + description: 'What data do you have about me?', + }, + RECTIFICATION: { + name: 'Rectification (Art. 16)', + description: 'Correct inaccurate data about me.', + }, + ERASURE: { + name: 'Erasure (Art. 17)', + description: 'Delete all my personal data.', + }, + PORTABILITY: { + name: 'Data Portability (Art. 20)', + description: 'Export my data in a machine-readable format.', + }, + RESTRICTION: { + name: 'Restriction (Art. 18)', + description: 'Restrict the processing of my data.', + }, + OBJECTION: { + name: 'Objection (Art. 21)', + description: 'I object to the processing of my data.', + }, + }, + }, +} as const + +export type DSRLanguage = keyof typeof DSR_TRANSLATIONS +export type DSRTranslations = (typeof DSR_TRANSLATIONS)[DSRLanguage] diff --git a/breakpilot-compliance-sdk/packages/vanilla/src/web-components/dsr-portal.ts b/breakpilot-compliance-sdk/packages/vanilla/src/web-components/dsr-portal.ts index bf30bfa..57f113a 100644 --- a/breakpilot-compliance-sdk/packages/vanilla/src/web-components/dsr-portal.ts +++ b/breakpilot-compliance-sdk/packages/vanilla/src/web-components/dsr-portal.ts @@ -8,103 +8,15 @@ * api-key="pk_live_xxx" * language="de"> * + * + * Split: translations → dsr-portal-translations.ts + * styles + HTML builders → dsr-portal-render.ts */ import type { DSRRequestType } from '@breakpilot/compliance-sdk-types' -import { BreakPilotElement, COMMON_STYLES } from './base' - -const TRANSLATIONS = { - de: { - title: 'Betroffenenrechte-Portal', - subtitle: - 'Hier können Sie Ihre Rechte gemäß DSGVO wahrnehmen. Wählen Sie die gewünschte Anfrage und füllen Sie das Formular aus.', - requestType: 'Art der Anfrage', - name: 'Ihr Name', - namePlaceholder: 'Max Mustermann', - email: 'E-Mail-Adresse', - emailPlaceholder: 'max@example.com', - additionalInfo: 'Zusätzliche Informationen (optional)', - additionalInfoPlaceholder: 'Weitere Details zu Ihrer Anfrage...', - submit: 'Anfrage einreichen', - submitting: 'Wird gesendet...', - successTitle: 'Anfrage eingereicht', - successMessage: - 'Wir werden Ihre Anfrage innerhalb von 30 Tagen bearbeiten. Sie erhalten eine Bestätigung per E-Mail an', - disclaimer: - 'Ihre Anfrage wird gemäß Art. 12 DSGVO innerhalb von einem Monat bearbeitet. In komplexen Fällen kann diese Frist um weitere zwei Monate verlängert werden.', - types: { - ACCESS: { - name: 'Auskunft (Art. 15)', - description: 'Welche Daten haben Sie über mich gespeichert?', - }, - RECTIFICATION: { - name: 'Berichtigung (Art. 16)', - description: 'Korrigieren Sie falsche Daten über mich.', - }, - ERASURE: { - name: 'Löschung (Art. 17)', - description: 'Löschen Sie alle meine personenbezogenen Daten.', - }, - PORTABILITY: { - name: 'Datenübertragbarkeit (Art. 20)', - description: 'Exportieren Sie meine Daten in einem maschinenlesbaren Format.', - }, - RESTRICTION: { - name: 'Einschränkung (Art. 18)', - description: 'Schränken Sie die Verarbeitung meiner Daten ein.', - }, - OBJECTION: { - name: 'Widerspruch (Art. 21)', - description: 'Ich widerspreche der Verarbeitung meiner Daten.', - }, - }, - }, - en: { - title: 'Data Subject Rights Portal', - subtitle: - 'Here you can exercise your rights under GDPR. Select the type of request and fill out the form.', - requestType: 'Request Type', - name: 'Your Name', - namePlaceholder: 'John Doe', - email: 'Email Address', - emailPlaceholder: 'john@example.com', - additionalInfo: 'Additional Information (optional)', - additionalInfoPlaceholder: 'Any additional details about your request...', - submit: 'Submit Request', - submitting: 'Submitting...', - successTitle: 'Request Submitted', - successMessage: - 'We will process your request within 30 days. You will receive a confirmation email at', - disclaimer: - 'Your request will be processed in accordance with Article 12 GDPR within one month. In complex cases, this period may be extended by up to two additional months.', - types: { - ACCESS: { - name: 'Access (Art. 15)', - description: 'What data do you have about me?', - }, - RECTIFICATION: { - name: 'Rectification (Art. 16)', - description: 'Correct inaccurate data about me.', - }, - ERASURE: { - name: 'Erasure (Art. 17)', - description: 'Delete all my personal data.', - }, - PORTABILITY: { - name: 'Data Portability (Art. 20)', - description: 'Export my data in a machine-readable format.', - }, - RESTRICTION: { - name: 'Restriction (Art. 18)', - description: 'Restrict the processing of my data.', - }, - OBJECTION: { - name: 'Objection (Art. 21)', - description: 'I object to the processing of my data.', - }, - }, - }, -} +import { BreakPilotElement } from './base' +import { DSR_TRANSLATIONS, type DSRLanguage } from './dsr-portal-translations' +import { DSR_PORTAL_STYLES, buildFormHtml, buildSuccessHtml } from './dsr-portal-render' export class DSRPortalElement extends BreakPilotElement { static get observedAttributes(): string[] { @@ -119,12 +31,12 @@ export class DSRPortalElement extends BreakPilotElement { private isSubmitted = false private error: string | null = null - private get language(): 'de' | 'en' { - return (this.getAttribute('language') as 'de' | 'en') || 'de' + private get language(): DSRLanguage { + return (this.getAttribute('language') as DSRLanguage) || 'de' } private get t() { - return TRANSLATIONS[this.language] + return DSR_TRANSLATIONS[this.language] } private handleTypeSelect = (type: DSRRequestType): void => { @@ -165,253 +77,23 @@ export class DSRPortalElement extends BreakPilotElement { } protected render(): void { - const styles = ` - ${COMMON_STYLES} - - :host { - max-width: 600px; - margin: 0 auto; - padding: 20px; - } - - .portal { - background: #fff; - } - - .title { - margin: 0 0 10px; - font-size: 24px; - font-weight: 600; - color: #1a1a1a; - } - - .subtitle { - margin: 0 0 20px; - color: #666; - } - - .form-group { - margin-bottom: 20px; - } - - .label { - display: block; - font-weight: 500; - margin-bottom: 10px; - } - - .type-options { - display: grid; - gap: 10px; - } - - .type-option { - display: flex; - padding: 15px; - border: 2px solid #ddd; - border-radius: 8px; - cursor: pointer; - background: #fff; - transition: all 0.2s; - } - - .type-option:hover { - border-color: #999; - } - - .type-option.selected { - border-color: #1a1a1a; - background: #f5f5f5; - } - - .type-option input { - margin-right: 15px; - } - - .type-name { - font-weight: 500; - } - - .type-description { - font-size: 13px; - color: #666; - } - - .input { - width: 100%; - padding: 12px; - font-size: 14px; - border: 1px solid #ddd; - border-radius: 4px; - box-sizing: border-box; - } - - .input:focus { - outline: none; - border-color: #1a1a1a; - } - - .textarea { - resize: vertical; - min-height: 100px; - } - - .error { - padding: 12px; - background: #fef2f2; - color: #dc2626; - border-radius: 4px; - margin-bottom: 15px; - } - - .btn-submit { - width: 100%; - padding: 12px 24px; - font-size: 16px; - font-weight: 500; - color: #fff; - background: #1a1a1a; - border: none; - border-radius: 4px; - cursor: pointer; - } - - .btn-submit:disabled { - opacity: 0.5; - cursor: not-allowed; - } - - .disclaimer { - margin-top: 20px; - font-size: 12px; - color: #666; - } - - .success { - text-align: center; - padding: 40px 20px; - background: #f0fdf4; - border-radius: 8px; - } - - .success-icon { - font-size: 48px; - margin-bottom: 20px; - } - - .success-title { - margin: 0 0 10px; - color: #166534; - } - - .success-message { - margin: 0; - color: #166534; - } - ` - if (this.isSubmitted) { - this.renderSuccess(styles) + this.shadow.innerHTML = buildSuccessHtml(DSR_PORTAL_STYLES, this.t, this.email) } else { - this.renderForm(styles) + this.renderForm() } } - private renderForm(styles: string): void { - const t = this.t - const types: DSRRequestType[] = [ - 'ACCESS', - 'RECTIFICATION', - 'ERASURE', - 'PORTABILITY', - 'RESTRICTION', - 'OBJECTION', - ] - - const typesHtml = types - .map( - type => ` - - ` - ) - .join('') - - const isValid = this.selectedType && this.email && this.name - const isDisabled = !isValid || this.isSubmitting - - this.shadow.innerHTML = ` - -
-

${t.title}

-

${t.subtitle}

- -
-
- -
- ${typesHtml} -
-
- -
- - -
- -
- - -
- -
- - -
- - ${this.error ? `
${this.error}
` : ''} - - -
- -

${t.disclaimer}

-
- ` + private renderForm(): void { + this.shadow.innerHTML = buildFormHtml(DSR_PORTAL_STYLES, { + t: this.t, + selectedType: this.selectedType, + name: this.name, + email: this.email, + additionalInfo: this.additionalInfo, + isSubmitting: this.isSubmitting, + error: this.error, + }) // Bind events const form = this.shadow.getElementById('dsr-form') as HTMLFormElement @@ -439,23 +121,6 @@ export class DSRPortalElement extends BreakPilotElement { } }) } - - private renderSuccess(styles: string): void { - const t = this.t - - this.shadow.innerHTML = ` - -
-
-
-

${t.successTitle}

-

- ${t.successMessage} ${this.email}. -

-
-
- ` - } } // Register the custom element diff --git a/breakpilot-compliance-sdk/packages/vue/src/composables/useRAG.ts b/breakpilot-compliance-sdk/packages/vue/src/composables/useRAG.ts index 7d61d4a..8ab8739 100644 --- a/breakpilot-compliance-sdk/packages/vue/src/composables/useRAG.ts +++ b/breakpilot-compliance-sdk/packages/vue/src/composables/useRAG.ts @@ -8,7 +8,7 @@ import type { SearchResponse, AssistantResponse, ChatMessage, - LegalDocument, + RagLegalDocument, } from '@breakpilot/compliance-sdk-types' export interface UseRAGReturn { @@ -23,7 +23,7 @@ export interface UseRAGReturn { isTyping: Ref // Documents - documents: ComputedRef + documents: ComputedRef availableRegulations: ComputedRef // Loading state diff --git a/compliance-tts-service/README.md b/compliance-tts-service/README.md new file mode 100644 index 0000000..c07e751 --- /dev/null +++ b/compliance-tts-service/README.md @@ -0,0 +1,30 @@ +# compliance-tts-service + +Python service generating German-language audio/video training materials using Piper TTS + FFmpeg. Outputs are stored in Hetzner Object Storage (S3-compatible). + +**Port:** `8095` (container: `bp-compliance-tts`) +**Stack:** Python 3.12, Piper TTS (`de_DE-thorsten-high.onnx`), FFmpeg, boto3. + +## Files + +- `main.py` — FastAPI entrypoint +- `tts_engine.py` — Piper wrapper +- `video_generator.py` — FFmpeg pipeline +- `storage.py` — S3 client + +## Run locally + +```bash +cd compliance-tts-service +pip install -r requirements.txt +# Piper model + ffmpeg must be available on PATH +uvicorn main:app --reload --port 8095 +``` + +## Tests + +0 test files today. Adding unit tests for the synthesis pipeline (mocked Piper + FFmpeg) and the S3 client is still in the backlog queue. + +## Architecture + +Follow `../AGENTS.python.md`. Keep the Piper model loading behind a single service instance — not loaded per request. diff --git a/consent-sdk/src/angular/index.ts b/consent-sdk/src/angular/index.ts index 6c4ece4..69d00c2 100644 --- a/consent-sdk/src/angular/index.ts +++ b/consent-sdk/src/angular/index.ts @@ -1,6 +1,9 @@ /** * Angular Integration fuer @breakpilot/consent-sdk * + * Phase 4 refactor: thin barrel. Interface, service, module definition, and + * template snippets live in sibling files. + * * @example * ```typescript * // app.module.ts @@ -16,494 +19,27 @@ * }) * export class AppModule {} * ``` + * + * @remarks + * Angular hat ein komplexeres Build-System (ngc, ng-packagr). Diese Dateien + * definieren die Schnittstelle — fuer Production muss ein separates Angular + * Library Package erstellt werden (`ng generate library @breakpilot/consent-sdk-angular`). */ -// ============================================================================= -// NOTE: Angular SDK Structure -// ============================================================================= -// -// Angular hat ein komplexeres Build-System (ngc, ng-packagr). -// Diese Datei definiert die Schnittstelle - fuer Production muss ein -// separates Angular Library Package erstellt werden: -// -// ng generate library @breakpilot/consent-sdk-angular -// -// Die folgende Implementation ist fuer direkten Import vorgesehen. -// ============================================================================= +export type { IConsentService } from './interface'; +export { ConsentServiceBase } from './service'; +export { + CONSENT_CONFIG, + CONSENT_SERVICE, + ConsentModuleDefinition, + consentServiceFactory, + type ConsentModuleConfig, +} from './module'; +export { CONSENT_BANNER_TEMPLATE, CONSENT_GATE_USAGE } from './templates'; -import { ConsentManager } from '../core/ConsentManager'; -import type { +export type { + ConsentCategories, + ConsentCategory, ConsentConfig, ConsentState, - ConsentCategory, - ConsentCategories, } from '../types'; - -// ============================================================================= -// Angular Service Interface -// ============================================================================= - -/** - * ConsentService Interface fuer Angular DI - * - * @example - * ```typescript - * @Component({...}) - * export class MyComponent { - * constructor(private consent: ConsentService) { - * if (this.consent.hasConsent('analytics')) { - * // Analytics laden - * } - * } - * } - * ``` - */ -export interface IConsentService { - /** Initialisiert? */ - readonly isInitialized: boolean; - - /** Laedt noch? */ - readonly isLoading: boolean; - - /** Banner sichtbar? */ - readonly isBannerVisible: boolean; - - /** Aktueller Consent-Zustand */ - readonly consent: ConsentState | null; - - /** Muss Consent eingeholt werden? */ - readonly needsConsent: boolean; - - /** Prueft Consent fuer Kategorie */ - hasConsent(category: ConsentCategory): boolean; - - /** Alle akzeptieren */ - acceptAll(): Promise; - - /** Alle ablehnen */ - rejectAll(): Promise; - - /** Auswahl speichern */ - saveSelection(categories: Partial): Promise; - - /** Banner anzeigen */ - showBanner(): void; - - /** Banner ausblenden */ - hideBanner(): void; - - /** Einstellungen oeffnen */ - showSettings(): void; -} - -// ============================================================================= -// ConsentService Implementation -// ============================================================================= - -/** - * ConsentService - Angular Service Wrapper - * - * Diese Klasse kann als Angular Service registriert werden: - * - * @example - * ```typescript - * // consent.service.ts - * import { Injectable } from '@angular/core'; - * import { ConsentServiceBase } from '@breakpilot/consent-sdk/angular'; - * - * @Injectable({ providedIn: 'root' }) - * export class ConsentService extends ConsentServiceBase { - * constructor() { - * super({ - * apiEndpoint: environment.consentApiEndpoint, - * siteId: environment.siteId, - * }); - * } - * } - * ``` - */ -export class ConsentServiceBase implements IConsentService { - private manager: ConsentManager; - private _consent: ConsentState | null = null; - private _isInitialized = false; - private _isLoading = true; - private _isBannerVisible = false; - - // Callbacks fuer Angular Change Detection - private changeCallbacks: Array<(consent: ConsentState) => void> = []; - private bannerShowCallbacks: Array<() => void> = []; - private bannerHideCallbacks: Array<() => void> = []; - - constructor(config: ConsentConfig) { - this.manager = new ConsentManager(config); - this.setupEventListeners(); - this.initialize(); - } - - // --------------------------------------------------------------------------- - // Getters - // --------------------------------------------------------------------------- - - get isInitialized(): boolean { - return this._isInitialized; - } - - get isLoading(): boolean { - return this._isLoading; - } - - get isBannerVisible(): boolean { - return this._isBannerVisible; - } - - get consent(): ConsentState | null { - return this._consent; - } - - get needsConsent(): boolean { - return this.manager.needsConsent(); - } - - // --------------------------------------------------------------------------- - // Methods - // --------------------------------------------------------------------------- - - hasConsent(category: ConsentCategory): boolean { - return this.manager.hasConsent(category); - } - - async acceptAll(): Promise { - await this.manager.acceptAll(); - } - - async rejectAll(): Promise { - await this.manager.rejectAll(); - } - - async saveSelection(categories: Partial): Promise { - await this.manager.setConsent(categories); - this.manager.hideBanner(); - } - - showBanner(): void { - this.manager.showBanner(); - } - - hideBanner(): void { - this.manager.hideBanner(); - } - - showSettings(): void { - this.manager.showSettings(); - } - - // --------------------------------------------------------------------------- - // Change Detection Support - // --------------------------------------------------------------------------- - - /** - * Registriert Callback fuer Consent-Aenderungen - * (fuer Angular Change Detection) - */ - onConsentChange(callback: (consent: ConsentState) => void): () => void { - this.changeCallbacks.push(callback); - return () => { - const index = this.changeCallbacks.indexOf(callback); - if (index > -1) { - this.changeCallbacks.splice(index, 1); - } - }; - } - - /** - * Registriert Callback wenn Banner angezeigt wird - */ - onBannerShow(callback: () => void): () => void { - this.bannerShowCallbacks.push(callback); - return () => { - const index = this.bannerShowCallbacks.indexOf(callback); - if (index > -1) { - this.bannerShowCallbacks.splice(index, 1); - } - }; - } - - /** - * Registriert Callback wenn Banner ausgeblendet wird - */ - onBannerHide(callback: () => void): () => void { - this.bannerHideCallbacks.push(callback); - return () => { - const index = this.bannerHideCallbacks.indexOf(callback); - if (index > -1) { - this.bannerHideCallbacks.splice(index, 1); - } - }; - } - - // --------------------------------------------------------------------------- - // Internal - // --------------------------------------------------------------------------- - - private setupEventListeners(): void { - this.manager.on('change', (consent) => { - this._consent = consent; - this.changeCallbacks.forEach((cb) => cb(consent)); - }); - - this.manager.on('banner_show', () => { - this._isBannerVisible = true; - this.bannerShowCallbacks.forEach((cb) => cb()); - }); - - this.manager.on('banner_hide', () => { - this._isBannerVisible = false; - this.bannerHideCallbacks.forEach((cb) => cb()); - }); - } - - private async initialize(): Promise { - try { - await this.manager.init(); - this._consent = this.manager.getConsent(); - this._isInitialized = true; - this._isBannerVisible = this.manager.isBannerVisible(); - } catch (error) { - console.error('Failed to initialize ConsentManager:', error); - } finally { - this._isLoading = false; - } - } -} - -// ============================================================================= -// Angular Module Configuration -// ============================================================================= - -/** - * Konfiguration fuer ConsentModule.forRoot() - */ -export interface ConsentModuleConfig extends ConsentConfig {} - -/** - * Token fuer Dependency Injection - * Verwendung mit Angular @Inject(): - * - * @example - * ```typescript - * constructor(@Inject(CONSENT_CONFIG) private config: ConsentConfig) {} - * ``` - */ -export const CONSENT_CONFIG = 'CONSENT_CONFIG'; -export const CONSENT_SERVICE = 'CONSENT_SERVICE'; - -// ============================================================================= -// Factory Functions fuer Angular DI -// ============================================================================= - -/** - * Factory fuer ConsentService - * - * @example - * ```typescript - * // app.module.ts - * providers: [ - * { provide: CONSENT_CONFIG, useValue: { apiEndpoint: '...', siteId: '...' } }, - * { provide: CONSENT_SERVICE, useFactory: consentServiceFactory, deps: [CONSENT_CONFIG] }, - * ] - * ``` - */ -export function consentServiceFactory(config: ConsentConfig): ConsentServiceBase { - return new ConsentServiceBase(config); -} - -// ============================================================================= -// Angular Module Definition (Template) -// ============================================================================= - -/** - * ConsentModule - Angular Module - * - * Dies ist eine Template-Definition. Fuer echte Angular-Nutzung - * muss ein separates Angular Library Package erstellt werden. - * - * @example - * ```typescript - * // In einem Angular Library Package: - * @NgModule({ - * declarations: [ConsentBannerComponent, ConsentGateDirective], - * exports: [ConsentBannerComponent, ConsentGateDirective], - * }) - * export class ConsentModule { - * static forRoot(config: ConsentModuleConfig): ModuleWithProviders { - * return { - * ngModule: ConsentModule, - * providers: [ - * { provide: CONSENT_CONFIG, useValue: config }, - * { provide: CONSENT_SERVICE, useFactory: consentServiceFactory, deps: [CONSENT_CONFIG] }, - * ], - * }; - * } - * } - * ``` - */ -export const ConsentModuleDefinition = { - /** - * Providers fuer Root-Module - */ - forRoot: (config: ConsentModuleConfig) => ({ - provide: CONSENT_CONFIG, - useValue: config, - }), -}; - -// ============================================================================= -// Component Templates (fuer Angular Library) -// ============================================================================= - -/** - * ConsentBannerComponent Template - * - * Fuer Angular Library Implementation: - * - * @example - * ```typescript - * @Component({ - * selector: 'bp-consent-banner', - * template: CONSENT_BANNER_TEMPLATE, - * styles: [CONSENT_BANNER_STYLES], - * }) - * export class ConsentBannerComponent { - * constructor(public consent: ConsentService) {} - * } - * ``` - */ -export const CONSENT_BANNER_TEMPLATE = ` - -`; - -/** - * ConsentGateDirective Template - * - * @example - * ```typescript - * @Directive({ - * selector: '[bpConsentGate]', - * }) - * export class ConsentGateDirective implements OnInit, OnDestroy { - * @Input('bpConsentGate') category!: ConsentCategory; - * - * private unsubscribe?: () => void; - * - * constructor( - * private templateRef: TemplateRef, - * private viewContainer: ViewContainerRef, - * private consent: ConsentService - * ) {} - * - * ngOnInit() { - * this.updateView(); - * this.unsubscribe = this.consent.onConsentChange(() => this.updateView()); - * } - * - * ngOnDestroy() { - * this.unsubscribe?.(); - * } - * - * private updateView() { - * if (this.consent.hasConsent(this.category)) { - * this.viewContainer.createEmbeddedView(this.templateRef); - * } else { - * this.viewContainer.clear(); - * } - * } - * } - * ``` - */ -export const CONSENT_GATE_USAGE = ` - -
- -
- - - - - - -

Bitte akzeptieren Sie Marketing-Cookies.

-
-`; - -// ============================================================================= -// RxJS Observable Wrapper (Optional) -// ============================================================================= - -/** - * RxJS Observable Wrapper fuer ConsentService - * - * Fuer Projekte die RxJS bevorzugen: - * - * @example - * ```typescript - * import { BehaviorSubject, Observable } from 'rxjs'; - * - * export class ConsentServiceRx extends ConsentServiceBase { - * private consentSubject = new BehaviorSubject(null); - * private bannerVisibleSubject = new BehaviorSubject(false); - * - * consent$ = this.consentSubject.asObservable(); - * isBannerVisible$ = this.bannerVisibleSubject.asObservable(); - * - * constructor(config: ConsentConfig) { - * super(config); - * this.onConsentChange((c) => this.consentSubject.next(c)); - * this.onBannerShow(() => this.bannerVisibleSubject.next(true)); - * this.onBannerHide(() => this.bannerVisibleSubject.next(false)); - * } - * } - * ``` - */ - -// ============================================================================= -// Exports -// ============================================================================= - -export type { ConsentConfig, ConsentState, ConsentCategory, ConsentCategories }; diff --git a/consent-sdk/src/angular/interface.ts b/consent-sdk/src/angular/interface.ts new file mode 100644 index 0000000..ab56956 --- /dev/null +++ b/consent-sdk/src/angular/interface.ts @@ -0,0 +1,64 @@ +/** + * Angular IConsentService — interface for DI. + * + * Phase 4: extracted from angular/index.ts. + */ + +import type { + ConsentCategories, + ConsentCategory, + ConsentState, +} from '../types'; + +/** + * ConsentService Interface fuer Angular DI + * + * @example + * ```typescript + * @Component({...}) + * export class MyComponent { + * constructor(private consent: ConsentService) { + * if (this.consent.hasConsent('analytics')) { + * // Analytics laden + * } + * } + * } + * ``` + */ +export interface IConsentService { + /** Initialisiert? */ + readonly isInitialized: boolean; + + /** Laedt noch? */ + readonly isLoading: boolean; + + /** Banner sichtbar? */ + readonly isBannerVisible: boolean; + + /** Aktueller Consent-Zustand */ + readonly consent: ConsentState | null; + + /** Muss Consent eingeholt werden? */ + readonly needsConsent: boolean; + + /** Prueft Consent fuer Kategorie */ + hasConsent(category: ConsentCategory): boolean; + + /** Alle akzeptieren */ + acceptAll(): Promise; + + /** Alle ablehnen */ + rejectAll(): Promise; + + /** Auswahl speichern */ + saveSelection(categories: Partial): Promise; + + /** Banner anzeigen */ + showBanner(): void; + + /** Banner ausblenden */ + hideBanner(): void; + + /** Einstellungen oeffnen */ + showSettings(): void; +} diff --git a/consent-sdk/src/angular/module.ts b/consent-sdk/src/angular/module.ts new file mode 100644 index 0000000..1f33af6 --- /dev/null +++ b/consent-sdk/src/angular/module.ts @@ -0,0 +1,79 @@ +/** + * Angular Module configuration — DI tokens, factory, module definition. + * + * Phase 4: extracted from angular/index.ts. + */ + +import type { ConsentConfig } from '../types'; +import { ConsentServiceBase } from './service'; + +/** + * Konfiguration fuer ConsentModule.forRoot() + */ +export interface ConsentModuleConfig extends ConsentConfig {} + +/** + * Token fuer Dependency Injection + * Verwendung mit Angular @Inject(): + * + * @example + * ```typescript + * constructor(@Inject(CONSENT_CONFIG) private config: ConsentConfig) {} + * ``` + */ +export const CONSENT_CONFIG = 'CONSENT_CONFIG'; +export const CONSENT_SERVICE = 'CONSENT_SERVICE'; + +/** + * Factory fuer ConsentService + * + * @example + * ```typescript + * // app.module.ts + * providers: [ + * { provide: CONSENT_CONFIG, useValue: { apiEndpoint: '...', siteId: '...' } }, + * { provide: CONSENT_SERVICE, useFactory: consentServiceFactory, deps: [CONSENT_CONFIG] }, + * ] + * ``` + */ +export function consentServiceFactory( + config: ConsentConfig +): ConsentServiceBase { + return new ConsentServiceBase(config); +} + +/** + * ConsentModule - Angular Module + * + * Dies ist eine Template-Definition. Fuer echte Angular-Nutzung + * muss ein separates Angular Library Package erstellt werden. + * + * @example + * ```typescript + * // In einem Angular Library Package: + * @NgModule({ + * declarations: [ConsentBannerComponent, ConsentGateDirective], + * exports: [ConsentBannerComponent, ConsentGateDirective], + * }) + * export class ConsentModule { + * static forRoot(config: ConsentModuleConfig): ModuleWithProviders { + * return { + * ngModule: ConsentModule, + * providers: [ + * { provide: CONSENT_CONFIG, useValue: config }, + * { provide: CONSENT_SERVICE, useFactory: consentServiceFactory, deps: [CONSENT_CONFIG] }, + * ], + * }; + * } + * } + * ``` + */ +export const ConsentModuleDefinition = { + /** + * Providers fuer Root-Module + */ + forRoot: (config: ConsentModuleConfig) => ({ + provide: CONSENT_CONFIG, + useValue: config, + }), +}; diff --git a/consent-sdk/src/angular/service.ts b/consent-sdk/src/angular/service.ts new file mode 100644 index 0000000..f908353 --- /dev/null +++ b/consent-sdk/src/angular/service.ts @@ -0,0 +1,190 @@ +/** + * ConsentServiceBase — Angular Service Wrapper. + * + * Phase 4: extracted from angular/index.ts. + */ + +import { ConsentManager } from '../core/ConsentManager'; +import type { + ConsentCategories, + ConsentCategory, + ConsentConfig, + ConsentState, +} from '../types'; +import type { IConsentService } from './interface'; + +/** + * ConsentService - Angular Service Wrapper + * + * Diese Klasse kann als Angular Service registriert werden: + * + * @example + * ```typescript + * // consent.service.ts + * import { Injectable } from '@angular/core'; + * import { ConsentServiceBase } from '@breakpilot/consent-sdk/angular'; + * + * @Injectable({ providedIn: 'root' }) + * export class ConsentService extends ConsentServiceBase { + * constructor() { + * super({ + * apiEndpoint: environment.consentApiEndpoint, + * siteId: environment.siteId, + * }); + * } + * } + * ``` + */ +export class ConsentServiceBase implements IConsentService { + private manager: ConsentManager; + private _consent: ConsentState | null = null; + private _isInitialized = false; + private _isLoading = true; + private _isBannerVisible = false; + + // Callbacks fuer Angular Change Detection + private changeCallbacks: Array<(consent: ConsentState) => void> = []; + private bannerShowCallbacks: Array<() => void> = []; + private bannerHideCallbacks: Array<() => void> = []; + + constructor(config: ConsentConfig) { + this.manager = new ConsentManager(config); + this.setupEventListeners(); + this.initialize(); + } + + // --------------------------------------------------------------------------- + // Getters + // --------------------------------------------------------------------------- + + get isInitialized(): boolean { + return this._isInitialized; + } + + get isLoading(): boolean { + return this._isLoading; + } + + get isBannerVisible(): boolean { + return this._isBannerVisible; + } + + get consent(): ConsentState | null { + return this._consent; + } + + get needsConsent(): boolean { + return this.manager.needsConsent(); + } + + // --------------------------------------------------------------------------- + // Methods + // --------------------------------------------------------------------------- + + hasConsent(category: ConsentCategory): boolean { + return this.manager.hasConsent(category); + } + + async acceptAll(): Promise { + await this.manager.acceptAll(); + } + + async rejectAll(): Promise { + await this.manager.rejectAll(); + } + + async saveSelection(categories: Partial): Promise { + await this.manager.setConsent(categories); + this.manager.hideBanner(); + } + + showBanner(): void { + this.manager.showBanner(); + } + + hideBanner(): void { + this.manager.hideBanner(); + } + + showSettings(): void { + this.manager.showSettings(); + } + + // --------------------------------------------------------------------------- + // Change Detection Support + // --------------------------------------------------------------------------- + + /** + * Registriert Callback fuer Consent-Aenderungen + * (fuer Angular Change Detection) + */ + onConsentChange(callback: (consent: ConsentState) => void): () => void { + this.changeCallbacks.push(callback); + return () => { + const index = this.changeCallbacks.indexOf(callback); + if (index > -1) { + this.changeCallbacks.splice(index, 1); + } + }; + } + + /** + * Registriert Callback wenn Banner angezeigt wird + */ + onBannerShow(callback: () => void): () => void { + this.bannerShowCallbacks.push(callback); + return () => { + const index = this.bannerShowCallbacks.indexOf(callback); + if (index > -1) { + this.bannerShowCallbacks.splice(index, 1); + } + }; + } + + /** + * Registriert Callback wenn Banner ausgeblendet wird + */ + onBannerHide(callback: () => void): () => void { + this.bannerHideCallbacks.push(callback); + return () => { + const index = this.bannerHideCallbacks.indexOf(callback); + if (index > -1) { + this.bannerHideCallbacks.splice(index, 1); + } + }; + } + + // --------------------------------------------------------------------------- + // Internal + // --------------------------------------------------------------------------- + + private setupEventListeners(): void { + this.manager.on('change', (consent) => { + this._consent = consent; + this.changeCallbacks.forEach((cb) => cb(consent)); + }); + + this.manager.on('banner_show', () => { + this._isBannerVisible = true; + this.bannerShowCallbacks.forEach((cb) => cb()); + }); + + this.manager.on('banner_hide', () => { + this._isBannerVisible = false; + this.bannerHideCallbacks.forEach((cb) => cb()); + }); + } + + private async initialize(): Promise { + try { + await this.manager.init(); + this._consent = this.manager.getConsent(); + this._isInitialized = true; + this._isBannerVisible = this.manager.isBannerVisible(); + } catch (error) { + console.error('Failed to initialize ConsentManager:', error); + } finally { + this._isLoading = false; + } + } +} diff --git a/consent-sdk/src/angular/templates.ts b/consent-sdk/src/angular/templates.ts new file mode 100644 index 0000000..4125253 --- /dev/null +++ b/consent-sdk/src/angular/templates.ts @@ -0,0 +1,142 @@ +/** + * Angular component templates — Banner + Gate directive reference snippets. + * + * Phase 4: extracted from angular/index.ts. + */ + +/** + * ConsentBannerComponent Template + * + * Fuer Angular Library Implementation: + * + * @example + * ```typescript + * @Component({ + * selector: 'bp-consent-banner', + * template: CONSENT_BANNER_TEMPLATE, + * styles: [CONSENT_BANNER_STYLES], + * }) + * export class ConsentBannerComponent { + * constructor(public consent: ConsentService) {} + * } + * ``` + */ +export const CONSENT_BANNER_TEMPLATE = ` + +`; + +/** + * ConsentGateDirective Template + * + * @example + * ```typescript + * @Directive({ + * selector: '[bpConsentGate]', + * }) + * export class ConsentGateDirective implements OnInit, OnDestroy { + * @Input('bpConsentGate') category!: ConsentCategory; + * + * private unsubscribe?: () => void; + * + * constructor( + * private templateRef: TemplateRef, + * private viewContainer: ViewContainerRef, + * private consent: ConsentService + * ) {} + * + * ngOnInit() { + * this.updateView(); + * this.unsubscribe = this.consent.onConsentChange(() => this.updateView()); + * } + * + * ngOnDestroy() { + * this.unsubscribe?.(); + * } + * + * private updateView() { + * if (this.consent.hasConsent(this.category)) { + * this.viewContainer.createEmbeddedView(this.templateRef); + * } else { + * this.viewContainer.clear(); + * } + * } + * } + * ``` + */ +export const CONSENT_GATE_USAGE = ` + +
+ +
+ + + + + + +

Bitte akzeptieren Sie Marketing-Cookies.

+
+`; + +/** + * RxJS Observable Wrapper fuer ConsentService + * + * Fuer Projekte die RxJS bevorzugen: + * + * @example + * ```typescript + * import { BehaviorSubject, Observable } from 'rxjs'; + * + * export class ConsentServiceRx extends ConsentServiceBase { + * private consentSubject = new BehaviorSubject(null); + * private bannerVisibleSubject = new BehaviorSubject(false); + * + * consent$ = this.consentSubject.asObservable(); + * isBannerVisible$ = this.bannerVisibleSubject.asObservable(); + * + * constructor(config: ConsentConfig) { + * super(config); + * this.onConsentChange((c) => this.consentSubject.next(c)); + * this.onBannerShow(() => this.bannerVisibleSubject.next(true)); + * this.onBannerHide(() => this.bannerVisibleSubject.next(false)); + * } + * } + * ``` + */ diff --git a/consent-sdk/src/core/ConsentManager.ts b/consent-sdk/src/core/ConsentManager.ts index be203d9..0a1e3ba 100644 --- a/consent-sdk/src/core/ConsentManager.ts +++ b/consent-sdk/src/core/ConsentManager.ts @@ -1,14 +1,9 @@ -/** - * ConsentManager - Hauptklasse fuer das Consent Management - * - * DSGVO/TTDSG-konformes Consent Management fuer Web, PWA und Mobile. - */ +/** ConsentManager - DSGVO/TTDSG-konformes Consent Management fuer Web, PWA und Mobile. */ import type { ConsentConfig, ConsentState, ConsentCategory, - ConsentCategories, ConsentInput, ConsentEventType, ConsentEventCallback, @@ -20,49 +15,16 @@ import { ConsentAPI } from './ConsentAPI'; import { EventEmitter } from '../utils/EventEmitter'; import { generateFingerprint } from '../utils/fingerprint'; import { SDK_VERSION } from '../version'; +import { mergeConsentConfig } from './consent-manager-config'; +import { updateGoogleConsentMode as applyGoogleConsent } from './consent-manager-google'; +import { + normalizeConsentInput, + isConsentExpired, + needsConsent, + ALL_CATEGORIES, + MINIMAL_CATEGORIES, +} from './consent-manager-helpers'; -/** - * Default-Konfiguration - */ -const DEFAULT_CONFIG: Partial = { - language: 'de', - fallbackLanguage: 'en', - ui: { - position: 'bottom', - layout: 'modal', - theme: 'auto', - zIndex: 999999, - blockScrollOnModal: true, - }, - consent: { - required: true, - rejectAllVisible: true, - acceptAllVisible: true, - granularControl: true, - vendorControl: false, - rememberChoice: true, - rememberDays: 365, - geoTargeting: false, - recheckAfterDays: 180, - }, - categories: ['essential', 'functional', 'analytics', 'marketing', 'social'], - debug: false, -}; - -/** - * Default Consent-State (nur Essential aktiv) - */ -const DEFAULT_CONSENT: ConsentCategories = { - essential: true, - functional: false, - analytics: false, - marketing: false, - social: false, -}; - -/** - * ConsentManager - Zentrale Klasse fuer Consent-Verwaltung - */ export class ConsentManager { private config: ConsentConfig; private storage: ConsentStorage; @@ -75,7 +37,7 @@ export class ConsentManager { private deviceFingerprint: string = ''; constructor(config: ConsentConfig) { - this.config = this.mergeConfig(config); + this.config = mergeConsentConfig(config); this.storage = new ConsentStorage(this.config); this.scriptBlocker = new ScriptBlocker(this.config); this.api = new ConsentAPI(this.config); @@ -106,7 +68,7 @@ export class ConsentManager { this.log('Loaded consent from storage:', this.currentConsent); // Pruefen ob Consent abgelaufen - if (this.isConsentExpired()) { + if (isConsentExpired(this.currentConsent, this.config)) { this.log('Consent expired, clearing'); this.storage.clear(); this.currentConsent = null; @@ -123,7 +85,7 @@ export class ConsentManager { this.emit('init', this.currentConsent); // Banner anzeigen falls noetig - if (this.needsConsent()) { + if (needsConsent(this.currentConsent, this.config)) { this.showBanner(); } @@ -134,9 +96,7 @@ export class ConsentManager { } } - // =========================================================================== - // Public API - // =========================================================================== + // --- Public API --- /** * Pruefen ob Consent fuer Kategorie vorhanden @@ -169,7 +129,7 @@ export class ConsentManager { * Consent setzen */ async setConsent(input: ConsentInput): Promise { - const categories = this.normalizeConsentInput(input); + const categories = normalizeConsentInput(input); // Essential ist immer aktiv categories.essential = true; @@ -218,15 +178,7 @@ export class ConsentManager { * Alle Kategorien akzeptieren */ async acceptAll(): Promise { - const allCategories: ConsentCategories = { - essential: true, - functional: true, - analytics: true, - marketing: true, - social: true, - }; - - await this.setConsent(allCategories); + await this.setConsent(ALL_CATEGORIES); this.emit('accept_all', this.currentConsent!); this.hideBanner(); } @@ -235,15 +187,7 @@ export class ConsentManager { * Alle nicht-essentiellen Kategorien ablehnen */ async rejectAll(): Promise { - const minimalCategories: ConsentCategories = { - essential: true, - functional: false, - analytics: false, - marketing: false, - social: false, - }; - - await this.setConsent(minimalCategories); + await this.setConsent(MINIMAL_CATEGORIES); this.emit('reject_all', this.currentConsent!); this.hideBanner(); } @@ -281,52 +225,23 @@ export class ConsentManager { return JSON.stringify(exportData, null, 2); } - // =========================================================================== - // Banner Control - // =========================================================================== + // --- Banner Control --- /** * Pruefen ob Consent-Abfrage noetig */ needsConsent(): boolean { - if (!this.currentConsent) { - return true; - } - - if (this.isConsentExpired()) { - return true; - } - - // Recheck nach X Tagen - if (this.config.consent?.recheckAfterDays) { - const consentDate = new Date(this.currentConsent.timestamp); - const recheckDate = new Date(consentDate); - recheckDate.setDate( - recheckDate.getDate() + this.config.consent.recheckAfterDays - ); - - if (new Date() > recheckDate) { - return true; - } - } - - return false; + return needsConsent(this.currentConsent, this.config); } /** * Banner anzeigen */ showBanner(): void { - if (this.bannerVisible) { - return; - } - + if (this.bannerVisible) return; this.bannerVisible = true; this.emit('banner_show', undefined); this.config.onBannerShow?.(); - - // Banner wird von UI-Komponente gerendert - // Hier nur Status setzen this.log('Banner shown'); } @@ -334,14 +249,10 @@ export class ConsentManager { * Banner verstecken */ hideBanner(): void { - if (!this.bannerVisible) { - return; - } - + if (!this.bannerVisible) return; this.bannerVisible = false; this.emit('banner_hide', undefined); this.config.onBannerHide?.(); - this.log('Banner hidden'); } @@ -360,9 +271,7 @@ export class ConsentManager { return this.bannerVisible; } - // =========================================================================== - // Event Handling - // =========================================================================== + // --- Event Handling --- /** * Event-Listener registrieren @@ -384,40 +293,13 @@ export class ConsentManager { this.events.off(event, callback); } - // =========================================================================== - // Internal Methods - // =========================================================================== - - /** - * Konfiguration zusammenfuehren - */ - private mergeConfig(config: ConsentConfig): ConsentConfig { - return { - ...DEFAULT_CONFIG, - ...config, - ui: { ...DEFAULT_CONFIG.ui, ...config.ui }, - consent: { ...DEFAULT_CONFIG.consent, ...config.consent }, - } as ConsentConfig; - } - - /** - * Consent-Input normalisieren - */ - private normalizeConsentInput(input: ConsentInput): ConsentCategories { - if ('categories' in input && input.categories) { - return { ...DEFAULT_CONSENT, ...input.categories }; - } - - return { ...DEFAULT_CONSENT, ...(input as Partial) }; - } + // --- Internal Methods --- /** * Consent anwenden (Skripte aktivieren/blockieren) */ private applyConsent(): void { - if (!this.currentConsent) { - return; - } + if (!this.currentConsent) return; for (const [category, allowed] of Object.entries( this.currentConsent.categories @@ -430,88 +312,26 @@ export class ConsentManager { } // Google Consent Mode aktualisieren - this.updateGoogleConsentMode(); + if (applyGoogleConsent(this.currentConsent)) { + this.log('Google Consent Mode updated'); + } } - /** - * Google Consent Mode v2 aktualisieren - */ - private updateGoogleConsentMode(): void { - if (typeof window === 'undefined' || !this.currentConsent) { - return; - } - - const gtag = (window as unknown as { gtag?: (...args: unknown[]) => void }).gtag; - if (typeof gtag !== 'function') { - return; - } - - const { categories } = this.currentConsent; - - gtag('consent', 'update', { - ad_storage: categories.marketing ? 'granted' : 'denied', - ad_user_data: categories.marketing ? 'granted' : 'denied', - ad_personalization: categories.marketing ? 'granted' : 'denied', - analytics_storage: categories.analytics ? 'granted' : 'denied', - functionality_storage: categories.functional ? 'granted' : 'denied', - personalization_storage: categories.functional ? 'granted' : 'denied', - security_storage: 'granted', - }); - - this.log('Google Consent Mode updated'); - } - - /** - * Pruefen ob Consent abgelaufen - */ - private isConsentExpired(): boolean { - if (!this.currentConsent?.expiresAt) { - // Fallback: Nach rememberDays ablaufen - if (this.currentConsent?.timestamp && this.config.consent?.rememberDays) { - const consentDate = new Date(this.currentConsent.timestamp); - const expiryDate = new Date(consentDate); - expiryDate.setDate( - expiryDate.getDate() + this.config.consent.rememberDays - ); - return new Date() > expiryDate; - } - return false; - } - - return new Date() > new Date(this.currentConsent.expiresAt); - } - - /** - * Event emittieren - */ - private emit( - event: T, - data: ConsentEventData[T] - ): void { + private emit(event: T, data: ConsentEventData[T]): void { this.events.emit(event, data); } - /** - * Fehler behandeln - */ private handleError(error: Error): void { this.log('Error:', error); this.emit('error', error); this.config.onError?.(error); } - /** - * Debug-Logging - */ private log(...args: unknown[]): void { - if (this.config.debug) { - console.log('[ConsentSDK]', ...args); - } + if (this.config.debug) console.log('[ConsentSDK]', ...args); } - // =========================================================================== - // Static Methods - // =========================================================================== + // --- Static Methods --- /** * SDK-Version abrufen diff --git a/consent-sdk/src/core/consent-manager-config.ts b/consent-sdk/src/core/consent-manager-config.ts new file mode 100644 index 0000000..f976eab --- /dev/null +++ b/consent-sdk/src/core/consent-manager-config.ts @@ -0,0 +1,58 @@ +/** + * ConsentManager default configuration + merge helpers. + * + * Phase 4: extracted from ConsentManager.ts to keep the main class under 500 LOC. + */ + +import type { ConsentCategories, ConsentConfig } from '../types'; + +/** + * Default configuration applied when a consumer omits optional fields. + */ +export const DEFAULT_CONFIG: Partial = { + language: 'de', + fallbackLanguage: 'en', + ui: { + position: 'bottom', + layout: 'modal', + theme: 'auto', + zIndex: 999999, + blockScrollOnModal: true, + }, + consent: { + required: true, + rejectAllVisible: true, + acceptAllVisible: true, + granularControl: true, + vendorControl: false, + rememberChoice: true, + rememberDays: 365, + geoTargeting: false, + recheckAfterDays: 180, + }, + categories: ['essential', 'functional', 'analytics', 'marketing', 'social'], + debug: false, +}; + +/** + * Default consent state — only essential category is active. + */ +export const DEFAULT_CONSENT: ConsentCategories = { + essential: true, + functional: false, + analytics: false, + marketing: false, + social: false, +}; + +/** + * Merge a user-supplied config onto DEFAULT_CONFIG, preserving nested objects. + */ +export function mergeConsentConfig(config: ConsentConfig): ConsentConfig { + return { + ...DEFAULT_CONFIG, + ...config, + ui: { ...DEFAULT_CONFIG.ui, ...config.ui }, + consent: { ...DEFAULT_CONFIG.consent, ...config.consent }, + } as ConsentConfig; +} diff --git a/consent-sdk/src/core/consent-manager-google.ts b/consent-sdk/src/core/consent-manager-google.ts new file mode 100644 index 0000000..6ebe50e --- /dev/null +++ b/consent-sdk/src/core/consent-manager-google.ts @@ -0,0 +1,38 @@ +/** + * Google Consent Mode v2 integration helper. + * + * Phase 4: extracted from ConsentManager.ts. Updates gtag() with the + * current consent category state whenever consent changes. + */ + +import type { ConsentState } from '../types'; + +/** + * Update Google Consent Mode v2 based on the current consent categories. + * No-op when running outside the browser or when gtag is not loaded. + * Returns true if the gtag update was actually applied. + */ +export function updateGoogleConsentMode(consent: ConsentState | null): boolean { + if (typeof window === 'undefined' || !consent) { + return false; + } + + const gtag = (window as unknown as { gtag?: (...args: unknown[]) => void }).gtag; + if (typeof gtag !== 'function') { + return false; + } + + const { categories } = consent; + + gtag('consent', 'update', { + ad_storage: categories.marketing ? 'granted' : 'denied', + ad_user_data: categories.marketing ? 'granted' : 'denied', + ad_personalization: categories.marketing ? 'granted' : 'denied', + analytics_storage: categories.analytics ? 'granted' : 'denied', + functionality_storage: categories.functional ? 'granted' : 'denied', + personalization_storage: categories.functional ? 'granted' : 'denied', + security_storage: 'granted', + }); + + return true; +} diff --git a/consent-sdk/src/core/consent-manager-helpers.ts b/consent-sdk/src/core/consent-manager-helpers.ts new file mode 100644 index 0000000..3e262b5 --- /dev/null +++ b/consent-sdk/src/core/consent-manager-helpers.ts @@ -0,0 +1,88 @@ +/** + * consent-manager-helpers.ts + * + * Pure helper functions used by ConsentManager that have no dependency on + * class instance state. Extracted to keep ConsentManager.ts under the + * 350 LOC soft target. + */ + +import type { + ConsentState, + ConsentCategories, + ConsentInput, + ConsentConfig, +} from '../types'; +import { DEFAULT_CONSENT } from './consent-manager-config'; + +/** All categories accepted (used by acceptAll). */ +export const ALL_CATEGORIES: ConsentCategories = { + essential: true, + functional: true, + analytics: true, + marketing: true, + social: true, +}; + +/** Only essential consent (used by rejectAll). */ +export const MINIMAL_CATEGORIES: ConsentCategories = { + essential: true, + functional: false, + analytics: false, + marketing: false, + social: false, +}; + +/** + * Normalise a ConsentInput into a full ConsentCategories map. + * Always returns a shallow copy — never mutates the input. + */ +export function normalizeConsentInput(input: ConsentInput): ConsentCategories { + if ('categories' in input && input.categories) { + return { ...DEFAULT_CONSENT, ...input.categories }; + } + return { ...DEFAULT_CONSENT, ...(input as Partial) }; +} + +/** + * Return true when the stored consent record has passed its expiry date. + * Falls back to `rememberDays` from config when `expiresAt` is absent. + */ +export function isConsentExpired( + consent: ConsentState | null, + config: ConsentConfig +): boolean { + if (!consent) return false; + + if (!consent.expiresAt) { + if (consent.timestamp && config.consent?.rememberDays) { + const consentDate = new Date(consent.timestamp); + const expiryDate = new Date(consentDate); + expiryDate.setDate(expiryDate.getDate() + config.consent.rememberDays); + return new Date() > expiryDate; + } + return false; + } + + return new Date() > new Date(consent.expiresAt); +} + +/** + * Return true when the user needs to be shown the consent banner. + */ +export function needsConsent( + consent: ConsentState | null, + config: ConsentConfig +): boolean { + if (!consent) return true; + + if (isConsentExpired(consent, config)) return true; + + if (config.consent?.recheckAfterDays) { + const consentDate = new Date(consent.timestamp); + const recheckDate = new Date(consentDate); + recheckDate.setDate(recheckDate.getDate() + config.consent.recheckAfterDays); + if (new Date() > recheckDate) return true; + } + + return false; +} diff --git a/consent-sdk/src/react/components.tsx b/consent-sdk/src/react/components.tsx new file mode 100644 index 0000000..d0fb714 --- /dev/null +++ b/consent-sdk/src/react/components.tsx @@ -0,0 +1,190 @@ +/** + * React UI components for the consent SDK. + * + * Phase 4: extracted from index.tsx to keep the main file under 500 LOC. + * Exports ConsentGate, ConsentPlaceholder, and ConsentBanner (all headless). + */ + +import type { FC, ReactNode } from 'react'; +import type { + ConsentCategories, + ConsentCategory, + ConsentState, +} from '../types'; +import { useConsent } from './hooks'; + +// ============================================================================= +// ConsentGate +// ============================================================================= + +interface ConsentGateProps { + /** Erforderliche Kategorie */ + category: ConsentCategory; + /** Inhalt bei Consent */ + children: ReactNode; + /** Inhalt ohne Consent */ + placeholder?: ReactNode; + /** Fallback waehrend Laden */ + fallback?: ReactNode; +} + +/** + * ConsentGate - zeigt Inhalt nur bei Consent. + */ +export const ConsentGate: FC = ({ + category, + children, + placeholder = null, + fallback = null, +}) => { + const { hasConsent, isLoading } = useConsent(); + + if (isLoading) { + return <>{fallback}; + } + + if (!hasConsent(category)) { + return <>{placeholder}; + } + + return <>{children}; +}; + +// ============================================================================= +// ConsentPlaceholder +// ============================================================================= + +interface ConsentPlaceholderProps { + category: ConsentCategory; + message?: string; + buttonText?: string; + className?: string; +} + +/** + * ConsentPlaceholder - Placeholder fuer blockierten Inhalt. + */ +export const ConsentPlaceholder: FC = ({ + category, + message, + buttonText, + className = '', +}) => { + const { showSettings } = useConsent(); + + const categoryNames: Record = { + essential: 'Essentielle Cookies', + functional: 'Funktionale Cookies', + analytics: 'Statistik-Cookies', + marketing: 'Marketing-Cookies', + social: 'Social Media-Cookies', + }; + + const defaultMessage = `Dieser Inhalt erfordert ${categoryNames[category]}.`; + + return ( +
+

{message || defaultMessage}

+ +
+ ); +}; + +// ============================================================================= +// ConsentBanner (headless) +// ============================================================================= + +export interface ConsentBannerRenderProps { + isVisible: boolean; + consent: ConsentState | null; + needsConsent: boolean; + onAcceptAll: () => void; + onRejectAll: () => void; + onSaveSelection: (categories: Partial) => void; + onShowSettings: () => void; + onClose: () => void; +} + +interface ConsentBannerProps { + render?: (props: ConsentBannerRenderProps) => ReactNode; + className?: string; +} + +/** + * ConsentBanner - Headless Banner-Komponente. + * Kann mit eigener UI gerendert werden oder nutzt Default-UI. + */ +export const ConsentBanner: FC = ({ render, className }) => { + const { + consent, + isBannerVisible, + needsConsent, + acceptAll, + rejectAll, + saveSelection, + showSettings, + hideBanner, + } = useConsent(); + + const renderProps: ConsentBannerRenderProps = { + isVisible: isBannerVisible, + consent, + needsConsent, + onAcceptAll: acceptAll, + onRejectAll: rejectAll, + onSaveSelection: saveSelection, + onShowSettings: showSettings, + onClose: hideBanner, + }; + + if (render) { + return <>{render(renderProps)}; + } + + if (!isBannerVisible) { + return null; + } + + return ( +
+
+

Datenschutzeinstellungen

+

+ Wir nutzen Cookies und aehnliche Technologien, um Ihnen ein optimales + Nutzererlebnis zu bieten. +

+ +
+ + + +
+
+
+ ); +}; diff --git a/consent-sdk/src/react/context.ts b/consent-sdk/src/react/context.ts new file mode 100644 index 0000000..3251abf --- /dev/null +++ b/consent-sdk/src/react/context.ts @@ -0,0 +1,44 @@ +/** + * Consent context definition — shared by the provider and hooks. + * + * Phase 4: extracted from index.tsx. + */ + +import { createContext } from 'react'; +import type { ConsentManager } from '../core/ConsentManager'; +import type { + ConsentCategories, + ConsentCategory, + ConsentState, +} from '../types'; + +export interface ConsentContextValue { + /** ConsentManager Instanz */ + manager: ConsentManager | null; + /** Aktueller Consent-State */ + consent: ConsentState | null; + /** Ist SDK initialisiert? */ + isInitialized: boolean; + /** Wird geladen? */ + isLoading: boolean; + /** Ist Banner sichtbar? */ + isBannerVisible: boolean; + /** Wird Consent benoetigt? */ + needsConsent: boolean; + /** Consent fuer Kategorie pruefen */ + hasConsent: (category: ConsentCategory) => boolean; + /** Alle akzeptieren */ + acceptAll: () => Promise; + /** Alle ablehnen */ + rejectAll: () => Promise; + /** Auswahl speichern */ + saveSelection: (categories: Partial) => Promise; + /** Banner anzeigen */ + showBanner: () => void; + /** Banner verstecken */ + hideBanner: () => void; + /** Einstellungen oeffnen */ + showSettings: () => void; +} + +export const ConsentContext = createContext(null); diff --git a/consent-sdk/src/react/hooks.ts b/consent-sdk/src/react/hooks.ts new file mode 100644 index 0000000..27ee2de --- /dev/null +++ b/consent-sdk/src/react/hooks.ts @@ -0,0 +1,43 @@ +/** + * React hooks for the consent SDK. + * + * Phase 4: extracted from index.tsx to keep the main file under 500 LOC. + */ + +import { useContext } from 'react'; +import type { ConsentCategory } from '../types'; +import type { ConsentManager } from '../core/ConsentManager'; +import { ConsentContext, type ConsentContextValue } from './context'; + +/** + * useConsent - Consent-Hook. + * Overloads: call without args for the full context; pass a category to also get `allowed`. + */ +export function useConsent(): ConsentContextValue; +export function useConsent( + category: ConsentCategory +): ConsentContextValue & { allowed: boolean }; +export function useConsent(category?: ConsentCategory) { + const context = useContext(ConsentContext); + + if (!context) { + throw new Error('useConsent must be used within a ConsentProvider'); + } + + if (category) { + return { + ...context, + allowed: context.hasConsent(category), + }; + } + + return context; +} + +/** + * useConsentManager - Direkter Zugriff auf ConsentManager. + */ +export function useConsentManager(): ConsentManager | null { + const context = useContext(ConsentContext); + return context?.manager ?? null; +} diff --git a/consent-sdk/src/react/index.tsx b/consent-sdk/src/react/index.tsx index abaf0bb..cb4a728 100644 --- a/consent-sdk/src/react/index.tsx +++ b/consent-sdk/src/react/index.tsx @@ -14,72 +14,35 @@ * ); * } * ``` + * + * Phase 4 refactor: provider stays here; hooks + components live in sibling + * files. Context definition is in ./context so hooks and provider can share it + * without circular imports. */ import { - createContext, - useContext, useEffect, useState, useCallback, useMemo, - type ReactNode, type FC, + type ReactNode, } from 'react'; import { ConsentManager } from '../core/ConsentManager'; import type { + ConsentCategories, + ConsentCategory, ConsentConfig, ConsentState, - ConsentCategory, - ConsentCategories, } from '../types'; - -// ============================================================================= -// Context -// ============================================================================= - -interface ConsentContextValue { - /** ConsentManager Instanz */ - manager: ConsentManager | null; - - /** Aktueller Consent-State */ - consent: ConsentState | null; - - /** Ist SDK initialisiert? */ - isInitialized: boolean; - - /** Wird geladen? */ - isLoading: boolean; - - /** Ist Banner sichtbar? */ - isBannerVisible: boolean; - - /** Wird Consent benoetigt? */ - needsConsent: boolean; - - /** Consent fuer Kategorie pruefen */ - hasConsent: (category: ConsentCategory) => boolean; - - /** Alle akzeptieren */ - acceptAll: () => Promise; - - /** Alle ablehnen */ - rejectAll: () => Promise; - - /** Auswahl speichern */ - saveSelection: (categories: Partial) => Promise; - - /** Banner anzeigen */ - showBanner: () => void; - - /** Banner verstecken */ - hideBanner: () => void; - - /** Einstellungen oeffnen */ - showSettings: () => void; -} - -const ConsentContext = createContext(null); +import { ConsentContext, type ConsentContextValue } from './context'; +import { useConsent, useConsentManager } from './hooks'; +import { + ConsentBanner, + ConsentGate, + ConsentPlaceholder, + type ConsentBannerRenderProps, +} from './components'; // ============================================================================= // Provider @@ -88,13 +51,12 @@ const ConsentContext = createContext(null); interface ConsentProviderProps { /** SDK-Konfiguration */ config: ConsentConfig; - /** Kinder-Komponenten */ children: ReactNode; } /** - * ConsentProvider - Stellt Consent-Kontext bereit + * ConsentProvider - Stellt Consent-Kontext bereit. */ export const ConsentProvider: FC = ({ config, @@ -228,284 +190,10 @@ export const ConsentProvider: FC = ({ }; // ============================================================================= -// Hooks -// ============================================================================= - -/** - * useConsent - Hook fuer Consent-Zugriff - * - * @example - * ```tsx - * const { hasConsent, acceptAll, rejectAll } = useConsent(); - * - * if (hasConsent('analytics')) { - * // Analytics laden - * } - * ``` - */ -export function useConsent(): ConsentContextValue; -export function useConsent( - category: ConsentCategory -): ConsentContextValue & { allowed: boolean }; -export function useConsent(category?: ConsentCategory) { - const context = useContext(ConsentContext); - - if (!context) { - throw new Error('useConsent must be used within a ConsentProvider'); - } - - if (category) { - return { - ...context, - allowed: context.hasConsent(category), - }; - } - - return context; -} - -/** - * useConsentManager - Direkter Zugriff auf ConsentManager - */ -export function useConsentManager(): ConsentManager | null { - const context = useContext(ConsentContext); - return context?.manager ?? null; -} - -// ============================================================================= -// Components -// ============================================================================= - -interface ConsentGateProps { - /** Erforderliche Kategorie */ - category: ConsentCategory; - - /** Inhalt bei Consent */ - children: ReactNode; - - /** Inhalt ohne Consent */ - placeholder?: ReactNode; - - /** Fallback waehrend Laden */ - fallback?: ReactNode; -} - -/** - * ConsentGate - Zeigt Inhalt nur bei Consent - * - * @example - * ```tsx - * } - * > - * - * - * ``` - */ -export const ConsentGate: FC = ({ - category, - children, - placeholder = null, - fallback = null, -}) => { - const { hasConsent, isLoading } = useConsent(); - - if (isLoading) { - return <>{fallback}; - } - - if (!hasConsent(category)) { - return <>{placeholder}; - } - - return <>{children}; -}; - -interface ConsentPlaceholderProps { - /** Kategorie */ - category: ConsentCategory; - - /** Custom Nachricht */ - message?: string; - - /** Custom Button-Text */ - buttonText?: string; - - /** Custom Styling */ - className?: string; -} - -/** - * ConsentPlaceholder - Placeholder fuer blockierten Inhalt - */ -export const ConsentPlaceholder: FC = ({ - category, - message, - buttonText, - className = '', -}) => { - const { showSettings } = useConsent(); - - const categoryNames: Record = { - essential: 'Essentielle Cookies', - functional: 'Funktionale Cookies', - analytics: 'Statistik-Cookies', - marketing: 'Marketing-Cookies', - social: 'Social Media-Cookies', - }; - - const defaultMessage = `Dieser Inhalt erfordert ${categoryNames[category]}.`; - - return ( -
-

{message || defaultMessage}

- -
- ); -}; - -// ============================================================================= -// Banner Component (Headless) -// ============================================================================= - -interface ConsentBannerRenderProps { - /** Ist Banner sichtbar? */ - isVisible: boolean; - - /** Aktueller Consent */ - consent: ConsentState | null; - - /** Wird Consent benoetigt? */ - needsConsent: boolean; - - /** Alle akzeptieren */ - onAcceptAll: () => void; - - /** Alle ablehnen */ - onRejectAll: () => void; - - /** Auswahl speichern */ - onSaveSelection: (categories: Partial) => void; - - /** Einstellungen oeffnen */ - onShowSettings: () => void; - - /** Banner schliessen */ - onClose: () => void; -} - -interface ConsentBannerProps { - /** Render-Funktion fuer Custom UI */ - render?: (props: ConsentBannerRenderProps) => ReactNode; - - /** Custom Styling */ - className?: string; -} - -/** - * ConsentBanner - Headless Banner-Komponente - * - * Kann mit eigener UI gerendert werden oder nutzt Default-UI. - * - * @example - * ```tsx - * // Mit eigener UI - * ( - * isVisible && ( - *
- * - * - *
- * ) - * )} - * /> - * - * // Mit Default-UI - * - * ``` - */ -export const ConsentBanner: FC = ({ render, className }) => { - const { - consent, - isBannerVisible, - needsConsent, - acceptAll, - rejectAll, - saveSelection, - showSettings, - hideBanner, - } = useConsent(); - - const renderProps: ConsentBannerRenderProps = { - isVisible: isBannerVisible, - consent, - needsConsent, - onAcceptAll: acceptAll, - onRejectAll: rejectAll, - onSaveSelection: saveSelection, - onShowSettings: showSettings, - onClose: hideBanner, - }; - - // Custom Render - if (render) { - return <>{render(renderProps)}; - } - - // Default UI - if (!isBannerVisible) { - return null; - } - - return ( -
-
-

Datenschutzeinstellungen

-

- Wir nutzen Cookies und aehnliche Technologien, um Ihnen ein optimales - Nutzererlebnis zu bieten. -

- -
- - - -
-
-
- ); -}; - -// ============================================================================= -// Exports +// Re-exports for the public @breakpilot/consent-sdk/react entrypoint // ============================================================================= +export { useConsent, useConsentManager }; +export { ConsentBanner, ConsentGate, ConsentPlaceholder }; export { ConsentContext }; export type { ConsentContextValue, ConsentBannerRenderProps }; diff --git a/consent-sdk/src/types/api.ts b/consent-sdk/src/types/api.ts new file mode 100644 index 0000000..9432b1d --- /dev/null +++ b/consent-sdk/src/types/api.ts @@ -0,0 +1,56 @@ +/** + * Consent SDK Types — API request/response shapes + */ + +import type { ConsentCategory, ConsentState } from './core'; +import type { ConsentUIConfig, TCFConfig } from './config'; +import type { ConsentVendor } from './vendor'; + +// ============================================================================= +// API Types +// ============================================================================= + +/** + * API-Antwort fuer Consent-Erstellung + */ +export interface ConsentAPIResponse { + consentId: string; + timestamp: string; + expiresAt: string; + version: string; +} + +/** + * API-Antwort fuer Site-Konfiguration + */ +export interface SiteConfigResponse { + siteId: string; + siteName: string; + categories: CategoryConfig[]; + ui: ConsentUIConfig; + legal: LegalConfig; + tcf?: TCFConfig; +} + +/** + * Kategorie-Konfiguration vom Server + */ +export interface CategoryConfig { + id: ConsentCategory; + name: Record; + description: Record; + required: boolean; + vendors: ConsentVendor[]; +} + +/** + * Rechtliche Konfiguration + */ +export interface LegalConfig { + privacyPolicyUrl: string; + imprintUrl: string; + dpo?: { + name: string; + email: string; + }; +} diff --git a/consent-sdk/src/types/config.ts b/consent-sdk/src/types/config.ts new file mode 100644 index 0000000..d14a223 --- /dev/null +++ b/consent-sdk/src/types/config.ts @@ -0,0 +1,166 @@ +/** + * Consent SDK Types — Configuration + */ + +import type { ConsentCategory, ConsentState } from './core'; + +// ============================================================================= +// Configuration +// ============================================================================= + +/** + * UI-Position des Banners + */ +export type BannerPosition = 'bottom' | 'top' | 'center'; + +/** + * Banner-Layout + */ +export type BannerLayout = 'bar' | 'modal' | 'floating'; + +/** + * Farbschema + */ +export type BannerTheme = 'light' | 'dark' | 'auto'; + +/** + * UI-Konfiguration + */ +export interface ConsentUIConfig { + /** Position des Banners */ + position?: BannerPosition; + + /** Layout-Typ */ + layout?: BannerLayout; + + /** Farbschema */ + theme?: BannerTheme; + + /** Pfad zu Custom CSS */ + customCss?: string; + + /** z-index fuer Banner */ + zIndex?: number; + + /** Scroll blockieren bei Modal */ + blockScrollOnModal?: boolean; + + /** Custom Container-ID */ + containerId?: string; +} + +/** + * Consent-Verhaltens-Konfiguration + */ +export interface ConsentBehaviorConfig { + /** Muss Nutzer interagieren? */ + required?: boolean; + + /** "Alle ablehnen" Button sichtbar */ + rejectAllVisible?: boolean; + + /** "Alle akzeptieren" Button sichtbar */ + acceptAllVisible?: boolean; + + /** Einzelne Kategorien waehlbar */ + granularControl?: boolean; + + /** Einzelne Vendors waehlbar */ + vendorControl?: boolean; + + /** Auswahl speichern */ + rememberChoice?: boolean; + + /** Speicherdauer in Tagen */ + rememberDays?: number; + + /** Nur in EU anzeigen (Geo-Targeting) */ + geoTargeting?: boolean; + + /** Erneut nachfragen nach X Tagen */ + recheckAfterDays?: number; +} + +/** + * TCF 2.2 Konfiguration + */ +export interface TCFConfig { + /** TCF aktivieren */ + enabled?: boolean; + + /** CMP ID */ + cmpId?: number; + + /** CMP Version */ + cmpVersion?: number; +} + +/** + * PWA-spezifische Konfiguration + */ +export interface PWAConfig { + /** Offline-Unterstuetzung aktivieren */ + offlineSupport?: boolean; + + /** Bei Reconnect synchronisieren */ + syncOnReconnect?: boolean; + + /** Cache-Strategie */ + cacheStrategy?: 'stale-while-revalidate' | 'network-first' | 'cache-first'; +} + +/** + * Haupt-Konfiguration fuer ConsentManager + */ +export interface ConsentConfig { + // Pflichtfelder + /** API-Endpunkt fuer Consent-Backend */ + apiEndpoint: string; + + /** Site-ID */ + siteId: string; + + // Sprache + /** Sprache (ISO 639-1) */ + language?: string; + + /** Fallback-Sprache */ + fallbackLanguage?: string; + + // UI + /** UI-Konfiguration */ + ui?: ConsentUIConfig; + + // Verhalten + /** Consent-Verhaltens-Konfiguration */ + consent?: ConsentBehaviorConfig; + + // Kategorien + /** Aktive Kategorien */ + categories?: ConsentCategory[]; + + // TCF + /** TCF 2.2 Konfiguration */ + tcf?: TCFConfig; + + // PWA + /** PWA-Konfiguration */ + pwa?: PWAConfig; + + // Callbacks + /** Callback bei Consent-Aenderung */ + onConsentChange?: (consent: ConsentState) => void; + + /** Callback wenn Banner angezeigt wird */ + onBannerShow?: () => void; + + /** Callback wenn Banner geschlossen wird */ + onBannerHide?: () => void; + + /** Callback bei Fehler */ + onError?: (error: Error) => void; + + // Debug + /** Debug-Modus aktivieren */ + debug?: boolean; +} diff --git a/consent-sdk/src/types/core.ts b/consent-sdk/src/types/core.ts new file mode 100644 index 0000000..bea5adc --- /dev/null +++ b/consent-sdk/src/types/core.ts @@ -0,0 +1,65 @@ +/** + * Consent SDK Types — Core: categories, state, input + */ + +// ============================================================================= +// Consent Categories +// ============================================================================= + +/** + * Standard-Consent-Kategorien nach IAB TCF 2.2 + */ +export type ConsentCategory = + | 'essential' // Technisch notwendig (TTDSG § 25 Abs. 2) + | 'functional' // Personalisierung, Komfortfunktionen + | 'analytics' // Anonyme Nutzungsanalyse + | 'marketing' // Werbung, Retargeting + | 'social'; // Social Media Plugins + +/** + * Consent-Status pro Kategorie + */ +export type ConsentCategories = Record; + +/** + * Consent-Status pro Vendor + */ +export type ConsentVendors = Record; + +// ============================================================================= +// Consent State +// ============================================================================= + +/** + * Aktueller Consent-Zustand + */ +export interface ConsentState { + /** Consent pro Kategorie */ + categories: ConsentCategories; + + /** Consent pro Vendor (optional, für granulare Kontrolle) */ + vendors: ConsentVendors; + + /** Zeitstempel der letzten Aenderung */ + timestamp: string; + + /** SDK-Version bei Erstellung */ + version: string; + + /** Eindeutige Consent-ID vom Backend */ + consentId?: string; + + /** Ablaufdatum */ + expiresAt?: string; + + /** IAB TCF String (falls aktiviert) */ + tcfString?: string; +} + +/** + * Minimaler Consent-Input fuer setConsent() + */ +export type ConsentInput = Partial | { + categories?: Partial; + vendors?: ConsentVendors; +}; diff --git a/consent-sdk/src/types/events.ts b/consent-sdk/src/types/events.ts new file mode 100644 index 0000000..08fad42 --- /dev/null +++ b/consent-sdk/src/types/events.ts @@ -0,0 +1,49 @@ +/** + * Consent SDK Types — Event system + */ + +import type { ConsentState } from './core'; + +// ============================================================================= +// Events +// ============================================================================= + +/** + * Event-Typen + */ +export type ConsentEventType = + | 'init' + | 'change' + | 'accept_all' + | 'reject_all' + | 'save_selection' + | 'banner_show' + | 'banner_hide' + | 'settings_open' + | 'settings_close' + | 'vendor_enable' + | 'vendor_disable' + | 'error'; + +/** + * Event-Listener Callback + */ +export type ConsentEventCallback = (data: T) => void; + +/** + * Event-Daten fuer verschiedene Events + */ +export type ConsentEventData = { + init: ConsentState | null; + change: ConsentState; + accept_all: ConsentState; + reject_all: ConsentState; + save_selection: ConsentState; + banner_show: undefined; + banner_hide: undefined; + settings_open: undefined; + settings_close: undefined; + vendor_enable: string; + vendor_disable: string; + error: Error; +}; diff --git a/consent-sdk/src/types/index.ts b/consent-sdk/src/types/index.ts index f017c07..f7e2caa 100644 --- a/consent-sdk/src/types/index.ts +++ b/consent-sdk/src/types/index.ts @@ -1,438 +1,21 @@ /** - * Consent SDK Types + * Consent SDK Types — barrel re-export * - * DSGVO/TTDSG-konforme Typdefinitionen für das Consent Management System. + * Domain files: + * core.ts — ConsentCategory, ConsentCategories, ConsentVendors, ConsentState, ConsentInput + * config.ts — BannerPosition, BannerLayout, BannerTheme, ConsentUIConfig, + * ConsentBehaviorConfig, TCFConfig, PWAConfig, ConsentConfig + * vendor.ts — CookieInfo, ConsentVendor + * api.ts — ConsentAPIResponse, SiteConfigResponse, CategoryConfig, LegalConfig + * events.ts — ConsentEventType, ConsentEventCallback, ConsentEventData + * storage.ts — ConsentStorageAdapter + * translations.ts — ConsentTranslations, SupportedLanguage */ -// ============================================================================= -// Consent Categories -// ============================================================================= - -/** - * Standard-Consent-Kategorien nach IAB TCF 2.2 - */ -export type ConsentCategory = - | 'essential' // Technisch notwendig (TTDSG § 25 Abs. 2) - | 'functional' // Personalisierung, Komfortfunktionen - | 'analytics' // Anonyme Nutzungsanalyse - | 'marketing' // Werbung, Retargeting - | 'social'; // Social Media Plugins - -/** - * Consent-Status pro Kategorie - */ -export type ConsentCategories = Record; - -/** - * Consent-Status pro Vendor - */ -export type ConsentVendors = Record; - -// ============================================================================= -// Consent State -// ============================================================================= - -/** - * Aktueller Consent-Zustand - */ -export interface ConsentState { - /** Consent pro Kategorie */ - categories: ConsentCategories; - - /** Consent pro Vendor (optional, für granulare Kontrolle) */ - vendors: ConsentVendors; - - /** Zeitstempel der letzten Aenderung */ - timestamp: string; - - /** SDK-Version bei Erstellung */ - version: string; - - /** Eindeutige Consent-ID vom Backend */ - consentId?: string; - - /** Ablaufdatum */ - expiresAt?: string; - - /** IAB TCF String (falls aktiviert) */ - tcfString?: string; -} - -/** - * Minimaler Consent-Input fuer setConsent() - */ -export type ConsentInput = Partial | { - categories?: Partial; - vendors?: ConsentVendors; -}; - -// ============================================================================= -// Configuration -// ============================================================================= - -/** - * UI-Position des Banners - */ -export type BannerPosition = 'bottom' | 'top' | 'center'; - -/** - * Banner-Layout - */ -export type BannerLayout = 'bar' | 'modal' | 'floating'; - -/** - * Farbschema - */ -export type BannerTheme = 'light' | 'dark' | 'auto'; - -/** - * UI-Konfiguration - */ -export interface ConsentUIConfig { - /** Position des Banners */ - position?: BannerPosition; - - /** Layout-Typ */ - layout?: BannerLayout; - - /** Farbschema */ - theme?: BannerTheme; - - /** Pfad zu Custom CSS */ - customCss?: string; - - /** z-index fuer Banner */ - zIndex?: number; - - /** Scroll blockieren bei Modal */ - blockScrollOnModal?: boolean; - - /** Custom Container-ID */ - containerId?: string; -} - -/** - * Consent-Verhaltens-Konfiguration - */ -export interface ConsentBehaviorConfig { - /** Muss Nutzer interagieren? */ - required?: boolean; - - /** "Alle ablehnen" Button sichtbar */ - rejectAllVisible?: boolean; - - /** "Alle akzeptieren" Button sichtbar */ - acceptAllVisible?: boolean; - - /** Einzelne Kategorien waehlbar */ - granularControl?: boolean; - - /** Einzelne Vendors waehlbar */ - vendorControl?: boolean; - - /** Auswahl speichern */ - rememberChoice?: boolean; - - /** Speicherdauer in Tagen */ - rememberDays?: number; - - /** Nur in EU anzeigen (Geo-Targeting) */ - geoTargeting?: boolean; - - /** Erneut nachfragen nach X Tagen */ - recheckAfterDays?: number; -} - -/** - * TCF 2.2 Konfiguration - */ -export interface TCFConfig { - /** TCF aktivieren */ - enabled?: boolean; - - /** CMP ID */ - cmpId?: number; - - /** CMP Version */ - cmpVersion?: number; -} - -/** - * PWA-spezifische Konfiguration - */ -export interface PWAConfig { - /** Offline-Unterstuetzung aktivieren */ - offlineSupport?: boolean; - - /** Bei Reconnect synchronisieren */ - syncOnReconnect?: boolean; - - /** Cache-Strategie */ - cacheStrategy?: 'stale-while-revalidate' | 'network-first' | 'cache-first'; -} - -/** - * Haupt-Konfiguration fuer ConsentManager - */ -export interface ConsentConfig { - // Pflichtfelder - /** API-Endpunkt fuer Consent-Backend */ - apiEndpoint: string; - - /** Site-ID */ - siteId: string; - - // Sprache - /** Sprache (ISO 639-1) */ - language?: string; - - /** Fallback-Sprache */ - fallbackLanguage?: string; - - // UI - /** UI-Konfiguration */ - ui?: ConsentUIConfig; - - // Verhalten - /** Consent-Verhaltens-Konfiguration */ - consent?: ConsentBehaviorConfig; - - // Kategorien - /** Aktive Kategorien */ - categories?: ConsentCategory[]; - - // TCF - /** TCF 2.2 Konfiguration */ - tcf?: TCFConfig; - - // PWA - /** PWA-Konfiguration */ - pwa?: PWAConfig; - - // Callbacks - /** Callback bei Consent-Aenderung */ - onConsentChange?: (consent: ConsentState) => void; - - /** Callback wenn Banner angezeigt wird */ - onBannerShow?: () => void; - - /** Callback wenn Banner geschlossen wird */ - onBannerHide?: () => void; - - /** Callback bei Fehler */ - onError?: (error: Error) => void; - - // Debug - /** Debug-Modus aktivieren */ - debug?: boolean; -} - -// ============================================================================= -// Vendor Configuration -// ============================================================================= - -/** - * Cookie-Information - */ -export interface CookieInfo { - /** Cookie-Name */ - name: string; - - /** Cookie-Domain */ - domain: string; - - /** Ablaufzeit (z.B. "2 Jahre", "Session") */ - expiration: string; - - /** Speichertyp */ - type: 'http' | 'localStorage' | 'sessionStorage' | 'indexedDB'; - - /** Beschreibung */ - description: string; -} - -/** - * Vendor-Definition - */ -export interface ConsentVendor { - /** Eindeutige Vendor-ID */ - id: string; - - /** Anzeigename */ - name: string; - - /** Kategorie */ - category: ConsentCategory; - - /** IAB TCF Purposes (falls relevant) */ - purposes?: number[]; - - /** Legitimate Interests */ - legitimateInterests?: number[]; - - /** Cookie-Liste */ - cookies: CookieInfo[]; - - /** Link zur Datenschutzerklaerung */ - privacyPolicyUrl: string; - - /** Datenaufbewahrung */ - dataRetention?: string; - - /** Datentransfer (z.B. "USA (EU-US DPF)", "EU") */ - dataTransfer?: string; -} - -// ============================================================================= -// API Types -// ============================================================================= - -/** - * API-Antwort fuer Consent-Erstellung - */ -export interface ConsentAPIResponse { - consentId: string; - timestamp: string; - expiresAt: string; - version: string; -} - -/** - * API-Antwort fuer Site-Konfiguration - */ -export interface SiteConfigResponse { - siteId: string; - siteName: string; - categories: CategoryConfig[]; - ui: ConsentUIConfig; - legal: LegalConfig; - tcf?: TCFConfig; -} - -/** - * Kategorie-Konfiguration vom Server - */ -export interface CategoryConfig { - id: ConsentCategory; - name: Record; - description: Record; - required: boolean; - vendors: ConsentVendor[]; -} - -/** - * Rechtliche Konfiguration - */ -export interface LegalConfig { - privacyPolicyUrl: string; - imprintUrl: string; - dpo?: { - name: string; - email: string; - }; -} - -// ============================================================================= -// Events -// ============================================================================= - -/** - * Event-Typen - */ -export type ConsentEventType = - | 'init' - | 'change' - | 'accept_all' - | 'reject_all' - | 'save_selection' - | 'banner_show' - | 'banner_hide' - | 'settings_open' - | 'settings_close' - | 'vendor_enable' - | 'vendor_disable' - | 'error'; - -/** - * Event-Listener Callback - */ -export type ConsentEventCallback = (data: T) => void; - -/** - * Event-Daten fuer verschiedene Events - */ -export type ConsentEventData = { - init: ConsentState | null; - change: ConsentState; - accept_all: ConsentState; - reject_all: ConsentState; - save_selection: ConsentState; - banner_show: undefined; - banner_hide: undefined; - settings_open: undefined; - settings_close: undefined; - vendor_enable: string; - vendor_disable: string; - error: Error; -}; - -// ============================================================================= -// Storage -// ============================================================================= - -/** - * Storage-Adapter Interface - */ -export interface ConsentStorageAdapter { - /** Consent laden */ - get(): ConsentState | null; - - /** Consent speichern */ - set(consent: ConsentState): void; - - /** Consent loeschen */ - clear(): void; - - /** Pruefen ob Consent existiert */ - exists(): boolean; -} - -// ============================================================================= -// Translations -// ============================================================================= - -/** - * Uebersetzungsstruktur - */ -export interface ConsentTranslations { - title: string; - description: string; - acceptAll: string; - rejectAll: string; - settings: string; - saveSelection: string; - close: string; - categories: { - [K in ConsentCategory]: { - name: string; - description: string; - }; - }; - footer: { - privacyPolicy: string; - imprint: string; - cookieDetails: string; - }; - accessibility: { - closeButton: string; - categoryToggle: string; - requiredCategory: string; - }; -} - -/** - * Alle unterstuetzten Sprachen - */ -export type SupportedLanguage = - | 'de' | 'en' | 'fr' | 'es' | 'it' | 'nl' | 'pl' | 'pt' - | 'cs' | 'da' | 'el' | 'fi' | 'hu' | 'ro' | 'sk' | 'sl' | 'sv'; +export * from './core'; +export * from './config'; +export * from './vendor'; +export * from './api'; +export * from './events'; +export * from './storage'; +export * from './translations'; diff --git a/consent-sdk/src/types/storage.ts b/consent-sdk/src/types/storage.ts new file mode 100644 index 0000000..8dcdebb --- /dev/null +++ b/consent-sdk/src/types/storage.ts @@ -0,0 +1,26 @@ +/** + * Consent SDK Types — Storage adapter interface + */ + +import type { ConsentState } from './core'; + +// ============================================================================= +// Storage +// ============================================================================= + +/** + * Storage-Adapter Interface + */ +export interface ConsentStorageAdapter { + /** Consent laden */ + get(): ConsentState | null; + + /** Consent speichern */ + set(consent: ConsentState): void; + + /** Consent loeschen */ + clear(): void; + + /** Pruefen ob Consent existiert */ + exists(): boolean; +} diff --git a/consent-sdk/src/types/translations.ts b/consent-sdk/src/types/translations.ts new file mode 100644 index 0000000..a892c1f --- /dev/null +++ b/consent-sdk/src/types/translations.ts @@ -0,0 +1,45 @@ +/** + * Consent SDK Types — Translations and i18n + */ + +import type { ConsentCategory } from './core'; + +// ============================================================================= +// Translations +// ============================================================================= + +/** + * Uebersetzungsstruktur + */ +export interface ConsentTranslations { + title: string; + description: string; + acceptAll: string; + rejectAll: string; + settings: string; + saveSelection: string; + close: string; + categories: { + [K in ConsentCategory]: { + name: string; + description: string; + }; + }; + footer: { + privacyPolicy: string; + imprint: string; + cookieDetails: string; + }; + accessibility: { + closeButton: string; + categoryToggle: string; + requiredCategory: string; + }; +} + +/** + * Alle unterstuetzten Sprachen + */ +export type SupportedLanguage = + | 'de' | 'en' | 'fr' | 'es' | 'it' | 'nl' | 'pl' | 'pt' + | 'cs' | 'da' | 'el' | 'fi' | 'hu' | 'ro' | 'sk' | 'sl' | 'sv'; diff --git a/consent-sdk/src/types/vendor.ts b/consent-sdk/src/types/vendor.ts new file mode 100644 index 0000000..4b4e4da --- /dev/null +++ b/consent-sdk/src/types/vendor.ts @@ -0,0 +1,61 @@ +/** + * Consent SDK Types — Vendor definitions + */ + +import type { ConsentCategory } from './core'; + +// ============================================================================= +// Vendor Configuration +// ============================================================================= + +/** + * Cookie-Information + */ +export interface CookieInfo { + /** Cookie-Name */ + name: string; + + /** Cookie-Domain */ + domain: string; + + /** Ablaufzeit (z.B. "2 Jahre", "Session") */ + expiration: string; + + /** Speichertyp */ + type: 'http' | 'localStorage' | 'sessionStorage' | 'indexedDB'; + + /** Beschreibung */ + description: string; +} + +/** + * Vendor-Definition + */ +export interface ConsentVendor { + /** Eindeutige Vendor-ID */ + id: string; + + /** Anzeigename */ + name: string; + + /** Kategorie */ + category: ConsentCategory; + + /** IAB TCF Purposes (falls relevant) */ + purposes?: number[]; + + /** Legitimate Interests */ + legitimateInterests?: number[]; + + /** Cookie-Liste */ + cookies: CookieInfo[]; + + /** Link zur Datenschutzerklaerung */ + privacyPolicyUrl: string; + + /** Datenaufbewahrung */ + dataRetention?: string; + + /** Datentransfer (z.B. "USA (EU-US DPF)", "EU") */ + dataTransfer?: string; +} diff --git a/consent-sdk/src/vue/components.ts b/consent-sdk/src/vue/components.ts new file mode 100644 index 0000000..4d64fcd --- /dev/null +++ b/consent-sdk/src/vue/components.ts @@ -0,0 +1,191 @@ +/** + * Vue consent components: ConsentProvider, ConsentGate, ConsentPlaceholder, + * ConsentBanner. + * + * Phase 4: extracted from vue/index.ts. + */ + +import { computed, defineComponent, h, type PropType } from 'vue'; +import type { ConsentCategory, ConsentConfig } from '../types'; +import { provideConsent, useConsent } from './composables'; + +/** + * ConsentProvider - Wrapper-Komponente. + */ +export const ConsentProvider = defineComponent({ + name: 'ConsentProvider', + props: { + config: { + type: Object as PropType, + required: true, + }, + }, + setup(props, { slots }) { + provideConsent(props.config); + return () => slots.default?.(); + }, +}); + +/** + * ConsentGate - Zeigt Inhalt nur bei Consent. + */ +export const ConsentGate = defineComponent({ + name: 'ConsentGate', + props: { + category: { + type: String as PropType, + required: true, + }, + }, + setup(props, { slots }) { + const { hasConsent, isLoading } = useConsent(); + + return () => { + if (isLoading.value) { + return slots.fallback?.() ?? null; + } + if (!hasConsent(props.category)) { + return slots.placeholder?.() ?? null; + } + return slots.default?.(); + }; + }, +}); + +/** + * ConsentPlaceholder - Placeholder fuer blockierten Inhalt. + */ +export const ConsentPlaceholder = defineComponent({ + name: 'ConsentPlaceholder', + props: { + category: { + type: String as PropType, + required: true, + }, + message: { + type: String, + default: '', + }, + buttonText: { + type: String, + default: 'Cookie-Einstellungen öffnen', + }, + }, + setup(props) { + const { showSettings } = useConsent(); + + const categoryNames: Record = { + essential: 'Essentielle Cookies', + functional: 'Funktionale Cookies', + analytics: 'Statistik-Cookies', + marketing: 'Marketing-Cookies', + social: 'Social Media-Cookies', + }; + + const displayMessage = computed(() => { + return ( + props.message || + `Dieser Inhalt erfordert ${categoryNames[props.category]}.` + ); + }); + + return () => + h('div', { class: 'bp-consent-placeholder' }, [ + h('p', displayMessage.value), + h( + 'button', + { + type: 'button', + onClick: showSettings, + }, + props.buttonText + ), + ]); + }, +}); + +/** + * ConsentBanner - Cookie-Banner Komponente (headless with default UI). + */ +export const ConsentBanner = defineComponent({ + name: 'ConsentBanner', + setup(_, { slots }) { + const { + consent, + isBannerVisible, + needsConsent, + acceptAll, + rejectAll, + saveSelection, + showSettings, + hideBanner, + } = useConsent(); + + const slotProps = computed(() => ({ + isVisible: isBannerVisible.value, + consent: consent.value, + needsConsent: needsConsent.value, + onAcceptAll: acceptAll, + onRejectAll: rejectAll, + onSaveSelection: saveSelection, + onShowSettings: showSettings, + onClose: hideBanner, + })); + + return () => { + if (slots.default) { + return slots.default(slotProps.value); + } + if (!isBannerVisible.value) { + return null; + } + return h( + 'div', + { + class: 'bp-consent-banner', + role: 'dialog', + 'aria-modal': 'true', + 'aria-label': 'Cookie-Einstellungen', + }, + [ + h('div', { class: 'bp-consent-banner-content' }, [ + h('h2', 'Datenschutzeinstellungen'), + h( + 'p', + 'Wir nutzen Cookies und ähnliche Technologien, um Ihnen ein optimales Nutzererlebnis zu bieten.' + ), + h('div', { class: 'bp-consent-banner-actions' }, [ + h( + 'button', + { + type: 'button', + class: 'bp-consent-btn bp-consent-btn-reject', + onClick: rejectAll, + }, + 'Alle ablehnen' + ), + h( + 'button', + { + type: 'button', + class: 'bp-consent-btn bp-consent-btn-settings', + onClick: showSettings, + }, + 'Einstellungen' + ), + h( + 'button', + { + type: 'button', + class: 'bp-consent-btn bp-consent-btn-accept', + onClick: acceptAll, + }, + 'Alle akzeptieren' + ), + ]), + ]), + ] + ); + }; + }, +}); diff --git a/consent-sdk/src/vue/composables.ts b/consent-sdk/src/vue/composables.ts new file mode 100644 index 0000000..1d1a34b --- /dev/null +++ b/consent-sdk/src/vue/composables.ts @@ -0,0 +1,135 @@ +/** + * Vue composables: useConsent + provideConsent. + * + * Phase 4: extracted from vue/index.ts. + */ + +import { + computed, + inject, + onMounted, + onUnmounted, + provide, + readonly, + ref, + type Ref, +} from 'vue'; +import { ConsentManager } from '../core/ConsentManager'; +import type { + ConsentCategories, + ConsentCategory, + ConsentConfig, + ConsentState, +} from '../types'; +import { CONSENT_KEY, type ConsentContext } from './context'; + +/** + * Haupt-Composable fuer Consent-Zugriff. + */ +export function useConsent(): ConsentContext { + const context = inject(CONSENT_KEY); + if (!context) { + throw new Error( + 'useConsent() must be used within a component that has called provideConsent() or is wrapped in ConsentProvider' + ); + } + return context; +} + +/** + * Consent-Provider einrichten (in App.vue aufrufen). + */ +export function provideConsent(config: ConsentConfig): ConsentContext { + const manager = ref(null); + const consent = ref(null); + const isInitialized = ref(false); + const isLoading = ref(true); + const isBannerVisible = ref(false); + + const needsConsent = computed(() => { + return manager.value?.needsConsent() ?? true; + }); + + onMounted(async () => { + const consentManager = new ConsentManager(config); + manager.value = consentManager; + + const unsubChange = consentManager.on('change', (newConsent) => { + consent.value = newConsent; + }); + const unsubBannerShow = consentManager.on('banner_show', () => { + isBannerVisible.value = true; + }); + const unsubBannerHide = consentManager.on('banner_hide', () => { + isBannerVisible.value = false; + }); + + try { + await consentManager.init(); + consent.value = consentManager.getConsent(); + isInitialized.value = true; + isBannerVisible.value = consentManager.isBannerVisible(); + } catch (error) { + console.error('Failed to initialize ConsentManager:', error); + } finally { + isLoading.value = false; + } + + onUnmounted(() => { + unsubChange(); + unsubBannerShow(); + unsubBannerHide(); + }); + }); + + const hasConsent = (category: ConsentCategory): boolean => { + return manager.value?.hasConsent(category) ?? category === 'essential'; + }; + + const acceptAll = async (): Promise => { + await manager.value?.acceptAll(); + }; + + const rejectAll = async (): Promise => { + await manager.value?.rejectAll(); + }; + + const saveSelection = async ( + categories: Partial + ): Promise => { + await manager.value?.setConsent(categories); + manager.value?.hideBanner(); + }; + + const showBanner = (): void => { + manager.value?.showBanner(); + }; + + const hideBanner = (): void => { + manager.value?.hideBanner(); + }; + + const showSettings = (): void => { + manager.value?.showSettings(); + }; + + const context: ConsentContext = { + manager: readonly(manager) as Ref, + consent: readonly(consent) as Ref, + isInitialized: readonly(isInitialized), + isLoading: readonly(isLoading), + isBannerVisible: readonly(isBannerVisible), + needsConsent, + hasConsent, + acceptAll, + rejectAll, + saveSelection, + showBanner, + hideBanner, + showSettings, + }; + + provide(CONSENT_KEY, context); + + return context; +} diff --git a/consent-sdk/src/vue/context.ts b/consent-sdk/src/vue/context.ts new file mode 100644 index 0000000..ce5a221 --- /dev/null +++ b/consent-sdk/src/vue/context.ts @@ -0,0 +1,31 @@ +/** + * Vue consent context — injection key + shape. + * + * Phase 4: extracted from vue/index.ts. + */ + +import type { InjectionKey, Ref } from 'vue'; +import type { ConsentManager } from '../core/ConsentManager'; +import type { + ConsentCategories, + ConsentCategory, + ConsentState, +} from '../types'; + +export interface ConsentContext { + manager: Ref; + consent: Ref; + isInitialized: Ref; + isLoading: Ref; + isBannerVisible: Ref; + needsConsent: Ref; + hasConsent: (category: ConsentCategory) => boolean; + acceptAll: () => Promise; + rejectAll: () => Promise; + saveSelection: (categories: Partial) => Promise; + showBanner: () => void; + hideBanner: () => void; + showSettings: () => void; +} + +export const CONSENT_KEY: InjectionKey = Symbol('consent'); diff --git a/consent-sdk/src/vue/index.ts b/consent-sdk/src/vue/index.ts index 9f157f7..1af08d6 100644 --- a/consent-sdk/src/vue/index.ts +++ b/consent-sdk/src/vue/index.ts @@ -1,6 +1,9 @@ /** * Vue 3 Integration fuer @breakpilot/consent-sdk * + * Phase 4 refactor: thin barrel. Composables, components, plugin, and the + * injection key live in sibling files. + * * @example * ```vue * - * ``` - */ -export function useConsent(): ConsentContext { - const context = inject(CONSENT_KEY); - - if (!context) { - throw new Error( - 'useConsent() must be used within a component that has called provideConsent() or is wrapped in ConsentProvider' - ); - } - - return context; -} - -/** - * Consent-Provider einrichten (in App.vue aufrufen) - * - * @example - * ```vue - * - * ``` - */ -export function provideConsent(config: ConsentConfig): ConsentContext { - const manager = ref(null); - const consent = ref(null); - const isInitialized = ref(false); - const isLoading = ref(true); - const isBannerVisible = ref(false); - - const needsConsent = computed(() => { - return manager.value?.needsConsent() ?? true; - }); - - // Initialisierung - onMounted(async () => { - const consentManager = new ConsentManager(config); - manager.value = consentManager; - - // Events abonnieren - const unsubChange = consentManager.on('change', (newConsent) => { - consent.value = newConsent; - }); - - const unsubBannerShow = consentManager.on('banner_show', () => { - isBannerVisible.value = true; - }); - - const unsubBannerHide = consentManager.on('banner_hide', () => { - isBannerVisible.value = false; - }); - - try { - await consentManager.init(); - consent.value = consentManager.getConsent(); - isInitialized.value = true; - isBannerVisible.value = consentManager.isBannerVisible(); - } catch (error) { - console.error('Failed to initialize ConsentManager:', error); - } finally { - isLoading.value = false; - } - - // Cleanup bei Unmount - onUnmounted(() => { - unsubChange(); - unsubBannerShow(); - unsubBannerHide(); - }); - }); - - // Methoden - const hasConsent = (category: ConsentCategory): boolean => { - return manager.value?.hasConsent(category) ?? category === 'essential'; - }; - - const acceptAll = async (): Promise => { - await manager.value?.acceptAll(); - }; - - const rejectAll = async (): Promise => { - await manager.value?.rejectAll(); - }; - - const saveSelection = async (categories: Partial): Promise => { - await manager.value?.setConsent(categories); - manager.value?.hideBanner(); - }; - - const showBanner = (): void => { - manager.value?.showBanner(); - }; - - const hideBanner = (): void => { - manager.value?.hideBanner(); - }; - - const showSettings = (): void => { - manager.value?.showSettings(); - }; - - const context: ConsentContext = { - manager: readonly(manager) as Ref, - consent: readonly(consent) as Ref, - isInitialized: readonly(isInitialized), - isLoading: readonly(isLoading), - isBannerVisible: readonly(isBannerVisible), - needsConsent, - hasConsent, - acceptAll, - rejectAll, - saveSelection, - showBanner, - hideBanner, - showSettings, - }; - - provide(CONSENT_KEY, context); - - return context; -} - -// ============================================================================= -// Components -// ============================================================================= - -/** - * ConsentProvider - Wrapper-Komponente - * - * @example - * ```vue - * - * - * - * ``` - */ -export const ConsentProvider = defineComponent({ - name: 'ConsentProvider', - props: { - config: { - type: Object as PropType, - required: true, - }, - }, - setup(props, { slots }) { - provideConsent(props.config); - return () => slots.default?.(); - }, -}); - -/** - * ConsentGate - Zeigt Inhalt nur bei Consent - * - * @example - * ```vue - * - * - * - * - * ``` - */ -export const ConsentGate = defineComponent({ - name: 'ConsentGate', - props: { - category: { - type: String as PropType, - required: true, - }, - }, - setup(props, { slots }) { - const { hasConsent, isLoading } = useConsent(); - - return () => { - if (isLoading.value) { - return slots.fallback?.() ?? null; - } - - if (!hasConsent(props.category)) { - return slots.placeholder?.() ?? null; - } - - return slots.default?.(); - }; - }, -}); - -/** - * ConsentPlaceholder - Placeholder fuer blockierten Inhalt - * - * @example - * ```vue - * - * ``` - */ -export const ConsentPlaceholder = defineComponent({ - name: 'ConsentPlaceholder', - props: { - category: { - type: String as PropType, - required: true, - }, - message: { - type: String, - default: '', - }, - buttonText: { - type: String, - default: 'Cookie-Einstellungen öffnen', - }, - }, - setup(props) { - const { showSettings } = useConsent(); - - const categoryNames: Record = { - essential: 'Essentielle Cookies', - functional: 'Funktionale Cookies', - analytics: 'Statistik-Cookies', - marketing: 'Marketing-Cookies', - social: 'Social Media-Cookies', - }; - - const displayMessage = computed(() => { - return props.message || `Dieser Inhalt erfordert ${categoryNames[props.category]}.`; - }); - - return () => - h('div', { class: 'bp-consent-placeholder' }, [ - h('p', displayMessage.value), - h( - 'button', - { - type: 'button', - onClick: showSettings, - }, - props.buttonText - ), - ]); - }, -}); - -/** - * ConsentBanner - Cookie-Banner Komponente - * - * @example - * ```vue - * - * - * - * ``` - */ -export const ConsentBanner = defineComponent({ - name: 'ConsentBanner', - setup(_, { slots }) { - const { - consent, - isBannerVisible, - needsConsent, - acceptAll, - rejectAll, - saveSelection, - showSettings, - hideBanner, - } = useConsent(); - - const slotProps = computed(() => ({ - isVisible: isBannerVisible.value, - consent: consent.value, - needsConsent: needsConsent.value, - onAcceptAll: acceptAll, - onRejectAll: rejectAll, - onSaveSelection: saveSelection, - onShowSettings: showSettings, - onClose: hideBanner, - })); - - return () => { - // Custom Slot - if (slots.default) { - return slots.default(slotProps.value); - } - - // Default UI - if (!isBannerVisible.value) { - return null; - } - - return h( - 'div', - { - class: 'bp-consent-banner', - role: 'dialog', - 'aria-modal': 'true', - 'aria-label': 'Cookie-Einstellungen', - }, - [ - h('div', { class: 'bp-consent-banner-content' }, [ - h('h2', 'Datenschutzeinstellungen'), - h( - 'p', - 'Wir nutzen Cookies und ähnliche Technologien, um Ihnen ein optimales Nutzererlebnis zu bieten.' - ), - h('div', { class: 'bp-consent-banner-actions' }, [ - h( - 'button', - { - type: 'button', - class: 'bp-consent-btn bp-consent-btn-reject', - onClick: rejectAll, - }, - 'Alle ablehnen' - ), - h( - 'button', - { - type: 'button', - class: 'bp-consent-btn bp-consent-btn-settings', - onClick: showSettings, - }, - 'Einstellungen' - ), - h( - 'button', - { - type: 'button', - class: 'bp-consent-btn bp-consent-btn-accept', - onClick: acceptAll, - }, - 'Alle akzeptieren' - ), - ]), - ]), - ] - ); - }; - }, -}); - -// ============================================================================= -// Plugin -// ============================================================================= - -/** - * Vue Plugin fuer globale Installation - * - * @example - * ```ts - * import { createApp } from 'vue'; - * import { ConsentPlugin } from '@breakpilot/consent-sdk/vue'; - * - * const app = createApp(App); - * app.use(ConsentPlugin, { - * apiEndpoint: 'https://consent.example.com/api/v1', - * siteId: 'site_abc123', - * }); - * ``` - */ -export const ConsentPlugin = { - install(app: { provide: (key: symbol | string, value: unknown) => void }, config: ConsentConfig) { - const manager = new ConsentManager(config); - const consent = ref(null); - const isInitialized = ref(false); - const isLoading = ref(true); - const isBannerVisible = ref(false); - - // Initialisieren - manager.init().then(() => { - consent.value = manager.getConsent(); - isInitialized.value = true; - isLoading.value = false; - isBannerVisible.value = manager.isBannerVisible(); - }); - - // Events - manager.on('change', (newConsent) => { - consent.value = newConsent; - }); - manager.on('banner_show', () => { - isBannerVisible.value = true; - }); - manager.on('banner_hide', () => { - isBannerVisible.value = false; - }); - - const context: ConsentContext = { - manager: ref(manager) as Ref, - consent: consent as Ref, - isInitialized, - isLoading, - isBannerVisible, - needsConsent: computed(() => manager.needsConsent()), - hasConsent: (category: ConsentCategory) => manager.hasConsent(category), - acceptAll: () => manager.acceptAll(), - rejectAll: () => manager.rejectAll(), - saveSelection: async (categories: Partial) => { - await manager.setConsent(categories); - manager.hideBanner(); - }, - showBanner: () => manager.showBanner(), - hideBanner: () => manager.hideBanner(), - showSettings: () => manager.showSettings(), - }; - - app.provide(CONSENT_KEY, context); - }, -}; - -// ============================================================================= -// Exports -// ============================================================================= - -export { CONSENT_KEY }; -export type { ConsentContext }; +export { CONSENT_KEY, type ConsentContext } from './context'; +export { useConsent, provideConsent } from './composables'; +export { + ConsentProvider, + ConsentGate, + ConsentPlaceholder, + ConsentBanner, +} from './components'; +export { ConsentPlugin } from './plugin'; diff --git a/consent-sdk/src/vue/plugin.ts b/consent-sdk/src/vue/plugin.ts new file mode 100644 index 0000000..fac957f --- /dev/null +++ b/consent-sdk/src/vue/plugin.ts @@ -0,0 +1,74 @@ +/** + * Vue plugin for global installation. + * + * Phase 4: extracted from vue/index.ts. + */ + +import { computed, ref, type Ref } from 'vue'; +import { ConsentManager } from '../core/ConsentManager'; +import type { + ConsentCategories, + ConsentCategory, + ConsentConfig, + ConsentState, +} from '../types'; +import { CONSENT_KEY, type ConsentContext } from './context'; + +/** + * Vue Plugin fuer globale Installation. + * + * @example + * ```ts + * app.use(ConsentPlugin, { apiEndpoint: '...', siteId: '...' }); + * ``` + */ +export const ConsentPlugin = { + install( + app: { provide: (key: symbol | string, value: unknown) => void }, + config: ConsentConfig + ) { + const manager = new ConsentManager(config); + const consent = ref(null); + const isInitialized = ref(false); + const isLoading = ref(true); + const isBannerVisible = ref(false); + + manager.init().then(() => { + consent.value = manager.getConsent(); + isInitialized.value = true; + isLoading.value = false; + isBannerVisible.value = manager.isBannerVisible(); + }); + + manager.on('change', (newConsent) => { + consent.value = newConsent; + }); + manager.on('banner_show', () => { + isBannerVisible.value = true; + }); + manager.on('banner_hide', () => { + isBannerVisible.value = false; + }); + + const context: ConsentContext = { + manager: ref(manager) as Ref, + consent: consent as Ref, + isInitialized, + isLoading, + isBannerVisible, + needsConsent: computed(() => manager.needsConsent()), + hasConsent: (category: ConsentCategory) => manager.hasConsent(category), + acceptAll: () => manager.acceptAll(), + rejectAll: () => manager.rejectAll(), + saveSelection: async (categories: Partial) => { + await manager.setConsent(categories); + manager.hideBanner(); + }, + showBanner: () => manager.showBanner(), + hideBanner: () => manager.hideBanner(), + showSettings: () => manager.showSettings(), + }; + + app.provide(CONSENT_KEY, context); + }, +}; diff --git a/developer-portal/README.md b/developer-portal/README.md new file mode 100644 index 0000000..71711ac --- /dev/null +++ b/developer-portal/README.md @@ -0,0 +1,26 @@ +# developer-portal + +Next.js 15 public API documentation portal — integration guides, SDK docs, BYOEH, development phases. Consumed by external customers. + +**Port:** `3006` (container: `bp-compliance-developer-portal`) +**Stack:** Next.js 15, React 18, TypeScript. + +## Run locally + +```bash +cd developer-portal +npm install +npm run dev +``` + +## Tests + +0 test files today. Phase 4 adds Playwright smoke tests for each top-level page and Vitest for `lib/` helpers. + +## Architecture + +Follow `../AGENTS.typescript.md`. MD/MDX content should live in a data directory, not inline in `page.tsx`. + +## Known debt + +- Several page files under `app/development/` exceed the 300 LOC soft target and are candidates for splitting as refactoring continues. diff --git a/developer-portal/app/api/iace/_components/AuditRagSdkSection.tsx b/developer-portal/app/api/iace/_components/AuditRagSdkSection.tsx new file mode 100644 index 0000000..6ad7259 --- /dev/null +++ b/developer-portal/app/api/iace/_components/AuditRagSdkSection.tsx @@ -0,0 +1,149 @@ +'use client' + +import { ApiEndpoint, CodeBlock, InfoBox } from '@/components/DevPortalLayout' + +export function AuditRagSdkSection() { + return ( + <> +

Audit Trail

+

Lueckenloser Audit-Trail aller Projektaenderungen fuer Compliance-Nachweise.

+ + + + +{`curl -X GET "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/audit-trail" \\ + -H "Authorization: Bearer YOUR_API_KEY"`} + + +

Response (200 OK)

+ +{`{ + "success": true, + "data": [ + { "id": "aud_001", "action": "hazard_created", "entity_type": "hazard", "entity_id": "haz_5678", "user_id": "user_abc", "changes": { "title": "Quetschgefahr durch Linearantrieb", "severity": "high" }, "timestamp": "2026-03-16T10:15:00Z" }, + { "id": "aud_002", "action": "risk_assessed", "entity_type": "hazard", "entity_id": "haz_5678", "user_id": "user_abc", "changes": { "inherent_risk": 12, "risk_level": "high" }, "timestamp": "2026-03-16T10:20:00Z" }, + { "id": "aud_003", "action": "tech_file_section_approved", "entity_type": "tech_file", "entity_id": "risk_assessment", "user_id": "user_def", "changes": { "status": "approved", "approved_by": "Dr. Mueller" }, "timestamp": "2026-03-16T15:00:00Z" } + ] +}`} + + +

RAG Library Search

+

+ Semantische Suche in der Compliance-Bibliothek via RAG (Retrieval-Augmented Generation). + Ermoeglicht kontextbasierte Anreicherung von Tech-File-Abschnitten. +

+ + + + +{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/library-search" \\ + -H "Authorization: Bearer YOUR_API_KEY" \\ + -H "Content-Type: application/json" \\ + -d '{ + "query": "Schutzeinrichtungen fuer Industrieroboter Maschinenverordnung", + "top_k": 5 + }'`} + + +

Response (200 OK)

+ +{`{ + "success": true, + "data": { + "query": "Schutzeinrichtungen fuer Industrieroboter Maschinenverordnung", + "results": [ + { "id": "mr-annex-iii-1.1.4", "title": "Maschinenverordnung Anhang III 1.1.4 — Schutzmassnahmen", "content": "Trennende Schutzeinrichtungen muessen fest angebracht oder verriegelt sein...", "source": "machinery_regulation", "score": 0.93 } + ], + "total_results": 5, + "search_time_ms": 38 + } +}`} + + + + + +{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/tech-file/safety_requirements/enrich" \\ + -H "Authorization: Bearer YOUR_API_KEY"`} + + +

Response (200 OK)

+ +{`{ + "success": true, + "data": { + "section": "safety_requirements", + "enriched_content": "... (aktualisierter Abschnitt mit Regulierungsreferenzen) ...", + "citations_added": 4, + "sources": [ + { "id": "mr-annex-iii-1.1.4", "title": "Maschinenverordnung Anhang III 1.1.4", "relevance_score": 0.93 } + ] + } +}`} + + +

SDK Integration

+

Beispiel fuer die Integration der IACE-API in eine Anwendung:

+ + +{`import { getSDKBackendClient } from '@breakpilot/compliance-sdk' + +const client = getSDKBackendClient() + +// 1. Projekt erstellen +const project = await client.post('/iace/projects', { + machine_name: 'RoboArm X500', + machine_type: 'Industrieroboter', + manufacturer: 'TechCorp GmbH' +}) + +// 2. Aus Firmenprofil initialisieren +await client.post(\`/iace/projects/\${project.id}/init-from-profile\`) + +// 3. Komponenten hinzufuegen +await client.post(\`/iace/projects/\${project.id}/components\`, { + name: 'Servo-Antrieb Achse 1', + component_type: 'actuator', + is_safety_relevant: true +}) + +// 4. Regulierungen klassifizieren +const classifications = await client.post(\`/iace/projects/\${project.id}/classify\`) + +// 5. Pattern-Matching ausfuehren +const patterns = await client.post(\`/iace/projects/\${project.id}/match-patterns\`) +console.log(\`\${patterns.matches} Gefahren erkannt von \${patterns.total_patterns_checked} Patterns\`) + +// 6. Erkannte Patterns als Gefahren uebernehmen +await client.post(\`/iace/projects/\${project.id}/apply-patterns\`) + +// 7. Risiken bewerten +for (const hazard of await client.get(\`/iace/projects/\${project.id}/hazards\`)) { + await client.post(\`/iace/projects/\${project.id}/hazards/\${hazard.id}/assess\`, { + severity: 3, exposure: 2, probability: 2, avoidance: 2 + }) +} + +// 8. Tech File generieren +const techFile = await client.post(\`/iace/projects/\${project.id}/tech-file/generate\`) +console.log(\`\${techFile.sections_generated} Abschnitte generiert\`) + +// 9. PDF exportieren +const pdf = await client.get(\`/iace/projects/\${project.id}/tech-file/export?format=pdf\`) +`} + + + + LLM-basierte Endpoints (Tech-File-Generierung, Hazard-Suggest, RAG-Enrichment) + verbrauchen LLM-Tokens. Professional-Plan: 50 Generierungen/Tag. + Enterprise-Plan: unbegrenzt. Implementieren Sie Caching fuer wiederholte Anfragen. + + + + Alle LLM-generierten Inhalte muessen vor der Freigabe manuell geprueft werden. + Die API erzwingt dies ueber den Approve-Workflow: generierte Abschnitte haben + den Status "generated" und muessen explizit auf "approved" gesetzt werden. + + + ) +} diff --git a/developer-portal/app/api/iace/_components/ComponentsSection.tsx b/developer-portal/app/api/iace/_components/ComponentsSection.tsx new file mode 100644 index 0000000..32286b3 --- /dev/null +++ b/developer-portal/app/api/iace/_components/ComponentsSection.tsx @@ -0,0 +1,52 @@ +'use client' + +import { ApiEndpoint, CodeBlock, ParameterTable } from '@/components/DevPortalLayout' + +export function ComponentsSection() { + return ( + <> +

Components

+

Verwalten Sie die Komponenten einer Maschine oder eines Produkts.

+ + + +

Request Body

+ + + +{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/components" \\ + -H "Authorization: Bearer YOUR_API_KEY" \\ + -H "Content-Type: application/json" \\ + -d '{ + "name": "Servo-Antrieb Achse 1", + "component_type": "actuator", + "is_safety_relevant": true + }'`} + + +

Response (201 Created)

+ +{`{ + "success": true, + "data": { + "id": "comp_1234abcd", + "name": "Servo-Antrieb Achse 1", + "component_type": "actuator", + "is_safety_relevant": true, + "created_at": "2026-03-16T10:05:00Z" + } +}`} + + + + + + + ) +} diff --git a/developer-portal/app/api/iace/_components/EvidenceVerificationSection.tsx b/developer-portal/app/api/iace/_components/EvidenceVerificationSection.tsx new file mode 100644 index 0000000..1f4c43b --- /dev/null +++ b/developer-portal/app/api/iace/_components/EvidenceVerificationSection.tsx @@ -0,0 +1,78 @@ +'use client' + +import { ApiEndpoint, CodeBlock } from '@/components/DevPortalLayout' + +export function EvidenceVerificationSection() { + return ( + <> +

Evidence

+

Evidenz-Dateien hochladen und verwalten (Pruefberichte, Zertifikate, Fotos, etc.).

+ + + + +{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/evidence" \\ + -H "Authorization: Bearer YOUR_API_KEY" \\ + -F "file=@pruefbericht_schutzgitter.pdf" \\ + -F "title=Pruefbericht Schutzgitter ISO 14120" \\ + -F "evidence_type=test_report" \\ + -F "linked_mitigation_id=mit_abcd1234"`} + + +

Response (201 Created)

+ +{`{ + "success": true, + "data": { + "id": "evi_xyz789", + "title": "Pruefbericht Schutzgitter ISO 14120", + "evidence_type": "test_report", + "file_name": "pruefbericht_schutzgitter.pdf", + "file_size": 245760, + "linked_mitigation_id": "mit_abcd1234", + "created_at": "2026-03-16T12:00:00Z" + } +}`} + + + + +

Verification Plans

+

Verifizierungsplaene erstellen und abarbeiten.

+ + + + +{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/verification-plan" \\ + -H "Authorization: Bearer YOUR_API_KEY" \\ + -H "Content-Type: application/json" \\ + -d '{ + "title": "Schutzgitter-Verifizierung", + "description": "Pruefung der Schutzgitter nach ISO 14120", + "method": "inspection", + "linked_mitigation_id": "mit_abcd1234", + "planned_date": "2026-04-15T00:00:00Z" + }'`} + + +

Response (201 Created)

+ +{`{ + "success": true, + "data": { + "id": "vp_plan001", + "title": "Schutzgitter-Verifizierung", + "method": "inspection", + "status": "planned", + "linked_mitigation_id": "mit_abcd1234", + "planned_date": "2026-04-15T00:00:00Z", + "created_at": "2026-03-16T12:30:00Z" + } +}`} + + + + + + ) +} diff --git a/developer-portal/app/api/iace/_components/HazardsSection.tsx b/developer-portal/app/api/iace/_components/HazardsSection.tsx new file mode 100644 index 0000000..8c42272 --- /dev/null +++ b/developer-portal/app/api/iace/_components/HazardsSection.tsx @@ -0,0 +1,95 @@ +'use client' + +import { ApiEndpoint, CodeBlock, InfoBox } from '@/components/DevPortalLayout' + +export function HazardsSection() { + return ( + <> +

Hazards & Pattern Matching

+

+ Gefahrenanalyse nach ISO 12100 mit 102 Hazard-Patterns. Die Pattern-Matching-Engine + erkennt automatisch Gefahren basierend auf Maschinentyp, Komponenten und Energiequellen. +

+ + + Die Engine enthaelt 102 vordefinierte Gefahrenmuster (HP001-HP102), die nach + ISO 12100 Anhang A kategorisiert sind: mechanisch, elektrisch, thermisch, Laerm, + Vibration, Strahlung, Materialien/Substanzen und ergonomisch. + + + + + + + + + +{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/hazards/suggest" \\ + -H "Authorization: Bearer YOUR_API_KEY"`} + + +

Response (200 OK)

+ +{`{ + "success": true, + "data": { + "suggestions": [ + { + "hazard_type": "mechanical", + "title": "Quetschgefahr durch bewegliche Roboterarme", + "description": "Unkontrollierte Bewegung der Achsen kann zu Quetschungen fuehren", + "iso_reference": "ISO 12100 Anhang A.1", + "severity": "high", + "confidence": 0.91 + }, + { + "hazard_type": "electrical", + "title": "Stromschlaggefahr bei Wartungsarbeiten", + "description": "Zugang zu spannungsfuehrenden Teilen bei geoeffnetem Schaltschrank", + "iso_reference": "ISO 12100 Anhang A.2", + "severity": "critical", + "confidence": 0.87 + } + ] + } +}`} + + + + + +{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/match-patterns" \\ + -H "Authorization: Bearer YOUR_API_KEY"`} + + +

Response (200 OK)

+ +{`{ + "success": true, + "data": { + "total_patterns_checked": 102, + "matches": 14, + "results": [ + { + "pattern_id": "HP003", + "title": "Crushing hazard from linear actuator", + "category": "mechanical", + "match_score": 0.94, + "matched_components": ["Servo-Antrieb Achse 1", "Linearfuehrung"], + "matched_energy_sources": ["EN03"], + "suggested_hazard": { + "title": "Quetschgefahr durch Linearantrieb", + "severity": "high" + } + } + ] + } +}`} + + + + + + + ) +} diff --git a/developer-portal/app/api/iace/_components/MitigationsSection.tsx b/developer-portal/app/api/iace/_components/MitigationsSection.tsx new file mode 100644 index 0000000..3e7cdf6 --- /dev/null +++ b/developer-portal/app/api/iace/_components/MitigationsSection.tsx @@ -0,0 +1,93 @@ +'use client' + +import { ApiEndpoint, CodeBlock, ParameterTable } from '@/components/DevPortalLayout' + +export function MitigationsSection() { + return ( + <> +

Mitigations

+

+ Massnahmenverwaltung nach der 3-Stufen-Hierarchie gemaess ISO 12100: +

+
    +
  1. Design — Inherent Safe Design (Gefahrenbeseitigung durch Konstruktion)
  2. +
  3. Protective — Schutzeinrichtungen und technische Schutzmassnahmen
  4. +
  5. Information — Benutzerinformation (Warnhinweise, Anleitungen, Schulungen)
  6. +
+ + + +

Request Body

+ + + +{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/hazards/haz_5678/mitigations" \\ + -H "Authorization: Bearer YOUR_API_KEY" \\ + -H "Content-Type: application/json" \\ + -d '{ + "title": "Schutzgitter mit Sicherheitsschalter", + "description": "Installation eines trennenden Schutzgitters mit Verriegelung nach ISO 14120", + "hierarchy_level": "protective", + "responsible": "Sicherheitsingenieur", + "deadline": "2026-04-30T00:00:00Z" + }'`} + + +

Response (201 Created)

+ +{`{ + "success": true, + "data": { + "id": "mit_abcd1234", + "hazard_id": "haz_5678", + "title": "Schutzgitter mit Sicherheitsschalter", + "hierarchy_level": "protective", + "status": "planned", + "verified": false, + "created_at": "2026-03-16T11:00:00Z" + } +}`} + + + + + + + +{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/validate-mitigation-hierarchy" \\ + -H "Authorization: Bearer YOUR_API_KEY"`} + + +

Response (200 OK)

+ +{`{ + "success": true, + "data": { + "valid": false, + "violations": [ + { + "hazard_id": "haz_5678", + "hazard_title": "Quetschgefahr durch Linearantrieb", + "issue": "Nur Information-Massnahmen vorhanden. Design- oder Schutzmassnahmen muessen vorrangig angewendet werden.", + "missing_levels": ["design", "protective"] + } + ], + "summary": { + "total_hazards_with_mitigations": 12, + "hierarchy_compliant": 9, + "hierarchy_violations": 3 + } + } +}`} + + + ) +} diff --git a/developer-portal/app/api/iace/_components/MonitoringLibrariesSection.tsx b/developer-portal/app/api/iace/_components/MonitoringLibrariesSection.tsx new file mode 100644 index 0000000..ff3935f --- /dev/null +++ b/developer-portal/app/api/iace/_components/MonitoringLibrariesSection.tsx @@ -0,0 +1,98 @@ +'use client' + +import { ApiEndpoint, CodeBlock } from '@/components/DevPortalLayout' + +export function MonitoringLibrariesSection() { + return ( + <> +

Monitoring

+

Post-Market-Surveillance: Monitoring-Ereignisse erfassen und verfolgen.

+ + + + +{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/monitoring" \\ + -H "Authorization: Bearer YOUR_API_KEY" \\ + -H "Content-Type: application/json" \\ + -d '{ + "event_type": "incident", + "title": "Schutzgitter-Sensor Fehlausloesung", + "description": "Sicherheitssensor hat ohne erkennbaren Grund ausgeloest", + "severity": "medium", + "occurred_at": "2026-03-15T14:30:00Z" + }'`} + + +

Response (201 Created)

+ +{`{ + "success": true, + "data": { + "id": "mon_evt001", + "event_type": "incident", + "title": "Schutzgitter-Sensor Fehlausloesung", + "severity": "medium", + "status": "open", + "occurred_at": "2026-03-15T14:30:00Z", + "created_at": "2026-03-16T08:00:00Z" + } +}`} + + + + + +

Libraries (projektunabhaengig)

+

+ Stammdaten-Bibliotheken fuer die Gefahrenanalyse. Diese Endpoints sind + projektunabhaengig und liefern die Referenzdaten fuer die gesamte IACE-Engine. +

+ + + + +{`curl -X GET "https://api.breakpilot.io/sdk/v1/iace/hazard-library" \\ + -H "Authorization: Bearer YOUR_API_KEY"`} + + +

Response (200 OK)

+ +{`{ + "success": true, + "data": [ + { + "id": "HP001", + "title": "Crushing hazard from closing mechanisms", + "category": "mechanical", + "iso_reference": "ISO 12100 Anhang A.1", + "typical_components": ["actuator", "press", "clamp"], + "severity_range": "medium-critical" + }, + { + "id": "HP045", + "title": "Electric shock from exposed conductors", + "category": "electrical", + "iso_reference": "ISO 12100 Anhang A.2", + "typical_components": ["power_supply", "motor", "controller"], + "severity_range": "high-critical" + } + ], + "meta": { + "total": 102, + "categories": { "mechanical": 28, "electrical": 15, "thermal": 10, "noise": 8, "vibration": 7, "radiation": 9, "materials": 12, "ergonomic": 13 } + } +}`} + + + + + + + + + + + + + ) +} diff --git a/developer-portal/app/api/iace/_components/OnboardingSection.tsx b/developer-portal/app/api/iace/_components/OnboardingSection.tsx new file mode 100644 index 0000000..f630a6e --- /dev/null +++ b/developer-portal/app/api/iace/_components/OnboardingSection.tsx @@ -0,0 +1,57 @@ +'use client' + +import { ApiEndpoint, CodeBlock } from '@/components/DevPortalLayout' + +export function OnboardingSection() { + return ( + <> +

Onboarding

+

Initialisierung aus Firmenprofil und Vollstaendigkeitspruefung.

+ + + + +{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/init-from-profile" \\ + -H "Authorization: Bearer YOUR_API_KEY"`} + + +

Response (200 OK)

+ +{`{ + "success": true, + "data": { + "initialized_fields": ["manufacturer", "description", "machine_type"], + "suggested_regulations": ["machinery_regulation", "low_voltage", "emc"], + "message": "Projekt aus Firmenprofil initialisiert. 3 Felder uebernommen." + } +}`} + + + + + +{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/completeness-check" \\ + -H "Authorization: Bearer YOUR_API_KEY"`} + + +

Response (200 OK)

+ +{`{ + "success": true, + "data": { + "score": 72, + "total_gates": 25, + "passed_gates": 18, + "gates": [ + { "id": "G01", "name": "Maschinenidentifikation", "status": "passed" }, + { "id": "G02", "name": "Komponentenliste", "status": "passed" }, + { "id": "G03", "name": "Regulatorische Klassifizierung", "status": "passed" }, + { "id": "G04", "name": "Gefahrenanalyse", "status": "warning", "message": "3 Gefahren ohne Massnahmen" }, + { "id": "G05", "name": "Risikobewertung", "status": "failed", "message": "5 Gefahren nicht bewertet" } + ] + } +}`} + + + ) +} diff --git a/developer-portal/app/api/iace/_components/ProjectManagementSection.tsx b/developer-portal/app/api/iace/_components/ProjectManagementSection.tsx new file mode 100644 index 0000000..5519e46 --- /dev/null +++ b/developer-portal/app/api/iace/_components/ProjectManagementSection.tsx @@ -0,0 +1,88 @@ +'use client' + +import { ApiEndpoint, CodeBlock, ParameterTable } from '@/components/DevPortalLayout' + +export function ProjectManagementSection() { + return ( + <> +

Project Management

+

Erstellen und verwalten Sie IACE-Projekte fuer einzelne Maschinen oder Produkte.

+ + + +

Request Body

+ + + +{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects" \\ + -H "Authorization: Bearer YOUR_API_KEY" \\ + -H "Content-Type: application/json" \\ + -d '{ + "machine_name": "RoboArm X500", + "machine_type": "Industrieroboter", + "manufacturer": "TechCorp GmbH", + "description": "6-Achsen-Industrieroboter fuer Montagearbeiten" + }'`} + + +

Response (201 Created)

+ +{`{ + "success": true, + "data": { + "id": "proj_a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "machine_name": "RoboArm X500", + "machine_type": "Industrieroboter", + "manufacturer": "TechCorp GmbH", + "description": "6-Achsen-Industrieroboter fuer Montagearbeiten", + "status": "draft", + "completeness_score": 0, + "created_at": "2026-03-16T10:00:00Z" + } +}`} + + + + + +{`curl -X GET "https://api.breakpilot.io/sdk/v1/iace/projects" \\ + -H "Authorization: Bearer YOUR_API_KEY"`} + + +

Response (200 OK)

+ +{`{ + "success": true, + "data": [ + { + "id": "proj_a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "machine_name": "RoboArm X500", + "machine_type": "Industrieroboter", + "status": "in_progress", + "completeness_score": 72, + "hazard_count": 14, + "created_at": "2026-03-16T10:00:00Z" + } + ] +}`} + + + + + +{`curl -X GET "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4" \\ + -H "Authorization: Bearer YOUR_API_KEY"`} + + + + + + ) +} diff --git a/developer-portal/app/api/iace/_components/RegulatoryClassificationSection.tsx b/developer-portal/app/api/iace/_components/RegulatoryClassificationSection.tsx new file mode 100644 index 0000000..f2facdd --- /dev/null +++ b/developer-portal/app/api/iace/_components/RegulatoryClassificationSection.tsx @@ -0,0 +1,54 @@ +'use client' + +import { ApiEndpoint, CodeBlock } from '@/components/DevPortalLayout' + +export function RegulatoryClassificationSection() { + return ( + <> +

Regulatory Classification

+

Automatische Klassifizierung nach anwendbaren Regulierungen (Maschinenverordnung, Niederspannung, EMV, etc.).

+ + + + +{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/classify" \\ + -H "Authorization: Bearer YOUR_API_KEY"`} + + +

Response (200 OK)

+ +{`{ + "success": true, + "data": { + "classifications": [ + { + "regulation": "machinery_regulation", + "title": "Maschinenverordnung (EU) 2023/1230", + "applicable": true, + "confidence": 0.95, + "reason": "Industrieroboter faellt unter Annex I der Maschinenverordnung" + }, + { + "regulation": "low_voltage", + "title": "Niederspannungsrichtlinie 2014/35/EU", + "applicable": true, + "confidence": 0.88, + "reason": "Betriebsspannung 400V AC" + }, + { + "regulation": "ai_act", + "title": "AI Act (EU) 2024/1689", + "applicable": false, + "confidence": 0.72, + "reason": "Keine KI-Komponente identifiziert" + } + ] + } +}`} + + + + + + ) +} diff --git a/developer-portal/app/api/iace/_components/RiskAssessmentSection.tsx b/developer-portal/app/api/iace/_components/RiskAssessmentSection.tsx new file mode 100644 index 0000000..50ae434 --- /dev/null +++ b/developer-portal/app/api/iace/_components/RiskAssessmentSection.tsx @@ -0,0 +1,95 @@ +'use client' + +import { ApiEndpoint, CodeBlock, ParameterTable } from '@/components/DevPortalLayout' + +export function RiskAssessmentSection() { + return ( + <> +

Risk Assessment

+

+ 4-Faktor-Risikobewertung nach ISO 12100 mit den Parametern Schwere (S), + Exposition (E), Eintrittswahrscheinlichkeit (P) und Vermeidbarkeit (A). +

+ + + +

Request Body

+ + + +{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/hazards/haz_5678/assess" \\ + -H "Authorization: Bearer YOUR_API_KEY" \\ + -H "Content-Type: application/json" \\ + -d '{ + "severity": 4, + "exposure": 3, + "probability": 2, + "avoidance": 3 + }'`} + + +

Response (200 OK)

+ +{`{ + "success": true, + "data": { + "hazard_id": "haz_5678", + "severity": 4, + "exposure": 3, + "probability": 2, + "avoidance": 3, + "inherent_risk": 12, + "risk_level": "high", + "c_eff": 0.65, + "residual_risk": 4.2, + "residual_risk_level": "medium", + "risk_acceptable": false, + "recommendation": "Zusaetzliche Schutzmassnahmen erforderlich. 3-Stufen-Hierarchie anwenden." + } +}`} + + + + + +{`curl -X GET "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/risk-summary" \\ + -H "Authorization: Bearer YOUR_API_KEY"`} + + +

Response (200 OK)

+ +{`{ + "success": true, + "data": { + "total_hazards": 14, + "assessed": 12, + "unassessed": 2, + "risk_distribution": { + "critical": 1, + "high": 4, + "medium": 5, + "low": 2 + }, + "residual_risk_distribution": { + "critical": 0, + "high": 1, + "medium": 3, + "low": 8 + }, + "average_c_eff": 0.71, + "overall_risk_acceptable": false + } +}`} + + + + + ) +} diff --git a/developer-portal/app/api/iace/_components/TechFileSection.tsx b/developer-portal/app/api/iace/_components/TechFileSection.tsx new file mode 100644 index 0000000..3e63cce --- /dev/null +++ b/developer-portal/app/api/iace/_components/TechFileSection.tsx @@ -0,0 +1,72 @@ +'use client' + +import { ApiEndpoint, CodeBlock, InfoBox, ParameterTable } from '@/components/DevPortalLayout' + +export function TechFileSection() { + return ( + <> +

CE Technical File

+

+ LLM-gestuetzte Generierung der Technischen Dokumentation (CE Technical File). + Die API generiert alle erforderlichen Abschnitte basierend auf den Projektdaten. +

+ + + Die Generierung verwendet einen LLM-Service (qwen3:30b-a3b oder claude-sonnet-4-5) + fuer kontextbasierte Texterstellung. Alle generierten Abschnitte muessen vor der + Freigabe manuell geprueft werden (Human Oversight). + + + + + +{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/tech-file/generate" \\ + -H "Authorization: Bearer YOUR_API_KEY"`} + + +

Response (200 OK)

+ +{`{ + "success": true, + "data": { + "sections_generated": 8, + "sections": [ + { "section": "general_description", "title": "Allgemeine Beschreibung", "status": "generated", "word_count": 450 }, + { "section": "risk_assessment", "title": "Risikobeurteilung", "status": "generated", "word_count": 1200 }, + { "section": "safety_requirements", "title": "Sicherheitsanforderungen", "status": "generated", "word_count": 800 }, + { "section": "verification_results", "title": "Verifizierungsergebnisse", "status": "generated", "word_count": 600 } + ], + "total_word_count": 4850, + "generation_time_ms": 12500 + } +}`} + + + + + + + + + +

Export-Formate

+ + + +{`# PDF Export +curl -X GET "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/tech-file/export?format=pdf" \\ + -H "Authorization: Bearer YOUR_API_KEY" \\ + -o technical-file.pdf + +# Markdown Export +curl -X GET "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/tech-file/export?format=md" \\ + -H "Authorization: Bearer YOUR_API_KEY" \\ + -o technical-file.md`} + + + ) +} diff --git a/developer-portal/app/api/iace/page.tsx b/developer-portal/app/api/iace/page.tsx index 8ac71d1..1e50906 100644 --- a/developer-portal/app/api/iace/page.tsx +++ b/developer-portal/app/api/iace/page.tsx @@ -1,6 +1,17 @@ 'use client' -import { DevPortalLayout, ApiEndpoint, CodeBlock, ParameterTable, InfoBox } from '@/components/DevPortalLayout' +import { DevPortalLayout, InfoBox } from '@/components/DevPortalLayout' +import { ProjectManagementSection } from './_components/ProjectManagementSection' +import { OnboardingSection } from './_components/OnboardingSection' +import { ComponentsSection } from './_components/ComponentsSection' +import { RegulatoryClassificationSection } from './_components/RegulatoryClassificationSection' +import { HazardsSection } from './_components/HazardsSection' +import { RiskAssessmentSection } from './_components/RiskAssessmentSection' +import { MitigationsSection } from './_components/MitigationsSection' +import { EvidenceVerificationSection } from './_components/EvidenceVerificationSection' +import { TechFileSection } from './_components/TechFileSection' +import { MonitoringLibrariesSection } from './_components/MonitoringLibrariesSection' +import { AuditRagSdkSection } from './_components/AuditRagSdkSection' export default function IACEApiPage() { return ( @@ -30,979 +41,17 @@ export default function IACEApiPage() { Authentifizierung erfolgt ueber Bearer Token im Authorization-Header. - {/* ============================================================ */} - {/* PROJECT MANAGEMENT */} - {/* ============================================================ */} - -

Project Management

-

Erstellen und verwalten Sie IACE-Projekte fuer einzelne Maschinen oder Produkte.

- - - -

Request Body

- - - -{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects" \\ - -H "Authorization: Bearer YOUR_API_KEY" \\ - -H "Content-Type: application/json" \\ - -d '{ - "machine_name": "RoboArm X500", - "machine_type": "Industrieroboter", - "manufacturer": "TechCorp GmbH", - "description": "6-Achsen-Industrieroboter fuer Montagearbeiten" - }'`} - - -

Response (201 Created)

- -{`{ - "success": true, - "data": { - "id": "proj_a1b2c3d4-e5f6-7890-abcd-ef1234567890", - "machine_name": "RoboArm X500", - "machine_type": "Industrieroboter", - "manufacturer": "TechCorp GmbH", - "description": "6-Achsen-Industrieroboter fuer Montagearbeiten", - "status": "draft", - "completeness_score": 0, - "created_at": "2026-03-16T10:00:00Z" - } -}`} - - - - - -{`curl -X GET "https://api.breakpilot.io/sdk/v1/iace/projects" \\ - -H "Authorization: Bearer YOUR_API_KEY"`} - - -

Response (200 OK)

- -{`{ - "success": true, - "data": [ - { - "id": "proj_a1b2c3d4-e5f6-7890-abcd-ef1234567890", - "machine_name": "RoboArm X500", - "machine_type": "Industrieroboter", - "status": "in_progress", - "completeness_score": 72, - "hazard_count": 14, - "created_at": "2026-03-16T10:00:00Z" - } - ] -}`} - - - - - -{`curl -X GET "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4" \\ - -H "Authorization: Bearer YOUR_API_KEY"`} - - - - - - {/* ============================================================ */} - {/* ONBOARDING */} - {/* ============================================================ */} - -

Onboarding

-

Initialisierung aus Firmenprofil und Vollstaendigkeitspruefung.

- - - - -{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/init-from-profile" \\ - -H "Authorization: Bearer YOUR_API_KEY"`} - - -

Response (200 OK)

- -{`{ - "success": true, - "data": { - "initialized_fields": ["manufacturer", "description", "machine_type"], - "suggested_regulations": ["machinery_regulation", "low_voltage", "emc"], - "message": "Projekt aus Firmenprofil initialisiert. 3 Felder uebernommen." - } -}`} - - - - - -{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/completeness-check" \\ - -H "Authorization: Bearer YOUR_API_KEY"`} - - -

Response (200 OK)

- -{`{ - "success": true, - "data": { - "score": 72, - "total_gates": 25, - "passed_gates": 18, - "gates": [ - { "id": "G01", "name": "Maschinenidentifikation", "status": "passed" }, - { "id": "G02", "name": "Komponentenliste", "status": "passed" }, - { "id": "G03", "name": "Regulatorische Klassifizierung", "status": "passed" }, - { "id": "G04", "name": "Gefahrenanalyse", "status": "warning", "message": "3 Gefahren ohne Massnahmen" }, - { "id": "G05", "name": "Risikobewertung", "status": "failed", "message": "5 Gefahren nicht bewertet" } - ] - } -}`} - - - {/* ============================================================ */} - {/* COMPONENTS */} - {/* ============================================================ */} - -

Components

-

Verwalten Sie die Komponenten einer Maschine oder eines Produkts.

- - - -

Request Body

- - - -{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/components" \\ - -H "Authorization: Bearer YOUR_API_KEY" \\ - -H "Content-Type: application/json" \\ - -d '{ - "name": "Servo-Antrieb Achse 1", - "component_type": "actuator", - "is_safety_relevant": true - }'`} - - -

Response (201 Created)

- -{`{ - "success": true, - "data": { - "id": "comp_1234abcd", - "name": "Servo-Antrieb Achse 1", - "component_type": "actuator", - "is_safety_relevant": true, - "created_at": "2026-03-16T10:05:00Z" - } -}`} - - - - - - - {/* ============================================================ */} - {/* REGULATORY CLASSIFICATION */} - {/* ============================================================ */} - -

Regulatory Classification

-

Automatische Klassifizierung nach anwendbaren Regulierungen (Maschinenverordnung, Niederspannung, EMV, etc.).

- - - - -{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/classify" \\ - -H "Authorization: Bearer YOUR_API_KEY"`} - - -

Response (200 OK)

- -{`{ - "success": true, - "data": { - "classifications": [ - { - "regulation": "machinery_regulation", - "title": "Maschinenverordnung (EU) 2023/1230", - "applicable": true, - "confidence": 0.95, - "reason": "Industrieroboter faellt unter Annex I der Maschinenverordnung" - }, - { - "regulation": "low_voltage", - "title": "Niederspannungsrichtlinie 2014/35/EU", - "applicable": true, - "confidence": 0.88, - "reason": "Betriebsspannung 400V AC" - }, - { - "regulation": "ai_act", - "title": "AI Act (EU) 2024/1689", - "applicable": false, - "confidence": 0.72, - "reason": "Keine KI-Komponente identifiziert" - } - ] - } -}`} - - - - - - {/* ============================================================ */} - {/* HAZARDS & PATTERN MATCHING */} - {/* ============================================================ */} - -

Hazards & Pattern Matching

-

- Gefahrenanalyse nach ISO 12100 mit 102 Hazard-Patterns. Die Pattern-Matching-Engine - erkennt automatisch Gefahren basierend auf Maschinentyp, Komponenten und Energiequellen. -

- - - Die Engine enthaelt 102 vordefinierte Gefahrenmuster (HP001-HP102), die nach - ISO 12100 Anhang A kategorisiert sind: mechanisch, elektrisch, thermisch, Laerm, - Vibration, Strahlung, Materialien/Substanzen und ergonomisch. - - - - - - - - - -{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/hazards/suggest" \\ - -H "Authorization: Bearer YOUR_API_KEY"`} - - -

Response (200 OK)

- -{`{ - "success": true, - "data": { - "suggestions": [ - { - "hazard_type": "mechanical", - "title": "Quetschgefahr durch bewegliche Roboterarme", - "description": "Unkontrollierte Bewegung der Achsen kann zu Quetschungen fuehren", - "iso_reference": "ISO 12100 Anhang A.1", - "severity": "high", - "confidence": 0.91 - }, - { - "hazard_type": "electrical", - "title": "Stromschlaggefahr bei Wartungsarbeiten", - "description": "Zugang zu spannungsfuehrenden Teilen bei geoeffnetem Schaltschrank", - "iso_reference": "ISO 12100 Anhang A.2", - "severity": "critical", - "confidence": 0.87 - } - ] - } -}`} - - - - - -{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/match-patterns" \\ - -H "Authorization: Bearer YOUR_API_KEY"`} - - -

Response (200 OK)

- -{`{ - "success": true, - "data": { - "total_patterns_checked": 102, - "matches": 14, - "results": [ - { - "pattern_id": "HP003", - "title": "Crushing hazard from linear actuator", - "category": "mechanical", - "match_score": 0.94, - "matched_components": ["Servo-Antrieb Achse 1", "Linearfuehrung"], - "matched_energy_sources": ["EN03"], - "suggested_hazard": { - "title": "Quetschgefahr durch Linearantrieb", - "severity": "high" - } - } - ] - } -}`} - - - - - - - {/* ============================================================ */} - {/* RISK ASSESSMENT */} - {/* ============================================================ */} - -

Risk Assessment

-

- 4-Faktor-Risikobewertung nach ISO 12100 mit den Parametern Schwere (S), - Exposition (E), Eintrittswahrscheinlichkeit (P) und Vermeidbarkeit (A). -

- - - -

Request Body

- - - -{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/hazards/haz_5678/assess" \\ - -H "Authorization: Bearer YOUR_API_KEY" \\ - -H "Content-Type: application/json" \\ - -d '{ - "severity": 4, - "exposure": 3, - "probability": 2, - "avoidance": 3 - }'`} - - -

Response (200 OK)

- -{`{ - "success": true, - "data": { - "hazard_id": "haz_5678", - "severity": 4, - "exposure": 3, - "probability": 2, - "avoidance": 3, - "inherent_risk": 12, - "risk_level": "high", - "c_eff": 0.65, - "residual_risk": 4.2, - "residual_risk_level": "medium", - "risk_acceptable": false, - "recommendation": "Zusaetzliche Schutzmassnahmen erforderlich. 3-Stufen-Hierarchie anwenden." - } -}`} - - - - - -{`curl -X GET "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/risk-summary" \\ - -H "Authorization: Bearer YOUR_API_KEY"`} - - -

Response (200 OK)

- -{`{ - "success": true, - "data": { - "total_hazards": 14, - "assessed": 12, - "unassessed": 2, - "risk_distribution": { - "critical": 1, - "high": 4, - "medium": 5, - "low": 2 - }, - "residual_risk_distribution": { - "critical": 0, - "high": 1, - "medium": 3, - "low": 8 - }, - "average_c_eff": 0.71, - "overall_risk_acceptable": false - } -}`} - - - - - {/* ============================================================ */} - {/* MITIGATIONS */} - {/* ============================================================ */} - -

Mitigations

-

- Massnahmenverwaltung nach der 3-Stufen-Hierarchie gemaess ISO 12100: -

-
    -
  1. Design — Inherent Safe Design (Gefahrenbeseitigung durch Konstruktion)
  2. -
  3. Protective — Schutzeinrichtungen und technische Schutzmassnahmen
  4. -
  5. Information — Benutzerinformation (Warnhinweise, Anleitungen, Schulungen)
  6. -
- - - -

Request Body

- - - -{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/hazards/haz_5678/mitigations" \\ - -H "Authorization: Bearer YOUR_API_KEY" \\ - -H "Content-Type: application/json" \\ - -d '{ - "title": "Schutzgitter mit Sicherheitsschalter", - "description": "Installation eines trennenden Schutzgitters mit Verriegelung nach ISO 14120", - "hierarchy_level": "protective", - "responsible": "Sicherheitsingenieur", - "deadline": "2026-04-30T00:00:00Z" - }'`} - - -

Response (201 Created)

- -{`{ - "success": true, - "data": { - "id": "mit_abcd1234", - "hazard_id": "haz_5678", - "title": "Schutzgitter mit Sicherheitsschalter", - "hierarchy_level": "protective", - "status": "planned", - "verified": false, - "created_at": "2026-03-16T11:00:00Z" - } -}`} - - - - - - - -{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/validate-mitigation-hierarchy" \\ - -H "Authorization: Bearer YOUR_API_KEY"`} - - -

Response (200 OK)

- -{`{ - "success": true, - "data": { - "valid": false, - "violations": [ - { - "hazard_id": "haz_5678", - "hazard_title": "Quetschgefahr durch Linearantrieb", - "issue": "Nur Information-Massnahmen vorhanden. Design- oder Schutzmassnahmen muessen vorrangig angewendet werden.", - "missing_levels": ["design", "protective"] - } - ], - "summary": { - "total_hazards_with_mitigations": 12, - "hierarchy_compliant": 9, - "hierarchy_violations": 3 - } - } -}`} - - - {/* ============================================================ */} - {/* EVIDENCE */} - {/* ============================================================ */} - -

Evidence

-

Evidenz-Dateien hochladen und verwalten (Pruefberichte, Zertifikate, Fotos, etc.).

- - - - -{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/evidence" \\ - -H "Authorization: Bearer YOUR_API_KEY" \\ - -F "file=@pruefbericht_schutzgitter.pdf" \\ - -F "title=Pruefbericht Schutzgitter ISO 14120" \\ - -F "evidence_type=test_report" \\ - -F "linked_mitigation_id=mit_abcd1234"`} - - -

Response (201 Created)

- -{`{ - "success": true, - "data": { - "id": "evi_xyz789", - "title": "Pruefbericht Schutzgitter ISO 14120", - "evidence_type": "test_report", - "file_name": "pruefbericht_schutzgitter.pdf", - "file_size": 245760, - "linked_mitigation_id": "mit_abcd1234", - "created_at": "2026-03-16T12:00:00Z" - } -}`} - - - - - {/* ============================================================ */} - {/* VERIFICATION PLANS */} - {/* ============================================================ */} - -

Verification Plans

-

Verifizierungsplaene erstellen und abarbeiten.

- - - - -{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/verification-plan" \\ - -H "Authorization: Bearer YOUR_API_KEY" \\ - -H "Content-Type: application/json" \\ - -d '{ - "title": "Schutzgitter-Verifizierung", - "description": "Pruefung der Schutzgitter nach ISO 14120", - "method": "inspection", - "linked_mitigation_id": "mit_abcd1234", - "planned_date": "2026-04-15T00:00:00Z" - }'`} - - -

Response (201 Created)

- -{`{ - "success": true, - "data": { - "id": "vp_plan001", - "title": "Schutzgitter-Verifizierung", - "method": "inspection", - "status": "planned", - "linked_mitigation_id": "mit_abcd1234", - "planned_date": "2026-04-15T00:00:00Z", - "created_at": "2026-03-16T12:30:00Z" - } -}`} - - - - - - {/* ============================================================ */} - {/* CE TECHNICAL FILE */} - {/* ============================================================ */} - -

CE Technical File

-

- LLM-gestuetzte Generierung der Technischen Dokumentation (CE Technical File). - Die API generiert alle erforderlichen Abschnitte basierend auf den Projektdaten. -

- - - Die Generierung verwendet einen LLM-Service (qwen3:30b-a3b oder claude-sonnet-4-5) - fuer kontextbasierte Texterstellung. Alle generierten Abschnitte muessen vor der - Freigabe manuell geprueft werden (Human Oversight). - - - - - -{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/tech-file/generate" \\ - -H "Authorization: Bearer YOUR_API_KEY"`} - - -

Response (200 OK)

- -{`{ - "success": true, - "data": { - "sections_generated": 8, - "sections": [ - { - "section": "general_description", - "title": "Allgemeine Beschreibung", - "status": "generated", - "word_count": 450 - }, - { - "section": "risk_assessment", - "title": "Risikobeurteilung", - "status": "generated", - "word_count": 1200 - }, - { - "section": "safety_requirements", - "title": "Sicherheitsanforderungen", - "status": "generated", - "word_count": 800 - }, - { - "section": "verification_results", - "title": "Verifizierungsergebnisse", - "status": "generated", - "word_count": 600 - } - ], - "total_word_count": 4850, - "generation_time_ms": 12500 - } -}`} - - - - - - - - - -

Export-Formate

- - - -{`# PDF Export -curl -X GET "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/tech-file/export?format=pdf" \\ - -H "Authorization: Bearer YOUR_API_KEY" \\ - -o technical-file.pdf - -# Markdown Export -curl -X GET "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/tech-file/export?format=md" \\ - -H "Authorization: Bearer YOUR_API_KEY" \\ - -o technical-file.md`} - - - {/* ============================================================ */} - {/* MONITORING */} - {/* ============================================================ */} - -

Monitoring

-

Post-Market-Surveillance: Monitoring-Ereignisse erfassen und verfolgen.

- - - - -{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/monitoring" \\ - -H "Authorization: Bearer YOUR_API_KEY" \\ - -H "Content-Type: application/json" \\ - -d '{ - "event_type": "incident", - "title": "Schutzgitter-Sensor Fehlausloesung", - "description": "Sicherheitssensor hat ohne erkennbaren Grund ausgeloest", - "severity": "medium", - "occurred_at": "2026-03-15T14:30:00Z" - }'`} - - -

Response (201 Created)

- -{`{ - "success": true, - "data": { - "id": "mon_evt001", - "event_type": "incident", - "title": "Schutzgitter-Sensor Fehlausloesung", - "severity": "medium", - "status": "open", - "occurred_at": "2026-03-15T14:30:00Z", - "created_at": "2026-03-16T08:00:00Z" - } -}`} - - - - - - {/* ============================================================ */} - {/* LIBRARIES (PROJECT-INDEPENDENT) */} - {/* ============================================================ */} - -

Libraries (projektunabhaengig)

-

- Stammdaten-Bibliotheken fuer die Gefahrenanalyse. Diese Endpoints sind - projektunabhaengig und liefern die Referenzdaten fuer die gesamte IACE-Engine. -

- - - - -{`curl -X GET "https://api.breakpilot.io/sdk/v1/iace/hazard-library" \\ - -H "Authorization: Bearer YOUR_API_KEY"`} - - -

Response (200 OK)

- -{`{ - "success": true, - "data": [ - { - "id": "HP001", - "title": "Crushing hazard from closing mechanisms", - "category": "mechanical", - "iso_reference": "ISO 12100 Anhang A.1", - "typical_components": ["actuator", "press", "clamp"], - "severity_range": "medium-critical" - }, - { - "id": "HP045", - "title": "Electric shock from exposed conductors", - "category": "electrical", - "iso_reference": "ISO 12100 Anhang A.2", - "typical_components": ["power_supply", "motor", "controller"], - "severity_range": "high-critical" - } - ], - "meta": { - "total": 102, - "categories": { - "mechanical": 28, - "electrical": 15, - "thermal": 10, - "noise": 8, - "vibration": 7, - "radiation": 9, - "materials": 12, - "ergonomic": 13 - } - } -}`} - - - - - - - - - - - - - {/* ============================================================ */} - {/* AUDIT TRAIL */} - {/* ============================================================ */} - -

Audit Trail

-

Lueckenloser Audit-Trail aller Projektaenderungen fuer Compliance-Nachweise.

- - - - -{`curl -X GET "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/audit-trail" \\ - -H "Authorization: Bearer YOUR_API_KEY"`} - - -

Response (200 OK)

- -{`{ - "success": true, - "data": [ - { - "id": "aud_001", - "action": "hazard_created", - "entity_type": "hazard", - "entity_id": "haz_5678", - "user_id": "user_abc", - "changes": { - "title": "Quetschgefahr durch Linearantrieb", - "severity": "high" - }, - "timestamp": "2026-03-16T10:15:00Z" - }, - { - "id": "aud_002", - "action": "risk_assessed", - "entity_type": "hazard", - "entity_id": "haz_5678", - "user_id": "user_abc", - "changes": { - "inherent_risk": 12, - "risk_level": "high" - }, - "timestamp": "2026-03-16T10:20:00Z" - }, - { - "id": "aud_003", - "action": "tech_file_section_approved", - "entity_type": "tech_file", - "entity_id": "risk_assessment", - "user_id": "user_def", - "changes": { - "status": "approved", - "approved_by": "Dr. Mueller" - }, - "timestamp": "2026-03-16T15:00:00Z" - } - ] -}`} - - - {/* ============================================================ */} - {/* RAG LIBRARY SEARCH */} - {/* ============================================================ */} - -

RAG Library Search

-

- Semantische Suche in der Compliance-Bibliothek via RAG (Retrieval-Augmented Generation). - Ermoeglicht kontextbasierte Anreicherung von Tech-File-Abschnitten. -

- - - - -{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/library-search" \\ - -H "Authorization: Bearer YOUR_API_KEY" \\ - -H "Content-Type: application/json" \\ - -d '{ - "query": "Schutzeinrichtungen fuer Industrieroboter Maschinenverordnung", - "top_k": 5 - }'`} - - -

Response (200 OK)

- -{`{ - "success": true, - "data": { - "query": "Schutzeinrichtungen fuer Industrieroboter Maschinenverordnung", - "results": [ - { - "id": "mr-annex-iii-1.1.4", - "title": "Maschinenverordnung Anhang III 1.1.4 — Schutzmassnahmen", - "content": "Trennende Schutzeinrichtungen muessen fest angebracht oder verriegelt sein...", - "source": "machinery_regulation", - "score": 0.93 - } - ], - "total_results": 5, - "search_time_ms": 38 - } -}`} - - - - - -{`curl -X POST "https://api.breakpilot.io/sdk/v1/iace/projects/proj_a1b2c3d4/tech-file/safety_requirements/enrich" \\ - -H "Authorization: Bearer YOUR_API_KEY"`} - - -

Response (200 OK)

- -{`{ - "success": true, - "data": { - "section": "safety_requirements", - "enriched_content": "... (aktualisierter Abschnitt mit Regulierungsreferenzen) ...", - "citations_added": 4, - "sources": [ - { - "id": "mr-annex-iii-1.1.4", - "title": "Maschinenverordnung Anhang III 1.1.4", - "relevance_score": 0.93 - } - ] - } -}`} - - - {/* ============================================================ */} - {/* SDK INTEGRATION */} - {/* ============================================================ */} - -

SDK Integration

-

- Beispiel fuer die Integration der IACE-API in eine Anwendung: -

- - -{`import { getSDKBackendClient } from '@breakpilot/compliance-sdk' - -const client = getSDKBackendClient() - -// 1. Projekt erstellen -const project = await client.post('/iace/projects', { - machine_name: 'RoboArm X500', - machine_type: 'Industrieroboter', - manufacturer: 'TechCorp GmbH' -}) - -// 2. Aus Firmenprofil initialisieren -await client.post(\`/iace/projects/\${project.id}/init-from-profile\`) - -// 3. Komponenten hinzufuegen -await client.post(\`/iace/projects/\${project.id}/components\`, { - name: 'Servo-Antrieb Achse 1', - component_type: 'actuator', - is_safety_relevant: true -}) - -// 4. Regulierungen klassifizieren -const classifications = await client.post( - \`/iace/projects/\${project.id}/classify\` -) - -// 5. Pattern-Matching ausfuehren -const patterns = await client.post( - \`/iace/projects/\${project.id}/match-patterns\` -) -console.log(\`\${patterns.matches} Gefahren erkannt von \${patterns.total_patterns_checked} Patterns\`) - -// 6. Erkannte Patterns als Gefahren uebernehmen -await client.post(\`/iace/projects/\${project.id}/apply-patterns\`) - -// 7. Risiken bewerten -for (const hazard of await client.get(\`/iace/projects/\${project.id}/hazards\`)) { - await client.post(\`/iace/projects/\${project.id}/hazards/\${hazard.id}/assess\`, { - severity: 3, exposure: 2, probability: 2, avoidance: 2 - }) -} - -// 8. Tech File generieren -const techFile = await client.post( - \`/iace/projects/\${project.id}/tech-file/generate\` -) -console.log(\`\${techFile.sections_generated} Abschnitte generiert\`) - -// 9. PDF exportieren -const pdf = await client.get( - \`/iace/projects/\${project.id}/tech-file/export?format=pdf\` -) -`} - - - - LLM-basierte Endpoints (Tech-File-Generierung, Hazard-Suggest, RAG-Enrichment) - verbrauchen LLM-Tokens. Professional-Plan: 50 Generierungen/Tag. - Enterprise-Plan: unbegrenzt. Implementieren Sie Caching fuer wiederholte Anfragen. - - - - Alle LLM-generierten Inhalte muessen vor der Freigabe manuell geprueft werden. - Die API erzwingt dies ueber den Approve-Workflow: generierte Abschnitte haben - den Status "generated" und muessen explizit auf "approved" gesetzt werden. - + + + + + + + + + + + ) } diff --git a/developer-portal/app/development/byoeh/_components/AuditApiSummarySection.tsx b/developer-portal/app/development/byoeh/_components/AuditApiSummarySection.tsx new file mode 100644 index 0000000..5107652 --- /dev/null +++ b/developer-portal/app/development/byoeh/_components/AuditApiSummarySection.tsx @@ -0,0 +1,123 @@ +import { InfoBox } from '@/components/DevPortalLayout' + +export function AuditApiSummarySection() { + return ( + <> +

10. Audit-Trail: Vollstaendige Nachvollziehbarkeit

+

+ Jede Aktion im Namespace wird revisionssicher im Audit-Log gespeichert. +

+ +
+ + + + + + + + + + + + + + + + + +
EventWas protokolliert wird
uploadDokument hochgeladen (Dateigroesse, Metadaten, Zeitstempel)
indexReferenzdokument indexiert (Anzahl Chunks, Dauer)
rag_queryRAG-Suchanfrage ausgefuehrt (Query-Hash, Anzahl Ergebnisse)
analyzeKI-Verarbeitung gestartet (Dokument-Token, Modell, Dauer)
shareNamespace mit anderem Nutzer geteilt (Empfaenger, Rolle)
revoke_shareZugriff widerrufen (wer, wann)
decryptErgebnis entschluesselt (durch wen, Zeitstempel)
deleteDokument geloescht (Soft Delete, bleibt in Logs)
+
+ +

11. API-Endpunkte (SDK-Referenz)

+

Authentifizierung erfolgt ueber API-Key + JWT-Token.

+ +

11.1 Namespace-Verwaltung

+
+ + + + + + + + + + + + + + +
MethodeEndpunktBeschreibung
POST/api/v1/namespace/uploadVerschluesseltes Dokument hochladen
GET/api/v1/namespace/documentsEigene Dokumente auflisten
GET/api/v1/namespace/documents/{'{id}'}Einzelnes Dokument abrufen
DELETE/api/v1/namespace/documents/{'{id}'}Dokument loeschen (Soft Delete)
+
+ +

11.2 Referenzdokumente & RAG

+
+ + + + + + + + + + + + + + +
MethodeEndpunktBeschreibung
POST/api/v1/namespace/references/uploadReferenzdokument hochladen
POST/api/v1/namespace/references/{'{id}'}/indexReferenz fuer RAG indexieren
POST/api/v1/namespace/rag-queryRAG-Suchanfrage ausfuehren
POST/api/v1/namespace/analyzeKI-Verarbeitung anstossen
+
+ +

11.3 Key Sharing

+
+ + + + + + + + + + + + + + +
MethodeEndpunktBeschreibung
POST/api/v1/namespace/shareNamespace mit anderem Nutzer teilen
GET/api/v1/namespace/sharesAktive Shares auflisten
DELETE/api/v1/namespace/shares/{'{shareId}'}Zugriff widerrufen
GET/api/v1/namespace/shared-with-meMit mir geteilte Namespaces
+
+ +

12. Zusammenfassung: Compliance-Garantien

+ +
+ + + + + + + + + + + + + + + + +
GarantieWie umgesetztRegelwerk
Keine PII verlaesst das KundensystemHeader-Redaction + verschluesselte Identity-MapDSGVO Art. 4 Nr. 5
Betreiber kann nicht mitlesenClient-seitige AES-256-GCM VerschluesselungDSGVO Art. 32
Kein Zugriff durch andere KundenTenant-Isolation (Namespace) auf allen 3 EbenenDSGVO Art. 25
Kein KI-Training mit Kundendatentraining_allowed: false auf allen VektorenAI Act Art. 10
Alles nachvollziehbarVollstaendiger Audit-Trail aller AktionenDSGVO Art. 5 Abs. 2
Kunde behaelt volle KontrolleJederzeitiger Widerruf, Loeschung, DatenexportDSGVO Art. 17, 20
+
+ + + Die Namespace-Technologie ermoeglicht KI-gestuetzte Datenverarbeitung in der Cloud, bei der + keine personenbezogenen Daten das Kundensystem verlassen, alle Daten + Ende-zu-Ende verschluesselt sind, jeder Kunde seinen + eigenen abgeschotteten Namespace hat, und ein + vollstaendiger Audit-Trail jede Aktion dokumentiert. + + + ) +} diff --git a/developer-portal/app/development/byoeh/_components/ByoehIntroSection.tsx b/developer-portal/app/development/byoeh/_components/ByoehIntroSection.tsx new file mode 100644 index 0000000..2a888e8 --- /dev/null +++ b/developer-portal/app/development/byoeh/_components/ByoehIntroSection.tsx @@ -0,0 +1,86 @@ +import { CodeBlock, InfoBox } from '@/components/DevPortalLayout' + +export function ByoehIntroSection() { + return ( + <> +

1. Was ist die Namespace-Technologie?

+

+ Unsere Namespace-Technologie (intern BYOEH -- Bring Your Own Expectation Horizon) + ist eine Privacy-First-Architektur, die es Geschaeftskunden ermoeglicht, sensible Daten + anonym und verschluesselt von KI-Services in der Cloud verarbeiten zu lassen -- ohne dass + personenbezogene Informationen jemals den Client verlassen. +

+
+ “Daten gehen pseudonymisiert und verschluesselt in die Cloud, werden dort + von KI verarbeitet, und kommen verarbeitet zurueck. Nur der Kunde kann die Ergebnisse + wieder den Originaldaten zuordnen -- denn nur sein System hat den Schluessel dafuer.” +
+

Die Architektur basiert auf vier Bausteinen:

+
    +
  1. Pseudonymisierung: Personenbezogene Daten werden durch zufaellige Tokens ersetzt. Nur der Kunde kennt die Zuordnung.
  2. +
  3. Client-seitige Verschluesselung: Alle Daten werden auf dem System des Kunden verschluesselt, bevor sie die Infrastruktur verlassen.
  4. +
  5. Namespace-Isolation: Jeder Kunde erhaelt einen eigenen, vollstaendig abgeschotteten Namespace.
  6. +
  7. KI-Verarbeitung in der Cloud: Die KI arbeitet mit den pseudonymisierten Daten. Ergebnisse gehen zurueck an den Kunden zur lokalen Entschluesselung.
  8. +
+ + + Breakpilot kann die Kundendaten nicht lesen. Der Server sieht nur + verschluesselte Blobs und einen Schluessel-Hash (nicht den Schluessel selbst). Die + Passphrase zum Entschluesseln existiert ausschliesslich auf dem System des Kunden. + + +

2. Typische Anwendungsfaelle

+
+ + + + + + + + + + + + + + + +
BrancheAnwendungsfallSensible Daten
BildungKI-gestuetzte KlausurkorrekturSchuelernamen, Noten, Leistungsdaten
GesundheitswesenMedizinische BefundanalysePatientennamen, Diagnosen, Befunde
RechtVertragsanalyse, Due DiligenceMandantendaten, Vertragsinhalte
PersonalwesenBewerbungsscreening, ZeugnisanalyseBewerberdaten, Gehaltsinformationen
FinanzwesenDokumentenpruefung, Compliance-ChecksKontodaten, Transaktionen, Identitaeten
+
+ +

3. Der komplette Ablauf im Ueberblick

+ +{`SCHRITT 1: DOKUMENTE ERFASSEN & PSEUDONYMISIEREN +SDK empfaengt Dokumente (PDF, Bild, Text) + → Personenbezogene Daten werden erkannt (Header, Namen, IDs) + → PII wird durch zufaellige Tokens ersetzt (doc_token, UUID4) + → Zuordnung "Token → Originalname" wird lokal gesichert + +SCHRITT 2: CLIENT-SEITIGE VERSCHLUESSELUNG +Kunde konfiguriert eine Passphrase im SDK + → SDK leitet daraus einen 256-Bit-Schluessel ab (PBKDF2, 100k Runden) + → Dokumente werden mit AES-256-GCM verschluesselt + → Nur der Hash des Schluessels wird an den Server gesendet + +SCHRITT 3: IDENTITAETS-MAP SICHERN + → Nur mit der Passphrase des Kunden rekonstruierbar + +SCHRITT 4: UPLOAD IN DEN KUNDEN-NAMESPACE + → Jeder Kunde hat eine eigene tenant_id + → Daten werden in MinIO (Storage) + Qdrant (Vektoren) gespeichert + +SCHRITT 5: KI-VERARBEITUNG IN DER CLOUD + → RAG-System durchsucht Referenzdokumente des Kunden + → KI generiert Ergebnisse basierend auf Kundenkontext + +SCHRITT 6: ERGEBNISSE ZURUECK + → SDK entschluesselt die Ergebnisse mit der Passphrase + +SCHRITT 7: RE-IDENTIFIZIERUNG & FINALISIERUNG + → Identitaets-Map wird entschluesselt + → Tokens werden wieder den echten Datensaetzen zugeordnet`} + + + ) +} diff --git a/developer-portal/app/development/byoeh/_components/EncryptionNamespaceSection.tsx b/developer-portal/app/development/byoeh/_components/EncryptionNamespaceSection.tsx new file mode 100644 index 0000000..fb88858 --- /dev/null +++ b/developer-portal/app/development/byoeh/_components/EncryptionNamespaceSection.tsx @@ -0,0 +1,110 @@ +import { CodeBlock, InfoBox } from '@/components/DevPortalLayout' + +export function EncryptionNamespaceSection() { + return ( + <> +

6. Ende-zu-Ende-Verschluesselung

+

+ Die Verschluesselung findet vollstaendig auf dem System des Kunden statt -- + der Cloud-Server bekommt nur verschluesselte Daten zu sehen. +

+ +

6.1 Der Verschluesselungsvorgang

+ +{`┌─────────────────────────────────────────────────────────────────┐ +│ System des Kunden (SDK) │ +├─────────────────────────────────────────────────────────────────┤ +│ 1. Kunde konfiguriert Passphrase im SDK │ +│ → Passphrase bleibt hier -- wird NIE gesendet │ +│ │ +│ 2. Schluessel-Ableitung: │ +│ PBKDF2-SHA256(Passphrase, zufaelliger Salt, 100.000 Runden) │ +│ → Ergebnis: 256-Bit-Schluessel (32 Bytes) │ +│ │ +│ 3. Verschluesselung: │ +│ AES-256-GCM(Schluessel, zufaelliger IV, Dokument) │ +│ → GCM: Garantiert Integritaet (Manipulation erkennbar) │ +│ │ +│ 4. Schluessel-Hash: │ +│ SHA-256(abgeleiteter Schluessel) → Hash fuer Verifikation │ +│ → Vom Hash kann der Schluessel NICHT zurueckberechnet werden│ +│ │ +│ 5. Upload: Nur diese Daten gehen an den Cloud-Server: │ +│ • Verschluesselter Blob • Salt • IV • Schluessel-Hash │ +│ │ +│ Was NICHT an den Server geht: │ +│ ✗ Passphrase ✗ Abgeleiteter Schluessel ✗ Klartext │ +└─────────────────────────────────────────────────────────────────┘`} + + +

6.2 Sicherheitsgarantien

+
+ + + + + + + + + + + + + + + +
AngriffsszenarioWas der Angreifer siehtErgebnis
Cloud-Server wird gehacktVerschluesselte Blobs + HashesKeine lesbaren Dokumente
Datenbank wird geleaktencrypted_identity_map (verschluesselt)Keine personenbezogenen Daten
Netzwerkverkehr abgefangenVerschluesselte Daten (TLS + AES)Doppelt verschluesselt
Betreiber (Breakpilot) will mitlesenVerschluesselte Blobs, kein SchluesselOperator Blindness
Anderer Kunde versucht ZugriffNichts (Tenant-Isolation)Namespace blockiert
+
+ +

7. Namespace-Isolation: Jeder Kunde hat seinen eigenen Bereich

+

+ Ein Namespace ist ein vollstaendig abgeschotteter Bereich im System -- wie + separate Tresorraeume in einer Bank. Jeder Kunde hat seinen eigenen Raum, + und kein Schluessel passt in einen anderen. +

+ + +{`Kunde A (tenant_id: "firma-alpha-001") +├── Dokument 1 (verschluesselt) +└── Referenz: Pruefkriterien 2025 + +Kunde B (tenant_id: "firma-beta-002") +└── Referenz: Compliance-Vorgaben 2025 + +Suchanfrage von Kunde A: + → Suche NUR in tenant_id = "firma-alpha-001" + → Kunde B's Daten sind UNSICHTBAR + +Jede Qdrant-Query hat diesen Pflichtfilter: + must_conditions = [ + FieldCondition(key="tenant_id", match="firma-alpha-001") + ]`} + + +

7.2 Drei Ebenen der Isolation

+
+ + + + + + + + + + + + + +
EbeneSystemIsolation
DateisystemMinIO (S3-Storage)Eigener Bucket/Pfad pro Kunde
VektordatenbankQdrantPflichtfilter tenant_id bei jeder Suche
Metadaten-DBPostgreSQLJede Tabelle hat tenant_id als Pflichtfeld
+
+ + + Auf allen Vektoren in Qdrant ist das Flag training_allowed: false gesetzt. + Kundeninhalte werden ausschliesslich fuer RAG-Suchen innerhalb des + Kunden-Namespace verwendet. + + + ) +} diff --git a/developer-portal/app/development/byoeh/_components/RagKeySharingSection.tsx b/developer-portal/app/development/byoeh/_components/RagKeySharingSection.tsx new file mode 100644 index 0000000..4d02fe6 --- /dev/null +++ b/developer-portal/app/development/byoeh/_components/RagKeySharingSection.tsx @@ -0,0 +1,100 @@ +import { CodeBlock } from '@/components/DevPortalLayout' + +export function RagKeySharingSection() { + return ( + <> +

8. RAG-Pipeline: KI-Verarbeitung mit Kundenkontext

+

+ Die KI nutzt die vom Kunden hochgeladenen Referenzdokumente als Wissensbasis. + Dieser Prozess heisst RAG (Retrieval Augmented Generation). +

+ +

8.1 Indexierung der Referenzdokumente

+ +{`Referenzdokument (verschluesselt auf Server) + | + v +┌────────────────────────────────────┐ +│ 1. Passphrase-Verifikation │ ← SDK sendet Schluessel-Hash +└──────────┬─────────────────────────┘ + v +┌────────────────────────────────────┐ +│ 2. Entschluesselung │ ← Temporaer im Arbeitsspeicher +└──────────┬─────────────────────────┘ + v +┌────────────────────────────────────┐ +│ 3. Text-Extraktion │ ← PDF → Klartext +└──────────┬─────────────────────────┘ + v +┌────────────────────────────────────┐ +│ 4. Chunking │ ← ~1.000-Zeichen-Abschnitte +└──────────┬─────────────────────────┘ + v +┌────────────────────────────────────┐ +│ 5. Embedding │ ← Text → 1.536 Zahlen +└──────────┬─────────────────────────┘ + v +┌────────────────────────────────────┐ +│ 6. Re-Encryption │ ← Jeder Chunk wird erneut verschluesselt +└──────────┬─────────────────────────┘ + v +┌────────────────────────────────────┐ +│ 7. Qdrant-Indexierung │ ← Vektor + verschluesselter Chunk +│ tenant_id: "firma-alpha-001" │ mit Tenant-Filter gespeichert +│ training_allowed: false │ +└────────────────────────────────────┘`} + + +

8.2 Wie die KI eine Anfrage bearbeitet (RAG-Query)

+
    +
  1. Anfrage formulieren: Das SDK sendet eine Suchanfrage mit dem zu verarbeitenden Dokument.
  2. +
  3. Semantische Suche: Die Anfrage wird in einen Vektor umgewandelt und gegen die Referenz-Vektoren in Qdrant gesucht -- nur im Namespace des Kunden.
  4. +
  5. Entschluesselung: Die gefundenen Chunks werden mit der Passphrase des Kunden entschluesselt.
  6. +
  7. KI-Antwort: Die entschluesselten Referenzpassagen werden als Kontext an die KI uebergeben.
  8. +
+ +

9. Key Sharing: Zusammenarbeit ermoeglichen

+

+ Das Key-Sharing-System ermoeglicht es dem Eigentuemer, seinen Namespace sicher mit + anderen zu teilen (z.B. fuer Vier-Augen-Prinzip, Qualitaetskontrolle oder externe Audits). +

+ +

9.1 Einladungs-Workflow

+ +{`Eigentuemer Server Eingeladener + │ │ │ + │ 1. Einladung senden │ │ + │─────────────────────────────────▶ │ + │ │ 2. Einladung erstellt │ + │ │ (14 Tage gueltig) │ + │ │ 3. Benachrichtigung ──────▶│ + │ │ 4. Einladung annehmen + │ │◀─────────────────────────────│ + │ │ 5. Key-Share erstellt │ + │ │ 6. Eingeladener kann ──────▶│ + │ │ Daten im Namespace │ + │ │ abfragen │ + │ 7. Zugriff widerrufen │ │ + │─────────────────────────────────▶ Share deaktiviert │`} + + +

9.2 Rollen beim Key-Sharing

+
+ + + + + + + + + + + + + +
RolleTypischer NutzerRechte
OwnerProjektverantwortlicherVollzugriff, kann teilen & widerrufen
ReviewerQualitaetssicherungLesen, RAG-Queries, eigene Anmerkungen
AuditorExterner PrueferNur Lesen (Aufsichtsfunktion)
+
+ + ) +} diff --git a/developer-portal/app/development/byoeh/_components/SdkPseudonymSection.tsx b/developer-portal/app/development/byoeh/_components/SdkPseudonymSection.tsx new file mode 100644 index 0000000..1f00fe0 --- /dev/null +++ b/developer-portal/app/development/byoeh/_components/SdkPseudonymSection.tsx @@ -0,0 +1,113 @@ +import { CodeBlock, InfoBox } from '@/components/DevPortalLayout' + +export function SdkPseudonymSection() { + return ( + <> +

4. SDK-Integration

+ +{`import { BreakpilotSDK, NamespaceClient } from '@breakpilot/compliance-sdk' + +// 1. SDK initialisieren mit API-Key +const sdk = new BreakpilotSDK({ + apiKey: process.env.BREAKPILOT_API_KEY, + endpoint: 'https://api.breakpilot.de' +}) + +// 2. Namespace-Client erstellen (pro Mandant/Abteilung) +const namespace = sdk.createNamespace({ + tenantId: 'kunde-firma-abc', + passphrase: process.env.ENCRYPTION_PASSPHRASE // Bleibt lokal! +}) + +// 3. Dokument pseudonymisieren & verschluesselt hochladen +const result = await namespace.upload({ + file: documentBuffer, + metadata: { type: 'vertrag', category: 'due-diligence' }, + pseudonymize: true, + headerRedaction: true +}) +// result.docToken = "a7f3c2d1-4e9b-4a5f-8c7d-..." + +// 4. Referenzdokument hochladen (z.B. Pruefkriterien) +await namespace.uploadReference({ + file: referenceBuffer, + title: 'Pruefkriterien Vertrag Typ A' +}) + +// 5. KI-Verarbeitung anstossen +const analysis = await namespace.analyze({ + docToken: result.docToken, + prompt: 'Pruefe den Vertrag gegen die Referenzkriterien', + useRAG: true +}) + +// 6. Ergebnisse entschluesseln (passiert automatisch im SDK) +console.log(analysis.findings) +console.log(analysis.score) + +// 7. Re-Identifizierung (Token → Originalname) +const identityMap = await namespace.getIdentityMap() +const originalName = identityMap[result.docToken]`} + + + + Die Passphrase verlässt niemals das System des Kunden. Das SDK verschluesselt + und entschluesselt ausschliesslich lokal. + + +

5. Pseudonymisierung: Wie personenbezogene Daten entfernt werden

+ +

5.1 Der doc_token: Ein zufaelliger Identifikator

+

+ Jedes Dokument erhaelt einen doc_token -- einen 128-Bit-Zufallscode im + UUID4-Format. Dieser Token ist kryptographisch zufaellig, kann + nicht zurueckgerechnet werden und dient als eindeutiger Schluessel + fuer die spaetere Re-Identifizierung. +

+ +

5.2 Header-Redaction: PII wird entfernt

+
+ + + + + + + + + + + + + + + + + + + + +
MethodeWie es funktioniertWann verwenden
Einfache RedactionDefinierter Bereich des Dokuments wird entferntStandardisierte Formulare mit festem Layout
Smarte RedactionOpenCV/NER erkennt Textbereiche mit PII und entfernt gezieltFreitext-Dokumente, variable Layouts
+
+ +

5.3 Die Identitaets-Map: Nur der Kunde kennt die Zuordnung

+ +{`NamespaceSession +├── tenant_id = "kunde-firma-abc" ← Pflichtfeld (Isolation) +├── encrypted_identity_map = [verschluesselte Bytes] ← Nur mit Passphrase lesbar +├── identity_map_iv = "a3f2c1..." +│ +└── PseudonymizedDocument (pro Dokument) + ├── doc_token = "a7f3c2d1-..." ← Zufaelliger Token (Primary Key) + ├── session_id = [Referenz] + └── (Kein Name, keine personenbezogenen Daten)`} + + + + Die Pseudonymisierung erfuellt die Definition der DSGVO: Personenbezogene Daten + koennen ohne Hinzuziehung zusaetzlicher Informationen + (der verschluesselten Identitaets-Map + der Passphrase) nicht mehr einer bestimmten Person zugeordnet werden. + + + ) +} diff --git a/developer-portal/app/development/byoeh/page.tsx b/developer-portal/app/development/byoeh/page.tsx index 3eb827e..db1c35b 100644 --- a/developer-portal/app/development/byoeh/page.tsx +++ b/developer-portal/app/development/byoeh/page.tsx @@ -1,4 +1,9 @@ -import { DevPortalLayout, CodeBlock, InfoBox } from '@/components/DevPortalLayout' +import { DevPortalLayout } from '@/components/DevPortalLayout' +import { ByoehIntroSection } from './_components/ByoehIntroSection' +import { SdkPseudonymSection } from './_components/SdkPseudonymSection' +import { EncryptionNamespaceSection } from './_components/EncryptionNamespaceSection' +import { RagKeySharingSection } from './_components/RagKeySharingSection' +import { AuditApiSummarySection } from './_components/AuditApiSummarySection' export default function BYOEHDocsPage() { return ( @@ -6,764 +11,11 @@ export default function BYOEHDocsPage() { title="Namespace-Technologie fuer Geschaeftskunden" description="Wie das SDK sensible Daten anonymisiert, verschluesselt und sicher in der Cloud verarbeiten laesst -- ohne dass der Betreiber Zugriff auf Klartext hat." > - {/* ============================================================ */} - {/* 1. EINLEITUNG */} - {/* ============================================================ */} -

1. Was ist die Namespace-Technologie?

-

- Unsere Namespace-Technologie (intern BYOEH -- Bring Your Own Expectation Horizon) - ist eine Privacy-First-Architektur, die es Geschaeftskunden ermoeglicht, sensible Daten - anonym und verschluesselt von KI-Services in der Cloud verarbeiten zu lassen -- ohne dass - personenbezogene Informationen jemals den Client verlassen. -

-
- “Daten gehen pseudonymisiert und verschluesselt in die Cloud, werden dort - von KI verarbeitet, und kommen verarbeitet zurueck. Nur der Kunde kann die Ergebnisse - wieder den Originaldaten zuordnen -- denn nur sein System hat den Schluessel dafuer.” -
-

- Das SDK loest ein grundlegendes Problem fuer Unternehmen: KI-gestuetzte - Datenverarbeitung ohne Datenschutzrisiko. Die Architektur basiert auf vier Bausteinen: -

-
    -
  1. - Pseudonymisierung: Personenbezogene Daten werden durch zufaellige - Tokens ersetzt. Nur der Kunde kennt die Zuordnung. -
  2. -
  3. - Client-seitige Verschluesselung: Alle Daten werden auf dem System - des Kunden verschluesselt, bevor sie die Infrastruktur verlassen. Der Cloud-Server - sieht nur verschluesselte Blobs. -
  4. -
  5. - Namespace-Isolation: Jeder Kunde erhaelt einen eigenen, vollstaendig - abgeschotteten Namespace. Kein Kunde kann auf Daten eines anderen zugreifen. -
  6. -
  7. - KI-Verarbeitung in der Cloud: Die KI arbeitet mit den pseudonymisierten - Daten und den vom Kunden bereitgestellten Referenzdokumenten. Ergebnisse gehen zurueck - an den Kunden zur lokalen Entschluesselung und Re-Identifizierung. -
  8. -
- - - Breakpilot kann die Kundendaten nicht lesen. Der Server sieht nur - verschluesselte Blobs und einen Schluessel-Hash (nicht den Schluessel selbst). Die - Passphrase zum Entschluesseln existiert ausschliesslich auf dem System des Kunden - und wird niemals uebertragen. Selbst ein Angriff auf die Cloud-Infrastruktur wuerde keine - Klartextdaten preisgeben. - - - {/* ============================================================ */} - {/* 2. ANWENDUNGSFAELLE */} - {/* ============================================================ */} -

2. Typische Anwendungsfaelle

-

- Die Namespace-Technologie ist ueberall einsetzbar, wo sensible Daten von einer KI - verarbeitet werden sollen, ohne den Datenschutz zu gefaehrden: -

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
BrancheAnwendungsfallSensible Daten
BildungKI-gestuetzte KlausurkorrekturSchuelernamen, Noten, Leistungsdaten
GesundheitswesenMedizinische BefundanalysePatientennamen, Diagnosen, Befunde
RechtVertragsanalyse, Due DiligenceMandantendaten, Vertragsinhalte
PersonalwesenBewerbungsscreening, ZeugnisanalyseBewerberdaten, Gehaltsinformationen
FinanzwesenDokumentenpruefung, Compliance-ChecksKontodaten, Transaktionen, Identitaeten
-
- - {/* ============================================================ */} - {/* 3. DER KOMPLETTE ABLAUF */} - {/* ============================================================ */} -

3. Der komplette Ablauf im Ueberblick

-

- Der Prozess laesst sich in sieben Schritte unterteilen. Die gesamte - Pseudonymisierung und Verschluesselung geschieht auf dem System des Kunden, - bevor Daten in die Cloud gesendet werden: -

- - -{`SCHRITT 1: DOKUMENTE ERFASSEN & PSEUDONYMISIEREN -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -SDK empfaengt Dokumente (PDF, Bild, Text) - → Personenbezogene Daten werden erkannt (Header, Namen, IDs) - → PII wird durch zufaellige Tokens ersetzt (doc_token, UUID4) - → Zuordnung "Token → Originalname" wird lokal gesichert - -SCHRITT 2: CLIENT-SEITIGE VERSCHLUESSELUNG -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Kunde konfiguriert eine Passphrase im SDK - → SDK leitet daraus einen 256-Bit-Schluessel ab (PBKDF2, 100k Runden) - → Dokumente werden mit AES-256-GCM verschluesselt - → Nur der Hash des Schluessels wird an den Server gesendet - → Passphrase und Schluessel verlassen NIEMALS das Kundensystem - -SCHRITT 3: IDENTITAETS-MAP SICHERN -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Die Zuordnung "Token → Originaldaten" wird verschluesselt gespeichert: - → Nur mit der Passphrase des Kunden rekonstruierbar - → Ohne Passphrase ist keine Re-Identifizierung moeglich - -SCHRITT 4: UPLOAD IN DEN KUNDEN-NAMESPACE -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Verschluesselte Dateien gehen in den isolierten Namespace: - → Jeder Kunde hat eine eigene tenant_id - → Daten werden in MinIO (Storage) + Qdrant (Vektoren) gespeichert - → Server sieht: verschluesselter Blob + Schluessel-Hash + Salt - -SCHRITT 5: KI-VERARBEITUNG IN DER CLOUD -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -KI verarbeitet die pseudonymisierten Daten: - → RAG-System durchsucht Referenzdokumente des Kunden - → KI generiert Ergebnisse basierend auf Kundenkontext - → Ergebnisse sind an den Namespace gebunden - -SCHRITT 6: ERGEBNISSE ZURUECK -━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -KI-Ergebnisse gehen an das Kundensystem: - → SDK entschluesselt die Ergebnisse mit der Passphrase - → Kunde sieht aufbereitete Ergebnisse im Klartext - -SCHRITT 7: RE-IDENTIFIZIERUNG & FINALISIERUNG -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Kunde ordnet Ergebnisse den Originaldaten zu: - → Identitaets-Map wird entschluesselt - → Tokens werden wieder den echten Datensaetzen zugeordnet - → Fertige Ergebnisse stehen im Originalsystem bereit`} - - - {/* ============================================================ */} - {/* 4. SDK-INTEGRATION */} - {/* ============================================================ */} -

4. SDK-Integration

-

- Die Integration in bestehende Systeme erfolgt ueber unser SDK. Nachfolgend ein - vereinfachtes Beispiel, wie ein Kunde das SDK nutzt: -

- - -{`import { BreakpilotSDK, NamespaceClient } from '@breakpilot/compliance-sdk' - -// 1. SDK initialisieren mit API-Key -const sdk = new BreakpilotSDK({ - apiKey: process.env.BREAKPILOT_API_KEY, - endpoint: 'https://api.breakpilot.de' -}) - -// 2. Namespace-Client erstellen (pro Mandant/Abteilung) -const namespace = sdk.createNamespace({ - tenantId: 'kunde-firma-abc', - passphrase: process.env.ENCRYPTION_PASSPHRASE // Bleibt lokal! -}) - -// 3. Dokument pseudonymisieren & verschluesselt hochladen -const result = await namespace.upload({ - file: documentBuffer, - metadata: { type: 'vertrag', category: 'due-diligence' }, - pseudonymize: true, // PII automatisch ersetzen - headerRedaction: true // Kopfbereich entfernen -}) -// result.docToken = "a7f3c2d1-4e9b-4a5f-8c7d-..." - -// 4. Referenzdokument hochladen (z.B. Pruefkriterien) -await namespace.uploadReference({ - file: referenceBuffer, - title: 'Pruefkriterien Vertrag Typ A' -}) - -// 5. KI-Verarbeitung anstossen -const analysis = await namespace.analyze({ - docToken: result.docToken, - prompt: 'Pruefe den Vertrag gegen die Referenzkriterien', - useRAG: true -}) - -// 6. Ergebnisse entschluesseln (passiert automatisch im SDK) -console.log(analysis.findings) // Klartext-Ergebnisse -console.log(analysis.score) // Bewertung - -// 7. Re-Identifizierung (Token → Originalname) -const identityMap = await namespace.getIdentityMap() -const originalName = identityMap[result.docToken]`} - - - - Die Passphrase verlässt niemals das System des Kunden. Das SDK verschluesselt - und entschluesselt ausschliesslich lokal. Breakpilot hat zu keinem - Zeitpunkt Zugriff auf Klartextdaten oder den Verschluesselungsschluessel. - - - {/* ============================================================ */} - {/* 5. PSEUDONYMISIERUNG */} - {/* ============================================================ */} -

5. Pseudonymisierung: Wie personenbezogene Daten entfernt werden

-

- Pseudonymisierung bedeutet: personenbezogene Daten werden durch zufaellige - Tokens ersetzt, sodass ohne Zusatzinformation kein Rueckschluss auf die Person - moeglich ist. Das SDK bietet zwei Mechanismen: -

- -

5.1 Der doc_token: Ein zufaelliger Identifikator

-

- Jedes Dokument erhaelt einen doc_token -- einen 128-Bit-Zufallscode im - UUID4-Format (z.B. a7f3c2d1-4e9b-4a5f-8c7d-6b2e1f0a9d3c). Dieser Token: -

-
    -
  • Ist kryptographisch zufaellig -- es gibt keinen Zusammenhang zwischen - Token und Originaldatensatz
  • -
  • Kann nicht zurueckgerechnet werden -- auch mit Kenntnis des Algorithmus - ist kein Rueckschluss moeglich
  • -
  • Dient als eindeutiger Schluessel, um Ergebnisse spaeter dem - Originaldokument zuzuordnen
  • -
- -

5.2 Header-Redaction: PII wird entfernt

-

- Bei Dokumenten mit erkennbarem Kopfbereich (Namen, Adressen, IDs) kann das SDK diesen - Bereich automatisch entfernen. Die Entfernung ist permanent: - Die Originaldaten werden nicht an den Server uebermittelt. -

- -
- - - - - - - - - - - - - - - - - - - - -
MethodeWie es funktioniertWann verwenden
Einfache RedactionDefinierter Bereich des Dokuments wird entferntStandardisierte Formulare mit festem Layout
Smarte RedactionOpenCV/NER erkennt Textbereiche mit PII und entfernt gezieltFreitext-Dokumente, variable Layouts
-
- -

5.3 Die Identitaets-Map: Nur der Kunde kennt die Zuordnung

-

- Die Zuordnung doc_token → Originaldaten wird als verschluesselte Tabelle - gespeichert. Das Datenmodell sieht vereinfacht so aus: -

- - -{`NamespaceSession -├── tenant_id = "kunde-firma-abc" ← Pflichtfeld (Isolation) -├── encrypted_identity_map = [verschluesselte Bytes] ← Nur mit Passphrase lesbar -├── identity_map_iv = "a3f2c1..." ← Initialisierungsvektor (fuer AES) -│ -└── PseudonymizedDocument (pro Dokument) - ├── doc_token = "a7f3c2d1-..." ← Zufaelliger Token (Primary Key) - ├── session_id = [Referenz] - └── (Kein Name, keine personenbezogenen Daten)`} - - - - Die Pseudonymisierung erfuellt die Definition der DSGVO: Personenbezogene Daten - koennen ohne Hinzuziehung zusaetzlicher Informationen - (der verschluesselten Identitaets-Map + der Passphrase des Kunden) nicht mehr einer - bestimmten Person zugeordnet werden. - - - {/* ============================================================ */} - {/* 6. VERSCHLUESSELUNG */} - {/* ============================================================ */} -

6. Ende-zu-Ende-Verschluesselung

-

- Die Verschluesselung ist das Herzstueck des Datenschutzes. Sie findet vollstaendig - auf dem System des Kunden statt -- der Cloud-Server bekommt nur verschluesselte - Daten zu sehen. -

- -

6.1 Der Verschluesselungsvorgang

- - -{`┌─────────────────────────────────────────────────────────────────┐ -│ System des Kunden (SDK) │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ 1. Kunde konfiguriert Passphrase im SDK │ -│ │ ↑ │ -│ │ │ Passphrase bleibt hier -- wird NIE gesendet │ -│ ▼ │ -│ 2. Schluessel-Ableitung: │ -│ PBKDF2-SHA256(Passphrase, zufaelliger Salt, 100.000 Runden) │ -│ │ │ -│ │ → Ergebnis: 256-Bit-Schluessel (32 Bytes) │ -│ │ → 100.000 Runden machen Brute-Force unpraktikabel │ -│ ▼ │ -│ 3. Verschluesselung: │ -│ AES-256-GCM(Schluessel, zufaelliger IV, Dokument) │ -│ │ │ -│ │ → AES-256: Militaerstandard, 2^256 moegliche Schluessel │ -│ │ → GCM: Garantiert Integritaet (Manipulation erkennbar) │ -│ ▼ │ -│ 4. Schluessel-Hash: │ -│ SHA-256(abgeleiteter Schluessel) → Hash fuer Verifikation │ -│ │ │ -│ │ → Server speichert nur diesen Hash │ -│ │ → Damit kann geprueft werden ob die Passphrase stimmt │ -│ │ → Vom Hash kann der Schluessel NICHT zurueckberechnet │ -│ │ werden │ -│ ▼ │ -│ 5. Upload: Nur diese Daten gehen an den Cloud-Server: │ -│ • Verschluesselter Blob (unlesbar ohne Schluessel) │ -│ • Salt (zufaellige Bytes, harmlos) │ -│ • IV (Initialisierungsvektor, harmlos) │ -│ • Schluessel-Hash (zur Verifikation, nicht umkehrbar) │ -│ │ -│ Was NICHT an den Server geht: │ -│ ✗ Passphrase │ -│ ✗ Abgeleiteter Schluessel │ -│ ✗ Unverschluesselter Klartext │ -└─────────────────────────────────────────────────────────────────┘`} - - -

6.2 Sicherheitsgarantien

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
AngriffsszenarioWas der Angreifer siehtErgebnis
Cloud-Server wird gehacktVerschluesselte Blobs + HashesKeine lesbaren Dokumente
Datenbank wird geleaktencrypted_identity_map (verschluesselt)Keine personenbezogenen Daten
Netzwerkverkehr abgefangenVerschluesselte Daten (TLS + AES)Doppelt verschluesselt
Betreiber (Breakpilot) will mitlesenVerschluesselte Blobs, kein SchluesselOperator Blindness
Anderer Kunde versucht ZugriffNichts (Tenant-Isolation)Namespace blockiert
-
- - {/* ============================================================ */} - {/* 7. NAMESPACE / TENANT-ISOLATION */} - {/* ============================================================ */} -

7. Namespace-Isolation: Jeder Kunde hat seinen eigenen Bereich

-

- Ein Namespace (auch “Tenant” genannt) ist ein vollstaendig - abgeschotteter Bereich im System. Man kann es sich wie separate Tresorraeume - in einer Bank vorstellen: Jeder Kunde hat seinen eigenen Raum, und kein Schluessel - passt in einen anderen. -

- -

7.1 Wie die Isolation funktioniert

-

- Jeder Kunde erhaelt eine eindeutige tenant_id. Diese ID wird - bei jeder einzelnen Datenbankabfrage als Pflichtfilter verwendet: -

- - -{`Kunde A (tenant_id: "firma-alpha-001") -├── Dokument 1 (verschluesselt) -├── Dokument 2 (verschluesselt) -└── Referenz: Pruefkriterien 2025 - -Kunde B (tenant_id: "firma-beta-002") -├── Dokument 1 (verschluesselt) -└── Referenz: Compliance-Vorgaben 2025 - -Suchanfrage von Kunde A: - "Welche Klauseln weichen von den Referenzkriterien ab?" - → Suche NUR in tenant_id = "firma-alpha-001" - → Kunde B's Daten sind UNSICHTBAR - -Jede Qdrant-Query hat diesen Pflichtfilter: - must_conditions = [ - FieldCondition(key="tenant_id", match="firma-alpha-001") - ] - -Es gibt KEINE Abfrage ohne tenant_id-Filter.`} - - -

7.2 Drei Ebenen der Isolation

-
- - - - - - - - - - - - - - - - - - - - - - - - - -
EbeneSystemIsolation
DateisystemMinIO (S3-Storage)Eigener Bucket/Pfad pro Kunde: /tenant-id/doc-id/encrypted.bin
VektordatenbankQdrantPflichtfilter tenant_id bei jeder Suche
Metadaten-DBPostgreSQLJede Tabelle hat tenant_id als Pflichtfeld
-
- - - Auf allen Vektoren in Qdrant ist das Flag training_allowed: false gesetzt. - Kundeninhalte werden ausschliesslich fuer RAG-Suchen innerhalb des - Kunden-Namespace verwendet und niemals zum Trainieren eines KI-Modells - eingesetzt. - - - {/* ============================================================ */} - {/* 8. RAG-PIPELINE */} - {/* ============================================================ */} -

8. RAG-Pipeline: KI-Verarbeitung mit Kundenkontext

-

- Die KI nutzt die vom Kunden hochgeladenen Referenzdokumente als Wissensbasis. - Dieser Prozess heisst RAG (Retrieval Augmented Generation): - Die KI “liest” zuerst die relevanten Referenzen und generiert dann - kontextbezogene Ergebnisse. -

- -

8.1 Indexierung der Referenzdokumente

- -{`Referenzdokument (verschluesselt auf Server) - | - v -┌────────────────────────────────────┐ -│ 1. Passphrase-Verifikation │ ← SDK sendet Schluessel-Hash -│ Hash pruefen │ Server vergleicht mit gespeichertem Hash -└──────────┬─────────────────────────┘ - | - v -┌────────────────────────────────────┐ -│ 2. Entschluesselung │ ← Temporaer im Arbeitsspeicher -│ AES-256-GCM Decrypt │ (wird nach Verarbeitung geloescht) -└──────────┬─────────────────────────┘ - | - v -┌────────────────────────────────────┐ -│ 3. Text-Extraktion │ ← PDF → Klartext -│ Tabellen, Listen, Absaetze │ -└──────────┬─────────────────────────┘ - | - v -┌────────────────────────────────────┐ -│ 4. Chunking │ ← Text in ~1.000-Zeichen-Abschnitte -│ Ueberlappung: 200 Zeichen │ (mit Ueberlappung fuer Kontexterhalt) -└──────────┬─────────────────────────┘ - | - v -┌────────────────────────────────────┐ -│ 5. Embedding │ ← Jeder Abschnitt wird in einen -│ Text → 1.536 Zahlen │ Bedeutungsvektor umgewandelt -└──────────┬─────────────────────────┘ - | - v -┌────────────────────────────────────┐ -│ 6. Re-Encryption │ ← Jeder Chunk wird ERNEUT verschluesselt -│ AES-256-GCM pro Chunk │ bevor er gespeichert wird -└──────────┬─────────────────────────┘ - | - v -┌────────────────────────────────────┐ -│ 7. Qdrant-Indexierung │ ← Vektor + verschluesselter Chunk -│ tenant_id: "firma-alpha-001" │ werden mit Tenant-Filter gespeichert -│ training_allowed: false │ -└────────────────────────────────────┘`} - - -

8.2 Wie die KI eine Anfrage bearbeitet (RAG-Query)

-
    -
  1. - Anfrage formulieren: Das SDK sendet eine Suchanfrage mit dem - zu verarbeitenden Dokument und den gewuenschten Kriterien. -
  2. -
  3. - Semantische Suche: Die Anfrage wird in einen Vektor umgewandelt und - gegen die Referenz-Vektoren in Qdrant gesucht -- nur im Namespace des Kunden. -
  4. -
  5. - Entschluesselung: Die gefundenen Chunks werden mit der Passphrase - des Kunden entschluesselt. -
  6. -
  7. - KI-Antwort: Die entschluesselten Referenzpassagen werden als Kontext - an die KI uebergeben, die daraus ein Ergebnis generiert. -
  8. -
- - {/* ============================================================ */} - {/* 9. KEY SHARING */} - {/* ============================================================ */} -

9. Key Sharing: Zusammenarbeit ermoeglichen

-

- In vielen Geschaeftsprozessen muessen mehrere Personen oder Abteilungen auf die gleichen - Daten zugreifen -- z.B. fuer Vier-Augen-Prinzip, Qualitaetskontrolle oder externe Audits. - Das Key-Sharing-System ermoeglicht es dem Eigentuemer, seinen Namespace sicher mit - anderen zu teilen. -

- -

9.1 Einladungs-Workflow

- -{`Eigentuemer Server Eingeladener - │ │ │ - │ 1. Einladung senden │ │ - │ (E-Mail + Rolle + Scope) │ │ - │─────────────────────────────────▶ │ - │ │ │ - │ │ 2. Einladung erstellt │ - │ │ (14 Tage gueltig) │ - │ │ │ - │ │ 3. Benachrichtigung ──────▶│ - │ │ │ - │ │ 4. Einladung annehmen - │ │◀─────────────────────────────│ - │ │ │ - │ │ 5. Key-Share erstellt │ - │ │ (verschluesselte │ - │ │ Passphrase) │ - │ │ │ - │ │ 6. Eingeladener kann ──────▶│ - │ │ jetzt Daten im │ - │ │ Namespace abfragen │ - │ │ │ - │ 7. Zugriff widerrufen │ │ - │ (jederzeit moeglich) │ │ - │─────────────────────────────────▶ │ - │ │ Share deaktiviert │`} - - -

9.2 Rollen beim Key-Sharing

-
- - - - - - - - - - - - - -
RolleTypischer NutzerRechte
OwnerProjektverantwortlicherVollzugriff, kann teilen & widerrufen
ReviewerQualitaetssicherungLesen, RAG-Queries, eigene Anmerkungen
AuditorExterner PrueferNur Lesen (Aufsichtsfunktion)
-
- - {/* ============================================================ */} - {/* 10. AUDIT-TRAIL */} - {/* ============================================================ */} -

10. Audit-Trail: Vollstaendige Nachvollziehbarkeit

-

- Jede Aktion im Namespace wird revisionssicher im Audit-Log gespeichert. - Das ist essenziell fuer Compliance-Anforderungen und externe Audits. -

- -
- - - - - - - - - - - - - - - - - -
EventWas protokolliert wird
uploadDokument hochgeladen (Dateigroesse, Metadaten, Zeitstempel)
indexReferenzdokument indexiert (Anzahl Chunks, Dauer)
rag_queryRAG-Suchanfrage ausgefuehrt (Query-Hash, Anzahl Ergebnisse)
analyzeKI-Verarbeitung gestartet (Dokument-Token, Modell, Dauer)
shareNamespace mit anderem Nutzer geteilt (Empfaenger, Rolle)
revoke_shareZugriff widerrufen (wer, wann)
decryptErgebnis entschluesselt (durch wen, Zeitstempel)
deleteDokument geloescht (Soft Delete, bleibt in Logs)
-
- - {/* ============================================================ */} - {/* 11. API-ENDPUNKTE */} - {/* ============================================================ */} -

11. API-Endpunkte (SDK-Referenz)

-

- Die folgenden Endpunkte sind ueber das SDK oder direkt via REST ansprechbar. - Authentifizierung erfolgt ueber API-Key + JWT-Token. -

- -

11.1 Namespace-Verwaltung

-
- - - - - - - - - - - - - - -
MethodeEndpunktBeschreibung
POST/api/v1/namespace/uploadVerschluesseltes Dokument hochladen
GET/api/v1/namespace/documentsEigene Dokumente auflisten
GET/api/v1/namespace/documents/{'{id}'}Einzelnes Dokument abrufen
DELETE/api/v1/namespace/documents/{'{id}'}Dokument loeschen (Soft Delete)
-
- -

11.2 Referenzdokumente & RAG

-
- - - - - - - - - - - - - - -
MethodeEndpunktBeschreibung
POST/api/v1/namespace/references/uploadReferenzdokument hochladen
POST/api/v1/namespace/references/{'{id}'}/indexReferenz fuer RAG indexieren
POST/api/v1/namespace/rag-queryRAG-Suchanfrage ausfuehren
POST/api/v1/namespace/analyzeKI-Verarbeitung anstossen
-
- -

11.3 Key Sharing

-
- - - - - - - - - - - - - - -
MethodeEndpunktBeschreibung
POST/api/v1/namespace/shareNamespace mit anderem Nutzer teilen
GET/api/v1/namespace/sharesAktive Shares auflisten
DELETE/api/v1/namespace/shares/{'{shareId}'}Zugriff widerrufen
GET/api/v1/namespace/shared-with-meMit mir geteilte Namespaces
-
- - {/* ============================================================ */} - {/* 12. ZUSAMMENFASSUNG */} - {/* ============================================================ */} -

12. Zusammenfassung: Compliance-Garantien

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
GarantieWie umgesetztRegelwerk
Keine PII verlaesst das KundensystemHeader-Redaction + verschluesselte Identity-MapDSGVO Art. 4 Nr. 5
Betreiber kann nicht mitlesenClient-seitige AES-256-GCM VerschluesselungDSGVO Art. 32
Kein Zugriff durch andere KundenTenant-Isolation (Namespace) auf allen 3 EbenenDSGVO Art. 25
Kein KI-Training mit Kundendatentraining_allowed: false auf allen VektorenAI Act Art. 10
Alles nachvollziehbarVollstaendiger Audit-Trail aller AktionenDSGVO Art. 5 Abs. 2
Kunde behaelt volle KontrolleJederzeitiger Widerruf, Loeschung, DatenexportDSGVO Art. 17, 20
-
- - - Die Namespace-Technologie ermoeglicht KI-gestuetzte Datenverarbeitung in der Cloud, bei der - keine personenbezogenen Daten das Kundensystem verlassen, alle Daten - Ende-zu-Ende verschluesselt sind, jeder Kunde seinen - eigenen abgeschotteten Namespace hat, und ein - vollstaendiger Audit-Trail jede Aktion dokumentiert. - + + + + + ) } diff --git a/developer-portal/app/development/docs/_components/ComplianceEngineSection.tsx b/developer-portal/app/development/docs/_components/ComplianceEngineSection.tsx new file mode 100644 index 0000000..88778e4 --- /dev/null +++ b/developer-portal/app/development/docs/_components/ComplianceEngineSection.tsx @@ -0,0 +1,119 @@ +import { CodeBlock, InfoBox } from '@/components/DevPortalLayout' + +export function ComplianceEngineSection() { + return ( + <> +

4. Die Compliance Engine: Wie Bewertungen funktionieren

+

+ Das Kernmodul des Compliance Hub ist die UCCA Engine (Unified Compliance + Control Assessment). Sie bewertet, ob ein geplanter KI-Anwendungsfall zulaessig ist. +

+ +

4.1 Der Fragebogen (Use Case Intake)

+
+ + + + + + + + + + + + + + + + + + +
BereichTypische FragenWarum relevant?
DatentypenWerden personenbezogene Daten verarbeitet? Besondere Kategorien (Art. 9)?Art. 9-Daten (Gesundheit, Religion, etc.) erfordern besondere Schutzmassnahmen
VerarbeitungszweckWird Profiling betrieben? Scoring? Automatisierte Entscheidungen?Art. 22 DSGVO schuetzt vor vollautomatischen Entscheidungen
ModellnutzungWird das Modell nur genutzt (Inference) oder mit Nutzerdaten trainiert (Fine-Tuning)?Training mit personenbezogenen Daten erfordert besondere Rechtsgrundlage
AutomatisierungsgradAssistenzsystem, teil- oder vollautomatisch?Vollautomatische Systeme unterliegen strengeren Auflagen
DatenspeicherungWie lange werden Daten gespeichert? Wo?DSGVO Art. 5: Speicherbegrenzung / Zweckbindung
Hosting-StandortEU, USA, oder anderswo?Drittlandtransfers erfordern zusaetzliche Garantien (SCC, DPF)
BrancheGesundheit, Finanzen, Bildung, Automotive, ...?Bestimmte Branchen unterliegen zusaetzlichen Regulierungen
Menschliche AufsichtGibt es einen Human-in-the-Loop?AI Act fordert menschliche Aufsicht fuer Hochrisiko-KI
+
+ +

4.2 Die Pruefregeln (Policy Engine)

+

+ Die Antworten des Fragebogens werden gegen ein Regelwerk von ueber 45 Regeln + geprueft. Jede Regel ist in einer YAML-Datei definiert. Die Regeln sind in 10 Kategorien organisiert: +

+
+ + + + + + + + + + + + + + + + + + + + + +
KategorieRegel-IDsPrueftBeispiel
A. DatenklassifikationR-001 bis R-006Welche Daten werden verarbeitet?R-001: Werden personenbezogene Daten verarbeitet? → +10 Risiko
B. Zweck & KontextR-010 bis R-013Warum und wie werden Daten genutzt?R-011: Profiling? → DSFA empfohlen
C. AutomatisierungR-020 bis R-025Wie stark ist die Automatisierung?R-023: Vollautomatisch? → Art. 22 Risiko
D. Training vs. NutzungR-030 bis R-035Wird das Modell trainiert?R-035: Training + Art. 9-Daten? → BLOCK
E. SpeicherungR-040 bis R-042Wie lange werden Daten gespeichert?R-041: Unbegrenzte Speicherung? → WARN
F. HostingR-050 bis R-052Wo werden Daten gehostet?R-051: Hosting in USA? → SCC/DPF pruefen
G. TransparenzR-060 bis R-062Werden Nutzer informiert?R-060: Keine Offenlegung? → AI Act Verstoss
H. BranchenspezifischR-070 bis R-074Gelten Sonderregeln fuer die Branche?R-070: Gesundheitsbranche? → zusaetzliche Anforderungen
I. AggregationR-090 bis R-092Meta-Regeln ueber andere RegelnR-090: Zu viele WARN-Regeln? → Gesamtrisiko erhoeht
J. ErklaerungR-100Warum hat das System so entschieden?Automatisch generierte Begruendung
+
+ + + Die Regeln sind bewusst in YAML-Dateien definiert: (1) Sie sind fuer Nicht-Programmierer + lesbar und damit auditierbar. (2) Sie koennen versioniert + werden -- wenn sich ein Gesetz aendert, wird die Regelaenderung im Versionsverlauf sichtbar. + + +

4.3 Das Ergebnis: Die Compliance-Bewertung

+
+ + + + + + + + + + + + + + + + + + +
ErgebnisBeschreibung
Machbarkeit + YES + CONDITIONAL + NO +
Risikoscore0-100 Punkte. Je hoeher, desto mehr Massnahmen sind erforderlich.
RisikostufeMINIMAL / LOW / MEDIUM / HIGH / UNACCEPTABLE
Ausgeloeste RegelnListe aller Regeln, die angeschlagen haben, mit Schweregrad und Gesetzesreferenz
Erforderliche ControlsKonkrete Massnahmen, die umgesetzt werden muessen
DSFA erforderlich?Ob eine Datenschutz-Folgenabschaetzung nach Art. 35 DSGVO durchgefuehrt werden muss
+
+ + +{`Anwendungsfall: "Chatbot fuer Kundenservice mit Zugriff auf Bestellhistorie" + +Machbarkeit: CONDITIONAL (bedingt zulaessig) +Risikoscore: 35/100 (LOW) + +Ausgeloeste Regeln: + R-001 WARN Personenbezogene Daten werden verarbeitet (Art. 6 DSGVO) + R-010 INFO Verarbeitungszweck: Kundenservice (Art. 5 DSGVO) + R-060 WARN Nutzer muessen ueber KI-Nutzung informiert werden (AI Act Art. 52) + +Erforderliche Controls: + C_EXPLICIT_CONSENT Einwilligung fuer Chatbot-Nutzung einholen + C_TRANSPARENCY Hinweis "Sie sprechen mit einer KI" + C_DATA_MINIMIZATION Nur notwendige Bestelldaten abrufen + +DSFA erforderlich: Nein (Risikoscore unter 40) +Eskalation: E0 (keine manuelle Pruefung noetig)`} + + + ) +} diff --git a/developer-portal/app/development/docs/_components/EscalationControlsSection.tsx b/developer-portal/app/development/docs/_components/EscalationControlsSection.tsx new file mode 100644 index 0000000..ae515e9 --- /dev/null +++ b/developer-portal/app/development/docs/_components/EscalationControlsSection.tsx @@ -0,0 +1,101 @@ +import { CodeBlock } from '@/components/DevPortalLayout' + +export function EscalationControlsSection() { + return ( + <> +

5. Das Eskalations-System: Wann Menschen entscheiden

+

+ Nicht jede Bewertung ist eindeutig. Fuer heikle Faelle gibt es ein abgestuftes + Eskalations-System, das sicherstellt, dass die richtigen Menschen die endgueltige + Entscheidung treffen. +

+ +
+ + + + + + + + + + + + + + + + +
StufeWann?Wer prueft?Frist (SLA)Beispiel
E0Nur INFO-Regeln, Risiko < 20Niemand (automatisch freigegeben)--Spam-Filter ohne personenbezogene Daten
E1WARN-Regeln, Risiko 20-39Teamleiter24 StundenChatbot mit Kundendaten
E2Art. 9-Daten ODER Risiko 40-59 ODER DSFA empfohlenDatenschutzbeauftragter (DSB)8 StundenKI-System, das Gesundheitsdaten verarbeitet
E3BLOCK-Regel ODER Risiko ≥ 60 ODER Art. 22-RisikoDSB + Rechtsabteilung4 StundenVollautomatische Kreditentscheidung
+
+ +

6. Controls, Nachweise und Risiken

+ +

6.1 Was sind Controls?

+

+ Ein Control ist eine konkrete Massnahme, die eine Organisation umsetzt, + um ein Compliance-Risiko zu beherrschen. Es gibt drei Arten: +

+
    +
  • Technische Controls: Verschluesselung, Zugangskontrollen, Firewalls, Pseudonymisierung
  • +
  • Organisatorische Controls: Schulungen, Richtlinien, Verantwortlichkeiten, Audits
  • +
  • Physische Controls: Zutrittskontrolle zu Serverraeumen, Schliesssysteme
  • +
+

+ Der Compliance Hub verwaltet einen Katalog von ueber 100 vordefinierten Controls, + die in 9 Domaenen organisiert sind: +

+
+
+ {[ + { code: 'AC', name: 'Zugriffsmanagement', desc: 'Wer darf was?' }, + { code: 'DP', name: 'Datenschutz', desc: 'Schutz personenbezogener Daten' }, + { code: 'NS', name: 'Netzwerksicherheit', desc: 'Sichere Kommunikation' }, + { code: 'IR', name: 'Incident Response', desc: 'Reaktion auf Sicherheitsvorfaelle' }, + { code: 'BC', name: 'Business Continuity', desc: 'Geschaeftskontinuitaet' }, + { code: 'VM', name: 'Vendor Management', desc: 'Dienstleister-Steuerung' }, + { code: 'AM', name: 'Asset Management', desc: 'Verwaltung von IT-Werten' }, + { code: 'CR', name: 'Kryptographie', desc: 'Verschluesselung & Schluessel' }, + { code: 'PS', name: 'Physische Sicherheit', desc: 'Gebaeude & Hardware' }, + ].map(d => ( +
+
{d.code}
+
{d.name}
+
{d.desc}
+
+ ))} +
+
+ +

6.2 Wie Controls mit Gesetzen verknuepft sind

+ +{`Control: AC-01 (Zugriffskontrolle) +├── DSGVO Art. 32 → "Sicherheit der Verarbeitung" +├── NIS2 Art. 21 → "Massnahmen zum Management von Cyberrisiken" +└── ISO 27001 A.9 → "Zugangskontrolle" + +Control: DP-03 (Datenverschluesselung) +├── DSGVO Art. 32 → "Verschluesselung personenbezogener Daten" +└── NIS2 Art. 21 → "Einsatz von Kryptographie"`} + + +

6.3 Evidence (Nachweise)

+

Nachweis-Typen, die das System verwaltet:

+
    +
  • Zertifikate: ISO 27001-Zertifikat, SOC2-Report
  • +
  • Richtlinien: Interne Datenschutzrichtlinie, Passwort-Policy
  • +
  • Audit-Berichte: Ergebnisse interner oder externer Pruefungen
  • +
  • Screenshots / Konfigurationen: Nachweis technischer Umsetzung
  • +
+

Jeder Nachweis hat ein Ablaufdatum. Das System warnt automatisch, wenn Nachweise bald ablaufen.

+ +

6.4 Risikobewertung

+

+ Risiken werden in einer 5x5-Risikomatrix dargestellt. Die beiden Achsen sind + Eintrittswahrscheinlichkeit und Auswirkung. Aus der Kombination ergibt sich die Risikostufe: + Minimal, Low, Medium, High oder Critical. +

+ + ) +} diff --git a/developer-portal/app/development/docs/_components/IntroArchitectureSection.tsx b/developer-portal/app/development/docs/_components/IntroArchitectureSection.tsx new file mode 100644 index 0000000..876671d --- /dev/null +++ b/developer-portal/app/development/docs/_components/IntroArchitectureSection.tsx @@ -0,0 +1,97 @@ +import { CodeBlock, InfoBox } from '@/components/DevPortalLayout' + +export function IntroArchitectureSection() { + return ( + <> +

1. Was ist der Compliance Hub?

+

+ Der BreakPilot Compliance Hub ist ein System, das Organisationen dabei + unterstuetzt, gesetzliche Vorschriften einzuhalten. Er beantwortet die zentrale Frage: +

+
+ “Duerfen wir das, was wir vorhaben, ueberhaupt so machen -- und wenn ja, welche + Auflagen muessen wir dafuer erfuellen?” +
+

+ Konkret geht es um EU- und deutsche Gesetze, die fuer den Umgang mit Daten und + kuenstlicher Intelligenz relevant sind: die DSGVO, den AI Act, + die NIS2-Richtlinie und viele weitere Regelwerke. Das System hat vier + Hauptaufgaben: +

+
    +
  1. Wissen bereitstellen: Hunderte Rechtstexte sind eingelesen und durchsuchbar -- nicht nur per Stichwort, sondern nach Bedeutung (semantische Suche).
  2. +
  3. Bewerten: Wenn ein Nutzer einen geplanten KI-Anwendungsfall beschreibt, bewertet das System automatisch, ob er zulaessig ist, welches Risiko besteht und welche Massnahmen noetig sind.
  4. +
  5. Dokumentieren: Das System erzeugt die Dokumente, die Aufsichtsbehoerden verlangen: Datenschutz-Folgenabschaetzungen (DSFA), technisch-organisatorische Massnahmen (TOM), Verarbeitungsverzeichnisse (VVT) und mehr.
  6. +
  7. Nachweisen: Jede Bewertung, jede Entscheidung und jeder Zugriff wird revisionssicher protokolliert -- als Nachweis gegenueber Pruefer und Behoerden.
  8. +
+ + + Die KI ist nicht die Entscheidungsinstanz. Alle + Compliance-Entscheidungen (zulaessig / bedingt zulaessig / nicht zulaessig) trifft ein + deterministisches Regelwerk. Das LLM (Sprachmodell) wird ausschliesslich dafuer verwendet, + Ergebnisse verstaendlich zu erklaeren -- niemals um sie zu treffen. + + +

2. Architektur im Ueberblick

+

+ Das System besteht aus mehreren Bausteinen, die jeweils eine klar abgegrenzte Aufgabe haben. + Man kann es sich wie ein Buero vorstellen: +

+ +
+ + + + + + + + + + + + + + + + + + + + +
BausteinAnalogieTechnologieAufgabe
API-GatewayEmpfang / RezeptionGo (Gin)Nimmt alle Anfragen entgegen, prueft Identitaet und leitet weiter
Compliance Engine (UCCA)SachbearbeiterGoBewertet Anwendungsfaelle gegen 45+ Regeln und berechnet Risikoscore
RAG ServiceRechtsbibliothekPython (FastAPI)Durchsucht Gesetze semantisch und beantwortet Rechtsfragen
Legal CorpusGesetzesbuecher im RegalYAML/JSON + QdrantEnthaelt alle Rechtstexte als durchsuchbare Wissensbasis
Policy EngineRegelbuch des SachbearbeitersYAML-Dateien45+ auditierbare Pruefregeln in maschinenlesbarer Form
Eskalations-SystemChef-UnterschriftGo + PostgreSQLLeitet kritische Faelle an menschliche Pruefer weiter
Admin DashboardSchreibtischNext.jsBenutzeroberflaeche fuer alle Funktionen
PostgreSQLAktenschrankSQL-DatenbankSpeichert Assessments, Eskalationen, Controls, Audit-Trail
QdrantSuchindex der BibliothekVektordatenbankErmoeglicht semantische Suche ueber Rechtstexte
+
+ +

Wie die Bausteine zusammenspielen

+ +{`Benutzer (Browser) + | + v +┌─────────────────────────────┐ +│ API-Gateway (Port 8080) │ ← Authentifizierung, Rate-Limiting, Tenant-Isolation +│ "Wer bist du? Darfst du?" │ +└──────────┬──────────────────┘ + | + ┌─────┼──────────────────────────────┐ + v v v +┌─────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Compliance │ │ RAG Service │ │ Security │ +│ Engine │ │ (Bibliothek)│ │ Scanner │ +│ (Bewertung) │ │ │ │ │ +└──────┬───┬──┘ └──────┬───────┘ └──────────────┘ + | | | + | | ┌──────┴───────┐ + | | │ Qdrant │ ← Vektordatenbank mit allen Rechtstexten + | | │ (Suchindex) │ + | | └──────────────┘ + | | + | └──────────────────────┐ + v v +┌──────────────┐ ┌──────────────┐ +│ PostgreSQL │ │ Eskalation │ +│ (Speicher) │ │ (E0-E3) │ +└──────────────┘ └──────────────┘`} + + + ) +} diff --git a/developer-portal/app/development/docs/_components/LegalCorpusSection.tsx b/developer-portal/app/development/docs/_components/LegalCorpusSection.tsx new file mode 100644 index 0000000..069e972 --- /dev/null +++ b/developer-portal/app/development/docs/_components/LegalCorpusSection.tsx @@ -0,0 +1,145 @@ +import { CodeBlock, InfoBox } from '@/components/DevPortalLayout' + +export function LegalCorpusSection() { + return ( + <> +

3. Der Katalogmanager: Die Wissensbasis

+

+ Das Herzstueck des Systems ist seine Wissensbasis -- eine Sammlung aller + relevanten Rechtstexte, die das System kennt und durchsuchen kann. Wir nennen das den + Legal Corpus (wörtlich: “Rechtlicher Koerper”). +

+ +

3.1 Welche Dokumente sind enthalten?

+

Der Legal Corpus ist in zwei Hauptbereiche gegliedert: EU-Recht und deutsches Recht.

+ +

EU-Verordnungen und -Richtlinien

+
+ + + + + + + + + + + + + + + + + + + +
RegelwerkAbkuerzungArtikelGueltig seitThema
Datenschutz-GrundverordnungDSGVO9925.05.2018Schutz personenbezogener Daten
KI-VerordnungAI Act11301.08.2024Regulierung kuenstlicher Intelligenz
Netz- und InformationssicherheitNIS24618.10.2024Cybersicherheit kritischer Infrastrukturen
ePrivacy-VerordnungePrivacy--in ArbeitVertraulichkeit elektronischer Kommunikation
Cyber Resilience ActCRA--2024Cybersicherheit von Produkten mit digitalen Elementen
Data ActDA--2024Zugang und Nutzung von Daten
Digital Markets ActDMA--2023Regulierung digitaler Gatekeeper
+
+ +

Deutsches Recht

+
+ + + + + + + + + + + + + + +
GesetzAbkuerzungThema
Telekommunikation-Digitale-Dienste-Datenschutz-GesetzTDDDGDatenschutz bei Telekommunikation und digitalen Diensten
BundesdatenschutzgesetzBDSGNationale Ergaenzung zur DSGVO
IT-SicherheitsgesetzIT-SiGIT-Sicherheit kritischer Infrastrukturen
BSI-KritisVKritisVBSI-Verordnung fuer kritische Infrastrukturen
+
+ +

Standards und Normen

+
+ + + + + + + + + + + + + + +
StandardThema
ISO 27001Informationssicherheits-Managementsystem (ISMS)
SOC2Trust Service Criteria (Sicherheit, Verfuegbarkeit, Vertraulichkeit)
BSI GrundschutzIT-Grundschutz des BSI
BSI TR-03161Technische Richtlinie fuer Anforderungen an Anwendungen im Gesundheitswesen
SCC (Standard Contractual Clauses)Standardvertragsklauseln fuer Drittlandtransfers
+
+ +

3.2 Wie werden Rechtstexte gespeichert?

+

+ Jeder Rechtstext durchlaeuft eine Verarbeitungspipeline, bevor er im + System durchsuchbar ist. Der Vorgang laesst sich mit dem Erstellen eines + Bibliothekskatalogs vergleichen: +

+
    +
  1. Erfassung (Ingestion): Der Rechtstext wird als Dokument (PDF, Markdown oder Klartext) in das System geladen. Fuer jede Verordnung gibt es eine metadata.json-Datei.
  2. +
  3. Zerkleinerung (Chunking): Lange Gesetzestexte werden in kleinere Abschnitte von ca. 512 Zeichen zerlegt. Dabei ueberlappen sich die Abschnitte um 50 Zeichen.
  4. +
  5. Vektorisierung (Embedding): Jeder Textabschnitt wird vom Embedding-Modell BGE-M3 in einen Vektor umgewandelt -- eine Liste von 1.024 Zahlen.
  6. +
  7. Indexierung: Die Vektoren werden in der Vektordatenbank Qdrant gespeichert. Zusammen mit jedem Vektor werden Metadaten hinterlegt.
  8. +
+ + +{`Rechtstext (z.B. DSGVO Art. 32) + | + v +┌────────────────────────┐ +│ 1. Einlesen │ ← PDF/Markdown/Klartext + metadata.json +└──────────┬─────────────┘ + v +┌────────────────────────┐ +│ 2. Chunking │ ← Text in 512-Zeichen-Abschnitte zerlegen +└──────────┬─────────────┘ + v +┌────────────────────────┐ +│ 3. Embedding │ ← BGE-M3 wandelt Text in 1024 Zahlen um +└──────────┬─────────────┘ + v +┌────────────────────────┐ +│ 4. Qdrant speichern │ ← Vektor + Metadaten werden indexiert +└────────────────────────┘`} + + + + Der Legal Corpus enthaelt derzeit ca. 2.274 Textabschnitte aus ueber + 400 Gesetzesartikeln. Darunter 99 DSGVO-Artikel, 85 AI-Act-Artikel, 46 NIS2-Artikel, + 86 BDSG-Paragraphen sowie zahlreiche Artikel aus TDDDG, CRA, Data Act und weiteren Regelwerken. + + +

3.3 Wie funktioniert die semantische Suche?

+

+ Klassische Suchmaschinen suchen nach Woertern. Unsere semantische Suche + funktioniert anders: Sie sucht nach Bedeutung. +

+

+ Beispiel: Wenn Sie fragen “Wann muss ich den Nutzer um Erlaubnis + bitten?”, findet das System Art. 7 DSGVO (Bedingungen fuer die Einwilligung), obwohl + Ihre Frage das Wort “Einwilligung” gar nicht enthaelt. +

+ +

3.4 Der KI-Rechtsassistent (Legal Q&A)

+

Ueber die reine Suche hinaus kann das System auch Fragen beantworten:

+
    +
  1. Suche: Das System findet die 5 relevantesten Gesetzesabschnitte zur Frage.
  2. +
  3. Kontext-Erstellung: Diese Abschnitte werden mit der Frage an das Sprachmodell (Qwen 2.5 32B) uebergeben.
  4. +
  5. Antwort-Generierung: Das Modell formuliert eine verstaendliche Antwort auf Deutsch und zitiert die verwendeten Rechtsquellen.
  6. +
  7. Quellenangabe: Jede Antwort enthaelt exakte Zitate mit Artikelangaben.
  8. +
+ + + Der Rechtsassistent gibt keine Rechtsberatung. Er hilft, relevante + Gesetzespassagen zu finden und verstaendlich zusammenzufassen. Die Antworten enthalten + immer einen Confidence-Score (0-1). + + + ) +} diff --git a/developer-portal/app/development/docs/_components/MultiTenancyLlmAuditSection.tsx b/developer-portal/app/development/docs/_components/MultiTenancyLlmAuditSection.tsx new file mode 100644 index 0000000..68b924c --- /dev/null +++ b/developer-portal/app/development/docs/_components/MultiTenancyLlmAuditSection.tsx @@ -0,0 +1,129 @@ +import { CodeBlock, InfoBox } from '@/components/DevPortalLayout' + +export function MultiTenancyLlmAuditSection() { + return ( + <> +

9. Multi-Tenancy und Zugriffskontrolle

+

+ Das System ist mandantenfaehig (Multi-Tenant): Mehrere Organisationen + koennen es gleichzeitig nutzen, ohne dass sie gegenseitig auf ihre Daten zugreifen koennen. +

+ +

9.1 Rollenbasierte Zugriffskontrolle (RBAC)

+
+ + + + + + + + + + + + + + +
RolleDarf
MitarbeiterAnwendungsfaelle einreichen, eigene Bewertungen einsehen
TeamleiterE1-Eskalationen pruefen, Team-Assessments einsehen
DSB (Datenschutzbeauftragter)E2/E3-Eskalationen pruefen, alle Assessments einsehen, Policies aendern
RechtsabteilungE3-Eskalationen pruefen, Grundsatzentscheidungen
AdministratorSystem konfigurieren, Nutzer verwalten, LLM-Policies festlegen
+
+ +

9.2 PII-Erkennung und -Schutz

+

+ Bevor Texte an ein Sprachmodell gesendet werden, durchlaufen sie eine automatische + PII-Erkennung. Das System erkennt ueber 20 Arten personenbezogener Daten + (E-Mail-Adressen, Telefonnummern, Namen, IP-Adressen, etc.). + Je nach Konfiguration werden erkannte PII-Daten geschwuerzt, maskiert + oder nur im Audit-Log markiert. +

+ +

10. Wie das System KI nutzt (und wie nicht)

+
+ + + + + + + + + + + + + + + + +
AufgabeEntschieden vonRolle der KI
Machbarkeit (YES/CONDITIONAL/NO)Deterministische RegelnKeine
Risikoscore berechnenRegelbasierte BerechnungKeine
Eskalation ausloesenSchwellenwerte + RegellogikKeine
Ergebnis erklaeren--LLM + RAG-Kontext
Rechtsfragen beantworten--LLM + RAG (Rechtskorpus)
Dokumente generieren (DSFA, TOM, VVT)--LLM + Vorlagen
+
+ +

LLM-Provider und Fallback

+
    +
  1. Primaer: Ollama (lokal) -- Qwen 2.5 32B bzw. Mistral, laeuft direkt auf dem Server. Keine Daten verlassen das lokale Netzwerk.
  2. +
  3. Fallback: Anthropic Claude -- Wird nur aktiviert, wenn das lokale Modell nicht verfuegbar ist.
  4. +
+ +

11. Audit-Trail: Alles wird protokolliert

+

Saemtliche Aktionen im System werden revisionssicher protokolliert:

+
    +
  • Jede Compliance-Bewertung mit allen Ein- und Ausgaben
  • +
  • Jede Eskalationsentscheidung mit Begruendung
  • +
  • Jeder LLM-Aufruf (wer hat was wann gefragt, welches Modell wurde verwendet)
  • +
  • Jede Aenderung an Controls, Evidence und Policies
  • +
  • Jeder Login und Daten-Export
  • +
+ + + Der Use-Case-Text wird nur mit Einwilligung des Nutzers gespeichert. + Standardmaessig wird nur ein SHA-256-Hash des Textes gespeichert. + + +

12. Security Scanner: Technische Sicherheitspruefung

+
    +
  • Container-Scanning (Trivy): Prueft Docker-Images auf bekannte Schwachstellen (CVEs)
  • +
  • Statische Code-Analyse (Semgrep): Sucht im Quellcode nach Sicherheitsluecken
  • +
  • Secret Detection (Gitleaks): Findet versehentlich eingecheckte Passwoerter, API-Keys und Tokens
  • +
  • SBOM-Generierung: Erstellt eine vollstaendige Liste aller verwendeten Bibliotheken und deren Lizenzen
  • +
+ +

13. Zusammenfassung: Der komplette Datenfluss

+ + +{`SCHRITT 1: FAKTEN SAMMELN +Nutzer fuellt Fragebogen aus: Welche Daten? Welcher Zweck? Welche Branche? Wo gehostet? + +SCHRITT 2: ANWENDBARKEIT PRUEFEN +Obligations Framework: DSGVO? AI Act? NIS2? + +SCHRITT 3: REGELN PRUEFEN (45+ Regeln) + R-001 (WARN): Personenbezogene Daten +10 Risiko + R-060 (WARN): KI-Transparenz fehlt +15 Risiko + → Gesamt-Risikoscore: 35/100 (LOW), Machbarkeit: CONDITIONAL + +SCHRITT 4: CONTROLS ZUORDNEN + C_EXPLICIT_CONSENT, C_TRANSPARENCY, C_DATA_MINIMIZATION + +SCHRITT 5: ESKALATION (bei Bedarf) + Score 35 → Stufe E1 → Teamleiter, SLA 24h + +SCHRITT 6: ERKLAERUNG GENERIEREN + LLM + RAG: Gesetzesartikel suchen, Erklaerungstext generieren + +SCHRITT 7: DOKUMENTATION + DSFA, TOM, VVT, Compliance-Report (PDF/ZIP/JSON) + +SCHRITT 8: MONITORING + Controls regelmaessig pruefen, Nachweise auf Ablauf ueberwachen`} + + + + Der Compliance Hub nimmt die Beschreibung eines KI-Vorhabens entgegen, prueft es gegen + ueber 45 deterministische Regeln und 400+ Gesetzesartikel, berechnet ein Risiko, ordnet + Massnahmen zu, eskaliert bei Bedarf an menschliche Pruefer und dokumentiert alles + revisionssicher -- wobei die KI nur fuer Erklaerungen und Zusammenfassungen eingesetzt wird, + niemals fuer die eigentliche Compliance-Entscheidung. + + + ) +} diff --git a/developer-portal/app/development/docs/_components/ObligationsDsgvoSection.tsx b/developer-portal/app/development/docs/_components/ObligationsDsgvoSection.tsx new file mode 100644 index 0000000..d292d6c --- /dev/null +++ b/developer-portal/app/development/docs/_components/ObligationsDsgvoSection.tsx @@ -0,0 +1,81 @@ +import { CodeBlock } from '@/components/DevPortalLayout' + +export function ObligationsDsgvoSection() { + return ( + <> +

7. Pflichten-Ableitung: Welche Gesetze gelten fuer mich?

+

+ Nicht jedes Gesetz gilt fuer jede Organisation. Das Obligations Framework + ermittelt automatisch, welche konkreten Pflichten sich aus der Situation einer Organisation + ergeben. +

+ +

Beispiel: NIS2-Anwendbarkeit

+ +{`Ist Ihr Unternehmen in einem der NIS2-Sektoren taetig? +(Energie, Transport, Banken, Gesundheit, Wasser, Digitale Infrastruktur, ...) + │ + ├── Nein → NIS2 gilt NICHT fuer Sie + │ + └── Ja → Wie gross ist Ihr Unternehmen? + │ + ├── >= 250 Mitarbeiter ODER >= 50 Mio. EUR Umsatz + │ → ESSENTIAL ENTITY (wesentliche Einrichtung) + │ → Volle NIS2-Pflichten, strenge Aufsicht + │ → Bussgelder bis 10 Mio. EUR oder 2% Jahresumsatz + │ + ├── >= 50 Mitarbeiter ODER >= 10 Mio. EUR Umsatz + │ → IMPORTANT ENTITY (wichtige Einrichtung) + │ → NIS2-Pflichten, reaktive Aufsicht + │ → Bussgelder bis 7 Mio. EUR oder 1,4% Jahresumsatz + │ + └── Kleiner → NIS2 gilt grundsaetzlich NICHT`} + + +

8. DSGVO-Compliance-Module im Detail

+

Fuer die Einhaltung der DSGVO bietet der Compliance Hub spezialisierte Module:

+ +

8.1 Consent Management (Einwilligungsverwaltung)

+

+ Verwaltet die Einwilligung von Nutzern gemaess Art. 6/7 DSGVO. Jede Einwilligung wird + protokolliert: wer hat wann, auf welchem Kanal, fuer welchen Zweck zugestimmt (oder abgelehnt)? +

+

+ Zwecke: Essential (funktionsnotwendig), Functional, Analytics, Marketing, + Personalization, Third-Party. +

+ +

8.2 DSR Management (Betroffenenrechte)

+

+ Verwaltet Antraege betroffener Personen nach Art. 15-21 DSGVO: Auskunft, Berichtigung, + Loeschung, Datenportabilitaet, Einschraenkung und Widerspruch. Das System ueberwacht die + 30-Tage-Frist (Art. 12) und eskaliert automatisch bei drohenden Fristverstossen. +

+ +

8.3 VVT (Verzeichnis von Verarbeitungstaetigkeiten)

+

+ Dokumentiert alle Datenverarbeitungen gemaess Art. 30 DSGVO: Welche Daten werden fuer + welchen Zweck, auf welcher Rechtsgrundlage, wie lange und von wem verarbeitet? +

+ +

8.4 DSFA (Datenschutz-Folgenabschaetzung)

+

+ Wenn eine Datenverarbeitung voraussichtlich ein hohes Risiko fuer die Rechte natuerlicher + Personen mit sich bringt, ist eine DSFA nach Art. 35 DSGVO Pflicht. +

+ +

8.5 TOM (Technisch-Organisatorische Massnahmen)

+

+ Dokumentiert die Schutzmassnahmen nach Art. 32 DSGVO. Fuer jede Massnahme wird erfasst: + Kategorie (z.B. Verschluesselung, Zugriffskontrolle), Status, Verantwortlicher und Nachweise. +

+ +

8.6 Loeschkonzept

+

+ Verwaltet Aufbewahrungsfristen und automatische Loeschung gemaess Art. 5/17 DSGVO. + Fuer jede Datenkategorie wird definiert: wie lange darf sie gespeichert werden, wann muss + sie geloescht werden und wie (z.B. Ueberschreiben, Schluesselloeschung). +

+ + ) +} diff --git a/developer-portal/app/development/docs/page.tsx b/developer-portal/app/development/docs/page.tsx index 44bd4f8..0a73b8b 100644 --- a/developer-portal/app/development/docs/page.tsx +++ b/developer-portal/app/development/docs/page.tsx @@ -1,4 +1,10 @@ -import { DevPortalLayout, CodeBlock, InfoBox } from '@/components/DevPortalLayout' +import { DevPortalLayout } from '@/components/DevPortalLayout' +import { IntroArchitectureSection } from './_components/IntroArchitectureSection' +import { LegalCorpusSection } from './_components/LegalCorpusSection' +import { ComplianceEngineSection } from './_components/ComplianceEngineSection' +import { EscalationControlsSection } from './_components/EscalationControlsSection' +import { ObligationsDsgvoSection } from './_components/ObligationsDsgvoSection' +import { MultiTenancyLlmAuditSection } from './_components/MultiTenancyLlmAuditSection' export default function ComplianceServiceDocsPage() { return ( @@ -6,886 +12,12 @@ export default function ComplianceServiceDocsPage() { title="Wie funktioniert der Compliance Service?" description="Eine umfassende Erklaerung des gesamten Systems -- vom Rechtstext bis zur Compliance-Bewertung." > - {/* ============================================================ */} - {/* 1. EINLEITUNG */} - {/* ============================================================ */} -

1. Was ist der Compliance Hub?

-

- Der BreakPilot Compliance Hub ist ein System, das Organisationen dabei - unterstuetzt, gesetzliche Vorschriften einzuhalten. Er beantwortet die zentrale Frage: -

-
- “Duerfen wir das, was wir vorhaben, ueberhaupt so machen -- und wenn ja, welche - Auflagen muessen wir dafuer erfuellen?” -
-

- Konkret geht es um EU- und deutsche Gesetze, die fuer den Umgang mit Daten und - kuenstlicher Intelligenz relevant sind: die DSGVO, den AI Act, - die NIS2-Richtlinie und viele weitere Regelwerke. Das System hat vier - Hauptaufgaben: -

-
    -
  1. - Wissen bereitstellen: Hunderte Rechtstexte sind eingelesen und - durchsuchbar -- nicht nur per Stichwort, sondern nach Bedeutung (semantische Suche). -
  2. -
  3. - Bewerten: Wenn ein Nutzer einen geplanten KI-Anwendungsfall beschreibt, - bewertet das System automatisch, ob er zulaessig ist, welches Risiko besteht und welche - Massnahmen noetig sind. -
  4. -
  5. - Dokumentieren: Das System erzeugt die Dokumente, die Aufsichtsbehoerden - verlangen: Datenschutz-Folgenabschaetzungen (DSFA), technisch-organisatorische Massnahmen - (TOM), Verarbeitungsverzeichnisse (VVT) und mehr. -
  6. -
  7. - Nachweisen: Jede Bewertung, jede Entscheidung und jeder Zugriff wird - revisionssicher protokolliert -- als Nachweis gegenueber Pruefer und Behoerden. -
  8. -
- - - Die KI ist nicht die Entscheidungsinstanz. Alle - Compliance-Entscheidungen (zulaessig / bedingt zulaessig / nicht zulaessig) trifft ein - deterministisches Regelwerk. Das LLM (Sprachmodell) wird ausschliesslich dafuer verwendet, - Ergebnisse verstaendlich zu erklaeren -- niemals um sie zu treffen. - - - {/* ============================================================ */} - {/* 2. ARCHITEKTUR-UEBERSICHT */} - {/* ============================================================ */} -

2. Architektur im Ueberblick

-

- Das System besteht aus mehreren Bausteinen, die jeweils eine klar abgegrenzte Aufgabe haben. - Man kann es sich wie ein Buero vorstellen: -

- -
- - - - - - - - - - - - - - - - - - - - -
BausteinAnalogieTechnologieAufgabe
API-GatewayEmpfang / RezeptionGo (Gin)Nimmt alle Anfragen entgegen, prueft Identitaet und leitet weiter
Compliance Engine (UCCA)SachbearbeiterGoBewertet Anwendungsfaelle gegen 45+ Regeln und berechnet Risikoscore
RAG ServiceRechtsbibliothekPython (FastAPI)Durchsucht Gesetze semantisch und beantwortet Rechtsfragen
Legal CorpusGesetzesbuecher im RegalYAML/JSON + QdrantEnthaelt alle Rechtstexte als durchsuchbare Wissensbasis
Policy EngineRegelbuch des SachbearbeitersYAML-Dateien45+ auditierbare Pruefregeln in maschinenlesbarer Form
Eskalations-SystemChef-UnterschriftGo + PostgreSQLLeitet kritische Faelle an menschliche Pruefer weiter
Admin DashboardSchreibtischNext.jsBenutzeroberflaeche fuer alle Funktionen
PostgreSQLAktenschrankSQL-DatenbankSpeichert Assessments, Eskalationen, Controls, Audit-Trail
QdrantSuchindex der BibliothekVektordatenbankErmoeglicht semantische Suche ueber Rechtstexte
-
- -

Wie die Bausteine zusammenspielen

- -{`Benutzer (Browser) - | - v -┌─────────────────────────────┐ -│ API-Gateway (Port 8080) │ ← Authentifizierung, Rate-Limiting, Tenant-Isolation -│ "Wer bist du? Darfst du?" │ -└──────────┬──────────────────┘ - | - ┌─────┼──────────────────────────────┐ - v v v -┌─────────────┐ ┌──────────────┐ ┌──────────────┐ -│ Compliance │ │ RAG Service │ │ Security │ -│ Engine │ │ (Bibliothek)│ │ Scanner │ -│ (Bewertung) │ │ │ │ │ -└──────┬───┬──┘ └──────┬───────┘ └──────────────┘ - | | | - | | ┌──────┴───────┐ - | | │ Qdrant │ ← Vektordatenbank mit allen Rechtstexten - | | │ (Suchindex) │ - | | └──────────────┘ - | | - | └──────────────────────┐ - v v -┌──────────────┐ ┌──────────────┐ -│ PostgreSQL │ │ Eskalation │ -│ (Speicher) │ │ (E0-E3) │ -└──────────────┘ └──────────────┘`} - - - {/* ============================================================ */} - {/* 3. DER KATALOGMANAGER / LEGAL CORPUS */} - {/* ============================================================ */} -

3. Der Katalogmanager: Die Wissensbasis

-

- Das Herzstueck des Systems ist seine Wissensbasis -- eine Sammlung aller - relevanten Rechtstexte, die das System kennt und durchsuchen kann. Wir nennen das den - Legal Corpus (wörtlich: “Rechtlicher Koerper”). -

- -

3.1 Welche Dokumente sind enthalten?

-

- Der Legal Corpus ist in zwei Hauptbereiche gegliedert: EU-Recht und - deutsches Recht. -

- -

EU-Verordnungen und -Richtlinien

-
- - - - - - - - - - - - - - - - - - - -
RegelwerkAbkuerzungArtikelGueltig seitThema
Datenschutz-GrundverordnungDSGVO9925.05.2018Schutz personenbezogener Daten
KI-VerordnungAI Act11301.08.2024Regulierung kuenstlicher Intelligenz
Netz- und InformationssicherheitNIS24618.10.2024Cybersicherheit kritischer Infrastrukturen
ePrivacy-VerordnungePrivacy--in ArbeitVertraulichkeit elektronischer Kommunikation
Cyber Resilience ActCRA--2024Cybersicherheit von Produkten mit digitalen Elementen
Data ActDA--2024Zugang und Nutzung von Daten
Digital Markets ActDMA--2023Regulierung digitaler Gatekeeper
-
- -

Deutsches Recht

-
- - - - - - - - - - - - - - -
GesetzAbkuerzungThema
Telekommunikation-Digitale-Dienste-Datenschutz-GesetzTDDDGDatenschutz bei Telekommunikation und digitalen Diensten
BundesdatenschutzgesetzBDSGNationale Ergaenzung zur DSGVO
IT-SicherheitsgesetzIT-SiGIT-Sicherheit kritischer Infrastrukturen
BSI-KritisVKritisVBSI-Verordnung fuer kritische Infrastrukturen
-
- -

Standards und Normen

-
- - - - - - - - - - - - - - -
StandardThema
ISO 27001Informationssicherheits-Managementsystem (ISMS)
SOC2Trust Service Criteria (Sicherheit, Verfuegbarkeit, Vertraulichkeit)
BSI GrundschutzIT-Grundschutz des BSI
BSI TR-03161Technische Richtlinie fuer Anforderungen an Anwendungen im Gesundheitswesen
SCC (Standard Contractual Clauses)Standardvertragsklauseln fuer Drittlandtransfers
-
- -

3.2 Wie werden Rechtstexte gespeichert?

-

- Jeder Rechtstext durchlaeuft eine Verarbeitungspipeline, bevor er im - System durchsuchbar ist. Der Vorgang laesst sich mit dem Erstellen eines - Bibliothekskatalogs vergleichen: -

-
    -
  1. - Erfassung (Ingestion): Der Rechtstext wird als Dokument (PDF, Markdown - oder Klartext) in das System geladen. Fuer jede Verordnung gibt es eine - metadata.json-Datei, die beschreibt, um welches Gesetz es sich handelt, - wie viele Artikel es hat und welche Schluesselbegriffe relevant sind. -
  2. -
  3. - Zerkleinerung (Chunking): Lange Gesetzestexte werden in kleinere - Abschnitte von ca. 512 Zeichen zerlegt. Dabei ueberlappen sich die Abschnitte um - 50 Zeichen, damit kein Kontext verloren geht. Stellen Sie sich vor, Sie zerschneiden - einen langen Brief in Absaetze, wobei jeder Absatz die letzten zwei Zeilen des - vorherigen enthaelt. -
  4. -
  5. - Vektorisierung (Embedding): Jeder Textabschnitt wird vom - Embedding-Modell BGE-M3 in einen Vektor umgewandelt -- eine - Liste von 1.024 Zahlen, die die Bedeutung des Textes repraesentieren. Texte - mit aehnlicher Bedeutung haben aehnliche Vektoren, unabhaengig von der Wortwahl. -
  6. -
  7. - Indexierung: Die Vektoren werden in der Vektordatenbank - Qdrant gespeichert. Zusammen mit jedem Vektor werden Metadaten - hinterlegt: zu welchem Gesetz der Text gehoert, welcher Artikel es ist und welcher - Paragraph. -
  8. -
- - -{`Rechtstext (z.B. DSGVO Art. 32) - | - v -┌────────────────────────┐ -│ 1. Einlesen │ ← PDF/Markdown/Klartext + metadata.json -│ Metadaten zuordnen │ -└──────────┬─────────────┘ - | - v -┌────────────────────────┐ -│ 2. Chunking │ ← Text in 512-Zeichen-Abschnitte zerlegen -│ Ueberlappung: 50 Zch. │ (mit 50 Zeichen Ueberlappung) -└──────────┬─────────────┘ - | - v -┌────────────────────────┐ -│ 3. Embedding │ ← BGE-M3 wandelt Text in 1024 Zahlen um -│ Text → Vektor │ (Bedeutungs-Repraesentation) -└──────────┬─────────────┘ - | - v -┌────────────────────────┐ -│ 4. Qdrant speichern │ ← Vektor + Metadaten werden indexiert -│ Sofort durchsuchbar │ (~2.274 Chunks insgesamt) -└────────────────────────┘`} - - - - Der Legal Corpus enthaelt derzeit ca. 2.274 Textabschnitte aus ueber - 400 Gesetzesartikeln. Darunter 99 DSGVO-Artikel, 85 AI-Act-Artikel, 46 NIS2-Artikel, - 86 BDSG-Paragraphen sowie zahlreiche Artikel aus TDDDG, CRA, Data Act und weiteren - Regelwerken. - - -

3.3 Wie funktioniert die semantische Suche?

-

- Klassische Suchmaschinen suchen nach Woertern. Wenn Sie “Einwilligung” - eingeben, finden sie nur Texte, die genau dieses Wort enthalten. Unsere semantische Suche - funktioniert anders: Sie sucht nach Bedeutung. -

-

- Beispiel: Wenn Sie fragen “Wann muss ich den Nutzer um Erlaubnis - bitten?”, findet das System Art. 7 DSGVO (Bedingungen fuer die Einwilligung), obwohl - Ihre Frage das Wort “Einwilligung” gar nicht enthaelt. Das funktioniert, weil - die Bedeutungsvektoren von “um Erlaubnis bitten” und “Einwilligung” - sehr aehnlich sind. -

-

Der Suchvorgang im Detail:

-
    -
  1. Ihre Suchanfrage wird vom gleichen Modell (BGE-M3) in einen Vektor umgewandelt.
  2. -
  3. Qdrant vergleicht diesen Vektor mit allen gespeicherten Vektoren (Kosinus-Aehnlichkeit).
  4. -
  5. Die aehnlichsten Textabschnitte werden zurueckgegeben, sortiert nach Relevanz (Score 0-1).
  6. -
  7. Optional kann nach bestimmten Gesetzen gefiltert werden (nur DSGVO, nur AI Act, etc.).
  8. -
- -

3.4 Der KI-Rechtsassistent (Legal Q&A)

-

- Ueber die reine Suche hinaus kann das System auch Fragen beantworten. - Dabei wird die semantische Suche mit einem Sprachmodell kombiniert: -

-
    -
  1. Suche: Das System findet die 5 relevantesten Gesetzesabschnitte zur Frage.
  2. -
  3. Kontext-Erstellung: Diese Abschnitte werden zusammen mit der Frage an das Sprachmodell (Qwen 2.5 32B) uebergeben.
  4. -
  5. Antwort-Generierung: Das Modell formuliert eine verstaendliche Antwort auf Deutsch und zitiert die verwendeten Rechtsquellen.
  6. -
  7. Quellenangabe: Jede Antwort enthaelt exakte Zitate mit Artikelangaben, damit die Aussagen nachpruefbar sind.
  8. -
- - - Der Rechtsassistent gibt keine Rechtsberatung. Er hilft, relevante - Gesetzespassagen zu finden und verstaendlich zusammenzufassen. Die Antworten enthalten - immer einen Confidence-Score (0-1), der angibt, wie sicher sich das System ist. Bei - niedrigem Score wird explizit auf die Unsicherheit hingewiesen. - - - {/* ============================================================ */} - {/* 4. DIE COMPLIANCE ENGINE (UCCA) */} - {/* ============================================================ */} -

4. Die Compliance Engine: Wie Bewertungen funktionieren

-

- Das Kernmodul des Compliance Hub ist die UCCA Engine (Unified Compliance - Control Assessment). Sie bewertet, ob ein geplanter KI-Anwendungsfall zulaessig ist. -

- -

4.1 Der Fragebogen (Use Case Intake)

-

- Alles beginnt mit einem strukturierten Fragebogen. Der Nutzer beschreibt seinen geplanten - Anwendungsfall, indem er Fragen zu folgenden Bereichen beantwortet: -

-
- - - - - - - - - - - - - - - - - - -
BereichTypische FragenWarum relevant?
DatentypenWerden personenbezogene Daten verarbeitet? Besondere Kategorien (Art. 9)?Art. 9-Daten (Gesundheit, Religion, etc.) erfordern besondere Schutzmassnahmen
VerarbeitungszweckWird Profiling betrieben? Scoring? Automatisierte Entscheidungen?Art. 22 DSGVO schuetzt vor vollautomatischen Entscheidungen
ModellnutzungWird das Modell nur genutzt (Inference) oder mit Nutzerdaten trainiert (Fine-Tuning)?Training mit personenbezogenen Daten erfordert besondere Rechtsgrundlage
AutomatisierungsgradAssistenzsystem, teil- oder vollautomatisch?Vollautomatische Systeme unterliegen strengeren Auflagen
DatenspeicherungWie lange werden Daten gespeichert? Wo?DSGVO Art. 5: Speicherbegrenzung / Zweckbindung
Hosting-StandortEU, USA, oder anderswo?Drittlandtransfers erfordern zusaetzliche Garantien (SCC, DPF)
BrancheGesundheit, Finanzen, Bildung, Automotive, ...?Bestimmte Branchen unterliegen zusaetzlichen Regulierungen
Menschliche AufsichtGibt es einen Human-in-the-Loop?AI Act fordert menschliche Aufsicht fuer Hochrisiko-KI
-
- -

4.2 Die Pruefregeln (Policy Engine)

-

- Die Antworten des Fragebogens werden gegen ein Regelwerk von ueber 45 Regeln - geprueft. Jede Regel ist in einer YAML-Datei definiert und hat folgende Struktur: -

-
    -
  • Bedingung: Wann greift die Regel? (z.B. “Art. 9-Daten werden verarbeitet”)
  • -
  • Schweregrad: INFO (Hinweis), WARN (Risiko, aber loesbar) oder BLOCK (grundsaetzlich nicht zulaessig)
  • -
  • Auswirkung: Was passiert, wenn die Regel greift? (Risikoerhoehung, zusaetzliche Controls, Eskalation)
  • -
  • Gesetzesreferenz: Auf welchen Artikel bezieht sich die Regel?
  • -
- -

Die Regeln sind in 10 Kategorien organisiert:

-
- - - - - - - - - - - - - - - - - - - - - -
KategorieRegel-IDsPrueftBeispiel
A. DatenklassifikationR-001 bis R-006Welche Daten werden verarbeitet?R-001: Werden personenbezogene Daten verarbeitet? → +10 Risiko
B. Zweck & KontextR-010 bis R-013Warum und wie werden Daten genutzt?R-011: Profiling? → DSFA empfohlen
C. AutomatisierungR-020 bis R-025Wie stark ist die Automatisierung?R-023: Vollautomatisch? → Art. 22 Risiko
D. Training vs. NutzungR-030 bis R-035Wird das Modell trainiert?R-035: Training + Art. 9-Daten? → BLOCK
E. SpeicherungR-040 bis R-042Wie lange werden Daten gespeichert?R-041: Unbegrenzte Speicherung? → WARN
F. HostingR-050 bis R-052Wo werden Daten gehostet?R-051: Hosting in USA? → SCC/DPF pruefen
G. TransparenzR-060 bis R-062Werden Nutzer informiert?R-060: Keine Offenlegung? → AI Act Verstoss
H. BranchenspezifischR-070 bis R-074Gelten Sonderregeln fuer die Branche?R-070: Gesundheitsbranche? → zusaetzliche Anforderungen
I. AggregationR-090 bis R-092Meta-Regeln ueber andere RegelnR-090: Zu viele WARN-Regeln? → Gesamtrisiko erhoeht
J. ErklaerungR-100Warum hat das System so entschieden?Automatisch generierte Begruendung
-
- - - Die Regeln sind bewusst in YAML-Dateien definiert und nicht im Programmcode versteckt. - Das hat zwei Vorteile: (1) Sie sind fuer Nicht-Programmierer lesbar und damit - auditierbar, d.h. ein Datenschutzbeauftragter oder Wirtschaftspruefer kann - pruefen, ob die Regeln korrekt sind. (2) Sie koennen versioniert werden -- - wenn sich ein Gesetz aendert, wird die Regelaenderung im Versionsverlauf sichtbar. - - -

4.3 Das Ergebnis: Die Compliance-Bewertung

-

- Nach der Pruefung aller Regeln erhaelt der Nutzer eine strukturierte Bewertung: -

-
- - - - - - - - - - - - - - - - - - - - -
ErgebnisBeschreibung
Machbarkeit - YES - CONDITIONAL - NO -
Risikoscore0-100 Punkte. Je hoeher, desto mehr Massnahmen sind erforderlich.
RisikostufeMINIMAL / LOW / MEDIUM / HIGH / UNACCEPTABLE
Ausgeloeste RegelnListe aller Regeln, die angeschlagen haben, mit Schweregrad und Gesetzesreferenz
Erforderliche ControlsKonkrete Massnahmen, die umgesetzt werden muessen (z.B. Verschluesselung, Einwilligung einholen)
Empfohlene ArchitekturTechnische Muster, die eingesetzt werden sollten (z.B. On-Premise statt Cloud)
Verbotene MusterTechnische Ansaetze, die vermieden werden muessen
DSFA erforderlich?Ob eine Datenschutz-Folgenabschaetzung nach Art. 35 DSGVO durchgefuehrt werden muss
-
- - -{`Anwendungsfall: "Chatbot fuer Kundenservice mit Zugriff auf Bestellhistorie" - -Machbarkeit: CONDITIONAL (bedingt zulaessig) -Risikoscore: 35/100 (LOW) -Risikostufe: LOW - -Ausgeloeste Regeln: - R-001 WARN Personenbezogene Daten werden verarbeitet (Art. 6 DSGVO) - R-010 INFO Verarbeitungszweck: Kundenservice (Art. 5 DSGVO) - R-020 INFO Assistenzsystem (nicht vollautomatisch) (Art. 22 DSGVO) - R-060 WARN Nutzer muessen ueber KI-Nutzung informiert werden (AI Act Art. 52) - -Erforderliche Controls: - C_EXPLICIT_CONSENT Einwilligung fuer Chatbot-Nutzung einholen - C_TRANSPARENCY Hinweis "Sie sprechen mit einer KI" - C_DATA_MINIMIZATION Nur notwendige Bestelldaten abrufen - -DSFA erforderlich: Nein (Risikoscore unter 40) -Eskalation: E0 (keine manuelle Pruefung noetig)`} - - - {/* ============================================================ */} - {/* 5. DAS ESKALATIONS-SYSTEM */} - {/* ============================================================ */} -

5. Das Eskalations-System: Wann Menschen entscheiden

-

- Nicht jede Bewertung ist eindeutig. Fuer heikle Faelle gibt es ein abgestuftes - Eskalations-System, das sicherstellt, dass die richtigen Menschen die endgueltige - Entscheidung treffen. -

- -
- - - - - - - - - - - - - - - - -
StufeWann?Wer prueft?Frist (SLA)Beispiel
E0Nur INFO-Regeln, Risiko < 20Niemand (automatisch freigegeben)--Spam-Filter ohne personenbezogene Daten
E1WARN-Regeln, Risiko 20-39Teamleiter24 StundenChatbot mit Kundendaten (unser Beispiel oben)
E2Art. 9-Daten ODER Risiko 40-59 ODER DSFA empfohlenDatenschutzbeauftragter (DSB)8 StundenKI-System, das Gesundheitsdaten verarbeitet
E3BLOCK-Regel ODER Risiko ≥ 60 ODER Art. 22-RisikoDSB + Rechtsabteilung4 StundenVollautomatische Kreditentscheidung
-
- -

- Zuweisung: Die Zuweisung erfolgt automatisch an den Pruefer mit der - geringsten aktuellen Arbeitslast (Workload-basiertes Round-Robin). Jeder Pruefer hat eine - konfigurierbare Obergrenze fuer gleichzeitige Reviews (z.B. 10 fuer Teamleiter, 5 fuer DSB, - 3 fuer Rechtsabteilung). -

-

- Entscheidung: Der Pruefer kann den Anwendungsfall freigeben, - ablehnen, mit Auflagen freigeben oder weiter eskalieren. - Jede Entscheidung wird mit Begruendung im Audit-Trail gespeichert. -

- - {/* ============================================================ */} - {/* 6. CONTROLS, EVIDENCE & RISIKEN */} - {/* ============================================================ */} -

6. Controls, Nachweise und Risiken

- -

6.1 Was sind Controls?

-

- Ein Control ist eine konkrete Massnahme, die eine Organisation umsetzt, - um ein Compliance-Risiko zu beherrschen. Es gibt drei Arten: -

-
    -
  • Technische Controls: Verschluesselung, Zugangskontrollen, Firewalls, Pseudonymisierung
  • -
  • Organisatorische Controls: Schulungen, Richtlinien, Verantwortlichkeiten, Audits
  • -
  • Physische Controls: Zutrittskontrolle zu Serverraeumen, Schliesssysteme
  • -
-

- Der Compliance Hub verwaltet einen Katalog von ueber 100 vordefinierten Controls, - die in 9 Domaenen organisiert sind: -

-
-
- {[ - { code: 'AC', name: 'Zugriffsmanagement', desc: 'Wer darf was?' }, - { code: 'DP', name: 'Datenschutz', desc: 'Schutz personenbezogener Daten' }, - { code: 'NS', name: 'Netzwerksicherheit', desc: 'Sichere Kommunikation' }, - { code: 'IR', name: 'Incident Response', desc: 'Reaktion auf Sicherheitsvorfaelle' }, - { code: 'BC', name: 'Business Continuity', desc: 'Geschaeftskontinuitaet' }, - { code: 'VM', name: 'Vendor Management', desc: 'Dienstleister-Steuerung' }, - { code: 'AM', name: 'Asset Management', desc: 'Verwaltung von IT-Werten' }, - { code: 'CR', name: 'Kryptographie', desc: 'Verschluesselung & Schluessel' }, - { code: 'PS', name: 'Physische Sicherheit', desc: 'Gebaeude & Hardware' }, - ].map(d => ( -
-
{d.code}
-
{d.name}
-
{d.desc}
-
- ))} -
-
- -

6.2 Wie Controls mit Gesetzen verknuepft sind

-

- Jeder Control ist mit einem oder mehreren Gesetzesartikeln verknuepft. Diese - Mappings machen sichtbar, warum eine Massnahme erforderlich ist: -

- - -{`Control: AC-01 (Zugriffskontrolle) -├── DSGVO Art. 32 → "Sicherheit der Verarbeitung" -├── NIS2 Art. 21 → "Massnahmen zum Management von Cyberrisiken" -├── ISO 27001 A.9 → "Zugangskontrolle" -└── BSI Grundschutz → "ORP.4 Identitaets- und Berechtigungsmanagement" - -Control: DP-03 (Datenverschluesselung) -├── DSGVO Art. 32 → "Verschluesselung personenbezogener Daten" -├── DSGVO Art. 34 → "Benachrichtigung ueber Datenverletzung" (Ausnahme bei Verschluesselung) -└── NIS2 Art. 21 → "Einsatz von Kryptographie"`} - - -

6.3 Evidence (Nachweise)

-

- Ein Control allein genuegt nicht -- man muss auch nachweisen, dass er - umgesetzt wurde. Das System verwaltet verschiedene Nachweis-Typen: -

-
    -
  • Zertifikate: ISO 27001-Zertifikat, SOC2-Report
  • -
  • Richtlinien: Interne Datenschutzrichtlinie, Passwort-Policy
  • -
  • Audit-Berichte: Ergebnisse interner oder externer Pruefungen
  • -
  • Screenshots / Konfigurationen: Nachweis technischer Umsetzung
  • -
-

- Jeder Nachweis hat ein Ablaufdatum. Das System warnt automatisch, - wenn Nachweise bald ablaufen (z.B. ein ISO-Zertifikat, das in 3 Monaten erneuert werden muss). -

- -

6.4 Risikobewertung

-

- Risiken werden in einer 5x5-Risikomatrix dargestellt. Die beiden Achsen sind: -

-
    -
  • Eintrittswahrscheinlichkeit: Wie wahrscheinlich ist es, dass das Risiko eintritt?
  • -
  • Auswirkung: Wie schwerwiegend waeren die Folgen?
  • -
-

- Aus der Kombination ergibt sich die Risikostufe: Minimal, Low, - Medium, High oder Critical. Fuer jedes identifizierte Risiko - wird dokumentiert, welche Controls es abmildern und wer dafuer verantwortlich ist. -

- - {/* ============================================================ */} - {/* 7. OBLIGATIONS FRAMEWORK */} - {/* ============================================================ */} -

7. Pflichten-Ableitung: Welche Gesetze gelten fuer mich?

-

- Nicht jedes Gesetz gilt fuer jede Organisation. Das Obligations Framework - ermittelt automatisch, welche konkreten Pflichten sich aus der Situation einer Organisation - ergeben. Dafuer werden “Fakten” ueber die Organisation gesammelt und gegen die - Anwendbarkeitsbedingungen der einzelnen Gesetze geprueft. -

- -

Beispiel: NIS2-Anwendbarkeit

- -{`Ist Ihr Unternehmen in einem der NIS2-Sektoren taetig? -(Energie, Transport, Banken, Gesundheit, Wasser, Digitale Infrastruktur, ...) - │ - ├── Nein → NIS2 gilt NICHT fuer Sie - │ - └── Ja → Wie gross ist Ihr Unternehmen? - │ - ├── >= 250 Mitarbeiter ODER >= 50 Mio. EUR Umsatz - │ → ESSENTIAL ENTITY (wesentliche Einrichtung) - │ → Volle NIS2-Pflichten, strenge Aufsicht - │ → Bussgelder bis 10 Mio. EUR oder 2% Jahresumsatz - │ - ├── >= 50 Mitarbeiter ODER >= 10 Mio. EUR Umsatz - │ → IMPORTANT ENTITY (wichtige Einrichtung) - │ → NIS2-Pflichten, reaktive Aufsicht - │ → Bussgelder bis 7 Mio. EUR oder 1,4% Jahresumsatz - │ - └── Kleiner → NIS2 gilt grundsaetzlich NICHT - (Ausnahmen fuer bestimmte Sektoren moeglich)`} - - -

- Aehnliche Entscheidungsbaeume existieren fuer DSGVO (Verarbeitung personenbezogener Daten?), - AI Act (KI-System im Einsatz? Welche Risikokategorie?) und alle anderen Regelwerke. - Das System leitet daraus konkrete Pflichten ab -- z.B. “Meldepflicht bei - Sicherheitsvorfaellen innerhalb von 72 Stunden” oder “Ernennung eines - Datenschutzbeauftragten”. -

- - {/* ============================================================ */} - {/* 8. DSGVO-MODULE */} - {/* ============================================================ */} -

8. DSGVO-Compliance-Module im Detail

-

- Fuer die Einhaltung der DSGVO bietet der Compliance Hub spezialisierte Module: -

- -

8.1 Consent Management (Einwilligungsverwaltung)

-

- Verwaltet die Einwilligung von Nutzern gemaess Art. 6/7 DSGVO. Jede Einwilligung wird - protokolliert: wer hat wann, auf welchem Kanal, fuer welchen Zweck zugestimmt (oder - abgelehnt)? Einwilligungen koennen jederzeit widerrufen werden, der Widerruf wird ebenfalls - dokumentiert. -

-

- Zwecke: Essential (funktionsnotwendig), Functional, Analytics, Marketing, - Personalization, Third-Party. -

- -

8.2 DSR Management (Betroffenenrechte)

-

- Verwaltet Antraege betroffener Personen nach Art. 15-21 DSGVO: Auskunft, Berichtigung, - Loeschung, Datenportabilitaet, Einschraenkung und Widerspruch. Das System ueberwacht die - 30-Tage-Frist (Art. 12) und eskaliert automatisch, wenn Fristen drohen - zu verstreichen. -

- -

8.3 VVT (Verzeichnis von Verarbeitungstaetigkeiten)

-

- Dokumentiert alle Datenverarbeitungen gemaess Art. 30 DSGVO: Welche Daten werden fuer - welchen Zweck, auf welcher Rechtsgrundlage, wie lange und von wem verarbeitet? Jede - Verarbeitungstaetigkeit wird mit ihren Datenkategorien, Empfaengern und - Loeschfristen erfasst. -

- -

8.4 DSFA (Datenschutz-Folgenabschaetzung)

-

- Wenn eine Datenverarbeitung voraussichtlich ein hohes Risiko fuer die Rechte natuerlicher - Personen mit sich bringt, ist eine DSFA nach Art. 35 DSGVO Pflicht. Das System unterstuetzt - den Prozess: Risiken identifizieren, bewerten, Gegenmassnahmen definieren und das Ergebnis - dokumentieren. -

- -

8.5 TOM (Technisch-Organisatorische Massnahmen)

-

- Dokumentiert die Schutzmassnahmen nach Art. 32 DSGVO. Fuer jede Massnahme wird erfasst: - Kategorie (z.B. Verschluesselung, Zugriffskontrolle), Status (implementiert / in - Bearbeitung / geplant), Verantwortlicher und Nachweise. -

- -

8.6 Loeschkonzept

-

- Verwaltet Aufbewahrungsfristen und automatische Loeschung gemaess Art. 5/17 DSGVO. - Fuer jede Datenkategorie wird definiert: wie lange darf sie gespeichert werden, wann muss - sie geloescht werden und wie (z.B. Ueberschreiben, Schluesselloeschung bei verschluesselten - Daten). -

- - {/* ============================================================ */} - {/* 9. MULTI-TENANCY & ZUGRIFFSKONTROLLE */} - {/* ============================================================ */} -

9. Multi-Tenancy und Zugriffskontrolle

-

- Das System ist mandantenfaehig (Multi-Tenant): Mehrere Organisationen - koennen es gleichzeitig nutzen, ohne dass sie gegenseitig auf ihre Daten zugreifen koennen. - Jede Anfrage enthaelt eine Tenant-ID, und die Datenbank-Abfragen filtern automatisch nach - dieser ID. -

- -

9.1 Rollenbasierte Zugriffskontrolle (RBAC)

-

- Innerhalb eines Mandanten gibt es verschiedene Rollen mit unterschiedlichen Berechtigungen: -

-
- - - - - - - - - - - - - - -
RolleDarf
MitarbeiterAnwendungsfaelle einreichen, eigene Bewertungen einsehen
TeamleiterE1-Eskalationen pruefen, Team-Assessments einsehen
DSB (Datenschutzbeauftragter)E2/E3-Eskalationen pruefen, alle Assessments einsehen, Policies aendern
RechtsabteilungE3-Eskalationen pruefen, Grundsatzentscheidungen
AdministratorSystem konfigurieren, Nutzer verwalten, LLM-Policies festlegen
-
- -

9.2 PII-Erkennung und -Schutz

-

- Bevor Texte an ein Sprachmodell gesendet werden, durchlaufen sie eine automatische - PII-Erkennung (Personally Identifiable Information). Das System erkennt - ueber 20 Arten personenbezogener Daten: -

-
    -
  • E-Mail-Adressen, Telefonnummern, Postanschriften
  • -
  • Sozialversicherungsnummern, Kreditkartennummern
  • -
  • Personennamen, IP-Adressen
  • -
  • und weitere...
  • -
-

- Je nach Konfiguration werden erkannte PII-Daten geschwuerzt (durch - Platzhalter ersetzt), maskiert (nur Anfang/Ende sichtbar) oder nur im - Audit-Log markiert. -

- - {/* ============================================================ */} - {/* 10. LLM-NUTZUNG */} - {/* ============================================================ */} -

10. Wie das System KI nutzt (und wie nicht)

-

- Der Compliance Hub setzt kuenstliche Intelligenz gezielt und kontrolliert ein. Es gibt - eine klare Trennung zwischen dem, was die KI tut, und dem, was sie nicht tun darf: -

- -
- - - - - - - - - - - - - - - - - - -
AufgabeEntschieden vonRolle der KI
Machbarkeit (YES/CONDITIONAL/NO)Deterministische RegelnKeine
Risikoscore berechnenRegelbasierte BerechnungKeine
Eskalation ausloesenSchwellenwerte + RegellogikKeine
Controls zuordnenRegel-zu-Control-MappingKeine
Ergebnis erklaeren--LLM + RAG-Kontext
Verbesserungsvorschlaege--LLM
Rechtsfragen beantworten--LLM + RAG (Rechtskorpus)
Dokumente generieren (DSFA, TOM, VVT)--LLM + Vorlagen
-
- -

LLM-Provider und Fallback

-

- Das System unterstuetzt mehrere KI-Anbieter mit automatischem Fallback: -

-
    -
  1. Primaer: Ollama (lokal) -- Qwen 2.5 32B bzw. Mistral, laeuft direkt auf dem Server. Keine Daten verlassen das lokale Netzwerk.
  2. -
  3. Fallback: Anthropic Claude -- Wird nur aktiviert, wenn das lokale Modell nicht verfuegbar ist.
  4. -
-

- Jeder LLM-Aufruf wird im Audit-Trail protokolliert: Prompt-Hash (SHA-256), verwendetes - Modell, Antwortzeit und ob PII erkannt wurde. -

- - {/* ============================================================ */} - {/* 11. AUDIT-TRAIL */} - {/* ============================================================ */} -

11. Audit-Trail: Alles wird protokolliert

-

- Saemtliche Aktionen im System werden revisionssicher protokolliert: -

-
    -
  • Jede Compliance-Bewertung mit allen Ein- und Ausgaben
  • -
  • Jede Eskalationsentscheidung mit Begruendung
  • -
  • Jeder LLM-Aufruf (wer hat was wann gefragt, welches Modell wurde verwendet)
  • -
  • Jede Aenderung an Controls, Evidence und Policies
  • -
  • Jeder Login und Daten-Export
  • -
-

- Der Audit-Trail kann als PDF, CSV oder JSON exportiert werden und dient als - Nachweis gegenueber Aufsichtsbehoerden, Wirtschaftspruefern und internen Revisoren. -

- - - Der Use-Case-Text (die Beschreibung des Anwendungsfalls) wird - nur mit Einwilligung des Nutzers gespeichert. Standardmaessig wird nur - ein SHA-256-Hash des Textes gespeichert -- damit kann nachgewiesen werden, dass - ein bestimmter Text bewertet wurde, ohne den Text selbst preiszugeben. - - - {/* ============================================================ */} - {/* 12. SECURITY SCANNER */} - {/* ============================================================ */} -

12. Security Scanner: Technische Sicherheitspruefung

-

- Ergaenzend zur rechtlichen Compliance prueft der Security Scanner die - technische Sicherheit: -

-
    -
  • Container-Scanning (Trivy): Prueft Docker-Images auf bekannte Schwachstellen (CVEs)
  • -
  • Statische Code-Analyse (Semgrep): Sucht im Quellcode nach Sicherheitsluecken (SQL Injection, XSS, etc.)
  • -
  • Secret Detection (Gitleaks): Findet versehentlich eingecheckte Passwoerter, API-Keys und Tokens
  • -
  • SBOM-Generierung: Erstellt eine Software Bill of Materials -- eine vollstaendige Liste aller verwendeten Bibliotheken und deren Lizenzen
  • -
-

- Gefundene Schwachstellen werden nach Schweregrad (Critical, High, Medium, Low) klassifiziert - und koennen direkt im System nachverfolgt und behoben werden. -

- - {/* ============================================================ */} - {/* 13. ZUSAMMENFASSUNG */} - {/* ============================================================ */} -

13. Zusammenfassung: Der komplette Datenfluss

-

- Hier ist der gesamte Prozess von Anfang bis Ende: -

- - -{`SCHRITT 1: FAKTEN SAMMELN -━━━━━━━━━━━━━━━━━━━━━━━━ -Nutzer fuellt Fragebogen aus: - → Welche Daten? Welcher Zweck? Welche Branche? Wo gehostet? - -SCHRITT 2: ANWENDBARKEIT PRUEFEN -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Obligations Framework ermittelt: - → DSGVO betroffen? → Ja (personenbezogene Daten) - → AI Act betroffen? → Ja (KI-System) - → NIS2 betroffen? → Nein (< 50 Mitarbeiter, kein KRITIS-Sektor) - -SCHRITT 3: REGELN PRUEFEN -━━━━━━━━━━━━━━━━━━━━━━━━ -Policy Engine wertet 45+ Regeln aus: - → R-001 (WARN): Personenbezogene Daten +10 Risiko - → R-020 (INFO): Assistenzsystem +0 Risiko - → R-060 (WARN): KI-Transparenz fehlt +15 Risiko - → ... - → Gesamt-Risikoscore: 35/100 (LOW) - → Machbarkeit: CONDITIONAL - -SCHRITT 4: CONTROLS ZUORDNEN -━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Jede ausgeloeste Regel triggert Controls: - → C_EXPLICIT_CONSENT: Einwilligung einholen - → C_TRANSPARENCY: KI-Nutzung offenlegen - → C_DATA_MINIMIZATION: Datenminimierung - -SCHRITT 5: ESKALATION (bei Bedarf) -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Score 35 → Stufe E1 → Teamleiter wird benachrichtigt - → SLA: 24 Stunden fuer Pruefung - → Entscheidung: Freigabe mit Auflagen - -SCHRITT 6: ERKLAERUNG GENERIEREN -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -LLM + RAG erstellen verstaendliche Erklaerung: - → Suche relevante Gesetzesartikel (Qdrant) - → Generiere Erklaerungstext (Qwen 2.5) - → Fuege Zitate und Quellen hinzu - -SCHRITT 7: DOKUMENTATION -━━━━━━━━━━━━━━━━━━━━━━━ -System erzeugt erforderliche Dokumente: - → DSFA (falls empfohlen) - → TOM-Dokumentation - → VVT-Eintrag - → Compliance-Report (PDF/ZIP/JSON) - -SCHRITT 8: MONITORING -━━━━━━━━━━━━━━━━━━━━ -Laufende Ueberwachung: - → Controls werden regelmaessig geprueft - → Nachweise werden auf Ablauf ueberwacht - → Gesetzesaenderungen fliessen in den Corpus ein`} - - - - Der Compliance Hub nimmt die Beschreibung eines KI-Vorhabens entgegen, prueft es gegen - ueber 45 deterministische Regeln und 400+ Gesetzesartikel, berechnet ein Risiko, ordnet - Massnahmen zu, eskaliert bei Bedarf an menschliche Pruefer und dokumentiert alles - revisionssicher -- wobei die KI nur fuer Erklaerungen und Zusammenfassungen eingesetzt wird, - niemals fuer die eigentliche Compliance-Entscheidung. - + + + + + + ) } diff --git a/docs-src/README.md b/docs-src/README.md new file mode 100644 index 0000000..8081848 --- /dev/null +++ b/docs-src/README.md @@ -0,0 +1,19 @@ +# docs-src + +MkDocs-based internal documentation site — system architecture, data models, runbooks, API references. + +**Port:** `8011` (container: `bp-compliance-docs`) +**Stack:** MkDocs + Material theme, served via nginx. + +## Build + serve locally + +```bash +cd docs-src +pip install -r requirements.txt +mkdocs serve # http://localhost:8000 +mkdocs build # static output to site/ +``` + +## Known debt (Phase 4) + +- `index.md` is 9436 lines — will be split into per-topic pages with proper mkdocs nav. Target: no single markdown file >500 lines except explicit reference tables. diff --git a/document-crawler/README.md b/document-crawler/README.md new file mode 100644 index 0000000..185a796 --- /dev/null +++ b/document-crawler/README.md @@ -0,0 +1,28 @@ +# document-crawler + +Python/FastAPI service for document ingestion and compliance gap analysis. Parses PDF, DOCX, XLSX, PPTX; runs gap analysis against compliance requirements; coordinates with `ai-compliance-sdk` via the LLM gateway; archives to `dsms-gateway`. + +**Port:** `8098` (container: `bp-compliance-document-crawler`) +**Stack:** Python 3.11, FastAPI. + +## Architecture + +Small service — already well under the LOC budget. Follow `../AGENTS.python.md` for any additions. + +## Run locally + +```bash +cd document-crawler +pip install -r requirements.txt +uvicorn main:app --reload --port 8098 +``` + +## Tests + +```bash +pytest tests/ -v +``` + +## Public API surface + +`GET /health`, document upload/parse endpoints, gap-analysis endpoints. See the OpenAPI doc at `/docs` when running. diff --git a/dsms-gateway/README.md b/dsms-gateway/README.md new file mode 100644 index 0000000..ecf758b --- /dev/null +++ b/dsms-gateway/README.md @@ -0,0 +1,56 @@ +# dsms-gateway + +Python/FastAPI gateway to the IPFS-backed document archival store. Upload, retrieve, verify, and archive legal documents with content-addressed immutability. + +**Port:** `8082` (container: `bp-compliance-dsms-gateway`) +**Stack:** Python 3.11, FastAPI, IPFS (Kubo via `dsms-node`). + +## Architecture + +Phase 4 refactor is complete. `main.py` is now a thin 41-LOC entry point: + +``` +dsms-gateway/ +├── main.py # FastAPI app factory, 41 LOC +├── routers/ +│ ├── documents.py # /documents, /legal-documents, /verify routes +│ └── node.py # /node routes +├── models.py # Pydantic models +├── dependencies.py # Shared FastAPI dependencies +└── config.py # Settings +``` + +See `../AGENTS.python.md`. + +## Run locally + +```bash +cd dsms-gateway +pip install -r requirements.txt +export IPFS_API_URL=http://localhost:5001 +uvicorn main:app --reload --port 8082 +``` + +## Tests + +```bash +pytest test_main.py -v +``` + +27/27 tests pass. Test coverage matches the current module structure. + +## Public API surface + +``` +GET /health +GET /api/v1/documents +POST /api/v1/documents +GET /api/v1/documents/{cid} +GET /api/v1/documents/{cid}/metadata +DELETE /api/v1/documents/{cid} +POST /api/v1/legal-documents/archive +GET /api/v1/verify/{cid} +GET /api/v1/node/info +``` + +Every path is a contract — updating requires synchronized updates in consumers. diff --git a/dsms-gateway/config.py b/dsms-gateway/config.py new file mode 100644 index 0000000..d468d39 --- /dev/null +++ b/dsms-gateway/config.py @@ -0,0 +1,9 @@ +""" +DSMS Gateway — runtime configuration from environment variables. +""" + +import os + +IPFS_API_URL = os.getenv("IPFS_API_URL", "http://dsms-node:5001") +IPFS_GATEWAY_URL = os.getenv("IPFS_GATEWAY_URL", "http://dsms-node:8080") +JWT_SECRET = os.getenv("JWT_SECRET", "your-super-secret-jwt-key-change-in-production") diff --git a/dsms-gateway/dependencies.py b/dsms-gateway/dependencies.py new file mode 100644 index 0000000..28cc1b8 --- /dev/null +++ b/dsms-gateway/dependencies.py @@ -0,0 +1,76 @@ +""" +DSMS Gateway — shared FastAPI dependencies and IPFS helper coroutines. +""" + +from typing import Optional + +import httpx +from fastapi import Header, HTTPException + +from config import IPFS_API_URL + + +async def verify_token(authorization: Optional[str] = Header(None)) -> dict: + """Verifiziert JWT Token (vereinfacht für MVP)""" + if not authorization: + raise HTTPException(status_code=401, detail="Authorization header fehlt") + + # In Produktion: JWT validieren + # Für MVP: Einfache Token-Prüfung + if not authorization.startswith("Bearer "): + raise HTTPException(status_code=401, detail="Ungültiges Token-Format") + + return {"valid": True} + + +async def ipfs_add(content: bytes, pin: bool = True) -> dict: + """Fügt Inhalt zu IPFS hinzu""" + async with httpx.AsyncClient(timeout=60.0) as client: + files = {"file": ("document", content)} + params = {"pin": str(pin).lower()} + + response = await client.post( + f"{IPFS_API_URL}/api/v0/add", + files=files, + params=params + ) + + if response.status_code != 200: + raise HTTPException( + status_code=502, + detail=f"IPFS Fehler: {response.text}" + ) + + return response.json() + + +async def ipfs_cat(cid: str) -> bytes: + """Liest Inhalt von IPFS""" + async with httpx.AsyncClient(timeout=60.0) as client: + response = await client.post( + f"{IPFS_API_URL}/api/v0/cat", + params={"arg": cid} + ) + + if response.status_code != 200: + raise HTTPException( + status_code=404, + detail=f"Dokument nicht gefunden: {cid}" + ) + + return response.content + + +async def ipfs_pin_ls() -> list: + """Listet alle gepinnten Objekte""" + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.post( + f"{IPFS_API_URL}/api/v0/pin/ls", + params={"type": "recursive"} + ) + + if response.status_code != 200: + return [] + + data = response.json() + return list(data.get("Keys", {}).keys()) diff --git a/dsms-gateway/main.py b/dsms-gateway/main.py index 0a2a390..a8d0a2f 100644 --- a/dsms-gateway/main.py +++ b/dsms-gateway/main.py @@ -3,17 +3,18 @@ DSMS Gateway - REST API für dezentrales Speichersystem Bietet eine vereinfachte API über IPFS für BreakPilot """ +import sys import os -import json -import httpx -import hashlib -from datetime import datetime -from typing import Optional -from fastapi import FastAPI, HTTPException, UploadFile, File, Header, Depends + +# Ensure the gateway directory itself is on the path so routers can use flat imports. +sys.path.insert(0, os.path.dirname(__file__)) + +from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import StreamingResponse -from pydantic import BaseModel -import io + +from models import DocumentMetadata, StoredDocument, DocumentList # noqa: F401 — re-exported for tests +from routers.documents import router as documents_router +from routers.node import router as node_router app = FastAPI( title="DSMS Gateway", @@ -30,436 +31,9 @@ app.add_middleware( allow_headers=["*"], ) -# Configuration -IPFS_API_URL = os.getenv("IPFS_API_URL", "http://dsms-node:5001") -IPFS_GATEWAY_URL = os.getenv("IPFS_GATEWAY_URL", "http://dsms-node:8080") -JWT_SECRET = os.getenv("JWT_SECRET", "your-super-secret-jwt-key-change-in-production") - - -# Models -class DocumentMetadata(BaseModel): - """Metadaten für gespeicherte Dokumente""" - document_type: str # 'legal_document', 'consent_record', 'audit_log' - document_id: Optional[str] = None - version: Optional[str] = None - language: Optional[str] = "de" - created_at: Optional[str] = None - checksum: Optional[str] = None - encrypted: bool = False - - -class StoredDocument(BaseModel): - """Antwort nach erfolgreichem Speichern""" - cid: str # Content Identifier (IPFS Hash) - size: int - metadata: DocumentMetadata - gateway_url: str - timestamp: str - - -class DocumentList(BaseModel): - """Liste der gespeicherten Dokumente""" - documents: list - total: int - - -# Helper Functions -async def verify_token(authorization: Optional[str] = Header(None)) -> dict: - """Verifiziert JWT Token (vereinfacht für MVP)""" - if not authorization: - raise HTTPException(status_code=401, detail="Authorization header fehlt") - - # In Produktion: JWT validieren - # Für MVP: Einfache Token-Prüfung - if not authorization.startswith("Bearer "): - raise HTTPException(status_code=401, detail="Ungültiges Token-Format") - - return {"valid": True} - - -async def ipfs_add(content: bytes, pin: bool = True) -> dict: - """Fügt Inhalt zu IPFS hinzu""" - async with httpx.AsyncClient(timeout=60.0) as client: - files = {"file": ("document", content)} - params = {"pin": str(pin).lower()} - - response = await client.post( - f"{IPFS_API_URL}/api/v0/add", - files=files, - params=params - ) - - if response.status_code != 200: - raise HTTPException( - status_code=502, - detail=f"IPFS Fehler: {response.text}" - ) - - return response.json() - - -async def ipfs_cat(cid: str) -> bytes: - """Liest Inhalt von IPFS""" - async with httpx.AsyncClient(timeout=60.0) as client: - response = await client.post( - f"{IPFS_API_URL}/api/v0/cat", - params={"arg": cid} - ) - - if response.status_code != 200: - raise HTTPException( - status_code=404, - detail=f"Dokument nicht gefunden: {cid}" - ) - - return response.content - - -async def ipfs_pin_ls() -> list: - """Listet alle gepinnten Objekte""" - async with httpx.AsyncClient(timeout=30.0) as client: - response = await client.post( - f"{IPFS_API_URL}/api/v0/pin/ls", - params={"type": "recursive"} - ) - - if response.status_code != 200: - return [] - - data = response.json() - return list(data.get("Keys", {}).keys()) - - -# API Endpoints -@app.get("/health") -async def health_check(): - """Health Check für DSMS Gateway""" - try: - async with httpx.AsyncClient(timeout=5.0) as client: - response = await client.post(f"{IPFS_API_URL}/api/v0/id") - ipfs_status = response.status_code == 200 - except Exception: - ipfs_status = False - - return { - "status": "healthy" if ipfs_status else "degraded", - "ipfs_connected": ipfs_status, - "timestamp": datetime.utcnow().isoformat() - } - - -@app.post("/api/v1/documents", response_model=StoredDocument) -async def store_document( - file: UploadFile = File(...), - document_type: str = "legal_document", - document_id: Optional[str] = None, - version: Optional[str] = None, - language: str = "de", - _auth: dict = Depends(verify_token) -): - """ - Speichert ein Dokument im DSMS. - - - **file**: Das zu speichernde Dokument - - **document_type**: Typ des Dokuments (legal_document, consent_record, audit_log) - - **document_id**: Optionale ID des Dokuments - - **version**: Optionale Versionsnummer - - **language**: Sprache (default: de) - """ - content = await file.read() - - # Checksum berechnen - checksum = hashlib.sha256(content).hexdigest() - - # Metadaten erstellen - metadata = DocumentMetadata( - document_type=document_type, - document_id=document_id, - version=version, - language=language, - created_at=datetime.utcnow().isoformat(), - checksum=checksum, - encrypted=False - ) - - # Dokument mit Metadaten als JSON verpacken - package = { - "metadata": metadata.model_dump(), - "content_base64": content.hex(), # Hex-encodiert für JSON - "filename": file.filename - } - - package_bytes = json.dumps(package).encode() - - # Zu IPFS hinzufügen - result = await ipfs_add(package_bytes) - - cid = result.get("Hash") - size = int(result.get("Size", 0)) - - return StoredDocument( - cid=cid, - size=size, - metadata=metadata, - gateway_url=f"{IPFS_GATEWAY_URL}/ipfs/{cid}", - timestamp=datetime.utcnow().isoformat() - ) - - -@app.get("/api/v1/documents/{cid}") -async def get_document( - cid: str, - _auth: dict = Depends(verify_token) -): - """ - Ruft ein Dokument aus dem DSMS ab. - - - **cid**: Content Identifier (IPFS Hash) - """ - content = await ipfs_cat(cid) - - try: - package = json.loads(content) - metadata = package.get("metadata", {}) - original_content = bytes.fromhex(package.get("content_base64", "")) - filename = package.get("filename", "document") - - return StreamingResponse( - io.BytesIO(original_content), - media_type="application/octet-stream", - headers={ - "Content-Disposition": f'attachment; filename="{filename}"', - "X-DSMS-Document-Type": metadata.get("document_type", "unknown"), - "X-DSMS-Checksum": metadata.get("checksum", ""), - "X-DSMS-Created-At": metadata.get("created_at", "") - } - ) - except json.JSONDecodeError: - # Wenn es kein DSMS-Paket ist, gib rohen Inhalt zurück - return StreamingResponse( - io.BytesIO(content), - media_type="application/octet-stream" - ) - - -@app.get("/api/v1/documents/{cid}/metadata") -async def get_document_metadata( - cid: str, - _auth: dict = Depends(verify_token) -): - """ - Ruft nur die Metadaten eines Dokuments ab. - - - **cid**: Content Identifier (IPFS Hash) - """ - content = await ipfs_cat(cid) - - try: - package = json.loads(content) - return { - "cid": cid, - "metadata": package.get("metadata", {}), - "filename": package.get("filename"), - "size": len(bytes.fromhex(package.get("content_base64", ""))) - } - except json.JSONDecodeError: - return { - "cid": cid, - "metadata": {}, - "raw_size": len(content) - } - - -@app.get("/api/v1/documents", response_model=DocumentList) -async def list_documents( - _auth: dict = Depends(verify_token) -): - """ - Listet alle gespeicherten Dokumente auf. - """ - cids = await ipfs_pin_ls() - - documents = [] - for cid in cids[:100]: # Limit auf 100 für Performance - try: - content = await ipfs_cat(cid) - package = json.loads(content) - documents.append({ - "cid": cid, - "metadata": package.get("metadata", {}), - "filename": package.get("filename") - }) - except Exception: - # Überspringe nicht-DSMS Objekte - continue - - return DocumentList( - documents=documents, - total=len(documents) - ) - - -@app.delete("/api/v1/documents/{cid}") -async def unpin_document( - cid: str, - _auth: dict = Depends(verify_token) -): - """ - Entfernt ein Dokument aus dem lokalen Pin-Set. - Das Dokument bleibt im Netzwerk, wird aber bei GC entfernt. - - - **cid**: Content Identifier (IPFS Hash) - """ - async with httpx.AsyncClient(timeout=30.0) as client: - response = await client.post( - f"{IPFS_API_URL}/api/v0/pin/rm", - params={"arg": cid} - ) - - if response.status_code != 200: - raise HTTPException( - status_code=404, - detail=f"Konnte Pin nicht entfernen: {cid}" - ) - - return { - "status": "unpinned", - "cid": cid, - "message": "Dokument wird bei nächster Garbage Collection entfernt" - } - - -@app.post("/api/v1/legal-documents/archive") -async def archive_legal_document( - document_id: str, - version: str, - content: str, - language: str = "de", - _auth: dict = Depends(verify_token) -): - """ - Archiviert eine rechtliche Dokumentversion dauerhaft. - Speziell für AGB, Datenschutzerklärung, etc. - - - **document_id**: ID des Legal Documents - - **version**: Versionsnummer - - **content**: HTML/Markdown Inhalt - - **language**: Sprache - """ - # Checksum berechnen - content_bytes = content.encode('utf-8') - checksum = hashlib.sha256(content_bytes).hexdigest() - - # Metadaten - metadata = { - "document_type": "legal_document", - "document_id": document_id, - "version": version, - "language": language, - "created_at": datetime.utcnow().isoformat(), - "checksum": checksum, - "content_type": "text/html" - } - - # Paket erstellen - package = { - "metadata": metadata, - "content": content, - "archived_at": datetime.utcnow().isoformat() - } - - package_bytes = json.dumps(package, ensure_ascii=False).encode('utf-8') - - # Zu IPFS hinzufügen - result = await ipfs_add(package_bytes) - - cid = result.get("Hash") - - return { - "cid": cid, - "document_id": document_id, - "version": version, - "checksum": checksum, - "archived_at": datetime.utcnow().isoformat(), - "verification_url": f"{IPFS_GATEWAY_URL}/ipfs/{cid}" - } - - -@app.get("/api/v1/verify/{cid}") -async def verify_document(cid: str): - """ - Verifiziert die Integrität eines Dokuments. - Öffentlich zugänglich für Audit-Zwecke. - - - **cid**: Content Identifier (IPFS Hash) - """ - try: - content = await ipfs_cat(cid) - package = json.loads(content) - - # Checksum verifizieren - stored_checksum = package.get("metadata", {}).get("checksum") - - if "content_base64" in package: - original_content = bytes.fromhex(package["content_base64"]) - calculated_checksum = hashlib.sha256(original_content).hexdigest() - elif "content" in package: - calculated_checksum = hashlib.sha256( - package["content"].encode('utf-8') - ).hexdigest() - else: - calculated_checksum = None - - integrity_valid = ( - stored_checksum == calculated_checksum - if stored_checksum and calculated_checksum - else None - ) - - return { - "cid": cid, - "exists": True, - "integrity_valid": integrity_valid, - "metadata": package.get("metadata", {}), - "stored_checksum": stored_checksum, - "calculated_checksum": calculated_checksum, - "verified_at": datetime.utcnow().isoformat() - } - except Exception as e: - return { - "cid": cid, - "exists": False, - "error": str(e), - "verified_at": datetime.utcnow().isoformat() - } - - -@app.get("/api/v1/node/info") -async def get_node_info(): - """ - Gibt Informationen über den DSMS Node zurück. - """ - try: - async with httpx.AsyncClient(timeout=10.0) as client: - # Node ID - id_response = await client.post(f"{IPFS_API_URL}/api/v0/id") - node_info = id_response.json() if id_response.status_code == 200 else {} - - # Repo Stats - stat_response = await client.post(f"{IPFS_API_URL}/api/v0/repo/stat") - repo_stats = stat_response.json() if stat_response.status_code == 200 else {} - - return { - "node_id": node_info.get("ID"), - "protocol_version": node_info.get("ProtocolVersion"), - "agent_version": node_info.get("AgentVersion"), - "repo_size": repo_stats.get("RepoSize"), - "storage_max": repo_stats.get("StorageMax"), - "num_objects": repo_stats.get("NumObjects"), - "addresses": node_info.get("Addresses", [])[:5] # Erste 5 - } - except Exception as e: - return {"error": str(e)} +# Router registration +app.include_router(node_router) +app.include_router(documents_router) if __name__ == "__main__": diff --git a/dsms-gateway/models.py b/dsms-gateway/models.py new file mode 100644 index 0000000..e5ec1eb --- /dev/null +++ b/dsms-gateway/models.py @@ -0,0 +1,32 @@ +""" +DSMS Gateway — Pydantic request/response models. +""" + +from typing import Optional +from pydantic import BaseModel + + +class DocumentMetadata(BaseModel): + """Metadaten für gespeicherte Dokumente""" + document_type: str # 'legal_document', 'consent_record', 'audit_log' + document_id: Optional[str] = None + version: Optional[str] = None + language: Optional[str] = "de" + created_at: Optional[str] = None + checksum: Optional[str] = None + encrypted: bool = False + + +class StoredDocument(BaseModel): + """Antwort nach erfolgreichem Speichern""" + cid: str # Content Identifier (IPFS Hash) + size: int + metadata: DocumentMetadata + gateway_url: str + timestamp: str + + +class DocumentList(BaseModel): + """Liste der gespeicherten Dokumente""" + documents: list + total: int diff --git a/dsms-gateway/routers/__init__.py b/dsms-gateway/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dsms-gateway/routers/documents.py b/dsms-gateway/routers/documents.py new file mode 100644 index 0000000..b45ffa5 --- /dev/null +++ b/dsms-gateway/routers/documents.py @@ -0,0 +1,256 @@ +""" +Documents router — handles /api/v1/documents and /api/v1/legal-documents endpoints. +""" + +import hashlib +import json +import io +from datetime import datetime +from typing import Optional + +import httpx +from fastapi import APIRouter, Depends, File, HTTPException, UploadFile +from fastapi.responses import StreamingResponse + +from models import DocumentList, DocumentMetadata, StoredDocument +from dependencies import verify_token, ipfs_add, ipfs_cat, ipfs_pin_ls +from config import IPFS_API_URL, IPFS_GATEWAY_URL + +router = APIRouter() + + +@router.post("/api/v1/documents", response_model=StoredDocument) +async def store_document( + file: UploadFile = File(...), + document_type: str = "legal_document", + document_id: Optional[str] = None, + version: Optional[str] = None, + language: str = "de", + _auth: dict = Depends(verify_token) +): + """ + Speichert ein Dokument im DSMS. + + - **file**: Das zu speichernde Dokument + - **document_type**: Typ des Dokuments (legal_document, consent_record, audit_log) + - **document_id**: Optionale ID des Dokuments + - **version**: Optionale Versionsnummer + - **language**: Sprache (default: de) + """ + content = await file.read() + + # Checksum berechnen + checksum = hashlib.sha256(content).hexdigest() + + # Metadaten erstellen + metadata = DocumentMetadata( + document_type=document_type, + document_id=document_id, + version=version, + language=language, + created_at=datetime.utcnow().isoformat(), + checksum=checksum, + encrypted=False + ) + + # Dokument mit Metadaten als JSON verpacken + package = { + "metadata": metadata.model_dump(), + "content_base64": content.hex(), # Hex-encodiert für JSON + "filename": file.filename + } + + package_bytes = json.dumps(package).encode() + + # Zu IPFS hinzufügen + result = await ipfs_add(package_bytes) + + cid = result.get("Hash") + size = int(result.get("Size", 0)) + + return StoredDocument( + cid=cid, + size=size, + metadata=metadata, + gateway_url=f"{IPFS_GATEWAY_URL}/ipfs/{cid}", + timestamp=datetime.utcnow().isoformat() + ) + + +@router.get("/api/v1/documents/{cid}") +async def get_document( + cid: str, + _auth: dict = Depends(verify_token) +): + """ + Ruft ein Dokument aus dem DSMS ab. + + - **cid**: Content Identifier (IPFS Hash) + """ + content = await ipfs_cat(cid) + + try: + package = json.loads(content) + metadata = package.get("metadata", {}) + original_content = bytes.fromhex(package.get("content_base64", "")) + filename = package.get("filename", "document") + + return StreamingResponse( + io.BytesIO(original_content), + media_type="application/octet-stream", + headers={ + "Content-Disposition": f'attachment; filename="{filename}"', + "X-DSMS-Document-Type": metadata.get("document_type", "unknown"), + "X-DSMS-Checksum": metadata.get("checksum", ""), + "X-DSMS-Created-At": metadata.get("created_at", "") + } + ) + except json.JSONDecodeError: + # Wenn es kein DSMS-Paket ist, gib rohen Inhalt zurück + return StreamingResponse( + io.BytesIO(content), + media_type="application/octet-stream" + ) + + +@router.get("/api/v1/documents/{cid}/metadata") +async def get_document_metadata( + cid: str, + _auth: dict = Depends(verify_token) +): + """ + Ruft nur die Metadaten eines Dokuments ab. + + - **cid**: Content Identifier (IPFS Hash) + """ + content = await ipfs_cat(cid) + + try: + package = json.loads(content) + return { + "cid": cid, + "metadata": package.get("metadata", {}), + "filename": package.get("filename"), + "size": len(bytes.fromhex(package.get("content_base64", ""))) + } + except json.JSONDecodeError: + return { + "cid": cid, + "metadata": {}, + "raw_size": len(content) + } + + +@router.get("/api/v1/documents", response_model=DocumentList) +async def list_documents( + _auth: dict = Depends(verify_token) +): + """ + Listet alle gespeicherten Dokumente auf. + """ + cids = await ipfs_pin_ls() + + documents = [] + for cid in cids[:100]: # Limit auf 100 für Performance + try: + content = await ipfs_cat(cid) + package = json.loads(content) + documents.append({ + "cid": cid, + "metadata": package.get("metadata", {}), + "filename": package.get("filename") + }) + except Exception: + # Überspringe nicht-DSMS Objekte + continue + + return DocumentList( + documents=documents, + total=len(documents) + ) + + +@router.delete("/api/v1/documents/{cid}") +async def unpin_document( + cid: str, + _auth: dict = Depends(verify_token) +): + """ + Entfernt ein Dokument aus dem lokalen Pin-Set. + Das Dokument bleibt im Netzwerk, wird aber bei GC entfernt. + + - **cid**: Content Identifier (IPFS Hash) + """ + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.post( + f"{IPFS_API_URL}/api/v0/pin/rm", + params={"arg": cid} + ) + + if response.status_code != 200: + raise HTTPException( + status_code=404, + detail=f"Konnte Pin nicht entfernen: {cid}" + ) + + return { + "status": "unpinned", + "cid": cid, + "message": "Dokument wird bei nächster Garbage Collection entfernt" + } + + +@router.post("/api/v1/legal-documents/archive") +async def archive_legal_document( + document_id: str, + version: str, + content: str, + language: str = "de", + _auth: dict = Depends(verify_token) +): + """ + Archiviert eine rechtliche Dokumentversion dauerhaft. + Speziell für AGB, Datenschutzerklärung, etc. + + - **document_id**: ID des Legal Documents + - **version**: Versionsnummer + - **content**: HTML/Markdown Inhalt + - **language**: Sprache + """ + # Checksum berechnen + content_bytes = content.encode('utf-8') + checksum = hashlib.sha256(content_bytes).hexdigest() + + # Metadaten + metadata = { + "document_type": "legal_document", + "document_id": document_id, + "version": version, + "language": language, + "created_at": datetime.utcnow().isoformat(), + "checksum": checksum, + "content_type": "text/html" + } + + # Paket erstellen + package = { + "metadata": metadata, + "content": content, + "archived_at": datetime.utcnow().isoformat() + } + + package_bytes = json.dumps(package, ensure_ascii=False).encode('utf-8') + + # Zu IPFS hinzufügen + result = await ipfs_add(package_bytes) + + cid = result.get("Hash") + + return { + "cid": cid, + "document_id": document_id, + "version": version, + "checksum": checksum, + "archived_at": datetime.utcnow().isoformat(), + "verification_url": f"{IPFS_GATEWAY_URL}/ipfs/{cid}" + } diff --git a/dsms-gateway/routers/node.py b/dsms-gateway/routers/node.py new file mode 100644 index 0000000..21883e9 --- /dev/null +++ b/dsms-gateway/routers/node.py @@ -0,0 +1,109 @@ +""" +Node router — handles /health, /api/v1/verify/{cid}, and /api/v1/node/info endpoints. +""" + +import hashlib +import json +from datetime import datetime + +import httpx +from fastapi import APIRouter + +from dependencies import ipfs_cat +from config import IPFS_API_URL + +router = APIRouter() + + +@router.get("/health") +async def health_check(): + """Health Check für DSMS Gateway""" + try: + async with httpx.AsyncClient(timeout=5.0) as client: + response = await client.post(f"{IPFS_API_URL}/api/v0/id") + ipfs_status = response.status_code == 200 + except Exception: + ipfs_status = False + + return { + "status": "healthy" if ipfs_status else "degraded", + "ipfs_connected": ipfs_status, + "timestamp": datetime.utcnow().isoformat() + } + + +@router.get("/api/v1/verify/{cid}") +async def verify_document(cid: str): + """ + Verifiziert die Integrität eines Dokuments. + Öffentlich zugänglich für Audit-Zwecke. + + - **cid**: Content Identifier (IPFS Hash) + """ + try: + content = await ipfs_cat(cid) + package = json.loads(content) + + # Checksum verifizieren + stored_checksum = package.get("metadata", {}).get("checksum") + + if "content_base64" in package: + original_content = bytes.fromhex(package["content_base64"]) + calculated_checksum = hashlib.sha256(original_content).hexdigest() + elif "content" in package: + calculated_checksum = hashlib.sha256( + package["content"].encode('utf-8') + ).hexdigest() + else: + calculated_checksum = None + + integrity_valid = ( + stored_checksum == calculated_checksum + if stored_checksum and calculated_checksum + else None + ) + + return { + "cid": cid, + "exists": True, + "integrity_valid": integrity_valid, + "metadata": package.get("metadata", {}), + "stored_checksum": stored_checksum, + "calculated_checksum": calculated_checksum, + "verified_at": datetime.utcnow().isoformat() + } + except Exception as e: + return { + "cid": cid, + "exists": False, + "error": str(e), + "verified_at": datetime.utcnow().isoformat() + } + + +@router.get("/api/v1/node/info") +async def get_node_info(): + """ + Gibt Informationen über den DSMS Node zurück. + """ + try: + async with httpx.AsyncClient(timeout=10.0) as client: + # Node ID + id_response = await client.post(f"{IPFS_API_URL}/api/v0/id") + node_info = id_response.json() if id_response.status_code == 200 else {} + + # Repo Stats + stat_response = await client.post(f"{IPFS_API_URL}/api/v0/repo/stat") + repo_stats = stat_response.json() if stat_response.status_code == 200 else {} + + return { + "node_id": node_info.get("ID"), + "protocol_version": node_info.get("ProtocolVersion"), + "agent_version": node_info.get("AgentVersion"), + "repo_size": repo_stats.get("RepoSize"), + "storage_max": repo_stats.get("StorageMax"), + "num_objects": repo_stats.get("NumObjects"), + "addresses": node_info.get("Addresses", [])[:5] # Erste 5 + } + except Exception as e: + return {"error": str(e)} diff --git a/dsms-gateway/test_main.py b/dsms-gateway/test_main.py index 8a40705..bde31b7 100644 --- a/dsms-gateway/test_main.py +++ b/dsms-gateway/test_main.py @@ -56,7 +56,7 @@ class TestHealthCheck: def test_health_check_ipfs_connected(self): """Test: Health Check wenn IPFS verbunden ist""" - with patch("main.httpx.AsyncClient") as mock_client: + with patch("routers.node.httpx.AsyncClient") as mock_client: mock_instance = AsyncMock() mock_instance.post.return_value = MagicMock(status_code=200) mock_client.return_value.__aenter__.return_value = mock_instance @@ -71,7 +71,7 @@ class TestHealthCheck: def test_health_check_ipfs_disconnected(self): """Test: Health Check wenn IPFS nicht erreichbar""" - with patch("main.httpx.AsyncClient") as mock_client: + with patch("routers.node.httpx.AsyncClient") as mock_client: mock_instance = AsyncMock() mock_instance.post.side_effect = Exception("Connection failed") mock_client.return_value.__aenter__.return_value = mock_instance @@ -104,7 +104,7 @@ class TestAuthorization: def test_documents_endpoint_with_valid_token_format(self, valid_auth_header): """Test: Gültiges Token-Format wird akzeptiert""" - with patch("main.ipfs_pin_ls", new_callable=AsyncMock) as mock_pin_ls: + with patch("routers.documents.ipfs_pin_ls", new_callable=AsyncMock) as mock_pin_ls: mock_pin_ls.return_value = [] response = client.get( @@ -122,7 +122,7 @@ class TestDocumentStorage: def test_store_document_success(self, valid_auth_header, mock_ipfs_response): """Test: Dokument erfolgreich speichern""" - with patch("main.ipfs_add", new_callable=AsyncMock) as mock_add: + with patch("routers.documents.ipfs_add", new_callable=AsyncMock) as mock_add: mock_add.return_value = mock_ipfs_response test_content = b"Test document content" @@ -148,7 +148,7 @@ class TestDocumentStorage: def test_store_document_calculates_checksum(self, valid_auth_header, mock_ipfs_response): """Test: Checksum wird korrekt berechnet""" - with patch("main.ipfs_add", new_callable=AsyncMock) as mock_add: + with patch("routers.documents.ipfs_add", new_callable=AsyncMock) as mock_add: mock_add.return_value = mock_ipfs_response test_content = b"Test content for checksum" @@ -191,7 +191,7 @@ class TestDocumentRetrieval: "filename": "test.txt" } - with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat: + with patch("routers.documents.ipfs_cat", new_callable=AsyncMock) as mock_cat: mock_cat.return_value = json.dumps(package).encode() response = client.get( @@ -204,7 +204,7 @@ class TestDocumentRetrieval: def test_get_document_not_found(self, valid_auth_header): """Test: Nicht existierendes Dokument gibt 404 zurück""" - with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat: + with patch("routers.documents.ipfs_cat", new_callable=AsyncMock) as mock_cat: from fastapi import HTTPException mock_cat.side_effect = HTTPException(status_code=404, detail="Not found") @@ -228,7 +228,7 @@ class TestDocumentRetrieval: "filename": "test.txt" } - with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat: + with patch("routers.documents.ipfs_cat", new_callable=AsyncMock) as mock_cat: mock_cat.return_value = json.dumps(package).encode() response = client.get( @@ -249,7 +249,7 @@ class TestDocumentList: def test_list_documents_empty(self, valid_auth_header): """Test: Leere Dokumentenliste""" - with patch("main.ipfs_pin_ls", new_callable=AsyncMock) as mock_pin_ls: + with patch("routers.documents.ipfs_pin_ls", new_callable=AsyncMock) as mock_pin_ls: mock_pin_ls.return_value = [] response = client.get( @@ -270,10 +270,10 @@ class TestDocumentList: "filename": "test.txt" } - with patch("main.ipfs_pin_ls", new_callable=AsyncMock) as mock_pin_ls: + with patch("routers.documents.ipfs_pin_ls", new_callable=AsyncMock) as mock_pin_ls: mock_pin_ls.return_value = ["QmCid1", "QmCid2"] - with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat: + with patch("routers.documents.ipfs_cat", new_callable=AsyncMock) as mock_cat: mock_cat.return_value = json.dumps(package).encode() response = client.get( @@ -293,7 +293,7 @@ class TestDocumentDeletion: def test_unpin_document_success(self, valid_auth_header): """Test: Dokument erfolgreich unpinnen""" - with patch("main.httpx.AsyncClient") as mock_client: + with patch("routers.documents.httpx.AsyncClient") as mock_client: mock_instance = AsyncMock() mock_instance.post.return_value = MagicMock(status_code=200) mock_client.return_value.__aenter__.return_value = mock_instance @@ -310,7 +310,7 @@ class TestDocumentDeletion: def test_unpin_document_not_found(self, valid_auth_header): """Test: Nicht existierendes Dokument unpinnen""" - with patch("main.httpx.AsyncClient") as mock_client: + with patch("routers.documents.httpx.AsyncClient") as mock_client: mock_instance = AsyncMock() mock_instance.post.return_value = MagicMock(status_code=404) mock_client.return_value.__aenter__.return_value = mock_instance @@ -330,7 +330,7 @@ class TestLegalDocumentArchive: def test_archive_legal_document_success(self, valid_auth_header, mock_ipfs_response): """Test: Legal Document erfolgreich archivieren""" - with patch("main.ipfs_add", new_callable=AsyncMock) as mock_add: + with patch("routers.documents.ipfs_add", new_callable=AsyncMock) as mock_add: mock_add.return_value = mock_ipfs_response response = client.post( @@ -357,7 +357,7 @@ class TestLegalDocumentArchive: content = "

Test Content

" expected_checksum = hashlib.sha256(content.encode('utf-8')).hexdigest() - with patch("main.ipfs_add", new_callable=AsyncMock) as mock_add: + with patch("routers.documents.ipfs_add", new_callable=AsyncMock) as mock_add: mock_add.return_value = mock_ipfs_response response = client.post( @@ -393,7 +393,7 @@ class TestDocumentVerification: "content": content } - with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat: + with patch("routers.node.ipfs_cat", new_callable=AsyncMock) as mock_cat: mock_cat.return_value = json.dumps(package).encode() response = client.get("/api/v1/verify/QmTestCid123") @@ -415,7 +415,7 @@ class TestDocumentVerification: "content": "Actual content" } - with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat: + with patch("routers.node.ipfs_cat", new_callable=AsyncMock) as mock_cat: mock_cat.return_value = json.dumps(package).encode() response = client.get("/api/v1/verify/QmTestCid123") @@ -427,7 +427,7 @@ class TestDocumentVerification: def test_verify_document_not_found(self): """Test: Nicht existierendes Dokument verifizieren""" - with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat: + with patch("routers.node.ipfs_cat", new_callable=AsyncMock) as mock_cat: mock_cat.side_effect = Exception("Not found") response = client.get("/api/v1/verify/QmNonExistent") @@ -444,7 +444,7 @@ class TestDocumentVerification: "content": "test" } - with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat: + with patch("routers.node.ipfs_cat", new_callable=AsyncMock) as mock_cat: mock_cat.return_value = json.dumps(package).encode() # Kein Authorization Header! @@ -472,7 +472,7 @@ class TestNodeInfo: "NumObjects": 42 } - with patch("main.httpx.AsyncClient") as mock_client: + with patch("routers.node.httpx.AsyncClient") as mock_client: mock_instance = AsyncMock() async def mock_post(url, **kwargs): @@ -497,7 +497,7 @@ class TestNodeInfo: def test_get_node_info_public_access(self): """Test: Node-Info ist öffentlich zugänglich""" - with patch("main.httpx.AsyncClient") as mock_client: + with patch("routers.node.httpx.AsyncClient") as mock_client: mock_instance = AsyncMock() mock_instance.post.return_value = MagicMock( status_code=200, diff --git a/dsms-node/README.md b/dsms-node/README.md new file mode 100644 index 0000000..d8335d7 --- /dev/null +++ b/dsms-node/README.md @@ -0,0 +1,15 @@ +# dsms-node + +IPFS Kubo node container — distributed document storage backend for the compliance platform. Participates in the BreakPilot IPFS swarm and serves as the storage layer behind `dsms-gateway`. + +**Image:** `ipfs/kubo:v0.24.0` +**Ports:** `4001` (swarm), `5001` (API), `8085` (HTTP gateway) +**Container:** `bp-compliance-dsms-node` + +## Operation + +No source code — this is a thin wrapper around the upstream IPFS Kubo image. Configuration is via environment and the compose file at repo root. + +## Don't touch + +This service is out of refactor scope. Do not modify without the infrastructure owner's sign-off. diff --git a/scripts/check-loc.sh b/scripts/check-loc.sh new file mode 100755 index 0000000..a303c79 --- /dev/null +++ b/scripts/check-loc.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# check-loc.sh — File-size budget enforcer for breakpilot-compliance. +# +# Soft target: 300 LOC. Hard cap: 500 LOC. +# +# Usage: +# scripts/check-loc.sh # scan whole repo, respect exceptions +# scripts/check-loc.sh --changed # only files changed vs origin/main +# scripts/check-loc.sh path/to/file.py # check specific files +# scripts/check-loc.sh --json # machine-readable output +# +# Exit codes: +# 0 — clean (no hard violations) +# 1 — at least one file exceeds the hard cap (500) +# 2 — invalid invocation +# +# Behavior: +# - Skips test files, generated files, vendor dirs, node_modules, .git, dist, build, +# .next, __pycache__, migrations, and anything matching .claude/rules/loc-exceptions.txt. +# - Counts non-blank, non-comment-only lines is NOT done — we count raw lines so the +# rule is unambiguous. If you want to game it with blank lines, you're missing the point. + +set -euo pipefail + +SOFT=300 +HARD=500 +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +EXCEPTIONS_FILE="$REPO_ROOT/.claude/rules/loc-exceptions.txt" + +CHANGED_ONLY=0 +JSON=0 +TARGETS=() + +for arg in "$@"; do + case "$arg" in + --changed) CHANGED_ONLY=1 ;; + --json) JSON=1 ;; + -h|--help) + sed -n '2,18p' "$0"; exit 0 ;; + -*) echo "unknown flag: $arg" >&2; exit 2 ;; + *) TARGETS+=("$arg") ;; + esac +done + +# Patterns excluded from the budget regardless of path. +is_excluded() { + local f="$1" + case "$f" in + */node_modules/*|*/.next/*|*/.git/*|*/dist/*|*/build/*|*/__pycache__/*|*/vendor/*) return 0 ;; + */migrations/*|*/alembic/versions/*) return 0 ;; + *_test.go|*.test.ts|*.test.tsx|*.spec.ts|*.spec.tsx) return 0 ;; + *_test.py|*/test_*.py|test_*.py) return 0 ;; + */tests/*|*/test/*) return 0 ;; + *.md|*.json|*.yaml|*.yml|*.lock|*.sum|*.mod|*.toml|*.cfg|*.ini) return 0 ;; + *.html|*.html.j2|*.jinja|*.jinja2) return 0 ;; + *.svg|*.png|*.jpg|*.jpeg|*.gif|*.ico|*.pdf|*.woff|*.woff2|*.ttf) return 0 ;; + *.generated.*|*.gen.*|*_pb.go|*_pb2.py|*.pb.go) return 0 ;; + esac + return 1 +} + +is_in_exceptions() { + [[ -f "$EXCEPTIONS_FILE" ]] || return 1 + local rel="${1#$REPO_ROOT/}" + grep -Fxq "$rel" "$EXCEPTIONS_FILE" +} + +collect_targets() { + if (( ${#TARGETS[@]} > 0 )); then + printf '%s\n' "${TARGETS[@]}" + elif (( CHANGED_ONLY )); then + git -C "$REPO_ROOT" diff --name-only --diff-filter=AM origin/main...HEAD 2>/dev/null \ + || git -C "$REPO_ROOT" diff --name-only --diff-filter=AM HEAD + else + git -C "$REPO_ROOT" ls-files + fi +} + +violations_hard=() +violations_soft=() + +while IFS= read -r f; do + [[ -z "$f" ]] && continue + abs="$f" + [[ "$abs" != /* ]] && abs="$REPO_ROOT/$f" + [[ -f "$abs" ]] || continue + is_excluded "$abs" && continue + is_in_exceptions "$abs" && continue + loc=$(wc -l < "$abs" | tr -d ' ') + if (( loc > HARD )); then + violations_hard+=("$loc $f") + elif (( loc > SOFT )); then + violations_soft+=("$loc $f") + fi +done < <(collect_targets) + +if (( JSON )); then + printf '{"hard":[' + first=1; for v in "${violations_hard[@]}"; do + loc="${v%% *}"; path="${v#* }" + (( first )) || printf ','; first=0 + printf '{"loc":%s,"path":"%s"}' "$loc" "$path" + done + printf '],"soft":[' + first=1; for v in "${violations_soft[@]}"; do + loc="${v%% *}"; path="${v#* }" + (( first )) || printf ','; first=0 + printf '{"loc":%s,"path":"%s"}' "$loc" "$path" + done + printf ']}\n' +else + if (( ${#violations_soft[@]} > 0 )); then + echo "::warning:: $((${#violations_soft[@]})) file(s) exceed soft target ($SOFT lines):" + printf ' %s\n' "${violations_soft[@]}" | sort -rn + fi + if (( ${#violations_hard[@]} > 0 )); then + echo "::error:: $((${#violations_hard[@]})) file(s) exceed HARD CAP ($HARD lines) — split required:" + printf ' %s\n' "${violations_hard[@]}" | sort -rn + echo + echo "If a file legitimately must exceed $HARD lines (generated code, large data tables)," + echo "add it to .claude/rules/loc-exceptions.txt with a one-line rationale comment above it." + fi +fi + +(( ${#violations_hard[@]} == 0 )) diff --git a/scripts/githooks/pre-commit b/scripts/githooks/pre-commit new file mode 100755 index 0000000..44d0314 --- /dev/null +++ b/scripts/githooks/pre-commit @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# pre-commit — enforces breakpilot-compliance structural guardrails. +# +# 1. Blocks commits that introduce a non-test, non-generated source file > 500 LOC. +# 2. Blocks commits that touch backend-compliance/migrations/ unless the commit message +# contains the marker [migration-approved] (last-resort escape hatch). +# 3. Blocks edits to .claude/settings.json, scripts/check-loc.sh, or +# .claude/rules/loc-exceptions.txt unless [guardrail-change] is in the commit message. +# +# Bypass with --no-verify is intentionally NOT supported by the team workflow. +# CI re-runs all of these on the server side anyway. + +set -euo pipefail +REPO_ROOT="$(git rev-parse --show-toplevel)" + +mapfile -t staged < <(git diff --cached --name-only --diff-filter=ACM) +[[ ${#staged[@]} -eq 0 ]] && exit 0 + +# 1. LOC budget on staged files. +loc_targets=() +for f in "${staged[@]}"; do + [[ -f "$REPO_ROOT/$f" ]] && loc_targets+=("$REPO_ROOT/$f") +done +if [[ ${#loc_targets[@]} -gt 0 ]]; then + if ! "$REPO_ROOT/scripts/check-loc.sh" "${loc_targets[@]}"; then + echo + echo "Commit blocked: file-size budget violated. See output above." + echo "Either split the file (preferred) or add an exception with rationale to" + echo " .claude/rules/loc-exceptions.txt" + exit 1 + fi +fi + +# 2. Migration directories are frozen unless explicitly approved. +if printf '%s\n' "${staged[@]}" | grep -qE '(^|/)(migrations|alembic/versions)/'; then + if ! git log --format=%B -n 1 HEAD 2>/dev/null | grep -q '\[migration-approved\]' \ + && ! grep -q '\[migration-approved\]' "$(git rev-parse --git-dir)/COMMIT_EDITMSG" 2>/dev/null; then + echo "Commit blocked: this change touches a migrations directory." + echo "Database schema changes require an explicit migration plan reviewed by the DB owner." + echo "If approved, add '[migration-approved]' to your commit message." + exit 1 + fi +fi + +# 3. Guardrail files are protected. +guarded='^(\.claude/settings\.json|\.claude/rules/loc-exceptions\.txt|scripts/check-loc\.sh|scripts/githooks/pre-commit|AGENTS\.(python|go|typescript)\.md)$' +if printf '%s\n' "${staged[@]}" | grep -qE "$guarded"; then + if ! grep -q '\[guardrail-change\]' "$(git rev-parse --git-dir)/COMMIT_EDITMSG" 2>/dev/null; then + echo "Commit blocked: this change modifies guardrail files." + echo "If intentional, add '[guardrail-change]' to your commit message and explain why in the body." + exit 1 + fi +fi + +exit 0 diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh new file mode 100755 index 0000000..51cb144 --- /dev/null +++ b/scripts/install-hooks.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# install-hooks.sh — installs git hooks that enforce repo guardrails locally. +# Idempotent. Safe to re-run. + +set -euo pipefail +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +HOOKS_DIR="$REPO_ROOT/.git/hooks" +SRC_DIR="$REPO_ROOT/scripts/githooks" + +if [[ ! -d "$REPO_ROOT/.git" ]]; then + echo "Not a git repository: $REPO_ROOT" >&2 + exit 1 +fi + +mkdir -p "$HOOKS_DIR" +for hook in pre-commit; do + src="$SRC_DIR/$hook" + dst="$HOOKS_DIR/$hook" + if [[ -f "$src" ]]; then + cp "$src" "$dst" + chmod +x "$dst" + echo "installed: $dst" + fi +done + +echo "Done. Hooks active for this clone."