Files
breakpilot-compliance/admin-compliance/app/sdk/cra/page.tsx
T
Benjamin Admin c5c168592b feat(licenses): Task #25 — SDK module attribution rollout (11 modules)
Per project_sdk_module_attribution_matrix.md the Stufe-3 rollout is
prioritized by audit visibility. This batch covers Schritte 2-9 in one
sweep:

New reusable component:
  components/sdk/LicenseModuleBanner.tsx — single-line license banner
  placed at the top of an SDK module page. Renders rule pill (R1/R2/R3),
  source label, descriptor and link to /sdk/licenses. Replaces the
  copy-paste banner blocks I inlined in the earlier modules.

Integration points (per cluster):

  Cluster B (DSGVO/EU-Recht, R1):
    - vvt: existing "Vorlage" pill upgraded with R1 marker + tooltip
      explaining Bundeslaender-DSGVO provenance
    - dsfa: inline R1 banner citing DSGVO Art. 35

  Cluster C (EU AI Act / CRA, R1):
    - ai-act: inline R1 banner citing EU 2024/1689
    - cra:    inline R1 banner citing EU 2024/2847 + ENISA-Guidance

  Cluster D (Mix R2/R3):
    - isms: R3 banner + ISO/IEC 27001 reference disclaimer
    - security-backlog: R2 banner with OWASP CC-BY-SA attribution

  Cluster A (Eigenwerk, R3):
    - tom-generator: R1 source (DSGVO Art. 32) + R3 own-work disclaimer
    - audit-checklist: R3 banner for own audit methodology
    - document-generator: own templates R3 + cited rights R1

  Cluster E (Direct controls listing):
    - catalog-manager: System/User tag upgraded with rule classification
    - iace hazards: pattern_id pill upgraded with R3 + tooltip explaining
      BreakPilot Pattern-Engine provenance

The 11-module sweep brings audit transparency to the modules a paying
customer encounters most often. Stufe 3 of the attribution renderer
is now actually visible across the platform — previously it shipped
only the reusable <SourceBadge> component without integration points.

Pre-existing TS errors (drafting-engine constraint-enforcer, dsfa
types tests) untouched — not in scope for this licensing rollout.
2026-05-21 23:16:09 +02:00

211 lines
8.1 KiB
TypeScript

