"use client"; import { useEffect, useState } from "react"; import { Loader2, CheckCircle2, XCircle, CloudOff } from "lucide-react"; import { cn } from "@/lib/utils"; import type { IngestionAccountStatus } from "@/lib/telegram/types"; interface IngestionStatusProps { initialStatus: IngestionAccountStatus[]; } /** * Polls /api/ingestion/status every 3 seconds while a run is active, * or every 30 seconds when idle. Shows a compact status banner with * a spinning throbber when ingestion is running. */ export function IngestionStatus({ initialStatus }: IngestionStatusProps) { const [accounts, setAccounts] = useState(initialStatus); const [error, setError] = useState(false); // Determine if any account is currently running const activeRun = accounts.find((a) => a.currentRun); const isRunning = !!activeRun; useEffect(() => { let timer: ReturnType; let mounted = true; const poll = async () => { try { const res = await fetch("/api/ingestion/status"); if (!res.ok) throw new Error("fetch failed"); const data = await res.json(); if (mounted) { setAccounts(data.accounts ?? []); setError(false); } } catch { if (mounted) setError(true); } if (mounted) { // Poll fast while running, slow when idle const interval = accounts.some((a) => a.currentRun) ? 3_000 : 30_000; timer = setTimeout(poll, interval); } }; // Start polling after a short delay to avoid double-fetching on mount timer = setTimeout(poll, 3_000); return () => { mounted = false; clearTimeout(timer); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [isRunning]); // Nothing to show if no accounts configured if (accounts.length === 0 && !error) return null; // If we can't reach the API, show a muted offline badge if (error) { return (
Sync status unavailable
); } // Active run — show throbber with live activity if (activeRun?.currentRun) { const run = activeRun.currentRun; return (

{run.currentActivity ?? "Syncing..."}

{run.downloadPercent != null && run.downloadPercent > 0 && (
{run.downloadPercent}%
)}
{run.totalFiles != null && run.currentFileNum != null && ( {run.currentFileNum}/{run.totalFiles} )}
); } // All idle — show last run summary const lastCompleted = accounts .filter((a) => a.lastRun) .sort( (a, b) => new Date(b.lastRun!.finishedAt ?? b.lastRun!.startedAt).getTime() - new Date(a.lastRun!.finishedAt ?? a.lastRun!.startedAt).getTime() )[0]; if (!lastCompleted?.lastRun) return null; const last = lastCompleted.lastRun; const isFailed = last.status === "FAILED"; const timeAgo = getTimeAgo(last.finishedAt ?? last.startedAt); return (
{isFailed ? ( ) : ( )} {isFailed ? `Last sync failed ${timeAgo}` : `Last sync ${timeAgo} — ${last.zipsIngested} new, ${last.zipsDuplicate} skipped`}
); } function getTimeAgo(dateStr: string): string { const diff = Date.now() - new Date(dateStr).getTime(); const mins = Math.floor(diff / 60_000); if (mins < 1) return "just now"; if (mins < 60) return `${mins}m ago`; const hours = Math.floor(mins / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); return `${days}d ago`; }