feat: Anti-Fake-Evidence System (Phase 1-4b)

Implement full evidence integrity pipeline to prevent compliance theater:
- Confidence levels (E0-E4), truth status tracking, assertion engine
- Four-Eyes approval workflow, audit trail, reject endpoint
- Evidence distribution dashboard, LLM audit routes
- Traceability matrix (backend endpoint + Compliance Hub UI tab)
- Anti-fake badges, control status machine, normative patterns
- 2 migrations, 4 test suites, MkDocs documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-23 17:15:45 +01:00
parent 48ca0a6bef
commit e6201d5239
36 changed files with 5627 additions and 189 deletions

View File

@@ -0,0 +1,111 @@
"use client"
import React from "react"
const badgeBase = "inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
// ---------------------------------------------------------------------------
// Confidence Level Badge (E0E4)
// ---------------------------------------------------------------------------
const confidenceColors: Record<string, string> = {
E0: "bg-red-100 text-red-800",
E1: "bg-yellow-100 text-yellow-800",
E2: "bg-blue-100 text-blue-800",
E3: "bg-green-100 text-green-800",
E4: "bg-emerald-100 text-emerald-800",
}
const confidenceLabels: Record<string, string> = {
E0: "E0 — Generiert",
E1: "E1 — Manuell",
E2: "E2 — Intern validiert",
E3: "E3 — System-beobachtet",
E4: "E4 — Extern auditiert",
}
export function ConfidenceLevelBadge({ level }: { level?: string | null }) {
if (!level) return null
const color = confidenceColors[level] || "bg-gray-100 text-gray-800"
const label = confidenceLabels[level] || level
return <span className={`${badgeBase} ${color}`}>{label}</span>
}
// ---------------------------------------------------------------------------
// Truth Status Badge
// ---------------------------------------------------------------------------
const truthColors: Record<string, string> = {
generated: "bg-violet-100 text-violet-800",
uploaded: "bg-gray-100 text-gray-800",
observed: "bg-blue-100 text-blue-800",
validated: "bg-green-100 text-green-800",
rejected: "bg-red-100 text-red-800",
audited: "bg-emerald-100 text-emerald-800",
}
const truthLabels: Record<string, string> = {
generated: "Generiert",
uploaded: "Hochgeladen",
observed: "Beobachtet",
validated: "Validiert",
rejected: "Abgelehnt",
audited: "Auditiert",
}
export function TruthStatusBadge({ status }: { status?: string | null }) {
if (!status) return null
const color = truthColors[status] || "bg-gray-100 text-gray-800"
const label = truthLabels[status] || status
return <span className={`${badgeBase} ${color}`}>{label}</span>
}
// ---------------------------------------------------------------------------
// Generation Mode Badge (sparkles icon)
// ---------------------------------------------------------------------------
export function GenerationModeBadge({ mode }: { mode?: string | null }) {
if (!mode) return null
return (
<span className={`${badgeBase} bg-violet-100 text-violet-800`}>
<svg className="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-2 0v-1H3a1 1 0 010-2h1v-1a1 1 0 011-1zm7-10a1 1 0 01.967.744L14.146 7.2 17.5 7.512a1 1 0 010 1.976l-3.354.313-1.18 4.456a1 1 0 01-1.932 0l-1.18-4.456-3.354-.313a1 1 0 010-1.976l3.354-.313 1.18-4.456A1 1 0 0112 2z" />
</svg>
KI-generiert
</span>
)
}
// ---------------------------------------------------------------------------
// Approval Status Badge (Four-Eyes)
// ---------------------------------------------------------------------------
const approvalColors: Record<string, string> = {
none: "bg-gray-100 text-gray-600",
pending_first: "bg-yellow-100 text-yellow-800",
first_approved: "bg-blue-100 text-blue-800",
approved: "bg-green-100 text-green-800",
rejected: "bg-red-100 text-red-800",
}
const approvalLabels: Record<string, string> = {
none: "Kein Review",
pending_first: "Warte auf 1. Review",
first_approved: "1. Review OK",
approved: "Genehmigt (4-Augen)",
rejected: "Abgelehnt",
}
export function ApprovalStatusBadge({
status,
requiresFourEyes,
}: {
status?: string | null
requiresFourEyes?: boolean | null
}) {
if (!requiresFourEyes) return null
const s = status || "none"
const color = approvalColors[s] || "bg-gray-100 text-gray-600"
const label = approvalLabels[s] || s
return <span className={`${badgeBase} ${color}`}>{label}</span>
}

