feat(iace): sync IACE frontend, API routes, and scope engine updates from breakpilot-pwa
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 35s
CI / test-python-backend-compliance (push) Successful in 29s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 21s

- Add IACE project pages (classification, evidence, hazards, mitigations, monitoring, tech-file, verification)
- Add IACE API catch-all route
- Update compliance-scope-engine with IACE AI Act product triggers
- Update compliance-scope-types, navigation, roles, and sidebar for IACE
- Update company-profile page
This commit is contained in:
Benjamin Boenisch
2026-02-25 23:03:03 +01:00
parent 5314db49e2
commit 03708d9e5b
18 changed files with 5565 additions and 110 deletions

View File

@@ -9,10 +9,19 @@ import {
TargetMarket,
CompanySize,
LegalForm,
MachineBuilderProfile,
MachineProductType,
AIIntegrationType,
HumanOversightLevel,
CriticalSector,
BUSINESS_MODEL_LABELS,
OFFERING_TYPE_LABELS,
TARGET_MARKET_LABELS,
COMPANY_SIZE_LABELS,
MACHINE_PRODUCT_TYPE_LABELS,
AI_INTEGRATION_TYPE_LABELS,
HUMAN_OVERSIGHT_LABELS,
CRITICAL_SECTOR_LABELS,
SDKCoverageAssessment,
} from '@/lib/sdk/types'
@@ -20,14 +29,26 @@ import {
// WIZARD STEPS
// =============================================================================
const WIZARD_STEPS = [
const BASE_WIZARD_STEPS = [
{ id: 1, name: 'Basisinfos', description: 'Firmenname und Rechtsform' },
{ id: 2, name: 'Geschäftsmodell', description: 'B2B, B2C und Angebote' },
{ id: 3, name: 'Firmengröße', description: 'Mitarbeiter und Umsatz' },
{ id: 4, name: 'Standorte', description: 'Hauptsitz und Zielmärkte' },
{ id: 2, name: 'Geschaeftsmodell', description: 'B2B, B2C und Angebote' },
{ id: 3, name: 'Firmengroesse', description: 'Mitarbeiter und Umsatz' },
{ id: 4, name: 'Standorte', description: 'Hauptsitz und Zielmaerkte' },
{ id: 5, name: 'Datenschutz', description: 'Rollen und KI-Nutzung' },
]
const MACHINE_BUILDER_STEP = { id: 6, name: 'Produkt & Maschine', description: 'Software, KI und CE in Ihrem Produkt' }
function getWizardSteps(industry: string) {
if (isMachineBuilderIndustry(industry)) {
return [...BASE_WIZARD_STEPS, MACHINE_BUILDER_STEP]
}
return BASE_WIZARD_STEPS
}
// Keep WIZARD_STEPS for backwards compat in static references
const WIZARD_STEPS = BASE_WIZARD_STEPS
// =============================================================================
// LEGAL FORMS
// =============================================================================
@@ -61,9 +82,25 @@ const INDUSTRIES = [
'Produktion / Industrie',
'Logistik / Transport',
'Immobilien',
'Maschinenbau',
'Anlagenbau',
'Automatisierung',
'Robotik',
'Messtechnik',
'Sonstige',
]
const MACHINE_BUILDER_INDUSTRIES = [
'Maschinenbau',
'Anlagenbau',
'Automatisierung',
'Robotik',
'Messtechnik',
]
const isMachineBuilderIndustry = (industry: string) =>
MACHINE_BUILDER_INDUSTRIES.includes(industry)
// =============================================================================
// HELPER: ASSESS SDK COVERAGE
// =============================================================================
@@ -504,6 +541,423 @@ function StepDataProtection({
)
}
// =============================================================================
// STEP 6: PRODUKT & MASCHINE (nur fuer Maschinenbauer)
// =============================================================================
const EMPTY_MACHINE_BUILDER: MachineBuilderProfile = {
productTypes: [],
productDescription: '',
productPride: '',
containsSoftware: false,
containsFirmware: false,
containsAI: false,
aiIntegrationType: [],
hasSafetyFunction: false,
safetyFunctionDescription: '',
autonomousBehavior: false,
humanOversightLevel: 'full',
isNetworked: false,
hasRemoteAccess: false,
hasOTAUpdates: false,
updateMechanism: '',
exportMarkets: [],
criticalSectorClients: false,
criticalSectors: [],
oemClients: false,
ceMarkingRequired: false,
existingCEProcess: false,
hasRiskAssessment: false,
}
function StepMachineBuilder({
data,
onChange,
}: {
data: Partial<CompanyProfile>
onChange: (updates: Partial<CompanyProfile>) => void
}) {
const mb = data.machineBuilder || EMPTY_MACHINE_BUILDER
const updateMB = (updates: Partial<MachineBuilderProfile>) => {
onChange({ machineBuilder: { ...mb, ...updates } })
}
const toggleProductType = (type: MachineProductType) => {
const current = mb.productTypes || []
if (current.includes(type)) {
updateMB({ productTypes: current.filter(t => t !== type) })
} else {
updateMB({ productTypes: [...current, type] })
}
}
const toggleAIType = (type: AIIntegrationType) => {
const current = mb.aiIntegrationType || []
if (current.includes(type)) {
updateMB({ aiIntegrationType: current.filter(t => t !== type) })
} else {
updateMB({ aiIntegrationType: [...current, type] })
}
}
const toggleCriticalSector = (sector: CriticalSector) => {
const current = mb.criticalSectors || []
if (current.includes(sector)) {
updateMB({ criticalSectors: current.filter(s => s !== sector) })
} else {
updateMB({ criticalSectors: [...current, sector] })
}
}
return (
<div className="space-y-8">
{/* Block 1: Erzaehlen Sie uns von Ihrer Anlage */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-1">Erzaehlen Sie uns von Ihrer Anlage</h3>
<p className="text-sm text-gray-500 mb-4">
Je besser wir Ihr Produkt verstehen, desto praeziser koennen wir die relevanten Vorschriften identifizieren.
</p>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Was baut Ihr Unternehmen? <span className="text-red-500">*</span>
</label>
<textarea
value={mb.productDescription}
onChange={e => updateMB({ productDescription: e.target.value })}
placeholder="z.B. Wir bauen automatisierte Pruefstaende fuer die Qualitaetskontrolle in der Automobilindustrie..."
rows={3}
className="w-full px-4 py-3 border border-gray-300 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-2">
Was macht Ihre Anlage besonders?
</label>
<textarea
value={mb.productPride}
onChange={e => updateMB({ productPride: e.target.value })}
placeholder="z.B. Unsere Anlage kann 500 Teile/Stunde mit 99.9% Erkennungsrate pruefen..."
rows={2}
className="w-full px-4 py-3 border border-gray-300 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-3">
Produkttyp <span className="text-gray-400">(Mehrfachauswahl)</span>
</label>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{Object.entries(MACHINE_PRODUCT_TYPE_LABELS).map(([value, label]) => (
<button
key={value}
type="button"
onClick={() => toggleProductType(value as MachineProductType)}
className={`px-4 py-3 rounded-lg border-2 text-sm font-medium transition-all ${
mb.productTypes.includes(value as MachineProductType)
? 'border-purple-500 bg-purple-50 text-purple-700'
: 'border-gray-200 hover:border-purple-300 text-gray-700'
}`}
>
{label}
</button>
))}
</div>
</div>
</div>
</div>
{/* Block 2: Software & KI */}
<div className="border-t border-gray-200 pt-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Software & KI in Ihrem Produkt</h3>
<div className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{[
{ key: 'containsSoftware', label: 'Enthaelt Software', desc: 'Anwendungssoftware in der Maschine' },
{ key: 'containsFirmware', label: 'Enthaelt Firmware', desc: 'Embedded Software / Steuerung' },
{ key: 'containsAI', label: 'Enthaelt KI/ML', desc: 'Kuenstliche Intelligenz / Machine Learning' },
].map(item => (
<label
key={item.key}
className={`flex items-start gap-3 p-4 rounded-xl border-2 cursor-pointer transition-all ${
(mb as any)[item.key]
? 'border-purple-500 bg-purple-50'
: 'border-gray-200 hover:border-purple-300'
}`}
>
<input
type="checkbox"
checked={(mb as any)[item.key] ?? false}
onChange={e => updateMB({ [item.key]: e.target.checked } as any)}
className="mt-1 w-5 h-5 text-purple-600 rounded focus:ring-purple-500"
/>
<div>
<div className="font-medium text-gray-900 text-sm">{item.label}</div>
<div className="text-xs text-gray-500">{item.desc}</div>
</div>
</label>
))}
</div>
{mb.containsAI && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Art der KI-Integration
</label>
<div className="grid grid-cols-2 gap-3">
{Object.entries(AI_INTEGRATION_TYPE_LABELS).map(([value, label]) => (
<button
key={value}
type="button"
onClick={() => toggleAIType(value as AIIntegrationType)}
className={`px-4 py-2 rounded-lg border text-sm transition-all ${
mb.aiIntegrationType.includes(value as AIIntegrationType)
? 'border-purple-500 bg-purple-50 text-purple-700'
: 'border-gray-200 hover:border-purple-300 text-gray-700'
}`}
>
{label}
</button>
))}
</div>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<label className={`flex items-start gap-3 p-4 rounded-xl border-2 cursor-pointer transition-all ${
mb.hasSafetyFunction ? 'border-red-400 bg-red-50' : 'border-gray-200 hover:border-gray-300'
}`}>
<input
type="checkbox"
checked={mb.hasSafetyFunction}
onChange={e => updateMB({ hasSafetyFunction: e.target.checked })}
className="mt-1 w-5 h-5 text-red-600 rounded focus:ring-red-500"
/>
<div>
<div className="font-medium text-gray-900 text-sm">Sicherheitsrelevante Funktion</div>
<div className="text-xs text-gray-500">KI/SW hat sicherheitsrelevante Funktion</div>
</div>
</label>
<label className={`flex items-start gap-3 p-4 rounded-xl border-2 cursor-pointer transition-all ${
mb.autonomousBehavior ? 'border-amber-400 bg-amber-50' : 'border-gray-200 hover:border-gray-300'
}`}>
<input
type="checkbox"
checked={mb.autonomousBehavior}
onChange={e => updateMB({ autonomousBehavior: e.target.checked })}
className="mt-1 w-5 h-5 text-amber-600 rounded focus:ring-amber-500"
/>
<div>
<div className="font-medium text-gray-900 text-sm">Autonomes Verhalten</div>
<div className="text-xs text-gray-500">System lernt oder handelt eigenstaendig</div>
</div>
</label>
</div>
{mb.hasSafetyFunction && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Beschreibung der Sicherheitsfunktion
</label>
<textarea
value={mb.safetyFunctionDescription}
onChange={e => updateMB({ safetyFunctionDescription: e.target.value })}
placeholder="z.B. KI-Vision ueberwacht den Schutzbereich und stoppt den Roboter bei Personenerkennung..."
rows={2}
className="w-full px-4 py-3 border border-gray-300 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-2">
Human Oversight Level
</label>
<select
value={mb.humanOversightLevel}
onChange={e => updateMB({ humanOversightLevel: e.target.value as HumanOversightLevel })}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
{Object.entries(HUMAN_OVERSIGHT_LABELS).map(([value, label]) => (
<option key={value} value={value}>{label}</option>
))}
</select>
</div>
</div>
</div>
{/* Block 3: Konnektivitaet & Updates */}
<div className="border-t border-gray-200 pt-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Konnektivitaet & Updates</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-4">
{[
{ key: 'isNetworked', label: 'Vernetzt', desc: 'Maschine ist mit Netzwerk verbunden' },
{ key: 'hasRemoteAccess', label: 'Remote-Zugriff', desc: 'Fernwartung / Remote-Zugang' },
{ key: 'hasOTAUpdates', label: 'OTA-Updates', desc: 'Drahtlose Software-/Firmware-Updates' },
].map(item => (
<label
key={item.key}
className={`flex items-start gap-3 p-4 rounded-xl border-2 cursor-pointer transition-all ${
(mb as any)[item.key]
? 'border-purple-500 bg-purple-50'
: 'border-gray-200 hover:border-purple-300'
}`}
>
<input
type="checkbox"
checked={(mb as any)[item.key] ?? false}
onChange={e => updateMB({ [item.key]: e.target.checked } as any)}
className="mt-1 w-5 h-5 text-purple-600 rounded focus:ring-purple-500"
/>
<div>
<div className="font-medium text-gray-900 text-sm">{item.label}</div>
<div className="text-xs text-gray-500">{item.desc}</div>
</div>
</label>
))}
</div>
{(mb.hasOTAUpdates || mb.hasRemoteAccess) && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Wie werden Updates eingespielt?
</label>
<input
type="text"
value={mb.updateMechanism}
onChange={e => updateMB({ updateMechanism: e.target.value })}
placeholder="z.B. VPN-gesicherter Remote-Zugang mit manueller Freigabe..."
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
)}
</div>
{/* Block 4: Markt & Kunden */}
<div className="border-t border-gray-200 pt-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Markt & Kunden</h3>
<div className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<label className={`flex items-start gap-3 p-4 rounded-xl border-2 cursor-pointer transition-all ${
mb.criticalSectorClients ? 'border-red-400 bg-red-50' : 'border-gray-200 hover:border-gray-300'
}`}>
<input
type="checkbox"
checked={mb.criticalSectorClients}
onChange={e => updateMB({ criticalSectorClients: e.target.checked })}
className="mt-1 w-5 h-5 text-red-600 rounded focus:ring-red-500"
/>
<div>
<div className="font-medium text-gray-900 text-sm">Liefert an KRITIS-Betreiber</div>
<div className="text-xs text-gray-500">Kunden in kritischer Infrastruktur</div>
</div>
</label>
<label className={`flex items-start gap-3 p-4 rounded-xl border-2 cursor-pointer transition-all ${
mb.oemClients ? 'border-purple-500 bg-purple-50' : 'border-gray-200 hover:border-gray-300'
}`}>
<input
type="checkbox"
checked={mb.oemClients}
onChange={e => updateMB({ oemClients: e.target.checked })}
className="mt-1 w-5 h-5 text-purple-600 rounded focus:ring-purple-500"
/>
<div>
<div className="font-medium text-gray-900 text-sm">OEM-Zulieferer</div>
<div className="text-xs text-gray-500">Liefern Komponenten an andere Hersteller</div>
</div>
</label>
</div>
{mb.criticalSectorClients && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Kritische Sektoren Ihrer Kunden
</label>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
{Object.entries(CRITICAL_SECTOR_LABELS).map(([value, label]) => (
<button
key={value}
type="button"
onClick={() => toggleCriticalSector(value as CriticalSector)}
className={`px-3 py-2 rounded-lg border text-sm transition-all ${
mb.criticalSectors.includes(value as CriticalSector)
? 'border-red-400 bg-red-50 text-red-700'
: 'border-gray-200 hover:border-gray-300 text-gray-700'
}`}
>
{label}
</button>
))}
</div>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<label className={`flex items-start gap-3 p-4 rounded-xl border-2 cursor-pointer transition-all ${
mb.ceMarkingRequired ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-gray-300'
}`}>
<input
type="checkbox"
checked={mb.ceMarkingRequired}
onChange={e => updateMB({ ceMarkingRequired: e.target.checked })}
className="mt-1 w-5 h-5 text-blue-600 rounded focus:ring-blue-500"
/>
<div>
<div className="font-medium text-gray-900 text-sm">CE-Kennzeichnung erforderlich</div>
<div className="text-xs text-gray-500">Produkt benoetigt CE-Zertifizierung</div>
</div>
</label>
<label className={`flex items-start gap-3 p-4 rounded-xl border-2 cursor-pointer transition-all ${
mb.existingCEProcess ? 'border-green-400 bg-green-50' : 'border-gray-200 hover:border-gray-300'
}`}>
<input
type="checkbox"
checked={mb.existingCEProcess}
onChange={e => updateMB({ existingCEProcess: e.target.checked })}
className="mt-1 w-5 h-5 text-green-600 rounded focus:ring-green-500"
/>
<div>
<div className="font-medium text-gray-900 text-sm">Bestehender CE-Prozess</div>
<div className="text-xs text-gray-500">Bereits ein CE-Verfahren etabliert</div>
</div>
</label>
</div>
{mb.ceMarkingRequired && (
<label className={`flex items-start gap-3 p-4 rounded-xl border-2 cursor-pointer transition-all ${
mb.hasRiskAssessment ? 'border-green-400 bg-green-50' : 'border-red-400 bg-red-50'
}`}>
<input
type="checkbox"
checked={mb.hasRiskAssessment}
onChange={e => updateMB({ hasRiskAssessment: e.target.checked })}
className="mt-1 w-5 h-5 text-green-600 rounded focus:ring-green-500"
/>
<div>
<div className="font-medium text-gray-900 text-sm">Bestehende Risikobeurteilung</div>
<div className="text-xs text-gray-500">
{mb.hasRiskAssessment
? 'Risikobeurteilung vorhanden'
: 'Keine bestehende Risikobeurteilung - IACE hilft Ihnen dabei!'}
</div>
</div>
</label>
)}
</div>
</div>
</div>
)
}
// =============================================================================
// COVERAGE ASSESSMENT COMPONENT
// =============================================================================
@@ -671,6 +1125,11 @@ export default function CompanyProfilePage() {
completedAt: null,
})
const showMachineBuilderStep = isMachineBuilderIndustry(formData.industry || '')
const wizardSteps = getWizardSteps(formData.industry || '')
const totalSteps = wizardSteps.length
const lastStep = wizardSteps[wizardSteps.length - 1].id
// Load existing profile
useEffect(() => {
if (state.companyProfile) {
@@ -687,22 +1146,33 @@ export default function CompanyProfilePage() {
}
const handleNext = () => {
if (currentStep < 5) {
setCurrentStep(prev => prev + 1)
if (currentStep < lastStep) {
// Skip step 6 if not a machine builder
const nextStep = currentStep + 1
if (nextStep === 6 && !showMachineBuilderStep) {
// Complete profile (was step 5, last step for non-machine-builders)
completeAndSaveProfile()
return
}
setCurrentStep(nextStep)
} else {
// Complete profile
const completeProfile: CompanyProfile = {
...formData,
isComplete: true,
completedAt: new Date(),
} as CompanyProfile
setCompanyProfile(completeProfile)
dispatch({ type: 'COMPLETE_STEP', payload: 'company-profile' })
goToNextStep()
completeAndSaveProfile()
}
}
const completeAndSaveProfile = () => {
const completeProfile: CompanyProfile = {
...formData,
isComplete: true,
completedAt: new Date(),
} as CompanyProfile
setCompanyProfile(completeProfile)
dispatch({ type: 'COMPLETE_STEP', payload: 'company-profile' })
goToNextStep()
}
const handleBack = () => {
if (currentStep > 1) {
setCurrentStep(prev => prev - 1)
@@ -721,11 +1191,16 @@ export default function CompanyProfilePage() {
return formData.headquartersCountry && (formData.targetMarkets?.length || 0) > 0
case 5:
return true
case 6:
// Machine builder step: require at least product description
return (formData.machineBuilder?.productDescription?.length || 0) > 0
default:
return false
}
}
const isLastStep = currentStep === lastStep || (currentStep === 5 && !showMachineBuilderStep)
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-6xl mx-auto px-4">
@@ -741,7 +1216,7 @@ export default function CompanyProfilePage() {
{/* Progress Steps */}
<div className="mb-8">
<div className="flex items-center justify-between">
{WIZARD_STEPS.map((step, index) => (
{wizardSteps.map((step, index) => (
<React.Fragment key={step.id}>
<div className="flex items-center">
<div
@@ -775,7 +1250,7 @@ export default function CompanyProfilePage() {
</div>
</div>
</div>
{index < WIZARD_STEPS.length - 1 && (
{index < wizardSteps.length - 1 && (
<div
className={`flex-1 h-0.5 mx-4 ${
step.id < currentStep ? 'bg-purple-600' : 'bg-gray-200'
@@ -794,9 +1269,9 @@ export default function CompanyProfilePage() {
<div className="bg-white rounded-xl border border-gray-200 p-8">
<div className="mb-6">
<h2 className="text-xl font-semibold text-gray-900">
{WIZARD_STEPS[currentStep - 1].name}
{(wizardSteps.find(s => s.id === currentStep) || wizardSteps[0]).name}
</h2>
<p className="text-gray-500">{WIZARD_STEPS[currentStep - 1].description}</p>
<p className="text-gray-500">{(wizardSteps.find(s => s.id === currentStep) || wizardSteps[0]).description}</p>
</div>
{currentStep === 1 && <StepBasicInfo data={formData} onChange={updateFormData} />}
@@ -804,6 +1279,7 @@ export default function CompanyProfilePage() {
{currentStep === 3 && <StepCompanySize data={formData} onChange={updateFormData} />}
{currentStep === 4 && <StepLocations data={formData} onChange={updateFormData} />}
{currentStep === 5 && <StepDataProtection data={formData} onChange={updateFormData} />}
{currentStep === 6 && showMachineBuilderStep && <StepMachineBuilder data={formData} onChange={updateFormData} />}
{/* Navigation */}
<div className="flex justify-between mt-8 pt-6 border-t border-gray-200">
@@ -819,7 +1295,7 @@ export default function CompanyProfilePage() {
disabled={!canProceed()}
className="px-8 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{currentStep === 5 ? 'Profil speichern & weiter' : 'Weiter'}
{isLastStep ? 'Profil speichern & weiter' : 'Weiter'}
</button>
</div>
</div>