// Initial-sync progress UX (SPEC §12 M2). Shows a small floating card while any // account is importing its history; reads the accounts query, which the WS // `sync.progress` event invalidates so the bar advances live. import { useQuery } from "@tanstack/react-query"; import { useEffect, useState } from "react"; import { useT } from "../i18n"; import { api } from "../lib/api"; export function BackfillBanner() { const t = useT(); const accounts = useQuery({ queryKey: ["accounts"], queryFn: api.accounts }); const importing = (accounts.data ?? []).filter( // Show while backfilling — including the brief start window before the worker has // counted the total (total === 0). Hide only once done, so a completed account // whose state event was missed (e.g. a dropped WS) never leaves a stuck banner. (a) => a.state === "backfilling" && (a.backfill_total === 0 || a.backfill_done < a.backfill_total), ); const active = importing.length > 0; // Dissolve the card 10s after it appears; the sidebar ring keeps showing // progress until the sync finishes. const [dismissed, setDismissed] = useState(false); useEffect(() => { if (!active) { setDismissed(false); return; } const timer = window.setTimeout(() => setDismissed(true), 10000); return () => window.clearTimeout(timer); }, [active]); if (!active) return null; return (
{importing.map((a) => { // total === 0: the worker hasn't counted yet — show an indeterminate bar. const preparing = a.backfill_total === 0; const done = Math.min(a.backfill_done, a.backfill_total); const pct = preparing ? 0 : Math.round((done / a.backfill_total) * 100); return (
{a.email}
{preparing ? t("backfill.preparing") : t("backfill.importing", { done: done.toLocaleString(), total: a.backfill_total.toLocaleString(), })}
{preparing ? (
) : (
)}
); })}
); }