View File

@@ -3,6 +3,12 @@
import React, { useState, useEffect, useRef } from 'react'
import { useSDK, Evidence as SDKEvidence, EvidenceType } from '@/lib/sdk'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
import {
ConfidenceLevelBadge,
TruthStatusBadge,
GenerationModeBadge,
ApprovalStatusBadge,
} from './components/anti-fake-badges'
// =============================================================================
// TYPES
@@ -28,6 +34,12 @@ interface DisplayEvidence {
status: DisplayStatus
fileSize: string
fileUrl: string | null
// Anti-Fake-Evidence Phase 2
confidenceLevel: string | null
truthStatus: string | null
generationMode: string | null
approvalStatus: string | null
requiresFourEyes: boolean
}
// =============================================================================
@@ -162,7 +174,327 @@ const evidenceTemplates: EvidenceTemplate[] = [
// COMPONENTS
// =============================================================================
function EvidenceCard({ evidence, onDelete, onView, onDownload }: { evidence: DisplayEvidence; onDelete: () => void; onView: () => void; onDownload: () => void }) {
// =============================================================================
// CONFIDENCE FILTER COLORS (matching anti-fake-badges)
// =============================================================================
const confidenceFilterColors: Record<string, string> = {
E0: 'bg-red-200 text-red-800',
E1: 'bg-yellow-200 text-yellow-800',
E2: 'bg-blue-200 text-blue-800',
E3: 'bg-green-200 text-green-800',
E4: 'bg-emerald-200 text-emerald-800',
}
// =============================================================================
// REVIEW MODAL
// =============================================================================
function ReviewModal({ evidence, onClose, onSuccess }: { evidence: DisplayEvidence; onClose: () => void; onSuccess: () => void }) {
const [confidenceLevel, setConfidenceLevel] = useState(evidence.confidenceLevel || 'E1')
const [truthStatus, setTruthStatus] = useState(evidence.truthStatus || 'uploaded')
const [reviewedBy, setReviewedBy] = useState('')
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleSubmit = async () => {
if (!reviewedBy.trim()) { setError('Bitte E-Mail-Adresse angeben'); return }
setSubmitting(true)
setError(null)
try {
const res = await fetch(`/api/sdk/v1/compliance/evidence/${evidence.id}/review`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ confidence_level: confidenceLevel, truth_status: truthStatus, reviewed_by: reviewedBy }),
})
if (!res.ok) {
const err = await res.json().catch(() => ({ detail: 'Review fehlgeschlagen' }))
throw new Error(typeof err.detail === 'string' ? err.detail : JSON.stringify(err.detail))
}
onSuccess()
} catch (err) {
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
} finally {
setSubmitting(false)
}
}
const confidenceLevels = [
{ value: 'E0', label: 'E0 — Generiert' },
{ value: 'E1', label: 'E1 — Manuell' },
{ value: 'E2', label: 'E2 — Intern validiert' },
{ value: 'E3', label: 'E3 — System-beobachtet' },
{ value: 'E4', label: 'E4 — Extern auditiert' },
]
const truthStatuses = [
{ value: 'generated', label: 'Generiert' },
{ value: 'uploaded', label: 'Hochgeladen' },
{ value: 'observed', label: 'Beobachtet' },
{ value: 'validated', label: 'Validiert' },
{ value: 'audited', label: 'Auditiert' },
]
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
<div className="bg-white rounded-2xl shadow-xl w-full max-w-lg mx-4 p-6" onClick={e => e.stopPropagation()}>
<h2 className="text-xl font-bold text-gray-900 mb-4">Evidence Reviewen</h2>
<p className="text-sm text-gray-500 mb-4">{evidence.name}</p>
{/* Current values */}
<div className="mb-4 p-3 bg-gray-50 rounded-lg text-sm space-y-1">
<div className="flex justify-between">
<span className="text-gray-500">Aktuelles Confidence-Level:</span>
<span className="font-medium">{evidence.confidenceLevel || '—'}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">Aktueller Truth-Status:</span>
<span className="font-medium">{evidence.truthStatus || '—'}</span>
</div>
</div>
{/* New confidence level */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">Neues Confidence-Level</label>
<select value={confidenceLevel} onChange={e => setConfidenceLevel(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent">
{confidenceLevels.map(l => <option key={l.value} value={l.value}>{l.label}</option>)}
</select>
</div>
{/* New truth status */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">Neuer Truth-Status</label>
<select value={truthStatus} onChange={e => setTruthStatus(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent">
{truthStatuses.map(s => <option key={s.value} value={s.value}>{s.label}</option>)}
</select>
</div>
{/* Reviewed by */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">Reviewer (E-Mail)</label>
<input type="email" value={reviewedBy} onChange={e => setReviewedBy(e.target.value)}
placeholder="reviewer@unternehmen.de"
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
</div>
{/* Four-eyes warning */}
{evidence.requiresFourEyes && evidence.approvalStatus !== 'approved' && (
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<div className="flex items-start gap-2">
<svg className="w-5 h-5 text-yellow-600 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div className="text-sm text-yellow-800">
<p className="font-medium">4-Augen-Prinzip aktiv</p>
<p>Dieser Nachweis erfordert eine zusaetzliche Freigabe durch einen zweiten Reviewer.</p>
</div>
</div>
</div>
)}
{/* Error */}
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">{error}</div>
)}
{/* Actions */}
<div className="flex justify-end gap-3">
<button onClick={onClose} className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
Abbrechen
</button>
<button onClick={handleSubmit} disabled={submitting}
className="px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50">
{submitting ? 'Wird gespeichert...' : 'Review abschliessen'}
</button>
</div>
</div>
</div>
)
}
// =============================================================================
// REJECT MODAL
// =============================================================================
function RejectModal({ evidence, onClose, onSuccess }: { evidence: DisplayEvidence; onClose: () => void; onSuccess: () => void }) {
const [reviewedBy, setReviewedBy] = useState('')
const [rejectionReason, setRejectionReason] = useState('')
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleSubmit = async () => {
if (!reviewedBy.trim()) { setError('Bitte E-Mail-Adresse angeben'); return }
if (!rejectionReason.trim()) { setError('Bitte Ablehnungsgrund angeben'); return }
setSubmitting(true)
setError(null)
try {
const res = await fetch(`/api/sdk/v1/compliance/evidence/${evidence.id}/reject`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reviewed_by: reviewedBy, rejection_reason: rejectionReason }),
})
if (!res.ok) {
const err = await res.json().catch(() => ({ detail: 'Ablehnung fehlgeschlagen' }))
throw new Error(typeof err.detail === 'string' ? err.detail : JSON.stringify(err.detail))
}
onSuccess()
} catch (err) {
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
} finally {
setSubmitting(false)
}
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
<div className="bg-white rounded-2xl shadow-xl w-full max-w-lg mx-4 p-6" onClick={e => e.stopPropagation()}>
<h2 className="text-xl font-bold text-gray-900 mb-4">Evidence Ablehnen</h2>
<p className="text-sm text-gray-500 mb-4">{evidence.name}</p>
{/* Reviewed by */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">Reviewer (E-Mail)</label>
<input type="email" value={reviewedBy} onChange={e => setReviewedBy(e.target.value)}
placeholder="reviewer@unternehmen.de"
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-red-500 focus:border-transparent" />
</div>
{/* Rejection reason */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">Ablehnungsgrund</label>
<textarea value={rejectionReason} onChange={e => setRejectionReason(e.target.value)}
placeholder="Bitte beschreiben Sie den Grund fuer die Ablehnung..."
rows={4}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-red-500 focus:border-transparent resize-none" />
</div>
{/* Error */}
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">{error}</div>
)}
{/* Actions */}
<div className="flex justify-end gap-3">
<button onClick={onClose} className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
Abbrechen
</button>
<button onClick={handleSubmit} disabled={submitting}
className="px-4 py-2 text-sm bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50">
{submitting ? 'Wird abgelehnt...' : 'Ablehnen'}
</button>
</div>
</div>
</div>
)
}
// =============================================================================
// AUDIT TRAIL PANEL
// =============================================================================
function AuditTrailPanel({ evidenceId, onClose }: { evidenceId: string; onClose: () => void }) {
const [entries, setEntries] = useState<{ id: string; action: string; actor: string; timestamp: string; details: Record<string, unknown> | null }[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch(`/api/sdk/v1/compliance/audit-trail?entity_type=evidence&entity_id=${evidenceId}`)
.then(res => res.json())
.then(data => {
const mapped = (data.entries || []).map((e: Record<string, unknown>) => ({
id: e.id as string,
action: e.action as string,
actor: (e.performed_by || 'System') as string,
timestamp: (e.performed_at || '') as string,
details: {
...(e.field_changed ? { field: e.field_changed } : {}),
...(e.old_value ? { old: e.old_value } : {}),
...(e.new_value ? { new: e.new_value } : {}),
...(e.change_summary ? { summary: e.change_summary } : {}),
} as Record<string, unknown>,
}))
setEntries(mapped)
})
.catch(() => {})
.finally(() => setLoading(false))
}, [evidenceId])
const actionLabels: Record<string, { label: string; color: string }> = {
created: { label: 'Erstellt', color: 'bg-blue-100 text-blue-700' },
uploaded: { label: 'Hochgeladen', color: 'bg-purple-100 text-purple-700' },
reviewed: { label: 'Reviewed', color: 'bg-green-100 text-green-700' },
rejected: { label: 'Abgelehnt', color: 'bg-red-100 text-red-700' },
updated: { label: 'Aktualisiert', color: 'bg-yellow-100 text-yellow-700' },
deleted: { label: 'Geloescht', color: 'bg-gray-100 text-gray-700' },
approved: { label: 'Genehmigt', color: 'bg-emerald-100 text-emerald-700' },
four_eyes_first: { label: '1. Review (4-Augen)', color: 'bg-blue-100 text-blue-700' },
four_eyes_final: { label: 'Finale Freigabe (4-Augen)', color: 'bg-emerald-100 text-emerald-700' },
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
<div className="bg-white rounded-2xl shadow-xl w-full max-w-2xl mx-4 p-6 max-h-[80vh] overflow-y-auto" onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-gray-900">Audit-Trail</h2>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 text-xl">&times;</button>
</div>
{loading ? (
<div className="flex justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
</div>
) : entries.length === 0 ? (
<div className="py-12 text-center text-gray-500">
<p>Keine Audit-Trail-Eintraege vorhanden.</p>
</div>
) : (
<div className="relative">
{/* Timeline line */}
<div className="absolute left-4 top-0 bottom-0 w-0.5 bg-gray-200" />
<div className="space-y-4">
{entries.map((entry, idx) => {
const meta = actionLabels[entry.action] || { label: entry.action, color: 'bg-gray-100 text-gray-700' }
return (
<div key={entry.id || idx} className="relative flex items-start gap-4 pl-10">
{/* Timeline dot */}
<div className="absolute left-2.5 top-1.5 w-3 h-3 rounded-full bg-white border-2 border-purple-400" />
<div className="flex-1 bg-gray-50 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1">
<span className={`px-2 py-0.5 text-xs rounded ${meta.color}`}>{meta.label}</span>
<span className="text-xs text-gray-400">
{entry.timestamp ? new Date(entry.timestamp).toLocaleString('de-DE') : '—'}
</span>
</div>
<div className="text-sm text-gray-600">
<span className="font-medium">{entry.actor || 'System'}</span>
</div>
{entry.details && Object.keys(entry.details).length > 0 && (
<div className="mt-2 text-xs text-gray-500 font-mono bg-white rounded p-2 border">
{Object.entries(entry.details).map(([k, v]) => (
<div key={k}><span className="text-gray-400">{k}:</span> {String(v)}</div>
))}
</div>
)}
</div>
</div>
)
})}
</div>
</div>
)}
</div>
</div>
)
}
// =============================================================================
// EVIDENCE CARD
// =============================================================================
function EvidenceCard({ evidence, onDelete, onView, onDownload, onReview, onReject, onShowHistory }: { evidence: DisplayEvidence; onDelete: () => void; onView: () => void; onDownload: () => void; onReview: () => void; onReject: () => void; onShowHistory: () => void }) {
const typeIcons = {
document: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -221,9 +553,15 @@ function EvidenceCard({ evidence, onDelete, onView, onDownload }: { evidence: Di
<div className="flex-1">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900">{evidence.name}</h3>
<span className={`px-3 py-1 text-xs rounded-full ${statusColors[evidence.status]}`}>
{statusLabels[evidence.status]}
</span>
<div className="flex items-center gap-1.5 flex-wrap">
<span className={`px-3 py-1 text-xs rounded-full ${statusColors[evidence.status]}`}>
{statusLabels[evidence.status]}
</span>
<ConfidenceLevelBadge level={evidence.confidenceLevel} />
<TruthStatusBadge status={evidence.truthStatus} />
<GenerationModeBadge mode={evidence.generationMode} />
<ApprovalStatusBadge status={evidence.approvalStatus} requiresFourEyes={evidence.requiresFourEyes} />
</div>
</div>
<p className="text-sm text-gray-500 mt-1">{evidence.description}</p>
@@ -275,6 +613,31 @@ function EvidenceCard({ evidence, onDelete, onView, onDownload }: { evidence: Di
>
Loeschen
</button>
{/* Review button — visible when review is possible */}
{(evidence.approvalStatus === 'none' || evidence.approvalStatus === 'pending_first' || evidence.approvalStatus === 'first_approved' || !evidence.approvalStatus) && evidence.approvalStatus !== 'approved' && evidence.approvalStatus !== 'rejected' && (
<button
onClick={onReview}
className="px-3 py-1 text-sm text-green-600 hover:bg-green-50 rounded-lg transition-colors font-medium"
>
Reviewen
</button>
)}
{/* Reject button — visible for four-eyes evidence that's not yet resolved */}
{evidence.requiresFourEyes && evidence.approvalStatus !== 'rejected' && evidence.approvalStatus !== 'approved' && (
<button
onClick={onReject}
className="px-3 py-1 text-sm text-orange-600 hover:bg-orange-50 rounded-lg transition-colors"
>
Ablehnen
</button>
)}
{/* History button */}
<button
onClick={onShowHistory}
className="px-3 py-1 text-sm text-gray-500 hover:bg-gray-100 rounded-lg transition-colors"
>
Historie
</button>
</div>
</div>
</div>
@@ -382,6 +745,15 @@ export default function EvidencePage() {
const [pageSize] = useState(20)
const [total, setTotal] = useState(0)
// Anti-Fake-Evidence metadata (keyed by evidence ID)
const [antiFakeMeta, setAntiFakeMeta] = useState<Record<string, {
confidenceLevel: string | null
truthStatus: string | null
generationMode: string | null
approvalStatus: string | null
requiresFourEyes: boolean
}>>({})
// Evidence Checks state
const [checks, setChecks] = useState<EvidenceCheck[]>([])
const [checksLoading, setChecksLoading] = useState(false)
@@ -393,6 +765,13 @@ export default function EvidencePage() {
const [coverageReport, setCoverageReport] = useState<CoverageReport | null>(null)
const [seedingChecks, setSeedingChecks] = useState(false)
// Phase 3: Review/Reject/AuditTrail state
const [reviewEvidence, setReviewEvidence] = useState<DisplayEvidence | null>(null)
const [rejectEvidence, setRejectEvidence] = useState<DisplayEvidence | null>(null)
const [auditTrailId, setAuditTrailId] = useState<string | null>(null)
const [confidenceFilter, setConfidenceFilter] = useState<string | null>(null)
const [refreshKey, setRefreshKey] = useState(0)
// Fetch evidence from backend on mount and when page changes
useEffect(() => {
const fetchEvidence = async () => {
@@ -404,18 +783,30 @@ export default function EvidencePage() {
if (data.total !== undefined) setTotal(data.total)
const backendEvidence = data.evidence || data
if (Array.isArray(backendEvidence) && backendEvidence.length > 0) {
const mapped: SDKEvidence[] = backendEvidence.map((e: Record<string, unknown>) => ({
id: (e.id || '') as string,
controlId: (e.control_id || '') as string,
type: ((e.evidence_type || 'DOCUMENT') as string).toUpperCase() as EvidenceType,
name: (e.title || e.name || '') as string,
description: (e.description || '') as string,
fileUrl: (e.artifact_url || null) as string | null,
validFrom: e.valid_from ? new Date(e.valid_from as string) : new Date(),
validUntil: e.valid_until ? new Date(e.valid_until as string) : null,
uploadedBy: (e.uploaded_by || 'System') as string,
uploadedAt: e.created_at ? new Date(e.created_at as string) : new Date(),
}))
const metaMap: typeof antiFakeMeta = {}
const mapped: SDKEvidence[] = backendEvidence.map((e: Record<string, unknown>) => {
const id = (e.id || '') as string
metaMap[id] = {
confidenceLevel: (e.confidence_level || null) as string | null,
truthStatus: (e.truth_status || null) as string | null,
generationMode: (e.generation_mode || null) as string | null,
approvalStatus: (e.approval_status || null) as string | null,
requiresFourEyes: !!e.requires_four_eyes,
}
return {
id,
controlId: (e.control_id || '') as string,
type: ((e.evidence_type || 'DOCUMENT') as string).toUpperCase() as EvidenceType,
name: (e.title || e.name || '') as string,
description: (e.description || '') as string,
fileUrl: (e.artifact_url || null) as string | null,
validFrom: e.valid_from ? new Date(e.valid_from as string) : new Date(),
validUntil: e.valid_until ? new Date(e.valid_until as string) : null,
uploadedBy: (e.uploaded_by || 'System') as string,
uploadedAt: e.created_at ? new Date(e.created_at as string) : new Date(),
}
})
setAntiFakeMeta(metaMap)
dispatch({ type: 'SET_STATE', payload: { evidence: mapped } })
setError(null)
return
@@ -463,12 +854,13 @@ export default function EvidencePage() {
}
fetchEvidence()
}, [page, pageSize]) // eslint-disable-line react-hooks/exhaustive-deps
}, [page, pageSize, refreshKey]) // eslint-disable-line react-hooks/exhaustive-deps
// Convert SDK evidence to display evidence
const displayEvidence: DisplayEvidence[] = state.evidence.map(ev => {
const template = evidenceTemplates.find(t => t.id === ev.id)
const meta = antiFakeMeta[ev.id]
return {
id: ev.id,
name: ev.name,
@@ -485,12 +877,18 @@ export default function EvidencePage() {
status: getEvidenceStatus(ev.validUntil),
fileSize: template?.fileSize || 'Unbekannt',
fileUrl: ev.fileUrl,
confidenceLevel: meta?.confidenceLevel || null,
truthStatus: meta?.truthStatus || null,
generationMode: meta?.generationMode || null,
approvalStatus: meta?.approvalStatus || null,
requiresFourEyes: meta?.requiresFourEyes || false,
}
})
const filteredEvidence = filter === 'all'
const filteredEvidence = (filter === 'all'
? displayEvidence
: displayEvidence.filter(e => e.status === filter || e.displayType === filter)
).filter(e => !confidenceFilter || e.confidenceLevel === confidenceFilter)
const validCount = displayEvidence.filter(e => e.status === 'valid').length
const expiredCount = displayEvidence.filter(e => e.status === 'expired').length
@@ -803,6 +1201,20 @@ export default function EvidencePage() {
f === 'certificate' ? 'Zertifikate' : 'Audit-Berichte'}
</button>
))}
<span className="text-gray-300 mx-1">|</span>
{['E0', 'E1', 'E2', 'E3', 'E4'].map(level => (
<button
key={level}
onClick={() => setConfidenceFilter(confidenceFilter === level ? null : level)}
className={`px-3 py-1 text-sm rounded-full transition-colors ${
confidenceFilter === level
? confidenceFilterColors[level]
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
{level}
</button>
))}
</div>
{/* Loading State */}
@@ -818,6 +1230,9 @@ export default function EvidencePage() {
onDelete={() => handleDelete(ev.id)}
onView={() => handleView(ev)}
onDownload={() => handleDownload(ev)}
onReview={() => setReviewEvidence(ev)}
onReject={() => setRejectEvidence(ev)}
onShowHistory={() => setAuditTrailId(ev.id)}
/>
))}
</div>
@@ -1106,6 +1521,28 @@ export default function EvidencePage() {
)}
</div>
)}
{/* Phase 3 Modals */}
{reviewEvidence && (
<ReviewModal
evidence={reviewEvidence}
onClose={() => setReviewEvidence(null)}
onSuccess={() => { setReviewEvidence(null); setRefreshKey(k => k + 1) }}
/>
)}
{rejectEvidence && (
<RejectModal
evidence={rejectEvidence}
onClose={() => setRejectEvidence(null)}
onSuccess={() => { setRejectEvidence(null); setRefreshKey(k => k + 1) }}
/>
)}
{auditTrailId && (
<AuditTrailPanel
evidenceId={auditTrailId}
onClose={() => setAuditTrailId(null)}
/>
)}
</div>
)
}