feat(sdk): Kunden-Dokumente + CRA-Meldewesen, Screening aus Frontend genommen
CI / detect-changes (push) Successful in 16s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Successful in 15s
CI / validate-canonical-controls (push) Successful in 13s
CI / loc-budget (push) Successful in 25s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 3m9s
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 31s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped

- /sdk/dokumente: Kundensicht nur auf veroeffentlichte Rechtsdokumente
  (Ansehen + Download); Proxy mit Allow-List nur /public — Templates/Drafts/
  Generator bleiben unerreichbar.
- /sdk/cra-meldewesen: CRA Art. 14 Meldewesen (24h/72h/14d-Kaskade) mit
  Fristen-Tracking + ENISA-SRP-Export-Entwurf (kein Live-API). Backend:
  cra_meldewesen (pure, getestet) + cra_incident_store (schema-neutral ueber
  compliance_cra_documents) + /api/v1/cra/incidents (additiv, contract-safe).
- Screening (Self-Scan) aus dem Frontend genommen: Flow-Stepper-Eintrag
  ausgeblendet (visibleWhen), Dashboard-Kachel + Import-Button entfernt.
  Repo-Scanning laeuft extern im Compliance-Scanner; Backend-Router bleibt
  vorerst gemountet (Contract-Stabilitaet).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Bönisch
