Files
breakpilot-compliance/admin-compliance/app/sdk/ai-registration/page.tsx
Benjamin Admin f17608a956 feat: EU AI Database Registration (Art. 49) — Backend + Frontend
Backend (Go):
- DB Migration 023: ai_system_registrations Tabelle
- RegistrationStore: CRUD + Status-Management + Export-JSON
- RegistrationHandlers: 7 Endpoints (Create, List, Get, Update, Status, Prefill, Export)
- Routes in main.go: /sdk/v1/ai-registration/*

Frontend (Next.js):
- 6-Step Wizard: Anbieter → System → Klassifikation → Konformitaet → Trainingsdaten → Pruefung
- System-Karten mit Status-Badges (Entwurf/Bereit/Eingereicht/Registriert)
- JSON-Export fuer EU-Datenbank-Submission
- Status-Workflow: draft → ready → submitted → registered
- API Proxy Routes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:13:39 +02:00

492 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import React, { useState, useEffect } from 'react'
interface Registration {
id: string
system_name: string
system_version: string
risk_classification: string
gpai_classification: string
registration_status: string
eu_database_id: string
provider_name: string
created_at: string
}
const STATUS_STYLES: Record<string, { bg: string; text: string; label: string }> = {
draft: { bg: 'bg-gray-100', text: 'text-gray-700', label: 'Entwurf' },
ready: { bg: 'bg-blue-100', text: 'text-blue-700', label: 'Bereit' },
submitted: { bg: 'bg-yellow-100', text: 'text-yellow-700', label: 'Eingereicht' },
registered: { bg: 'bg-green-100', text: 'text-green-700', label: 'Registriert' },
update_required: { bg: 'bg-orange-100', text: 'text-orange-700', label: 'Update noetig' },
withdrawn: { bg: 'bg-red-100', text: 'text-red-700', label: 'Zurueckgezogen' },
}
const RISK_STYLES: Record<string, { bg: string; text: string }> = {
high_risk: { bg: 'bg-red-100', text: 'text-red-700' },
limited_risk: { bg: 'bg-yellow-100', text: 'text-yellow-700' },
minimal_risk: { bg: 'bg-green-100', text: 'text-green-700' },
not_classified: { bg: 'bg-gray-100', text: 'text-gray-500' },
}
const INITIAL_FORM = {
system_name: '',
system_version: '1.0',
system_description: '',
intended_purpose: '',
provider_name: '',
provider_legal_form: '',
provider_address: '',
provider_country: 'DE',
eu_representative_name: '',
eu_representative_contact: '',
risk_classification: 'not_classified',
annex_iii_category: '',
gpai_classification: 'none',
conformity_assessment_type: 'internal',
notified_body_name: '',
notified_body_id: '',
ce_marking: false,
training_data_summary: '',
}
export default function AIRegistrationPage() {
const [registrations, setRegistrations] = useState<Registration[]>([])
const [loading, setLoading] = useState(true)
const [showWizard, setShowWizard] = useState(false)
const [wizardStep, setWizardStep] = useState(1)
const [form, setForm] = useState({ ...INITIAL_FORM })
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => { loadRegistrations() }, [])
async function loadRegistrations() {
try {
setLoading(true)
const resp = await fetch('/api/sdk/v1/ai-registration')
if (resp.ok) {
const data = await resp.json()
setRegistrations(data.registrations || [])
}
} catch {
setError('Fehler beim Laden')
} finally {
setLoading(false)
}
}
async function handleSubmit() {
setSubmitting(true)
try {
const resp = await fetch('/api/sdk/v1/ai-registration', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form),
})
if (resp.ok) {
setShowWizard(false)
setForm({ ...INITIAL_FORM })
setWizardStep(1)
loadRegistrations()
} else {
const data = await resp.json()
setError(data.error || 'Fehler beim Erstellen')
}
} catch {
setError('Netzwerkfehler')
} finally {
setSubmitting(false)
}
}
async function handleExport(id: string) {
try {
const resp = await fetch(`/api/sdk/v1/ai-registration/${id}`)
if (resp.ok) {
const reg = await resp.json()
// Build export JSON client-side
const exportData = {
schema_version: '1.0',
submission_type: 'ai_system_registration',
regulation: 'EU AI Act (EU) 2024/1689',
article: 'Art. 49',
provider: { name: reg.provider_name, address: reg.provider_address, country: reg.provider_country },
system: { name: reg.system_name, version: reg.system_version, description: reg.system_description, purpose: reg.intended_purpose },
classification: { risk_level: reg.risk_classification, annex_iii: reg.annex_iii_category, gpai: reg.gpai_classification },
conformity: { type: reg.conformity_assessment_type, ce_marking: reg.ce_marking },
}
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `eu_ai_registration_${reg.system_name.replace(/\s+/g, '_')}.json`
a.click()
URL.revokeObjectURL(url)
}
} catch {
setError('Export fehlgeschlagen')
}
}
async function handleStatusChange(id: string, status: string) {
try {
await fetch(`/api/sdk/v1/ai-registration/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status }),
})
loadRegistrations()
} catch {
setError('Status-Aenderung fehlgeschlagen')
}
}
const updateForm = (updates: Partial<typeof form>) => setForm(prev => ({ ...prev, ...updates }))
const STEPS = [
{ id: 1, title: 'Anbieter', desc: 'Unternehmensangaben' },
{ id: 2, title: 'System', desc: 'KI-System Details' },
{ id: 3, title: 'Klassifikation', desc: 'Risikoeinstufung' },
{ id: 4, title: 'Konformitaet', desc: 'CE & Notified Body' },
{ id: 5, title: 'Trainingsdaten', desc: 'Datenzusammenfassung' },
{ id: 6, title: 'Pruefung', desc: 'Zusammenfassung & Export' },
]
return (
<div className="max-w-5xl mx-auto p-6">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-2xl font-bold text-gray-900">EU AI Database Registrierung</h1>
<p className="text-sm text-gray-500 mt-1">Art. 49 KI-Verordnung (EU) 2024/1689 Registrierung von Hochrisiko-KI-Systemen</p>
</div>
<button
onClick={() => setShowWizard(true)}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
+ Neue Registrierung
</button>
</div>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
{error}
<button onClick={() => setError(null)} className="ml-2 underline">Schliessen</button>
</div>
)}
{/* Stats */}
<div className="grid grid-cols-4 gap-4 mb-8">
{['draft', 'ready', 'submitted', 'registered'].map(status => {
const count = registrations.filter(r => r.registration_status === status).length
const style = STATUS_STYLES[status]
return (
<div key={status} className={`p-4 rounded-xl border ${style.bg}`}>
<div className={`text-2xl font-bold ${style.text}`}>{count}</div>
<div className="text-sm text-gray-600">{style.label}</div>
</div>
)
})}
</div>
{/* Registrations List */}
{loading ? (
<div className="text-center py-12 text-gray-500">Lade...</div>
) : registrations.length === 0 ? (
<div className="text-center py-12 text-gray-400">
<p className="text-lg mb-2">Noch keine Registrierungen</p>
<p className="text-sm">Erstelle eine neue Registrierung fuer dein Hochrisiko-KI-System.</p>
</div>
) : (
<div className="space-y-4">
{registrations.map(reg => {
const status = STATUS_STYLES[reg.registration_status] || STATUS_STYLES.draft
const risk = RISK_STYLES[reg.risk_classification] || RISK_STYLES.not_classified
return (
<div key={reg.id} className="bg-white rounded-xl border border-gray-200 p-6 hover:border-purple-300 transition-all">
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-2 mb-1">
<h3 className="text-lg font-semibold text-gray-900">{reg.system_name}</h3>
<span className="text-sm text-gray-400">v{reg.system_version}</span>
<span className={`px-2 py-0.5 text-xs rounded-full ${status.bg} ${status.text}`}>{status.label}</span>
<span className={`px-2 py-0.5 text-xs rounded-full ${risk.bg} ${risk.text}`}>{reg.risk_classification.replace('_', ' ')}</span>
{reg.gpai_classification !== 'none' && (
<span className="px-2 py-0.5 text-xs rounded-full bg-blue-100 text-blue-700">GPAI: {reg.gpai_classification}</span>
)}
</div>
<div className="text-sm text-gray-500">
{reg.provider_name && <span>{reg.provider_name} · </span>}
{reg.eu_database_id && <span>EU-ID: {reg.eu_database_id} · </span>}
<span>{new Date(reg.created_at).toLocaleDateString('de-DE')}</span>
</div>
</div>
<div className="flex gap-2">
<button onClick={() => handleExport(reg.id)} className="px-3 py-1.5 text-sm border border-gray-300 rounded-lg hover:bg-gray-50">
JSON Export
</button>
{reg.registration_status === 'draft' && (
<button onClick={() => handleStatusChange(reg.id, 'ready')} className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700">
Bereit markieren
</button>
)}
{reg.registration_status === 'ready' && (
<button onClick={() => handleStatusChange(reg.id, 'submitted')} className="px-3 py-1.5 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700">
Als eingereicht markieren
</button>
)}
</div>
</div>
</div>
)
})}
</div>
)}
{/* Wizard Modal */}
{showWizard && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-3xl max-h-[90vh] overflow-y-auto">
<div className="p-6 border-b">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-gray-900">Neue EU AI Registrierung</h2>
<button onClick={() => { setShowWizard(false); setWizardStep(1) }} className="text-gray-400 hover:text-gray-600 text-2xl">&times;</button>
</div>
{/* Step Indicator */}
<div className="flex gap-1">
{STEPS.map(step => (
<button key={step.id} onClick={() => setWizardStep(step.id)}
className={`flex-1 py-2 text-xs rounded-lg transition-all ${
wizardStep === step.id ? 'bg-purple-100 text-purple-700 font-medium' :
wizardStep > step.id ? 'bg-green-50 text-green-700' : 'bg-gray-50 text-gray-400'
}`}>
{wizardStep > step.id ? '✓ ' : ''}{step.title}
</button>
))}
</div>
</div>
<div className="p-6 space-y-4">
{/* Step 1: Provider */}
{wizardStep === 1 && (
<>
<h3 className="font-semibold text-gray-900">Anbieter-Informationen</h3>
<p className="text-sm text-gray-500">Angaben zum Anbieter des KI-Systems gemaess Art. 49 KI-VO.</p>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Firmenname *</label>
<input value={form.provider_name} onChange={e => updateForm({ provider_name: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="Acme GmbH" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Rechtsform</label>
<input value={form.provider_legal_form} onChange={e => updateForm({ provider_legal_form: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="GmbH" />
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Adresse</label>
<input value={form.provider_address} onChange={e => updateForm({ provider_address: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="Musterstr. 1, 20095 Hamburg" />
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Land</label>
<select value={form.provider_country} onChange={e => updateForm({ provider_country: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
<option value="DE">Deutschland</option>
<option value="AT">Oesterreich</option>
<option value="CH">Schweiz</option>
<option value="OTHER">Anderes Land</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">EU-Repraesentant (falls Non-EU)</label>
<input value={form.eu_representative_name} onChange={e => updateForm({ eu_representative_name: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="Optional" />
</div>
</div>
</>
)}
{/* Step 2: System */}
{wizardStep === 2 && (
<>
<h3 className="font-semibold text-gray-900">KI-System Details</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Systemname *</label>
<input value={form.system_name} onChange={e => updateForm({ system_name: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="z.B. HR Copilot" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Version</label>
<input value={form.system_version} onChange={e => updateForm({ system_version: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="1.0" />
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Systembeschreibung</label>
<textarea value={form.system_description} onChange={e => updateForm({ system_description: e.target.value })} rows={3}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="Beschreibe was das KI-System tut..." />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Einsatzzweck (Intended Purpose)</label>
<textarea value={form.intended_purpose} onChange={e => updateForm({ intended_purpose: e.target.value })} rows={2}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="Wofuer wird das System eingesetzt?" />
</div>
</>
)}
{/* Step 3: Classification */}
{wizardStep === 3 && (
<>
<h3 className="font-semibold text-gray-900">Risiko-Klassifikation</h3>
<p className="text-sm text-gray-500">Basierend auf dem AI Act Decision Tree oder manueller Einstufung.</p>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Risikoklasse</label>
<select value={form.risk_classification} onChange={e => updateForm({ risk_classification: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
<option value="not_classified">Noch nicht klassifiziert</option>
<option value="minimal_risk">Minimal Risk</option>
<option value="limited_risk">Limited Risk</option>
<option value="high_risk">High Risk</option>
</select>
</div>
{form.risk_classification === 'high_risk' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Annex III Kategorie</label>
<select value={form.annex_iii_category} onChange={e => updateForm({ annex_iii_category: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
<option value="">Bitte waehlen...</option>
<option value="biometric">1. Biometrische Identifizierung</option>
<option value="critical_infrastructure">2. Kritische Infrastruktur</option>
<option value="education">3. Bildung und Berufsausbildung</option>
<option value="employment">4. Beschaeftigung und Arbeitnehmerverwaltung</option>
<option value="essential_services">5. Zugang zu wesentlichen Diensten</option>
<option value="law_enforcement">6. Strafverfolgung</option>
<option value="migration">7. Migration und Grenzkontrolle</option>
<option value="justice">8. Rechtspflege und Demokratie</option>
</select>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">GPAI Klassifikation</label>
<select value={form.gpai_classification} onChange={e => updateForm({ gpai_classification: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
<option value="none">Kein GPAI</option>
<option value="standard">GPAI (Standard)</option>
<option value="systemic">GPAI mit systemischem Risiko</option>
</select>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 text-sm text-blue-800">
<strong>Tipp:</strong> Nutze den <a href="/sdk/ai-act" className="underline">AI Act Decision Tree</a> fuer eine strukturierte Klassifikation.
</div>
</>
)}
{/* Step 4: Conformity */}
{wizardStep === 4 && (
<>
<h3 className="font-semibold text-gray-900">Konformitaetsbewertung</h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Art der Konformitaetsbewertung</label>
<select value={form.conformity_assessment_type} onChange={e => updateForm({ conformity_assessment_type: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
<option value="not_required">Nicht erforderlich</option>
<option value="internal">Interne Konformitaetsbewertung</option>
<option value="third_party">Drittpartei-Bewertung (Notified Body)</option>
</select>
</div>
{form.conformity_assessment_type === 'third_party' && (
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Notified Body Name</label>
<input value={form.notified_body_name} onChange={e => updateForm({ notified_body_name: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Notified Body ID</label>
<input value={form.notified_body_id} onChange={e => updateForm({ notified_body_id: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
</div>
</div>
)}
<label className="flex items-center gap-3 p-3 rounded-lg border hover:bg-gray-50 cursor-pointer">
<input type="checkbox" checked={form.ce_marking} onChange={e => updateForm({ ce_marking: e.target.checked })}
className="w-4 h-4 rounded border-gray-300 text-purple-600" />
<span className="text-sm font-medium text-gray-900">CE-Kennzeichnung angebracht</span>
</label>
</>
)}
{/* Step 5: Training Data */}
{wizardStep === 5 && (
<>
<h3 className="font-semibold text-gray-900">Trainingsdaten-Zusammenfassung</h3>
<p className="text-sm text-gray-500">Art. 10 KI-VO Keine vollstaendige Offenlegung, sondern Kategorien und Herkunft.</p>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Zusammenfassung der Trainingsdaten</label>
<textarea value={form.training_data_summary} onChange={e => updateForm({ training_data_summary: e.target.value })} rows={5}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
placeholder="Beschreibe die verwendeten Datenquellen:&#10;- Oeffentliche Daten (z.B. Wikipedia, Common Crawl)&#10;- Lizenzierte Daten (z.B. Fachpublikationen)&#10;- Synthetische Daten&#10;- Unternehmensinterne Daten" />
</div>
</>
)}
{/* Step 6: Review */}
{wizardStep === 6 && (
<>
<h3 className="font-semibold text-gray-900">Zusammenfassung</h3>
<div className="space-y-3 text-sm">
<div className="grid grid-cols-2 gap-4 p-4 bg-gray-50 rounded-lg">
<div><span className="text-gray-500">Anbieter:</span> <strong>{form.provider_name || ''}</strong></div>
<div><span className="text-gray-500">Land:</span> <strong>{form.provider_country}</strong></div>
<div><span className="text-gray-500">System:</span> <strong>{form.system_name || ''}</strong></div>
<div><span className="text-gray-500">Version:</span> <strong>{form.system_version}</strong></div>
<div><span className="text-gray-500">Risiko:</span> <strong>{form.risk_classification}</strong></div>
<div><span className="text-gray-500">GPAI:</span> <strong>{form.gpai_classification}</strong></div>
<div><span className="text-gray-500">Konformitaet:</span> <strong>{form.conformity_assessment_type}</strong></div>
<div><span className="text-gray-500">CE:</span> <strong>{form.ce_marking ? 'Ja' : 'Nein'}</strong></div>
</div>
{form.intended_purpose && (
<div className="p-4 bg-gray-50 rounded-lg">
<span className="text-gray-500">Zweck:</span> {form.intended_purpose}
</div>
)}
</div>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 text-sm text-yellow-800">
<strong>Hinweis:</strong> Die EU AI Datenbank befindet sich noch im Aufbau. Die Registrierung wird lokal gespeichert und kann spaeter uebermittelt werden.
</div>
</>
)}
</div>
{/* Navigation */}
<div className="p-6 border-t flex justify-between">
<button onClick={() => wizardStep > 1 ? setWizardStep(wizardStep - 1) : setShowWizard(false)}
className="px-4 py-2 text-gray-700 border rounded-lg hover:bg-gray-50">
{wizardStep === 1 ? 'Abbrechen' : 'Zurueck'}
</button>
{wizardStep < 6 ? (
<button onClick={() => setWizardStep(wizardStep + 1)}
disabled={wizardStep === 2 && !form.system_name}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50">
Weiter
</button>
) : (
<button onClick={handleSubmit} disabled={submitting || !form.system_name}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50">
{submitting ? 'Speichere...' : 'Registrierung erstellen'}
</button>
)}
</div>
</div>
</div>
)}
</div>
)
}