import { ChangeEvent, useEffect, useState, type ReactElement } from 'react'; import { Settings } from '../../server/vault.js'; import { OtpModal } from '../components/otp-modal.js'; import { SkeletonRow } from '../components/skeleton.js'; import { TaskRow } from '../components/task-row.js'; import { getSources, loadSettings, saveSettings } from '../lib/api.js'; import type { UseRunSocketResult } from '../lib/ws.js'; import type { SourceConfig } from './config/source-types.js'; import { SOURCE_LABELS } from './config/source-types.js'; function nickname(src: SourceConfig): string { if (src.nickname) return `${SOURCE_LABELS[src.type]}: ${src.nickname}`; return `${SOURCE_LABELS[src.type]} (${src.id.slice(0, 6)})`; } type RunProps = UseRunSocketResult & { onNavigateAccounts?: () => void; isVisible?: boolean }; export function Run({ send, taskStates, runStatus, summary, dismissOtp, onNavigateAccounts, isVisible = true, }: RunProps): ReactElement { const [sources, setSources] = useState([]); const [sourcesLoading, setSourcesLoading] = useState(true); const [selected, setSelected] = useState>(new Set()); const [months, setMonths] = useState(3); const [useCustomRange, setUseCustomRange] = useState(false); const [dateFrom, setDateFrom] = useState(''); const [dateTo, setDateTo] = useState(''); const [error, setError] = useState(null); const [shouldFetchRates, setShouldFetchRates] = useState(false); useEffect(() => { loadSettings() .then(s => setShouldFetchRates(s.fetchBankOfIsraelRates)) .catch(() => setError('Failed to load settings')); }, []); useEffect(() => { if (!isVisible || selected.size > 0) return; setSourcesLoading(true); getSources() .then(srcs => { setSources( srcs.sort( (a, b) => SOURCE_LABELS[a.type].localeCompare(SOURCE_LABELS[b.type]) || (a.nickname || a.id).localeCompare(b.nickname || b.id), ), ); setSelected(new Set(srcs.map(s => s.id))); }) .finally(() => setSourcesLoading(false)); }, [isVisible]); function toggleSource(id: string) { setSelected(prev => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); } async function autoSave(patch: Partial) { try { const updated = await saveSettings(patch); setShouldFetchRates(updated.fetchBankOfIsraelRates); } catch { setError('Failed to toggle currency rates setting'); } } const handleToggleRates = (e: ChangeEvent) => { const value = e.target.checked; setShouldFetchRates(value); void autoSave({ fetchBankOfIsraelRates: value }); }; function handleRun() { const sourceIds = [...selected]; const msg = { type: 'run-start' as const, sourceIds, dateFrom: useCustomRange ? dateFrom || undefined : new Date(new Date().getFullYear(), new Date().getMonth() - months, new Date().getDate()) .toISOString() .split('T')[0], dateTo: useCustomRange ? dateTo || undefined : undefined, }; send(msg); } // Find any task waiting for OTP const otpEntry = [...taskStates.entries()].find(([, s]) => s.status === 'otp-required'); return (

Run Scrapers

{error && (

{error}

)} {/* Source checklist */}

Sources

{sourcesLoading ? (
) : sources.length === 0 ? (

No sources configured.

) : (
{sources.map(src => ( ))}
)}
{/* Date range */}

Date Range

{useCustomRange ? (
) : ( )}
{/* Run button */} {/* Task list */} {taskStates.size > 0 && (

Tasks

{[...taskStates.entries()].map(([sourceId, state]) => { const src = sources.find(s => s.id === sourceId); return ( ); })}
)} {/* Summary panel */} {runStatus === 'complete' && summary && (

Run Complete

↑ {summary.totalInserted} new ↷ {summary.totalSkipped} skipped {summary.errors > 0 && ( ✕ {summary.errors} errors )}
)} {/* OTP modal */} {otpEntry && ( { send(msg); dismissOtp(otpEntry[0]); }} onClose={() => dismissOtp(otpEntry[0])} /> )}
); }