import { useEffect, useRef, useState } from 'react' import type { DrugIngestStatus, DrugDownloadStatus, DrugIngestPhaseStatus, } from '../../../types/drug_reference' interface Props { status: DrugIngestStatus /** Called to refresh status — typically polls /api/drug-reference/status */ onRefresh?: () => void /** Poll interval in ms while running. Default 3000. */ pollIntervalMs?: number } /** Format a millisecond duration as "1h 04m", "6m 12s", or "12s". */ function fmtDuration(ms: number): string { if (!isFinite(ms) || ms < 0) return '—' const totalSec = Math.floor(ms / 1000) const h = Math.floor(totalSec / 3600) const m = Math.floor((totalSec % 3600) / 60) const sec = totalSec % 60 if (h > 0) return `${h}h ${String(m).padStart(2, '0')}m` if (m > 0) return `${m}m ${String(sec).padStart(2, '0')}s` return `${sec}s` } /** Format a byte count as "128 MB" / "1.7 GB". */ function fmtBytes(bytes: number): string { if (!isFinite(bytes) || bytes <= 0) return '' const mb = bytes / (1024 * 1024) if (mb >= 1024) return `${(mb / 1024).toFixed(1)} GB` return `${Math.round(mb)} MB` } /** One phase block: headline, explainer, progress bar, optional timing row. */ function PhaseBlock({ title, state, pct, explainer, accentRunning, counterLeft, timing, failedReason, }: { title: string state: 'idle' | 'running' | 'completed' | 'failed' pct: number explainer: string accentRunning: string counterLeft?: React.ReactNode timing?: React.ReactNode failedReason?: string }) { const running = state === 'running' const completed = state === 'completed' const failed = state === 'failed' const accent = failed ? 'text-red-700' : completed ? 'text-green-700' : running ? accentRunning : 'text-text-secondary' const barColor = failed ? 'bg-red-500' : completed ? 'bg-green-500' : running ? accentRunning.replace('text-', 'bg-') : 'bg-surface-elevated' return (
{title} {running ? ' …' : ''} {(running || completed) && ( {pct}% )}

{explainer}

{counterLeft && (
{counterLeft}
)} {(running || completed || failed) && (
)} {timing} {failed && failedReason && (

{failedReason}

)}
) } /** Download-phase explainer copy. */ function downloadExplainer(d: DrugDownloadStatus): string { if (d.state === 'failed') { return 'The download failed — it retries automatically; if it stays failed, press "Download FDA data" to restart. Finished parts are kept and resume where they left off.' } if (d.state === 'completed') { return 'All parts are on disk. Indexing them into search next.' } if (d.state === 'running') { return d.totalParts > 0 ? `Pulling part ${d.partsDone + 1} of ${d.totalParts} — ~1.7 GB total across all parts.` : 'Reading the openFDA download manifest…' } return 'Not started.' } /** Ingest-phase explainer copy. */ function ingestExplainer(i: DrugIngestPhaseStatus, rowCount: number): string { if (i.state === 'failed') { return 'Indexing failed — press "Ingest into search" to retry from the downloaded files (no re-download). Already-indexed labels are kept (the refresh is idempotent).' } if (i.state === 'completed') { return `${rowCount.toLocaleString()} labels are now searchable offline.` } if (i.state === 'running') { return i.totalParts > 0 ? `Writing part ${i.partsDone + 1} of ${i.totalParts} into the offline database.` : 'Writing labels into the offline database.' } return 'Waiting for downloaded data.' } /** * Two-phase status panel for the Drug Reference download + ingest. * * Renders a download block (parts/bytes progress) and an ingest block (records * "X of ~Nk labels" counter). Each block scopes its own failed/stalled copy. * The top-level status.phase drives elapsed/ETA placement on the active phase. * Auto-polls /status while either phase is running and ticks the clock between * polls. */ export default function IngestStatus({ status, onRefresh, pollIntervalMs = 3000 }: Props) { const pollRef = useRef | null>(null) const [now, setNow] = useState(() => Date.now()) const busy = status.phase === 'downloading' || status.phase === 'ingesting' // Poll status while a phase is running. useEffect(() => { if (busy && onRefresh) { pollRef.current = setInterval(onRefresh, pollIntervalMs) } else if (pollRef.current) { clearInterval(pollRef.current) } return () => { if (pollRef.current) clearInterval(pollRef.current) } }, [busy, onRefresh, pollIntervalMs]) // Tick a local clock every second while busy so elapsed/ETA advance smoothly. useEffect(() => { if (!busy) return const id = setInterval(() => setNow(Date.now()), 1000) return () => clearInterval(id) }, [busy]) const { download, ingest } = status // ── Download bar ───────────────────────────────────────────────────────── const dlPct = download.state === 'completed' ? 100 : download.totalParts > 0 ? Math.min(100, Math.round((download.partsDone / download.totalParts) * 100)) : 0 const dlBytes = download.bytesDownloaded ? fmtBytes(download.bytesDownloaded) : '' // ── Ingest bar (records-based) ─────────────────────────────────────────── const expected = ingest.expectedTotal > 0 ? ingest.expectedTotal : 0 const ingPct = ingest.state === 'completed' ? 100 : expected > 0 ? Math.min(100, Math.round((ingest.records / expected) * 100)) : ingest.totalParts > 0 ? Math.min(100, Math.round((ingest.partsDone / ingest.totalParts) * 100)) : 0 // ── Timing (on the active phase only) ──────────────────────────────────── const elapsedMs = status.startedAtMs ? Math.max(0, now - status.startedAtMs) : null const etaMs = status.phase === 'ingesting' && elapsedMs !== null && expected > 0 && ingest.records > 2000 ? (elapsedMs * (expected - ingest.records)) / ingest.records : null const downloadTiming = status.phase === 'downloading' && elapsedMs !== null ? (
Elapsed {fmtDuration(elapsedMs)} {dlBytes && {dlBytes} this part}
) : null const ingestTiming = status.phase === 'ingesting' && elapsedMs !== null ? (
Elapsed {fmtDuration(elapsedMs)} {etaMs !== null && ( ~{fmtDuration(etaMs)} left (estimate) )}
) : null return (
{/* Download phase */} {/* Ingest phase */} {ingest.records.toLocaleString()} {expected > 0 && ( of ~{expected.toLocaleString()} labels )} ) : undefined } timing={ingestTiming} failedReason={ingest.failedReason} /> {/* Reassurance while busy */} {busy && (

Runs in the background — you can leave this page and it keeps going. Search turns on automatically when ingest finishes.

)} {/* Ready footer */} {status.phase === 'ready' && status.lastUpdated && (

FDA data version {status.lastUpdated}.

)}
) }