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 ?? '-'}
Details
|
Scan ID
|
{scan.id}
|
| Root |
{scan.scan_root}
|
| Engine |
{scan.engine_version || '-'} |
| Started |
{fmtDate(scan.started_at)} |
| Finished |
{fmtDate(scan.finished_at)} |
{scan.error && (
| 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 (
<>
| Severity |
Rule |
File |
Line |
Confidence |
{data.findings.map((f) => (
navigate(`/findings/${f.index}`)}
>
|
{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 ? (
) : (
{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' && }
>
);
}