/ src / components / ops / AuditTrace.tsx
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  }