fix: SDK-Module Frontend-Backend-Mismatches beheben + fehlende Proxy-Routes

- P0: enableBackendSync=true in SDKProvider aktiviert (PostgreSQL State-Persistenz)
- P0: Source Policy 4 Tabs an Backend-Schema angepasst (is_active→active,
  data.logs→data.entries, is_allowed→allowed, rule_type→category, severity→action)
- P0: OperationsMatrixTab holt jetzt Sources+Operations separat und joint client-side
- P0: PIIRulesTab PII-Test auf client-side Regex umgestellt (kein Backend-Endpoint noetig)
- P1: GET Proxy-Routes fuer Import, Screening und UCCA [id] (GET+DELETE) erstellt
- P1: Compliance Scope ScopeOverviewTab/ScopeExportTab Prop-Interfaces erweitert
- P2: Company Profile speichert jetzt auch zum dedizierten Backend-Endpoint
- P2: UCCA Wizard von 5 auf 8 Steps erweitert (Rechtsgrundlage, Datentransfer, Vertraege)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-02 11:40:44 +01:00
parent e6d666b89b
commit 80a988dc58
12 changed files with 563 additions and 181 deletions

View File

@@ -1161,7 +1161,7 @@ export default function CompanyProfilePage() {
}
}
const completeAndSaveProfile = () => {
const completeAndSaveProfile = async () => {
const completeProfile: CompanyProfile = {
...formData,
isComplete: true,
@@ -1170,6 +1170,41 @@ export default function CompanyProfilePage() {
setCompanyProfile(completeProfile)
dispatch({ type: 'COMPLETE_STEP', payload: 'company-profile' })
// Also persist to dedicated backend endpoint
try {
await fetch('/api/sdk/v1/company-profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
company_name: formData.companyName || '',
legal_form: formData.legalForm || 'GmbH',
industry: formData.industry || '',
founded_year: formData.foundedYear || null,
business_model: formData.businessModel || 'B2B',
offerings: formData.offerings || [],
company_size: formData.companySize || 'small',
employee_count: formData.employeeCount || '',
annual_revenue: formData.annualRevenue || '',
headquarters_country: formData.headquartersCountry || 'DE',
headquarters_city: formData.headquartersCity || '',
has_international_locations: formData.hasInternationalLocations || false,
international_countries: formData.internationalCountries || [],
target_markets: formData.targetMarkets || [],
primary_jurisdiction: formData.primaryJurisdiction || 'DE',
is_data_controller: formData.isDataController ?? true,
is_data_processor: formData.isDataProcessor ?? false,
uses_ai: formData.usesAI ?? false,
ai_use_cases: formData.aiUseCases || [],
dpo_name: formData.dpoName || '',
dpo_email: formData.dpoEmail || '',
is_complete: true,
}),
})
} catch (err) {
console.error('Failed to save company profile to backend:', err)
}
goToNextStep()
}

View File

