Files
breakpilot-compliance/admin-compliance/app/sdk/cra-meldewesen/_components/IncidentCard.tsx
T
Benjamin Bönisch 8f21650d74
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
feat(sdk): Kunden-Dokumente + CRA-Meldewesen, Screening aus Frontend genommen
- /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>
2026-06-17 21:21:28 +02:00

89 lines
4.0 KiB
TypeScript

'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>
)
}