From 1e84df9769700257b4f5379af99b18546b25b165 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sat, 7 Mar 2026 14:12:34 +0100 Subject: [PATCH] feat(sdk): Multi-Tenancy, Versionierung, Change-Requests, Dokumentengenerierung (Phase 1-6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 6-Phasen-Implementation fuer cloud-faehiges, mandantenfaehiges Compliance SDK: Phase 1: Multi-Tenancy Fix - Shared tenant_utils.py Dependency (UUID-Validierung, kein "default" mehr) - VVT tenant_id Column + tenant-scoped Queries - DSFA/Vendor DEFAULT_TENANT_ID von "default" auf UUID migriert - Migration 035 Phase 2: Stammdaten-Erweiterung - Company Profile um JSONB-Felder erweitert (processing_systems, ai_systems, technical_contacts) - Regulierungs-Flags (NIS2, AI Act, ISO 27001) - GET /template-context Endpoint - Migration 036 Phase 3: Dokument-Versionierung - 5 Versions-Tabellen (DSFA, VVT, TOM, Loeschfristen, Obligations) - Shared versioning_utils.py Helper - /{id}/versions Endpoints auf allen 5 Dokumenttypen - Migration 037 Phase 4: Change-Request System - Zentrale CR-Inbox mit CRUD + Accept/Reject/Edit Workflow - Regelbasierte CR-Engine (VVT DPIA → DSFA CR, Datenkategorien → Loeschfristen CR) - Audit-Trail - Migration 038 Phase 5: Dokumentengenerierung - 5 Template-Generatoren (DSFA, VVT, TOM, Loeschfristen, Obligations) - Preview + Apply Endpoints (erzeugt CRs, keine direkten Dokumente) Phase 6: Frontend-Integration - Change-Request Inbox Page mit Stats, Filtern, Modals - VersionHistory Timeline-Komponente - SDKSidebar CR-Badge (60s Polling) - Company Profile: 2 neue Wizard-Steps + "Dokumente generieren" CTA Docs: 5 neue MkDocs-Seiten, CLAUDE.md aktualisiert Tests: 97 neue Tests (alle bestanden) Co-Authored-By: Claude Opus 4.6 --- .claude/CLAUDE.md | 43 +- .../app/(sdk)/sdk/change-requests/page.tsx | 345 ++++++++++++++ .../app/sdk/company-profile/page.tsx | 396 +++++++++++++++- .../components/sdk/Sidebar/SDKSidebar.tsx | 48 ++ .../components/sdk/VersionHistory.tsx | 110 +++++ backend-compliance/compliance/api/__init__.py | 6 + .../compliance/api/change_request_engine.py | 181 ++++++++ .../compliance/api/change_request_routes.py | 427 ++++++++++++++++++ .../compliance/api/company_profile_routes.py | 162 ++++++- .../api/document_templates/__init__.py | 15 + .../api/document_templates/dsfa_template.py | 82 ++++ .../loeschfristen_template.py | 49 ++ .../document_templates/obligation_template.py | 141 ++++++ .../api/document_templates/tom_template.py | 69 +++ .../api/document_templates/vvt_template.py | 53 +++ .../compliance/api/dsfa_routes.py | 36 +- .../compliance/api/generation_routes.py | 186 ++++++++ .../compliance/api/loeschfristen_routes.py | 32 ++ .../compliance/api/obligation_routes.py | 32 ++ .../compliance/api/tenant_utils.py | 58 +++ .../compliance/api/tom_routes.py | 34 ++ .../api/vendor_compliance_routes.py | 3 +- .../compliance/api/versioning_utils.py | 175 +++++++ .../compliance/api/vvt_routes.py | 122 ++++- .../compliance/db/vvt_models.py | 7 +- .../migrations/035_vvt_tenant_isolation.sql | 105 +++++ .../migrations/036_company_profile_extend.sql | 34 ++ .../migrations/037_document_versions.sql | 141 ++++++ .../migrations/038_change_requests.sql | 64 +++ .../tests/test_change_request_routes.py | 329 ++++++++++++++ .../tests/test_company_profile_extend.py | 268 +++++++++++ .../tests/test_document_versions.py | 234 ++++++++++ .../tests/test_generation_routes.py | 233 ++++++++++ backend-compliance/tests/test_vvt_routes.py | 3 +- .../tests/test_vvt_tenant_isolation.py | 205 +++++++++ .../services/sdk-modules/change-requests.md | 125 +++++ .../sdk-modules/dokumentengenerierung.md | 82 ++++ .../services/sdk-modules/multi-tenancy.md | 60 +++ docs-src/services/sdk-modules/stammdaten.md | 90 ++++ .../services/sdk-modules/versionierung.md | 80 ++++ mkdocs.yml | 5 + 41 files changed, 4818 insertions(+), 52 deletions(-) create mode 100644 admin-compliance/app/(sdk)/sdk/change-requests/page.tsx create mode 100644 admin-compliance/components/sdk/VersionHistory.tsx create mode 100644 backend-compliance/compliance/api/change_request_engine.py create mode 100644 backend-compliance/compliance/api/change_request_routes.py create mode 100644 backend-compliance/compliance/api/document_templates/__init__.py create mode 100644 backend-compliance/compliance/api/document_templates/dsfa_template.py create mode 100644 backend-compliance/compliance/api/document_templates/loeschfristen_template.py create mode 100644 backend-compliance/compliance/api/document_templates/obligation_template.py create mode 100644 backend-compliance/compliance/api/document_templates/tom_template.py create mode 100644 backend-compliance/compliance/api/document_templates/vvt_template.py create mode 100644 backend-compliance/compliance/api/generation_routes.py create mode 100644 backend-compliance/compliance/api/tenant_utils.py create mode 100644 backend-compliance/compliance/api/versioning_utils.py create mode 100644 backend-compliance/migrations/035_vvt_tenant_isolation.sql create mode 100644 backend-compliance/migrations/036_company_profile_extend.sql create mode 100644 backend-compliance/migrations/037_document_versions.sql create mode 100644 backend-compliance/migrations/038_change_requests.sql create mode 100644 backend-compliance/tests/test_change_request_routes.py create mode 100644 backend-compliance/tests/test_company_profile_extend.py create mode 100644 backend-compliance/tests/test_document_versions.py create mode 100644 backend-compliance/tests/test_generation_routes.py create mode 100644 backend-compliance/tests/test_vvt_tenant_isolation.py create mode 100644 docs-src/services/sdk-modules/change-requests.md create mode 100644 docs-src/services/sdk-modules/dokumentengenerierung.md create mode 100644 docs-src/services/sdk-modules/multi-tenancy.md create mode 100644 docs-src/services/sdk-modules/stammdaten.md create mode 100644 docs-src/services/sdk-modules/versionierung.md diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index d320d9c..b18ba44 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -269,18 +269,55 @@ POST/GET /api/v1/compliance/evidence POST/GET /api/v1/dsr/requests POST/GET /api/v1/gdpr/exports POST/GET /api/v1/consent/admin + +# Stammdaten, Versionierung & Change-Requests (Phase 1-6, 2026-03-07) +GET/POST/DELETE /api/compliance/company-profile +GET /api/compliance/company-profile/template-context +GET /api/compliance/change-requests +GET /api/compliance/change-requests/stats +POST /api/compliance/change-requests/{id}/accept +POST /api/compliance/change-requests/{id}/reject +POST /api/compliance/change-requests/{id}/edit +GET /api/compliance/generation/preview/{doc_type} +POST /api/compliance/generation/apply/{doc_type} +GET /api/compliance/{doc}/{id}/versions ``` +### Multi-Tenancy +- Shared Dependency: `compliance/api/tenant_utils.py` (`get_tenant_id()`) +- UUID-Format, kein `"default"` mehr +- Header `X-Tenant-ID` > Query `tenant_id` > ENV-Fallback + +### Migrations (035-038) +| Nr | Datei | Beschreibung | +|----|-------|--------------| +| 035 | `migrations/035_vvt_tenant_isolation.sql` | VVT tenant_id + DSFA/Vendor default→UUID | +| 036 | `migrations/036_company_profile_extend.sql` | Stammdaten JSONB + Regulierungs-Flags | +| 037 | `migrations/037_document_versions.sql` | 5 Versions-Tabellen + current_version | +| 038 | `migrations/038_change_requests.sql` | Change-Requests + Audit-Log | + +### Neue Backend-Module +| Datei | Beschreibung | +|-------|--------------| +| `compliance/api/tenant_utils.py` | Shared Tenant-ID Dependency | +| `compliance/api/versioning_utils.py` | Shared Versioning Helper | +| `compliance/api/change_request_routes.py` | CR CRUD + Accept/Reject/Edit | +| `compliance/api/change_request_engine.py` | Regelbasierte CR-Generierung | +| `compliance/api/generation_routes.py` | Dokumentengenerierung aus Stammdaten | +| `compliance/api/document_templates/` | 5 Template-Generatoren (DSFA, VVT, TOM, etc.) | + --- ## Wichtige Dateien (Referenz) | Datei | Beschreibung | |-------|--------------| -| `admin-compliance/app/(sdk)/` | Alle 37 SDK-Routes | -| `admin-compliance/components/sdk/SDKSidebar.tsx` | SDK Navigation | +| `admin-compliance/app/(sdk)/` | Alle 37+ SDK-Routes | +| `admin-compliance/app/(sdk)/sdk/change-requests/page.tsx` | Change-Request Inbox | +| `admin-compliance/components/sdk/Sidebar/SDKSidebar.tsx` | SDK Navigation (mit CR-Badge) | +| `admin-compliance/components/sdk/VersionHistory.tsx` | Versions-Timeline-Komponente | | `admin-compliance/components/sdk/CommandBar.tsx` | Command Palette | | `admin-compliance/lib/sdk/context.tsx` | SDK State (Provider) | -| `backend-compliance/compliance/` | Haupt-Package (40 Dateien) | +| `backend-compliance/compliance/` | Haupt-Package (50+ Dateien) | | `ai-compliance-sdk/` | KI-Compliance Analyse | | `developer-portal/` | API-Dokumentation | diff --git a/admin-compliance/app/(sdk)/sdk/change-requests/page.tsx b/admin-compliance/app/(sdk)/sdk/change-requests/page.tsx new file mode 100644 index 0000000..4ada0b4 --- /dev/null +++ b/admin-compliance/app/(sdk)/sdk/change-requests/page.tsx @@ -0,0 +1,345 @@ +'use client' + +import { useState, useEffect, useCallback } from 'react' + +interface ChangeRequest { + id: string + triggerType: string + targetDocumentType: string + targetDocumentId: string | null + targetSection: string | null + proposalTitle: string + proposalBody: string | null + proposedChanges: Record + status: 'pending' | 'accepted' | 'rejected' | 'edited_and_accepted' + priority: 'low' | 'normal' | 'high' | 'critical' + decidedBy: string | null + decidedAt: string | null + rejectionReason: string | null + createdBy: string + createdAt: string +} + +interface Stats { + total_pending: number + critical_count: number + total_accepted: number + total_rejected: number + by_document_type: Record +} + +const API_BASE = '/api/sdk/v1/compliance/change-requests' + +const DOC_TYPE_LABELS: Record = { + dsfa: 'DSFA', + vvt: 'VVT', + tom: 'TOM', + loeschfristen: 'Löschfristen', + obligation: 'Pflichten', +} + +const PRIORITY_COLORS: Record = { + critical: 'bg-red-100 text-red-800', + high: 'bg-orange-100 text-orange-800', + normal: 'bg-blue-100 text-blue-800', + low: 'bg-gray-100 text-gray-700', +} + +const STATUS_COLORS: Record = { + pending: 'bg-yellow-100 text-yellow-800', + accepted: 'bg-green-100 text-green-800', + rejected: 'bg-red-100 text-red-800', + edited_and_accepted: 'bg-emerald-100 text-emerald-800', +} + +function snakeToCamel(obj: Record): ChangeRequest { + return { + id: obj.id as string, + triggerType: obj.trigger_type as string, + targetDocumentType: obj.target_document_type as string, + targetDocumentId: obj.target_document_id as string | null, + targetSection: obj.target_section as string | null, + proposalTitle: obj.proposal_title as string, + proposalBody: obj.proposal_body as string | null, + proposedChanges: (obj.proposed_changes || {}) as Record, + status: obj.status as ChangeRequest['status'], + priority: obj.priority as ChangeRequest['priority'], + decidedBy: obj.decided_by as string | null, + decidedAt: obj.decided_at as string | null, + rejectionReason: obj.rejection_reason as string | null, + createdBy: obj.created_by as string, + createdAt: obj.created_at as string, + } +} + +export default function ChangeRequestsPage() { + const [requests, setRequests] = useState([]) + const [stats, setStats] = useState(null) + const [filter, setFilter] = useState('all') + const [statusFilter, setStatusFilter] = useState('pending') + const [loading, setLoading] = useState(true) + const [actionModal, setActionModal] = useState<{ type: 'accept' | 'reject' | 'edit'; cr: ChangeRequest } | null>(null) + const [rejectReason, setRejectReason] = useState('') + const [editBody, setEditBody] = useState('') + + const loadData = useCallback(async () => { + try { + const statsRes = await fetch(`${API_BASE}/stats`) + if (statsRes.ok) setStats(await statsRes.json()) + + let url = `${API_BASE}?status=${statusFilter}` + if (filter !== 'all') url += `&target_document_type=${filter}` + const res = await fetch(url) + if (res.ok) { + const data = await res.json() + setRequests((Array.isArray(data) ? data : []).map(snakeToCamel)) + } + } catch (e) { + console.error('Failed to load change requests:', e) + } finally { + setLoading(false) + } + }, [filter, statusFilter]) + + useEffect(() => { + loadData() + const interval = setInterval(loadData, 60000) + return () => clearInterval(interval) + }, [loadData]) + + const handleAccept = async (cr: ChangeRequest) => { + const res = await fetch(`${API_BASE}/${cr.id}/accept`, { method: 'POST' }) + if (res.ok) { + setActionModal(null) + loadData() + } + } + + const handleReject = async (cr: ChangeRequest) => { + const res = await fetch(`${API_BASE}/${cr.id}/reject`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ rejection_reason: rejectReason }), + }) + if (res.ok) { + setActionModal(null) + setRejectReason('') + loadData() + } + } + + const handleEdit = async (cr: ChangeRequest) => { + const res = await fetch(`${API_BASE}/${cr.id}/edit`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ proposal_body: editBody }), + }) + if (res.ok) { + setActionModal(null) + setEditBody('') + loadData() + } + } + + const handleDelete = async (id: string) => { + if (!confirm('Änderungsanfrage wirklich löschen?')) return + const res = await fetch(`${API_BASE}/${id}`, { method: 'DELETE' }) + if (res.ok) loadData() + } + + return ( +
+

Änderungsanfragen

+ + {/* Stats Bar */} + {stats && ( +
+
+
{stats.total_pending}
+
Offen
+
+
+
{stats.critical_count}
+
Kritisch
+
+
+
{stats.total_accepted}
+
Angenommen
+
+
+
{stats.total_rejected}
+
Abgelehnt
+
+
+ )} + + {/* Filters */} +
+
+ {['pending', 'accepted', 'rejected'].map(s => ( + + ))} +
+
+ {['all', 'dsfa', 'vvt', 'tom', 'loeschfristen', 'obligation'].map(t => ( + + ))} +
+
+ + {/* Request List */} + {loading ? ( +
Laden...
+ ) : requests.length === 0 ? ( +
+ Keine Änderungsanfragen {statusFilter === 'pending' ? 'offen' : 'gefunden'} +
+ ) : ( +
+ {requests.map(cr => ( +
+
+
+
+ + {cr.priority} + + + {cr.status} + + + {DOC_TYPE_LABELS[cr.targetDocumentType] || cr.targetDocumentType} + + {cr.triggerType !== 'manual' && ( + + {cr.triggerType} + + )} +
+

{cr.proposalTitle}

+ {cr.proposalBody && ( +

{cr.proposalBody}

+ )} +
+ {new Date(cr.createdAt).toLocaleDateString('de-DE')} — {cr.createdBy} + {cr.rejectionReason && ( + Grund: {cr.rejectionReason} + )} +
+
+ + {cr.status === 'pending' && ( +
+ + + + +
+ )} +
+
+ ))} +
+ )} + + {/* Action Modal */} + {actionModal && ( +
setActionModal(null)}> +
e.stopPropagation()}> + {actionModal.type === 'accept' && ( + <> +

Änderung annehmen?

+

{actionModal.cr.proposalTitle}

+ {Object.keys(actionModal.cr.proposedChanges).length > 0 && ( +
+                    {JSON.stringify(actionModal.cr.proposedChanges, null, 2)}
+                  
+ )} +
+ + +
+ + )} + {actionModal.type === 'reject' && ( + <> +

Änderung ablehnen

+