'use client'
import React, { useState, useEffect, useCallback } from 'react'
import { useRouter } from 'next/navigation'
import { ClassificationBadge } from './_components/ClassificationBadge'
interface CRAProject {
id: string
name: string
description: string
cra_classification: string | null
conformity_path: string | null
status: string
created_at: string
}
const PATH_LABEL: Record<string, string> = {
self_assessment: 'Modul A (Self-Assessment)',
harmonized_standard: 'Modul B (Harmonized)',
eucc: 'Modul H (EUCC)',
notified_body: 'Modul C (Notified Body)',
}
const STATUS_LABEL: Record<string, string> = {
draft: 'Entwurf',
scoped: 'Intake erfasst',
classified: 'Klassifiziert',
path_selected: 'Pfad gewaehlt',
requirements_mapped: 'Requirements',
evidence_pending: 'Evidence',
gaps_open: 'Gaps offen',
remediation: 'Remediation',
ready_for_review: 'In Pruefung',
declaration_ready: 'DoC bereit',
post_market: 'Post-Market',
archived: 'Archiviert',
}
export default function CRAProjectsPage() {
const router = useRouter()
const [projects, setProjects] = useState<CRAProject[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [showModal, setShowModal] = useState(false)
const [newName, setNewName] = useState('')
const [newDescription, setNewDescription] = useState('')
const [creating, setCreating] = useState(false)
const tenantHeader = '00000000-0000-0000-0000-000000000001'
const loadProjects = useCallback(async () => {
try {
const res = await fetch('/api/sdk/v1/cra/projects', {
headers: { 'X-Tenant-ID': tenantHeader },
})
if (!res.ok) throw new Error(await res.text())
const data = await res.json()
setProjects(data.projects || [])
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler beim Laden')
} finally {
setLoading(false)
}
}, [])
useEffect(() => { loadProjects() }, [loadProjects])
const createProject = async () => {
if (!newName.trim()) return
setCreating(true)
setError('')
try {
const res = await fetch('/api/sdk/v1/cra/projects', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': tenantHeader },
body: JSON.stringify({ name: newName, description: newDescription }),
})
if (!res.ok) throw new Error(await res.text())
const project = await res.json()
router.push(`/sdk/cra/${project.id}/intake`)
} catch (e) {
setError(e instanceof Error ? e.message : 'Anlegen fehlgeschlagen')
} finally {
setCreating(false)
}
}
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-6xl mx-auto px-4">
<div className="mb-6">
<h1 className="text-3xl font-bold text-gray-900">CRA Compliance</h1>
<p className="text-gray-600 mt-2">
Cyber Resilience Act Konformitaets-Workflow fuer Produkte mit digitalen Elementen.
</p>
<p className="text-sm text-gray-500 mt-1">
Fuer Entwickler / Tech-Experten. Hardware-CE-Risikobeurteilung siehe{' '}
<a href="/sdk/iace" className="text-blue-600 hover:underline">iACE</a>.
</p>
</div>
<div className="mb-4 px-4 py-2 bg-emerald-50 border border-emerald-200 rounded-lg text-xs text-emerald-800 flex items-start gap-2">
<span className="font-semibold">Quellen &amp; Lizenz:</span>
<span>
Inhalte gemaess <strong>EU-Verordnung 2024/2847 (Cyber Resilience Act)</strong>
Lizenzregel R1 (EU_LAW, woertlich uebernehmbar). ENISA-Implementation-Guidance
ergaenzend (R1 EU_PUBLIC).{' '}
<a href="/sdk/licenses" className="underline">Quellenverzeichnis</a>
</span>
</div>
{error && (
<div className="mb-4 bg-red-50 border border-red-200 rounded-lg p-4 text-sm text-red-700">
{error}
<button onClick={() => setError('')} className="ml-3 underline">Schliessen</button>
</div>
)}
<button
onClick={() => setShowModal(true)}
className="mb-6 w-full py-4 border-2 border-dashed border-red-300 rounded-xl text-red-600 hover:bg-red-50 hover:border-red-400 transition-colors font-medium"
>
+ Neues CRA-Projekt
</button>
{loading ? (
<div className="text-center text-gray-500 py-12">Laedt...</div>
) : projects.length === 0 ? (
<p className="text-center text-gray-500 mt-8">
Noch keine Projekte. Starten Sie Ihre erste CRA-Konformitaetsanalyse.
</p>
) : (
<div className="space-y-3">
<h2 className="text-lg font-semibold text-gray-800">Projekte</h2>
{projects.map(p => (
<a
key={p.id}
href={`/sdk/cra/${p.id}`}
className="block bg-white rounded-xl shadow-sm border border-gray-200 p-5 hover:shadow-md hover:border-red-300 transition-all"
>
<div className="flex items-center justify-between gap-4">
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-gray-900 truncate">{p.name}</h3>
{p.description && (
<p className="text-sm text-gray-500 mt-1 truncate">{p.description}</p>
)}
</div>
<div className="flex items-center gap-3 flex-shrink-0">
<ClassificationBadge value={p.cra_classification} size="sm" />
{p.conformity_path && (
<span className="px-2 py-0.5 text-xs rounded-full bg-purple-100 text-purple-800">
{PATH_LABEL[p.conformity_path] || p.conformity_path}
</span>
)}
<span className="px-2 py-0.5 text-xs rounded-full bg-gray-100 text-gray-700">
{STATUS_LABEL[p.status] || p.status}
</span>
<span className="text-xs text-gray-400">
{new Date(p.created_at).toLocaleDateString('de-DE')}
</span>
</div>
</div>
</a>
))}
</div>
)}
{showModal && (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-xl max-w-md w-full mx-4 p-6">
<h3 className="text-lg font-semibold mb-4">Neues CRA-Projekt anlegen</h3>
<div className="space-y-3">
<input
type="text"
placeholder="Projektname (z.B. SmartHome Gateway v3)"
value={newName}
onChange={e => setNewName(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500"
/>
<textarea
placeholder="Kurzbeschreibung (optional)"
value={newDescription}
onChange={e => setNewDescription(e.target.value)}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500"
/>
</div>
<div className="flex gap-3 mt-5">
<button
onClick={() => { setShowModal(false); setNewName(''); setNewDescription('') }}
disabled={creating}
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
>
Abbrechen
</button>
<button
onClick={createProject}
disabled={creating || !newName.trim()}
className="flex-1 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:bg-gray-300"
>
{creating ? 'Erstelle...' : 'Anlegen'}
</button>
</div>
</div>
</div>
)}
</div>
</div>
)
}