/** * BatchMonitor — client component that polls /api/admin/batch every 30s * and renders progress bar + stats + recent docs. */ "use client"; import { useEffect, useState } from "react"; interface Stats { total_cost_usd: number; total_chunks: number; total_images: number; total_pages_processed: number; avg_seconds_per_doc: number | null; avg_chunks_per_doc: number | null; throughput_docs_per_hour: number | null; eta_minutes: number | null; } interface RecentDoc { doc_id: string; pages?: number; chunks?: number; cost_usd?: number | null; wall_s?: number; success?: boolean; finished_at?: string; } interface BatchPayload { status: string; queue_total: number; completed: number; successes: number; failures: number; progress_pct: number; quota_state?: "ok" | "throttled"; quota_resume_eta_minutes?: number | null; latest_quota_at?: string | null; stats: Stats | null; recent_docs?: RecentDoc[]; failed_docs?: Array<{ doc_id: string; timed_out?: boolean; returncode?: number }>; } export function BatchMonitor() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [lastFetched, setLastFetched] = useState(0); useEffect(() => { let alive = true; async function tick() { try { const r = await fetch("/api/admin/batch"); if (!r.ok) return; const j = (await r.json()) as BatchPayload; if (alive) { setData(j); setLastFetched(Date.now()); setLoading(false); } } catch { if (alive) setLoading(false); } } tick(); const interval = setInterval(tick, 30_000); return () => { alive = false; clearInterval(interval); }; }, []); if (loading && !data) { return (
carregando progresso…
); } if (!data || data.status === "no_log") { return (

no progress.jsonl found

run python3 scripts/28-batch-rebuild-all.py to start a rebuild

); } const stats = data.stats; const eta = stats?.eta_minutes; return (
{/* Quota throttle banner */} {data.quota_state === "throttled" && (
💸
Anthropic quota throttled — janela rolling 5h
último 429:{" "} {data.latest_quota_at ? new Date(data.latest_quota_at).toLocaleString("pt-BR") : "?"} · reset estimado em{" "} {data.quota_resume_eta_minutes && data.quota_resume_eta_minutes >= 60 ? `${(data.quota_resume_eta_minutes / 60).toFixed(1)}h` : `${data.quota_resume_eta_minutes ?? "?"}min`}
python3 scripts/28-batch-rebuild-all.py --workers 2{" "} quando o reset acontecer (script é idempotente, skipa docs prontos)
)} {/* Progress bar */}

{data.completed} of {data.queue_total} docs · {data.progress_pct}%

atualizado {Math.round((Date.now() - lastFetched) / 1000)}s atrás
✓ {data.successes} ✗ {data.failures} {eta != null && eta > 0 ? `ETA ~${eta < 60 ? `${eta}min` : `${(eta / 60).toFixed(1)}h`}` : "—"}
{/* Stats grid */} {stats && (
)} {/* Recent docs */}

últimos 20 documentos

    {(data.recent_docs ?? []).map((d, i) => (
  • {d.success ? "✓" : "✗"} {d.doc_id} {d.pages ?? 0} pg {d.chunks ?? 0} ch ${(d.cost_usd ?? 0).toFixed(2)} {d.wall_s ?? 0}s
  • ))}
{/* Failures */} {(data.failed_docs?.length ?? 0) > 0 && (

falhas — {data.failed_docs?.length ?? 0}

    {(data.failed_docs ?? []).map((d, i) => (
  • {d.doc_id} {d.timed_out && timeout} {!d.timed_out && d.returncode != null && ( rc={d.returncode} )}
  • ))}
)}
); } function Stat({ label, value, accent }: { label: string; value: string; accent?: string }) { return (
{label}
{value}
); }