2026-06-17 21:21:28 +02:00
parent 72093e5501
commit 8f21650d74
17 changed files with 1155 additions and 17 deletions
@@ -0,0 +1,88 @@
'use client'
import { Incident, Deadline, downloadStageExport } from '../_hooks/useMeldewesen'
const STATUS_STYLE: Record<string, string> = {
submitted: 'bg-green-100 text-green-800 border-green-300',
overdue: 'bg-red-100 text-red-800 border-red-300',
due_soon: 'bg-amber-100 text-amber-800 border-amber-300',
pending: 'bg-gray-100 text-gray-700 border-gray-300',
}
const STATUS_LABEL: Record<string, string> = {
submitted: 'gemeldet', overdue: 'überfällig', due_soon: 'bald fällig', pending: 'offen',
}
const SEV_STYLE: Record<string, string> = {
critical: 'bg-red-600', high: 'bg-orange-500', medium: 'bg-amber-500', low: 'bg-gray-400',
}
function remaining(sec: number | null): string {
if (sec === null) return ''
const past = sec < 0
const a = Math.abs(sec)
const h = Math.floor(a / 3600)
const txt = h >= 48 ? `${Math.floor(h / 24)} Tage` : `${h} h`
return past ? `seit ${txt} überfällig` : `noch ${txt}`
}
function fmt(iso: string | null): string {
if (!iso) return '—'
try { return new Date(iso).toLocaleString('de-DE', { dateStyle: 'medium', timeStyle: 'short' }) } catch { return iso }
}
function StageRow({ d, incidentId, summary, onSubmit }: {
d: Deadline; incidentId: string; summary: string; onSubmit: (stage: string) => void
}) {
return (
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 py-2 border-t border-gray-100 dark:border-gray-700">
<span className={`text-xs font-medium px-2 py-0.5 rounded border ${STATUS_STYLE[d.status]}`}>
{STATUS_LABEL[d.status]}
</span>
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">{d.label}</span>
<span className="text-xs text-gray-400">{d.article}</span>
<span className="text-xs text-gray-500">
Frist: {fmt(d.due_at)}{d.status !== 'submitted' && d.remaining_seconds !== null ? ` · ${remaining(d.remaining_seconds)}` : ''}
{d.status === 'submitted' ? ` · übermittelt ${fmt(d.submitted_at)}` : ''}
</span>
<div className="ml-auto flex items-center gap-2">
<button onClick={() => downloadStageExport(incidentId, d.key, summary)}
className="text-xs rounded border border-gray-300 dark:border-gray-600 px-2 py-1 hover:bg-gray-50 dark:hover:bg-gray-700">
ENISA-Entwurf
</button>
{d.status !== 'submitted' && (
<button onClick={() => onSubmit(d.key)}
className="text-xs rounded bg-indigo-600 hover:bg-indigo-700 text-white px-2 py-1">
Als gemeldet markieren
</button>
)}
</div>
</div>
)
}
export function IncidentCard({ inc, onSubmit }: { inc: Incident; onSubmit: (id: string, stage: string) => void }) {
return (
<div className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4">
<div className="flex flex-wrap items-start justify-between gap-2">
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className={`w-2.5 h-2.5 rounded-full ${SEV_STYLE[inc.severity || 'low']}`} title={inc.severity} />
<h3 className="text-base font-semibold text-gray-900 dark:text-gray-100 truncate">{inc.summary || 'Vorfall'}</h3>
</div>
<p className="text-xs text-gray-500 mt-0.5">
{inc.product_name} {inc.product_version} · {inc.kind === 'exploited_vulnerability' ? 'ausgenutzte Schwachstelle' : 'schwerer Vorfall'} · bekannt seit {fmt(inc.aware_at || null)}
</p>
</div>
<span className="text-xs text-gray-400 shrink-0">Status: {inc.status}</span>
</div>
<div className="mt-3">
{inc.deadlines.map((d) => (
<StageRow key={d.key} d={d} incidentId={inc.id} summary={inc.summary}
onSubmit={(stage) => onSubmit(inc.id, stage)} />
))}
</div>
<p className="text-[11px] text-gray-400 italic mt-2">
Übermittlung an die ENISA Single Reporting Platform erfolgt manuell mit dem Entwurf keine automatische Übertragung.
</p>
</div>
)
}
@@ -0,0 +1,66 @@
'use client'
import { useState } from 'react'
import { Meta } from '../_hooks/useMeldewesen'
const KIND_LABEL: Record<string, string> = {
exploited_vulnerability: 'Aktiv ausgenutzte Schwachstelle',
severe_incident: 'Schwerwiegender Sicherheitsvorfall',
}
const SEV_LABEL: Record<string, string> = {
critical: 'kritisch', high: 'hoch', medium: 'mittel', low: 'niedrig',
}
export function NewIncidentForm({ meta, onCreate, onCancel }: {
meta: Meta | null
onCreate: (body: Record<string, unknown>) => Promise<boolean>
onCancel: () => void
}) {
const [f, setF] = useState<Record<string, string>>({
summary: '', product_name: '', product_version: '', manufacturer: '',
kind: 'exploited_vulnerability', severity: 'high', contact: '', impact: '',
})
const [busy, setBusy] = useState(false)
const set = (k: string, v: string) => setF((p) => ({ ...p, [k]: v }))
const field = 'w-full text-sm rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 p-2'
const submit = async () => {
setBusy(true)
try { if (await onCreate(f)) onCancel() } finally { setBusy(false) }
}
return (
<div className="rounded-xl border border-indigo-200 dark:border-indigo-800 bg-indigo-50/40 dark:bg-indigo-900/20 p-4 space-y-3">
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">Neue CRA-Meldung erfassen</h3>
<p className="text-xs text-gray-600 dark:text-gray-300">
Die 24h/72h/14-Tage-Fristen laufen ab dem Zeitpunkt, an dem Sie Kenntnis erlangt haben.
</p>
<input className={field} placeholder="Kurzbeschreibung des Vorfalls *"
value={f.summary} onChange={(e) => set('summary', e.target.value)} />
<div className="grid grid-cols-2 gap-2">
<input className={field} placeholder="Produkt" value={f.product_name} onChange={(e) => set('product_name', e.target.value)} />
<input className={field} placeholder="Version" value={f.product_version} onChange={(e) => set('product_version', e.target.value)} />
<input className={field} placeholder="Hersteller" value={f.manufacturer} onChange={(e) => set('manufacturer', e.target.value)} />
<input className={field} placeholder="Kontakt (PSIRT-E-Mail)" value={f.contact} onChange={(e) => set('contact', e.target.value)} />
<select className={field} value={f.kind} onChange={(e) => set('kind', e.target.value)}>
{(meta?.kinds || ['exploited_vulnerability', 'severe_incident']).map((k) => (
<option key={k} value={k}>{KIND_LABEL[k] || k}</option>
))}
</select>
<select className={field} value={f.severity} onChange={(e) => set('severity', e.target.value)}>
{(meta?.severities || ['low', 'medium', 'high', 'critical']).map((s) => (
<option key={s} value={s}>{SEV_LABEL[s] || s}</option>
))}
</select>
</div>
<textarea className={field} rows={2} placeholder="Auswirkung (kurz)" value={f.impact} onChange={(e) => set('impact', e.target.value)} />
<div className="flex items-center gap-2">
<button onClick={submit} disabled={busy || f.summary.trim().length < 3}
className="rounded bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 text-white text-sm px-4 py-2">
{busy ? 'Lege an …' : 'Meldung anlegen'}
</button>
<button onClick={onCancel} className="text-sm text-gray-500 hover:underline">Abbrechen</button>
</div>
</div>
)
}
@@ -0,0 +1,126 @@
'use client'
import { useCallback, useEffect, useState } from 'react'
// CRA Art. 14 Meldewesen: incident reporting cascade (24h/72h/14d) to ENISA SRP.
// Incidents are scoped to a CRA project; results of the cascade are exported as a
// structured draft (no live ENISA API yet).
export interface Deadline {
key: string
label: string
article: string
due_at: string | null
submitted_at: string | null
status: 'submitted' | 'overdue' | 'due_soon' | 'pending'
remaining_seconds: number | null
}
export interface Incident {
id: string
status: string
summary: string
product_name?: string
product_version?: string
manufacturer?: string
kind?: string
severity?: string
aware_at?: string
contact?: string
impact?: string
root_cause?: string
corrective_measures?: string
patch_available?: boolean
personal_data_affected?: boolean
deadlines: Deadline[]
next_stage: string | null
submissions?: Record<string, { submitted_at: string; report: Record<string, unknown> }>
}
export interface Meta {
stages: { key: string; label: string; article: string; hours: number }[]
severities: string[]
kinds: string[]
reporting_active_from: string
}
interface Project { id: string; name: string }
const j = (r: Response) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))
export function useMeldewesen() {
const [meta, setMeta] = useState<Meta | null>(null)
const [projects, setProjects] = useState<Project[]>([])
const [projectId, setProjectId] = useState('')
const [incidents, setIncidents] = useState<Incident[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
fetch('/api/sdk/v1/cra/incidents/meta').then(j).then(setMeta).catch(() => {})
fetch('/api/sdk/v1/cra/projects')
.then(j)
.then((d) => {
const list: Project[] = (d.projects || d || []).map((p: any) => ({ id: p.id, name: p.name || p.machine_name || p.id }))
setProjects(list)
if (list.length && !projectId) setProjectId(list[0].id)
})
.catch(() => {})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const loadIncidents = useCallback((pid: string) => {
if (!pid) { setIncidents([]); return }
setLoading(true)
setError(null)
fetch(`/api/sdk/v1/cra/incidents?cra_project_id=${encodeURIComponent(pid)}`)
.then(j)
.then((d) => setIncidents(d.incidents || []))
.catch((e) => setError(String(e?.message || e)))
.finally(() => setLoading(false))
}, [])
useEffect(() => { loadIncidents(projectId) }, [projectId, loadIncidents])
const createIncident = useCallback(async (body: Record<string, unknown>) => {
const r = await fetch('/api/sdk/v1/cra/incidents', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...body, cra_project_id: projectId }),
})
if (r.ok) loadIncidents(projectId)
return r.ok
}, [projectId, loadIncidents])
const submitStage = useCallback(async (incidentId: string, stage: string) => {
const r = await fetch(`/api/sdk/v1/cra/incidents/${incidentId}/submit/${stage}`, { method: 'POST' })
if (r.ok) loadIncidents(projectId)
return r.ok
}, [projectId, loadIncidents])
const patchIncident = useCallback(async (incidentId: string, patch: Record<string, unknown>) => {
const r = await fetch(`/api/sdk/v1/cra/incidents/${incidentId}`, {
method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(patch),
})
if (r.ok) loadIncidents(projectId)
return r.ok
}, [projectId, loadIncidents])
return {
meta, projects, projectId, setProjectId, incidents, loading, error,
createIncident, submitStage, patchIncident, reload: () => loadIncidents(projectId),
}
}
// Download the ENISA export draft for a stage as JSON.
export async function downloadStageExport(incidentId: string, stage: string, summary: string): Promise<void> {
const r = await fetch(`/api/sdk/v1/cra/incidents/${incidentId}/export/${stage}`)
if (!r.ok) return
const data = await r.json()
const safe = (summary || 'meldung').replace(/[^\w\-äöüÄÖÜß ]/g, '').trim().replace(/\s+/g, '_').slice(0, 40)
const blob = new Blob([JSON.stringify(data.report, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `ENISA_${stage}_${safe || 'meldung'}.json`
document.body.appendChild(a)
a.click()
a.remove()
URL.revokeObjectURL(url)
}
@@ -0,0 +1,85 @@
'use client'
import { useState } from 'react'
import { useMeldewesen } from './_hooks/useMeldewesen'
import { IncidentCard } from './_components/IncidentCard'
import { NewIncidentForm } from './_components/NewIncidentForm'
// CRA Article 14 Meldewesen: the 24h/72h/14d incident-reporting cascade to ENISA.
// Customer-facing; deadlines + report drafts. No live ENISA API (manual export).
export default function MeldewesenPage() {
const m = useMeldewesen()
const [showForm, setShowForm] = useState(false)
return (
<div className="space-y-6">
<header className="flex flex-wrap items-start justify-between gap-3">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">CRA-Meldewesen</h1>
<p className="text-sm text-gray-600 dark:text-gray-300 mt-1 max-w-2xl">
Meldepflichten nach CRA Artikel 14: Frühwarnung (24 h), Meldung (72 h) und Abschlussbericht
(14 Tage) an die ENISA Single Reporting Platform. Wir behalten die Fristen im Blick und
erstellen die Berichtsentwürfe die Übermittlung bestätigen Sie selbst.
</p>
</div>
{m.meta?.reporting_active_from && (
<span className="text-xs rounded-full bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 px-3 py-1">
Meldepflicht aktiv ab {new Date(m.meta.reporting_active_from).toLocaleDateString('de-DE')}
</span>
)}
</header>
<div className="flex flex-wrap items-center gap-3">
<label className="text-sm text-gray-600 dark:text-gray-300">Projekt:</label>
<select
value={m.projectId}
onChange={(e) => m.setProjectId(e.target.value)}
className="text-sm rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 p-2 min-w-[16rem]"
>
{m.projects.length === 0 && <option value=""> kein CRA-Projekt </option>}
{m.projects.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
{m.projectId && !showForm && (
<button onClick={() => setShowForm(true)}
className="ml-auto rounded bg-indigo-600 hover:bg-indigo-700 text-white text-sm px-4 py-2">
+ Neue Meldung
</button>
)}
</div>
{showForm && m.projectId && (
<NewIncidentForm meta={m.meta} onCreate={m.createIncident} onCancel={() => setShowForm(false)} />
)}
{!m.projectId && (
<div className="rounded-xl border border-dashed border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 p-8 text-center text-sm text-gray-600 dark:text-gray-300">
Legen Sie zuerst ein CRA-Projekt an, um Vorfälle zu erfassen.
</div>
)}
{m.loading && <div className="text-sm text-gray-500">Lade Meldungen </div>}
{m.error && !m.loading && (
<div className="rounded-xl border border-amber-300 bg-amber-50 dark:bg-amber-900/20 text-amber-900 dark:text-amber-200 p-4 text-sm">
Meldungen konnten nicht geladen werden ({m.error}).{' '}
<button onClick={m.reload} className="underline font-medium">Erneut versuchen</button>
</div>
)}
{m.projectId && !m.loading && !m.error && m.incidents.length === 0 && (
<div className="rounded-xl border border-dashed border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 p-8 text-center">
<p className="text-sm text-gray-700 dark:text-gray-200 font-medium">Keine offenen Meldungen</p>
<p className="text-xs text-gray-500 mt-1">Im Ernstfall erfassen Sie hier den Vorfall die Fristen laufen dann automatisch mit.</p>
</div>
)}
{m.incidents.length > 0 && (
<div className="grid gap-3">
{m.incidents.map((inc) => (
<IncidentCard key={inc.id} inc={inc} onSubmit={m.submitStage} />
))}
</div>
)}
</div>
)
}
@@ -0,0 +1,75 @@
'use client'
import { useCallback, useEffect, useState } from 'react'
// Customer "Dokumente" view: lists ONLY published legal documents (the
// ready-to-use output), never templates or drafts. Backed by
// GET /api/sdk/v1/legal-documents/public (published-only, tenant-scoped).
export interface PublishedDoc {
id: string
type: string
name: string
version: number
title: string
content: string
language: string
published_at: string | null
}
// Human-readable German labels for the known document types. Internal type keys
// are never shown to the customer — only this Klartext.
const TYPE_LABEL: Record<string, string> = {
impressum: 'Impressum',
privacy_policy: 'Datenschutzerklärung',
datenschutz: 'Datenschutzerklärung',
dse: 'Datenschutzerklärung',
agb: 'AGB',
terms_of_service: 'Nutzungsbedingungen',
widerruf: 'Widerrufsbelehrung',
cookie_policy: 'Cookie-Richtlinie',
cookie_banner: 'Cookie-Banner-Text',
dpa: 'Auftragsverarbeitungsvertrag (AVV)',
nda: 'Geheimhaltungsvereinbarung (NDA)',
sla: 'Service-Level-Agreement (SLA)',
legal_notice: 'Rechtlicher Hinweis',
}
export function docLabel(type: string): string {
return TYPE_LABEL[type] || type.replace(/_/g, ' ')
}
export function useDokumente() {
const [docs, setDocs] = useState<PublishedDoc[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const load = useCallback(() => {
setLoading(true)
setError(null)
fetch('/api/sdk/v1/legal-documents/public')
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
.then((data: PublishedDoc[]) => setDocs(Array.isArray(data) ? data : []))
.catch((e) => setError(String(e?.message || e)))
.finally(() => setLoading(false))
}, [])
useEffect(() => { load() }, [load])
return { docs, loading, error, reload: load }
}
// Trigger a client-side download of a document's content as a .md file.
export function downloadDoc(doc: PublishedDoc): void {
const safe = (doc.title || docLabel(doc.type) || 'dokument')
.replace(/[^\w\-äöüÄÖÜß ]/g, '').trim().replace(/\s+/g, '_')
const blob = new Blob([doc.content || ''], { type: 'text/markdown;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${safe || 'dokument'}_v${doc.version}.md`
document.body.appendChild(a)
a.click()
a.remove()
URL.revokeObjectURL(url)
}
+104
View File
@@ -0,0 +1,104 @@
'use client'
import { useState } from 'react'
import { useDokumente, docLabel, downloadDoc, PublishedDoc } from './_hooks/useDokumente'
// Customer-facing "Dokumente": the finished, published legal documents the
// customer can read and download. Deliberately shows NO templates, NO drafts and
// NO generator — only what has been approved and published.
function fmtDate(iso: string | null): string {
if (!iso) return '—'
try {
return new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: 'long', year: 'numeric' })
} catch {
return iso
}
}
function DocCard({ doc }: { doc: PublishedDoc }) {
const [open, setOpen] = useState(false)
return (
<div className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="rounded bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 text-xs font-medium px-2 py-0.5">
{docLabel(doc.type)}
</span>
<span className="text-xs text-gray-400">v{doc.version} · {doc.language?.toUpperCase()}</span>
</div>
<h3 className="mt-1.5 text-base font-semibold text-gray-900 dark:text-gray-100 truncate">
{doc.title || docLabel(doc.type)}
</h3>
<p className="text-xs text-gray-500 mt-0.5">Veröffentlicht am {fmtDate(doc.published_at)}</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<button
onClick={() => setOpen((v) => !v)}
className="rounded border border-gray-300 dark:border-gray-600 text-sm px-3 py-1.5 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
>
{open ? 'Schließen' : 'Ansehen'}
</button>
<button
onClick={() => downloadDoc(doc)}
className="rounded bg-indigo-600 hover:bg-indigo-700 text-white text-sm px-3 py-1.5"
>
Herunterladen
</button>
</div>
</div>
{open && (
<article className="mt-4 max-h-[28rem] overflow-auto rounded-lg border border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/40 p-4 text-sm leading-relaxed text-gray-800 dark:text-gray-200 whitespace-pre-wrap">
{doc.content || 'Kein Inhalt hinterlegt.'}
</article>
)}
</div>
)
}
export default function DokumentePage() {
const { docs, loading, error, reload } = useDokumente()
return (
<div className="space-y-6">
<header>
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Dokumente</h1>
<p className="text-sm text-gray-600 dark:text-gray-300 mt-1 max-w-2xl">
Ihre freigegebenen Rechtsdokumente fertig zum Ansehen und Herunterladen. Hier erscheinen
ausschließlich <span className="font-medium">veröffentlichte</span> Dokumente; Entwürfe und
interne Vorlagen sind bewusst nicht enthalten.
</p>
</header>
{loading && (
<div className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-8 text-center text-sm text-gray-500">
Lade Dokumente
</div>
)}
{error && !loading && (
<div className="rounded-xl border border-amber-300 bg-amber-50 dark:bg-amber-900/20 text-amber-900 dark:text-amber-200 p-4 text-sm">
Dokumente konnten gerade nicht geladen werden ({error}).{' '}
<button onClick={reload} className="underline font-medium">Erneut versuchen</button>
</div>
)}
{!loading && !error && docs.length === 0 && (
<div className="rounded-xl border border-dashed border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 p-8 text-center">
<p className="text-sm text-gray-700 dark:text-gray-200 font-medium">Noch keine veröffentlichten Dokumente</p>
<p className="text-xs text-gray-500 mt-1 max-w-md mx-auto">
Sobald ein Dokument intern und von Ihnen freigegeben und veröffentlicht wurde, erscheint es
hier automatisch zum Download.
</p>
</div>
)}
{!loading && !error && docs.length > 0 && (
<div className="grid gap-3">
{docs.map((d) => <DocCard key={`${d.id}-${d.version}`} doc={d} />)}
</div>
)}
</div>
)
}
+2 -10
View File
@@ -94,19 +94,11 @@ export default function ImportPage() {
{/* Continue Button */}
{analysisResult && (
<div className="flex justify-between items-center pt-4 border-t border-gray-200">
<div className="pt-4 border-t border-gray-200">
{/* "Weiter zum Screening" entfernt: Repo-Scanning läuft extern im Compliance-Scanner. */}
<p className="text-sm text-gray-500">
Die Gap-Analyse wurde gespeichert. Sie koennen jetzt mit dem Compliance-Assessment fortfahren.
</p>
<button
onClick={() => router.push('/sdk/screening')}
className="px-6 py-2.5 bg-green-600 text-white font-medium rounded-lg hover:bg-green-700 transition-colors flex items-center gap-2"
>
Weiter zum Screening
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
</button>
</div>
)}
+2 -3
View File
@@ -267,9 +267,8 @@ export default function SDKDashboard() {
<QuickActionCard title="Neuen Use Case erstellen" description="Starten Sie den 5-Schritte-Wizard"
icon={<svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" /></svg>}
href="/sdk/advisory-board" color="bg-purple-50" projectId={projectId} />
<QuickActionCard title="Security Screening" description="SBOM generieren und Schwachstellen scannen"
icon={<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>}
href="/sdk/screening" color="bg-red-50" projectId={projectId} />
{/* "Security Screening" entfernt: Repo-Scanning (SBOM/SAST/DAST/Vuln) läuft
extern im Compliance-Scanner (CERTifAI); das Ergebnis wird dort angezeigt. */}
<QuickActionCard title="DSFA generieren" description="Datenschutz-Folgenabschaetzung erstellen"
icon={<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>}
href="/sdk/dsfa" color="bg-blue-50" projectId={projectId} />