'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 }> } 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(null) const [projects, setProjects] = useState([]) const [projectId, setProjectId] = useState('') const [incidents, setIncidents] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState(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) => { 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) => { 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 { 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) }