@@ -11,9 +11,12 @@ import { AssessmentResultCard } from '@/components/sdk/use-case-assessment/Asses
const WIZARD_STEPS = [
{ id: 1, title: 'Grundlegendes', description: 'Titel und Beschreibung' },
{ id: 2, title: 'Datenkategorien', description: 'Welche Daten werden verarbeitet?' },
{ id: 3, title: 'Automatisierung', description: 'Grad der Automatisierung' },
{ id: 4, title: 'Hosting & Modell', description: 'Technische Details' },
{ id: 5, title: 'Datenhaltung', description: 'Aufbewahrung und Speicherung' },
{ id: 3, title: 'Verarbeitungszweck', description: 'Rechtsgrundlage und Zweck' },
{ id: 4, title: 'Automatisierung', description: 'Grad der Automatisierung' },
{ id: 5, title: 'Hosting & Modell', description: 'Technische Details' },
{ id: 6, title: 'Datentransfer', description: 'Internationaler Datentransfer' },
{ id: 7, title: 'Datenhaltung', description: 'Aufbewahrung und Speicherung' },
{ id: 8, title: 'Vertraege', description: 'Compliance und Vereinbarungen' },
]
const DOMAINS = [
@@ -70,9 +73,20 @@ export default function NewUseCasePage() {
model_finetune: false,
model_training: false,
model_inference: true,
// Retention
// Legal Basis (Step 3)
legal_basis: 'consent' as 'consent' | 'contract' | 'legitimate_interest' | 'legal_obligation' | 'vital_interest' | 'public_interest',
// Data Transfer (Step 6)
international_transfer: false,
transfer_countries: [] as string[],
transfer_mechanism: 'none' as 'none' | 'scc' | 'bcr' | 'adequacy' | 'derogation',
// Retention (Step 7)
retention_days: 90,
retention_purpose: '',
// Contracts (Step 8)
has_dpa: false,
has_aia_documentation: false,
has_risk_assessment: false,
subprocessors: '',
})
const updateForm = (updates: Partial<typeof form>) => {
@@ -113,10 +127,22 @@ export default function NewUseCasePage() {
training: form.model_training,
inference: form.model_inference,
},
legal_basis: form.legal_basis,
international_transfer: {
enabled: form.international_transfer,
countries: form.transfer_countries,
mechanism: form.transfer_mechanism,
},
retention: {
days: form.retention_days,
purpose: form.retention_purpose,
},
contracts: {
has_dpa: form.has_dpa,
has_aia_documentation: form.has_aia_documentation,
has_risk_assessment: form.has_risk_assessment,
subprocessors: form.subprocessors,
},
store_raw_text: true,
}
@@ -276,6 +302,29 @@ export default function NewUseCasePage() {
</div>
</label>
))}
</div>
)}
{/* Step 3: Verarbeitungszweck & Rechtsgrundlage */}
{currentStep === 3 && (
<div className="space-y-4">
<h2 className="text-lg font-semibold text-gray-900">Verarbeitungszweck & Rechtsgrundlage</h2>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Rechtsgrundlage (Art. 6 DSGVO)</label>
<select
value={form.legal_basis}
onChange={e => updateForm({ legal_basis: e.target.value as typeof form.legal_basis })}
className="w-full px-4 py-2 border border-gray-300 rounded-lg"
>
<option value="consent">Einwilligung (Art. 6 Abs. 1a)</option>
<option value="contract">Vertragserfullung (Art. 6 Abs. 1b)</option>
<option value="legal_obligation">Rechtliche Verpflichtung (Art. 6 Abs. 1c)</option>
<option value="vital_interest">Lebenswichtige Interessen (Art. 6 Abs. 1d)</option>
<option value="public_interest">Oeffentliches Interesse (Art. 6 Abs. 1e)</option>
<option value="legitimate_interest">Berechtigtes Interesse (Art. 6 Abs. 1f)</option>
</select>
</div>
<h3 className="text-sm font-medium text-gray-700 mt-4">Zweck der Verarbeitung</h3>
{[
@@ -301,8 +350,8 @@ export default function NewUseCasePage() {
</div>
)}
{/* Step 3: Automatisierung */}
{currentStep === 3 && (
{/* Step 4: Automatisierung */}
{currentStep === 4 && (
<div className="space-y-4">
<h2 className="text-lg font-semibold text-gray-900">Grad der Automatisierung</h2>
{[
@@ -335,8 +384,8 @@ export default function NewUseCasePage() {
</div>
)}
{/* Step 4: Hosting & Modell */}
{currentStep === 4 && (
{/* Step 5: Hosting & Modell */}
{currentStep === 5 && (
<div className="space-y-4">
<h2 className="text-lg font-semibold text-gray-900">Technische Details</h2>
<div>
@@ -390,8 +439,59 @@ export default function NewUseCasePage() {
</div>
)}
{/* Step 5: Datenhaltung */}
{currentStep === 5 && (
{/* Step 6: Internationaler Datentransfer */}
{currentStep === 6 && (
<div className="space-y-4">
<h2 className="text-lg font-semibold text-gray-900">Internationaler Datentransfer</h2>
<label className="flex items-start gap-3 p-4 bg-gray-50 rounded-lg cursor-pointer hover:bg-gray-100">
<input
type="checkbox"
checked={form.international_transfer}
onChange={e => updateForm({ international_transfer: e.target.checked })}
className="mt-1 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
/>
<div>
<div className="font-medium text-gray-900">Daten werden in Drittlaender uebermittelt</div>
<div className="text-sm text-gray-500">Ausserhalb des EWR (z.B. USA, UK, Schweiz)</div>
</div>
</label>
{form.international_transfer && (
<>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Ziellaender</label>
<input
type="text"
value={form.transfer_countries.join(', ')}
onChange={e => updateForm({ transfer_countries: e.target.value.split(',').map(s => s.trim()).filter(Boolean) })}
placeholder="z.B. USA, UK, CH"
className="w-full px-4 py-2 border border-gray-300 rounded-lg"
/>
<p className="text-xs text-gray-500 mt-1">Kommagetrennte Laenderkuerzel</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Transfer-Mechanismus</label>
<select
value={form.transfer_mechanism}
onChange={e => updateForm({ transfer_mechanism: e.target.value as typeof form.transfer_mechanism })}
className="w-full px-4 py-2 border border-gray-300 rounded-lg"
>
<option value="none">Noch nicht festgelegt</option>
<option value="adequacy">Angemessenheitsbeschluss</option>
<option value="scc">Standardvertragsklauseln (SCC)</option>
<option value="bcr">Binding Corporate Rules (BCR)</option>
<option value="derogation">Ausnahmeregelung (Art. 49 DSGVO)</option>
</select>
</div>
</>
)}
</div>
)}
{/* Step 7: Datenhaltung */}
{currentStep === 7 && (
<div className="space-y-4">
<h2 className="text-lg font-semibold text-gray-900">Datenhaltung & Aufbewahrung</h2>
<div>
@@ -420,6 +520,43 @@ export default function NewUseCasePage() {
</div>
</div>
)}
{/* Step 8: Vertraege & Compliance */}
{currentStep === 8 && (
<div className="space-y-4">
<h2 className="text-lg font-semibold text-gray-900">Vertraege & Compliance-Dokumentation</h2>
{[
{ key: 'has_dpa', label: 'Auftragsverarbeitungsvertrag (AVV/DPA)', desc: 'Vertrag mit KI-Anbieter / Subprozessor nach Art. 28 DSGVO' },
{ key: 'has_aia_documentation', label: 'AI Act Dokumentation', desc: 'Risikoklassifizierung und technische Dokumentation nach EU AI Act' },
{ key: 'has_risk_assessment', label: 'Risikobewertung / DSFA', desc: 'Datenschutz-Folgenabschaetzung nach Art. 35 DSGVO' },
].map(item => (
<label key={item.key} className="flex items-start gap-3 p-3 bg-gray-50 rounded-lg cursor-pointer hover:bg-gray-100">
<input
type="checkbox"
checked={form[item.key as keyof typeof form] as boolean}
onChange={e => updateForm({ [item.key]: e.target.checked })}
className="mt-1 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
/>
<div>
<div className="font-medium text-gray-900">{item.label}</div>
<div className="text-sm text-gray-500">{item.desc}</div>
</div>
</label>
))}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Subprozessoren</label>
<textarea
value={form.subprocessors}
onChange={e => updateForm({ subprocessors: e.target.value })}
rows={3}
placeholder="z.B. OpenAI (USA, SCC), Hetzner Cloud (DE)..."
className="w-full px-4 py-2 border border-gray-300 rounded-lg"
/>
</div>
</div>
)}
</div>
{/* Navigation Buttons */}
@@ -431,7 +568,7 @@ export default function NewUseCasePage() {
{currentStep === 1 ? 'Abbrechen' : 'Zurueck'}
</button>
{currentStep < 5 ? (
{currentStep < 8 ? (
<button
onClick={() => setCurrentStep(currentStep + 1)}
disabled={currentStep === 1 && !form.title}