AuditTrace.tsx
1 // Copyright (c) 2026 VPL Solutions. All rights reserved. 2 // Licensed under the MIT License. See LICENSE for details. 3 4 import { Shield, Clock, ArrowRight } from 'lucide-react'; 5 import type { StepSummary } from '../../api/investigation'; 6 import { AGENT_ROLE_META } from '../../data/investigationStates'; 7 8 interface AuditTraceProps { 9 steps: StepSummary[]; 10 } 11 12 function formatTimestamp(ts: string): string { 13 try { 14 const d = new Date(ts); 15 return d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit' }); 16 } catch { 17 return ts; 18 } 19 } 20 21 function formatElapsed(ms: number): string { 22 if (ms < 1000) return `${ms}ms`; 23 return `${(ms / 1000).toFixed(1)}s`; 24 } 25 26 export function AuditTrace({ steps }: AuditTraceProps) { 27 if (!steps.length) { 28 return ( 29 <div className="text-center py-8 text-gray-400 dark:text-gray-500 text-sm"> 30 No audit steps recorded yet. 31 </div> 32 ); 33 } 34 35 return ( 36 <div className="relative"> 37 {/* Vertical line */} 38 <div className="absolute left-4 top-0 bottom-0 w-px bg-gray-200 dark:bg-gray-700" /> 39 40 <div className="space-y-0"> 41 {steps.map((step, i) => { 42 const roleMeta = AGENT_ROLE_META[step.agent_role] ?? AGENT_ROLE_META.system; 43 const isTransition = step.status_before !== step.status_after && step.status_after; 44 45 return ( 46 <div key={`${step.step_number}`} className="relative pl-10 pb-4"> 47 {/* Step marker */} 48 <div className="absolute left-2 w-5 h-5 rounded-full bg-white dark:bg-gray-900 border-2 border-gray-300 dark:border-gray-600 flex items-center justify-center z-10"> 49 <span className="text-[9px] font-bold text-gray-500 dark:text-gray-400">{step.step_number}</span> 50 </div> 51 52 <div className="p-3 rounded-lg bg-white dark:bg-white/[0.03] border border-gray-200 dark:border-white/10 hover:border-gray-300 dark:hover:border-white/20 transition-colors"> 53 {/* Header row */} 54 <div className="flex items-center justify-between gap-2 flex-wrap"> 55 <div className="flex items-center gap-2"> 56 <span className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold uppercase tracking-wider ${roleMeta.bgColor} ${roleMeta.color}`}> 57 {roleMeta.label} 58 </span> 59 <span className="text-sm text-gray-800 dark:text-gray-200">{step.action}</span> 60 </div> 61 <div className="flex items-center gap-3 text-xs text-gray-400 dark:text-gray-500"> 62 <span className="flex items-center gap-1"> 63 <Clock className="w-3 h-3" /> 64 {formatTimestamp(step.timestamp)} 65 </span> 66 {step.elapsed_ms > 0 && ( 67 <span>{formatElapsed(step.elapsed_ms)}</span> 68 )} 69 </div> 70 </div> 71 72 {/* State transition */} 73 {isTransition && ( 74 <div className="mt-2 flex items-center gap-2"> 75 <span className="text-xs font-mono text-gray-500 dark:text-gray-400">{step.status_before}</span> 76 <ArrowRight className="w-3 h-3 text-gray-400" /> 77 <span className="text-xs font-mono font-semibold text-gray-700 dark:text-gray-300">{step.status_after}</span> 78 </div> 79 )} 80 </div> 81 82 {/* Governance boundary marker */} 83 {i < steps.length - 1 84 && step.status_after === 'AWAITING_APPROVAL' 85 && steps[i + 1]?.status_before === 'AWAITING_APPROVAL' && ( 86 <div className="relative pl-0 py-2 ml-[-2rem]"> 87 <div className="flex items-center gap-2 pl-10"> 88 <div className="flex-1 h-px bg-amber-300 dark:bg-amber-700" /> 89 <span className="text-[10px] font-semibold uppercase tracking-wider text-amber-600 dark:text-amber-400 whitespace-nowrap px-2"> 90 No execution without approval 91 </span> 92 <div className="flex-1 h-px bg-amber-300 dark:bg-amber-700" /> 93 </div> 94 </div> 95 )} 96 </div> 97 ); 98 })} 99 </div> 100 101 {/* Trace integrity footer */} 102 <div className="mt-4 flex items-center gap-2 text-xs text-gray-400 dark:text-gray-500"> 103 <Shield className="w-3.5 h-3.5" /> 104 <span>{steps.length} steps recorded — append-only audit trail — all actions audited</span> 105 </div> 106 </div> 107 ); 108 }