import { useState, useMemo, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { useScans } from '../api/queries/scans'; import { useDeleteScan } from '../api/mutations/scans'; import { useSSE } from '../contexts/SSEContext'; import { LoadingState } from '../components/ui/LoadingState'; import { ErrorState } from '../components/ui/ErrorState'; import { usePageTitle } from '../hooks/usePageTitle'; import type { ScanView } from '../api/types'; function relTime(iso?: string): string { if (!iso) return '-'; const d = new Date(iso); const diff = Date.now() - d.getTime(); if (diff < 60000) return 'just now'; if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`; if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`; return d.toLocaleDateString(); } function truncPath(p?: string, max = 50): string { if (!p) return ''; if (p.length <= max) return p; return '...' + p.slice(p.length - max + 3); } function ScanProgress({ data, }: { data: NonNullable['scanProgress']>; }) { const stages = [ 'discovering', 'indexing', 'loading_summaries', 'building_call_graph', 'analyzing', 'post_processing', 'complete', ] as const; const stageLabels: Record = { discovering: 'Discovering', indexing: 'Indexing', loading_summaries: 'Loading Summaries', building_call_graph: 'Call Graph', analyzing: 'Analyzing', post_processing: 'Post-Process', complete: 'Complete', }; const currentIdx = stages.indexOf(data.stage as (typeof stages)[number]); const total = data.files_discovered || 1; const processed = data.stage === 'indexing' ? data.files_parsed : data.stage === 'analyzing' || data.stage === 'post_processing' ? data.files_analyzed : data.stage === 'complete' ? total : 0; const pct = Math.min(100, (processed / total) * 100); const elapsed = data.elapsed_ms ? (data.elapsed_ms / 1000).toFixed(1) + 's' : '-'; return (

Scan in Progress

{elapsed} elapsed
{stages.map((s, i) => { const cls = i < currentIdx ? 'done' : i === currentIdx ? 'active' : ''; return (
{stageLabels[s]}
); })}
{processed} / {data.files_discovered || 0} files {pct.toFixed(0)}%
{data.files_parsed || 0} indexed {data.files_skipped || 0} reused {data.files_analyzed || 0} analyzed
{data.batches_total > 0 && (
Batch {Math.min(data.batches_completed, data.batches_total)} /{' '} {data.batches_total} {stageLabels[data.stage] || data.stage}
)}
Walk {data.timing.walk_ms}ms Index {data.timing.pass1_ms}ms Graph {data.timing.call_graph_ms}ms Analyze {data.timing.pass2_ms}ms
{data.current_file && (
{truncPath(data.current_file, 80)}
)}
); } export function ScansPage() { usePageTitle('Scans'); const navigate = useNavigate(); const { data: scans, isLoading, error } = useScans(); const deleteScan = useDeleteScan(); const { scanProgress, isScanRunning } = useSSE(); const [selectedScans, setSelectedScans] = useState>(new Set()); const completedScans = useMemo( () => (scans || []).filter((s) => s.status === 'completed'), [scans], ); const runningScans = useMemo( () => (scans || []).filter((s) => s.status === 'running'), [scans], ); const handleCheckbox = useCallback((e: React.MouseEvent, scanId: string) => { e.stopPropagation(); setSelectedScans((prev) => { const next = new Set(prev); if (next.has(scanId)) { next.delete(scanId); } else { if (next.size >= 2) return prev; next.add(scanId); } return next; }); }, []); const handleCompare = useCallback(() => { if (selectedScans.size !== 2) return; const ids = [...selectedScans]; // Sort by started_at so left=older, right=newer const scanMap = new Map((scans || []).map((s) => [s.id, s])); ids.sort((a, b) => (scanMap.get(a)?.started_at || '').localeCompare( scanMap.get(b)?.started_at || '', ), ); navigate(`/scans/compare/${ids[0]}/${ids[1]}`); }, [selectedScans, scans, navigate]); if (isLoading) return ; if (error) return ; const showCheckboxes = completedScans.length >= 2; return (
{(runningScans.length > 0 || isScanRunning) && scanProgress && ( )} {selectedScans.size > 0 && (
{selectedScans.size === 2 ? '2 scans selected' : `Select ${2 - selectedScans.size} more completed scan${selectedScans.size === 0 ? 's' : ''}`}
)} {!scans || scans.length === 0 ? (

No scans yet

Use the "Start Scan" button in the header to start your first scan.

) : (
{showCheckboxes && } {scans.map((s: ScanView) => { const languages = s.languages || []; const visibleLanguages = languages.slice(0, 4); const hiddenLanguageCount = languages.length - visibleLanguages.length; return ( navigate(`/scans/${s.id}`)} > {showCheckboxes && ( )} ); })}
Status Root Duration Findings Languages Started
{s.status === 'completed' && ( handleCheckbox(e, s.id)} onChange={() => {}} /> )} {s.status} {truncPath(s.scan_root)} {s.duration_secs != null ? s.duration_secs.toFixed(2) + 's' : '-'} {s.finding_count ?? '-'} {languages.length > 0 ? ( {visibleLanguages.map((l) => ( {l} ))} {hiddenLanguageCount > 0 && ( +{hiddenLanguageCount} )} ) : ( '-' )} {relTime(s.started_at)} {s.status !== 'running' && ( )}
)}
); }