import { useCallback, useEffect, useState, type ReactElement } from 'react'; import type { RunRecord, SourceRunRecord } from '../../shared/types.js'; import { SkeletonTable } from '../components/skeleton.js'; import { getHistory } from '../lib/api.js'; function formatDateTime(iso: string): string { return new Date(iso).toLocaleString(); } export function formatDuration(startedAt: string, completedAt: string): string { const ms = new Date(completedAt).getTime() - new Date(startedAt).getTime(); const totalSeconds = Math.max(0, Math.floor(ms / 1000)); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return `${minutes}m ${seconds}s`; } const SOURCE_STATUS_STYLE: Record = { done: { background: '#dcfce7', color: '#15803d' }, error: { background: '#fee2e2', color: '#b91c1c' }, blocked: { background: '#fef9c3', color: '#854d0e' }, }; const SOURCE_STATUS_LABEL: Record = { done: 'done', error: 'error', blocked: 'blocked', }; function SourceRow({ src }: { src: SourceRunRecord }): ReactElement { const style = SOURCE_STATUS_STYLE[src.status] ?? SOURCE_STATUS_STYLE['done']!; const [expanded, setExpanded] = useState(false); return ( <> {src.nickname || src.sourceId} {src.sourceType} {SOURCE_STATUS_LABEL[src.status] ?? src.status} {src.inserted} {src.skipped} {src.status === 'blocked' ? ( ) : ( (src.error ?? '—') )} {expanded && src.blockedAccounts && src.blockedAccounts.length > 0 && ( {src.blockedAccounts.join(', ')} )} ); } function RunRow({ record }: { record: RunRecord }): ReactElement { const [expanded, setExpanded] = useState(false); return ( <> setExpanded(e => !e)} onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setExpanded(prev => !prev); } }} tabIndex={0} role="button" style={{ cursor: 'pointer', background: expanded ? '#f9fafb' : undefined }} aria-expanded={expanded} > {formatDateTime(record.startedAt)} {formatDuration(record.startedAt, record.completedAt)} {record.sources.length} {record.totalInserted} {record.totalSkipped} 0 ? '#b91c1c' : undefined, fontWeight: record.errorCount > 0 ? 600 : undefined, }} > {record.errorCount} {expanded ? '▲' : '▼'} {expanded && record.sources.length > 0 && ( {record.sources.map(src => ( ))}
Source Type Status New Skipped Error
)} {expanded && record.sources.length === 0 && ( No per-source breakdown available. )} ); } export function History(): ReactElement { const [records, setRecords] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const load = useCallback(() => { setLoading(true); setError(null); getHistory() .then(setRecords) .catch(err => setError(err instanceof Error ? err.message : String(err))) .finally(() => setLoading(false)); }, []); useEffect(() => { load(); }, [load]); return (

Scrape History

{loading ? ( ) : error ? (

{error}

) : records.length === 0 ? (

No scrape runs recorded yet.

) : ( {records.map(r => ( ))}
Date / Time Duration Sources New Skipped Errors
)}
); }