'use client' import React, { useEffect, useMemo, useState } from 'react' type Finding = { id: number source_type: string doc_type: string severity: string status: string regulation: string label: string hint: string action_recipe: Record anchor_excerpt: string anchor_conf: number vendor_name: string category: string payload: Record } type Summary = { total: number by_source: Record by_severity: Record by_status: Record by_doc_type: Record } type Resp = { found: boolean summary: Summary count: number findings: Finding[] } const SOURCE_LABEL: Record = { all: 'Alle Quellen', mc: 'Master-Controls', pflichtangabe: 'Pflichtangaben', vendor: 'Vendor-Findings', redundanz: 'Redundanzen', } const SEVERITY_COLOR: Record = { CRITICAL: 'bg-red-600 text-white', HIGH: 'bg-red-100 text-red-800', MEDIUM: 'bg-amber-100 text-amber-800', LOW: 'bg-blue-100 text-blue-800', INFO: 'bg-gray-100 text-gray-600', } const STATUS_LABEL: Record = { failed: 'Fail', passed: 'Pass', skipped: 'Skip', na: 'N/A', info: 'Info', } const SEVERITY_OPTS = ['all', 'CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO'] const STATUS_OPTS = ['all', 'failed', 'passed', 'skipped', 'na', 'info'] export default function FindingsTab({ checkId }: { checkId: string }) { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [source, setSource] = useState('all') const [severity, setSeverity] = useState('all') const [docType, setDocType] = useState('all') const [status, setStatus] = useState('failed') const [q, setQ] = useState('') const [expanded, setExpanded] = useState(null) useEffect(() => { let cancelled = false setLoading(true) const qs = new URLSearchParams({ source, severity, doc_type: docType, status, q, limit: '1500', }).toString() fetch(`/api/sdk/v1/agent/findings/${checkId}?${qs}`) .then(r => r.json()) .then(d => { if (!cancelled) setData(d) }) .catch(e => { if (!cancelled) setError(String(e)) }) .finally(() => { if (!cancelled) setLoading(false) }) return () => { cancelled = true } }, [checkId, source, severity, docType, status, q]) const docTypes = useMemo( () => Object.keys(data?.summary?.by_doc_type ?? {}).filter(d => d !== '-').sort(), [data], ) const csvExport = () => { const rows = data?.findings ?? [] const head = ['Quelle', 'Doc', 'Severity', 'Status', 'Regulation', 'Label', 'Vendor', 'Hint'] const lines = [head.join(',')] for (const r of rows) { const cells = [ r.source_type, r.doc_type, r.severity, r.status, r.regulation, r.label, r.vendor_name, r.hint, ].map(c => `"${String(c ?? '').replace(/"/g, '""').replace(/\n/g, ' ')}"`) lines.push(cells.join(',')) } const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `findings-${checkId}.csv` a.click() URL.revokeObjectURL(url) } if (loading && !data) return
Lade Voll-Audit…
if (error) return
Fehler: {error}
if (!data?.found) { return (
Keine unified findings für diesen Run gespeichert (alter Run vor P5?).
) } const sum = data.summary const findings = data.findings return (
{/* Summary Cards */}
{Object.entries(SOURCE_LABEL).filter(([k]) => k !== 'all').map(([k, label]) => { const count = sum.by_source?.[k] ?? 0 return ( ) })}
{/* Filter row */}
setQ(e.target.value)} placeholder="Suche Label / Anbieter…" className="border border-gray-200 rounded px-2 py-1 min-w-[180px]" /> {data.count} Treffer
{/* Findings table */}
{findings.map(f => ( setExpanded(expanded === f.id ? null : f.id)}> {expanded === f.id && ( )} ))} {findings.length === 0 && ( )}
Quelle Doc Sev Status Finding
{f.source_type} {f.doc_type === '-' ? '—' : f.doc_type} {f.severity} {STATUS_LABEL[f.status] ?? f.status} {f.label} {f.vendor_name && ( · {f.vendor_name} )} {(() => { const rl = String(f.payload?.risk_label ?? '') if (!rl) return null const cls = rl === 'kritisch' ? 'bg-red-600 text-white' : rl === 'hoch' ? 'bg-red-100 text-red-800' : rl === 'mittel' ? 'bg-amber-100 text-amber-800' : rl === 'gering' ? 'bg-green-50 text-green-700' : 'bg-gray-100 text-gray-500' return Risk: {rl} })()}
{f.hint && (
{f.hint}
)} {f.action_recipe?.fix_text && (
Empfehlung
{f.action_recipe.fix_text}
{f.action_recipe.where && (
Einfuegen in: {f.action_recipe.where}
)}
)} {f.anchor_excerpt && (
Fundstelle im Dokument (Konfidenz {Math.round((f.anchor_conf || 0) * 100)}%)
"{f.anchor_excerpt}"
)}
Source: {f.source_type} · Regulation: {f.regulation || '—'} {f.category && ` · Kategorie: ${f.category}`}
Keine Findings fuer die aktuellen Filter.
) }