'use client' /** * Evidence Management Page * * Features: * - List evidence by control * - File upload * - URL/Link adding * - Evidence status tracking */ import { useState, useEffect, useRef, Suspense } from 'react' import { useSearchParams } from 'next/navigation' import Link from 'next/link' import AdminLayout from '@/components/admin/AdminLayout' interface Evidence { id: string control_id: string evidence_type: string title: string description: string artifact_path: string | null artifact_url: string | null artifact_hash: string | null file_size_bytes: number | null mime_type: string | null status: string source: string ci_job_id: string | null valid_from: string valid_until: string | null collected_at: string } interface Control { id: string control_id: string title: string } const EVIDENCE_TYPES = [ { value: 'scan_report', label: 'Scan Report', icon: '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' }, { value: 'policy_document', label: 'Policy Dokument', icon: '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' }, { value: 'config_snapshot', label: 'Config Snapshot', icon: 'M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4' }, { value: 'test_result', label: 'Test Ergebnis', icon: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z' }, { value: 'screenshot', label: 'Screenshot', icon: 'M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z' }, { value: 'external_link', label: 'Externer Link', icon: 'M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14' }, { value: 'manual_upload', label: 'Manueller Upload', icon: 'M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12' }, ] const STATUS_STYLES: Record = { valid: 'bg-green-100 text-green-700', expired: 'bg-red-100 text-red-700', pending: 'bg-yellow-100 text-yellow-700', failed: 'bg-red-100 text-red-700', } function EvidencePageContent({ initialControlId }: { initialControlId: string | null }) { const [evidence, setEvidence] = useState([]) const [controls, setControls] = useState([]) const [loading, setLoading] = useState(true) const [filterControlId, setFilterControlId] = useState(initialControlId || '') const [filterType, setFilterType] = useState('') const [uploadModalOpen, setUploadModalOpen] = useState(false) const [linkModalOpen, setLinkModalOpen] = useState(false) const [uploading, setUploading] = useState(false) const [newEvidence, setNewEvidence] = useState({ control_id: initialControlId || '', evidence_type: 'manual_upload', title: '', description: '', artifact_url: '', }) const fileInputRef = useRef(null) const [selectedFile, setSelectedFile] = useState(null) const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000' useEffect(() => { loadData() }, [filterControlId, filterType]) const loadData = async () => { setLoading(true) try { const params = new URLSearchParams() if (filterControlId) params.append('control_id', filterControlId) if (filterType) params.append('evidence_type', filterType) const [evidenceRes, controlsRes] = await Promise.all([ fetch(`${BACKEND_URL}/api/v1/compliance/evidence?${params}`), fetch(`${BACKEND_URL}/api/v1/compliance/controls`), ]) if (evidenceRes.ok) { const data = await evidenceRes.json() setEvidence(data.evidence || []) } if (controlsRes.ok) { const data = await controlsRes.json() setControls(data.controls || []) } } catch (error) { console.error('Failed to load data:', error) } finally { setLoading(false) } } const handleFileUpload = async () => { if (!selectedFile || !newEvidence.control_id || !newEvidence.title) { alert('Bitte alle Pflichtfelder ausfuellen') return } setUploading(true) try { const formData = new FormData() formData.append('file', selectedFile) const params = new URLSearchParams({ control_id: newEvidence.control_id, evidence_type: newEvidence.evidence_type, title: newEvidence.title, }) if (newEvidence.description) { params.append('description', newEvidence.description) } const res = await fetch(`${BACKEND_URL}/api/v1/compliance/evidence/upload?${params}`, { method: 'POST', body: formData, }) if (res.ok) { setUploadModalOpen(false) resetForm() loadData() } else { const error = await res.text() alert(`Upload fehlgeschlagen: ${error}`) } } catch (error) { console.error('Upload failed:', error) alert('Upload fehlgeschlagen') } finally { setUploading(false) } } const handleLinkSubmit = async () => { if (!newEvidence.control_id || !newEvidence.title || !newEvidence.artifact_url) { alert('Bitte alle Pflichtfelder ausfuellen') return } setUploading(true) try { const res = await fetch(`${BACKEND_URL}/api/v1/compliance/evidence`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ control_id: newEvidence.control_id, evidence_type: 'external_link', title: newEvidence.title, description: newEvidence.description, artifact_url: newEvidence.artifact_url, source: 'manual', }), }) if (res.ok) { setLinkModalOpen(false) resetForm() loadData() } else { const error = await res.text() alert(`Fehler: ${error}`) } } catch (error) { console.error('Failed to add link:', error) alert('Fehler beim Hinzufuegen') } finally { setUploading(false) } } const resetForm = () => { setNewEvidence({ control_id: filterControlId || '', evidence_type: 'manual_upload', title: '', description: '', artifact_url: '', }) setSelectedFile(null) } const formatFileSize = (bytes: number | null) => { if (!bytes) return '-' if (bytes < 1024) return `${bytes} B` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` return `${(bytes / (1024 * 1024)).toFixed(1)} MB` } const getControlTitle = (controlUuid: string) => { const control = controls.find((c) => c.id === controlUuid) return control?.control_id || controlUuid } return ( {/* Header */}
Zurueck
{/* Filters */}
{evidence.length} Nachweise
{/* Evidence List */} {loading ? (
) : evidence.length === 0 ? (

Keine Nachweise gefunden

) : (
{evidence.map((ev) => (
t.value === ev.evidence_type)?.icon || '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'} />
{ev.status}
{getControlTitle(ev.control_id)}

{ev.title}

{ev.description && (

{ev.description}

)}
{ev.evidence_type.replace('_', ' ')} {formatFileSize(ev.file_size_bytes)}
{ev.artifact_url && ( {ev.artifact_url} )}
Erfasst: {new Date(ev.collected_at).toLocaleDateString('de-DE')}
))}
)} {/* Upload Modal */} {uploadModalOpen && (

Datei hochladen

setNewEvidence({ ...newEvidence, title: e.target.value })} placeholder="z.B. Semgrep Scan Report 2026-01" className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500" />