import { useState, useMemo } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useScan, useScans, useScanFindings, useScanLogs, useScanMetrics, } from '../api/queries/scans'; import { LoadingState } from '../components/ui/LoadingState'; import { ErrorState } from '../components/ui/ErrorState'; import { usePageTitle } from '../hooks/usePageTitle'; import type { ScanView, ScanLogEntry, ScanMetricsSnapshot } from '../api/types'; function truncPath(p?: string, max = 50): string { if (!p) return ''; if (p.length <= max) return p; return '...' + p.slice(p.length - max + 3); } function fmtDate(iso?: string): string { return iso ? new Date(iso).toLocaleString() : '-'; } function fmtNum(n?: number | null): string { return n != null ? n.toLocaleString() : '-'; } // ── Summary Tab ────────────────────────────────────────────────────────────── function SummaryTab({ scan }: { scan: ScanView }) { const duration = scan.duration_secs != null ? scan.duration_secs.toFixed(2) + 's' : '-'; const langs = (scan.languages || []).join(', ') || '-'; const timing = scan.timing; let total = 0; if (timing) { total = timing.walk_ms + timing.pass1_ms + timing.call_graph_ms + timing.pass2_ms + timing.post_process_ms; } const pct = (ms: number) => ((ms / total) * 100).toFixed(1); return ( <>
Files Scanned
{scan.files_scanned ?? '-'}
Findings
{scan.finding_count ?? '-'}
Duration
{duration}
Languages
{langs}
Details
{scan.error && ( )}
Scan ID {scan.id}
Root {scan.scan_root}
Engine {scan.engine_version || '-'}
Started {fmtDate(scan.started_at)}
Finished {fmtDate(scan.finished_at)}
Error {scan.error}
{timing && total > 0 && (
Timing Breakdown
{' '} Walk {timing.walk_ms}ms {' '} Pass 1 {timing.pass1_ms}ms {' '} Call Graph {timing.call_graph_ms}ms {' '} Pass 2 {timing.pass2_ms}ms {' '} Post {timing.post_process_ms}ms
)} ); } // ── Findings Tab ───────────────────────────────────────────────────────────── function FindingsTab({ scanId }: { scanId: string }) { const navigate = useNavigate(); const { data, isLoading, error } = useScanFindings(scanId); if (isLoading) return ; if (error) return ; if (!data?.findings || data.findings.length === 0) { return (

No findings

This scan produced no findings.

); } return ( <>
{data.findings.map((f) => ( navigate(`/findings/${f.index}`)} > ))}
Severity Rule File Line Confidence
{f.severity} {f.rule_id} {truncPath(f.path)} {f.line} {f.confidence ? ( {f.confidence} ) : ( '-' )}
Showing {data.findings.length} of {data.total} findings
); } // ── Logs Tab ───────────────────────────────────────────────────────────────── function LogsTab({ scanId }: { scanId: string }) { const [levelFilter, setLevelFilter] = useState(undefined); const { data: logs, isLoading, error } = useScanLogs(scanId, levelFilter); if (isLoading) return ; if (error) return ; const levels: Array<{ value: string | undefined; label: string }> = [ { value: undefined, label: 'All' }, { value: 'info', label: 'Info' }, { value: 'warn', label: 'Warn' }, { value: 'error', label: 'Error' }, ]; return ( <>
{levels.map((l) => ( ))}
{!logs || logs.length === 0 ? (

No log entries

) : (
{logs.map((l: ScanLogEntry, i: number) => (
{l.level} {new Date(l.timestamp).toLocaleTimeString()} {l.message} {l.file_path && ( {' '} {l.file_path} )}
))}
)} ); } // ── Metrics Tab ────────────────────────────────────────────────────────────── function MetricsTab({ scanId, scan }: { scanId: string; scan: ScanView }) { const { data: fetchedMetrics } = useScanMetrics(scanId); const metrics: ScanMetricsSnapshot | undefined = scan.metrics || fetchedMetrics || undefined; if (!metrics) { return (

No metrics available for this scan.

); } return (
CFG Nodes
{fmtNum(metrics.cfg_nodes)}
Call Edges
{fmtNum(metrics.call_edges)}
Functions Analyzed
{fmtNum(metrics.functions_analyzed)}
Summaries Reused
{fmtNum(metrics.summaries_reused)}
Unresolved Calls
{fmtNum(metrics.unresolved_calls)}
); } // ── Scan Detail Page ───────────────────────────────────────────────────────── type TabId = 'summary' | 'findings' | 'logs' | 'metrics'; export function ScanDetailPage() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const { data: scan, isLoading, error } = useScan(id || ''); const { data: allScans } = useScans(); const [activeTab, setActiveTab] = useState('summary'); usePageTitle(scan ? `Scan ${scan.id.slice(0, 8)}` : 'Scan'); const prevScanId = useMemo(() => { if (!scan || scan.status !== 'completed' || !allScans) return null; const completed = allScans .filter((s) => s.status === 'completed' && s.started_at) .sort((a, b) => (a.started_at || '').localeCompare(b.started_at || '')); const myIdx = completed.findIndex((s) => s.id === id); if (myIdx > 0) return completed[myIdx - 1].id; return null; }, [scan, allScans, id]); if (isLoading) return ; if (error || !scan) { return ( ); } const tabs: { id: TabId; label: string }[] = [ { id: 'summary', label: 'Summary' }, { id: 'findings', label: 'Findings' }, { id: 'logs', label: 'Logs' }, { id: 'metrics', label: 'Metrics' }, ]; return ( <>
{prevScanId && ( )}

Scan Detail

{scan.status}
{tabs.map((tab) => ( ))}
{activeTab === 'summary' && } {activeTab === 'findings' && } {activeTab === 'logs' && } {activeTab === 'metrics' && }
); }