feat(sdk): Multi-Tenancy, Versionierung, Change-Requests, Dokumentengenerierung (Phase 1-6)
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 18s
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 18s
6-Phasen-Implementation fuer cloud-faehiges, mandantenfaehiges Compliance SDK:
Phase 1: Multi-Tenancy Fix
- Shared tenant_utils.py Dependency (UUID-Validierung, kein "default" mehr)
- VVT tenant_id Column + tenant-scoped Queries
- DSFA/Vendor DEFAULT_TENANT_ID von "default" auf UUID migriert
- Migration 035
Phase 2: Stammdaten-Erweiterung
- Company Profile um JSONB-Felder erweitert (processing_systems, ai_systems, technical_contacts)
- Regulierungs-Flags (NIS2, AI Act, ISO 27001)
- GET /template-context Endpoint
- Migration 036
Phase 3: Dokument-Versionierung
- 5 Versions-Tabellen (DSFA, VVT, TOM, Loeschfristen, Obligations)
- Shared versioning_utils.py Helper
- /{id}/versions Endpoints auf allen 5 Dokumenttypen
- Migration 037
Phase 4: Change-Request System
- Zentrale CR-Inbox mit CRUD + Accept/Reject/Edit Workflow
- Regelbasierte CR-Engine (VVT DPIA → DSFA CR, Datenkategorien → Loeschfristen CR)
- Audit-Trail
- Migration 038
Phase 5: Dokumentengenerierung
- 5 Template-Generatoren (DSFA, VVT, TOM, Loeschfristen, Obligations)
- Preview + Apply Endpoints (erzeugt CRs, keine direkten Dokumente)
Phase 6: Frontend-Integration
- Change-Request Inbox Page mit Stats, Filtern, Modals
- VersionHistory Timeline-Komponente
- SDKSidebar CR-Badge (60s Polling)
- Company Profile: 2 neue Wizard-Steps + "Dokumente generieren" CTA
Docs: 5 neue MkDocs-Seiten, CLAUDE.md aktualisiert
Tests: 97 neue Tests (alle bestanden)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -342,6 +342,25 @@ function CorpusStalenessInfo({ ragCorpusStatus }: { ragCorpusStatus: RAGCorpusSt
|
||||
export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarProps) {
|
||||
const pathname = usePathname()
|
||||
const { state, packageCompletion, completionPercentage, getCheckpointStatus } = useSDK()
|
||||
const [pendingCRCount, setPendingCRCount] = React.useState(0)
|
||||
|
||||
// Poll pending change-request count every 60s
|
||||
React.useEffect(() => {
|
||||
let active = true
|
||||
async function fetchCRCount() {
|
||||
try {
|
||||
const res = await fetch('/api/sdk/v1/compliance/change-requests/stats')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
if (active) setPendingCRCount(data.total_pending || 0)
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
fetchCRCount()
|
||||
const interval = setInterval(fetchCRCount, 60000)
|
||||
return () => { active = false; clearInterval(interval) }
|
||||
}, [])
|
||||
|
||||
const [expandedPackages, setExpandedPackages] = React.useState<Record<SDKPackageId, boolean>>({
|
||||
'vorbereitung': true,
|
||||
'analyse': false,
|
||||
@@ -695,6 +714,35 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP
|
||||
isActive={pathname === '/sdk/catalog-manager'}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
<Link
|
||||
href="/sdk/change-requests"
|
||||
className={`flex items-center gap-3 px-4 py-2.5 text-sm transition-colors ${
|
||||
collapsed ? 'justify-center' : ''
|
||||
} ${
|
||||
pathname === '/sdk/change-requests'
|
||||
? 'bg-purple-100 text-purple-900 font-medium'
|
||||
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
|
||||
}`}
|
||||
title={collapsed ? `Änderungsanfragen (${pendingCRCount})` : undefined}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
||||
</svg>
|
||||
{!collapsed && (
|
||||
<span className="flex items-center gap-2">
|
||||
Änderungsanfragen
|
||||
{pendingCRCount > 0 && (
|
||||
<span className="px-1.5 py-0.5 text-xs font-bold bg-red-500 text-white rounded-full min-w-[1.25rem] text-center">
|
||||
{pendingCRCount}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{collapsed && pendingCRCount > 0 && (
|
||||
<span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full" />
|
||||
)}
|
||||
</Link>
|
||||
<AdditionalModuleItem
|
||||
href="https://macmini:3006"
|
||||
icon={
|
||||
|
||||
110
admin-compliance/components/sdk/VersionHistory.tsx
Normal file
110
admin-compliance/components/sdk/VersionHistory.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
interface Version {
|
||||
id: string
|
||||
versionNumber: number
|
||||
status: string
|
||||
changeSummary: string | null
|
||||
changedSections: string[]
|
||||
createdBy: string
|
||||
approvedBy: string | null
|
||||
approvedAt: string | null
|
||||
createdAt: string | null
|
||||
}
|
||||
|
||||
interface VersionHistoryProps {
|
||||
documentType: 'dsfa' | 'vvt' | 'tom' | 'loeschfristen' | 'obligation'
|
||||
documentId: string
|
||||
apiPath: string
|
||||
}
|
||||
|
||||
export default function VersionHistory({ documentType, documentId, apiPath }: VersionHistoryProps) {
|
||||
const [versions, setVersions] = useState<Version[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
try {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/${apiPath}`)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setVersions(
|
||||
(Array.isArray(data) ? data : []).map((v: Record<string, unknown>) => ({
|
||||
id: v.id as string,
|
||||
versionNumber: v.version_number as number,
|
||||
status: v.status as string,
|
||||
changeSummary: v.change_summary as string | null,
|
||||
changedSections: (v.changed_sections || []) as string[],
|
||||
createdBy: v.created_by as string,
|
||||
approvedBy: v.approved_by as string | null,
|
||||
approvedAt: v.approved_at as string | null,
|
||||
createdAt: v.created_at as string | null,
|
||||
}))
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load versions:', e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
if (documentId) load()
|
||||
}, [documentId, apiPath])
|
||||
|
||||
if (loading) return null
|
||||
if (versions.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="flex items-center gap-2 text-sm text-purple-600 hover:text-purple-800 font-medium"
|
||||
>
|
||||
<svg className={`w-4 h-4 transition-transform ${expanded ? 'rotate-90' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
Versionen ({versions.length})
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div className="mt-3 border-l-2 border-purple-200 pl-4 space-y-3">
|
||||
{versions.map(v => (
|
||||
<div key={v.id} className="relative">
|
||||
<div className="absolute -left-[1.35rem] top-1.5 w-2.5 h-2.5 rounded-full bg-purple-400 border-2 border-white" />
|
||||
<div className="text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-gray-900">v{v.versionNumber}</span>
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs ${
|
||||
v.status === 'approved' ? 'bg-green-100 text-green-700' :
|
||||
v.status === 'draft' ? 'bg-yellow-100 text-yellow-700' :
|
||||
'bg-gray-100 text-gray-600'
|
||||
}`}>
|
||||
{v.status}
|
||||
</span>
|
||||
{v.createdAt && (
|
||||
<span className="text-gray-400 text-xs">
|
||||
{new Date(v.createdAt).toLocaleDateString('de-DE')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{v.changeSummary && (
|
||||
<p className="text-gray-600 text-xs mt-0.5">{v.changeSummary}</p>
|
||||
)}
|
||||
{v.changedSections.length > 0 && (
|
||||
<div className="flex gap-1 mt-1">
|
||||
{v.changedSections.map((s, i) => (
|
||||
<span key={i} className="px-1.5 py-0.5 bg-purple-50 text-purple-600 rounded text-xs">{s}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user