email: live backfill progress on Email Capture panel — v0.1.0:61

The first Gmail backfill leaves the account at "pending · never synced" until it
fully completes (the sync_runs row only finalizes at the end), so there was no
feedback. /api/email/status now also returns captured_emails (total, which climbs
page-by-page during backfill), the latest sync run, and a backfilling flag. The
panel shows a "Backfilling… N captured so far" banner + an Emails Captured count
and auto-refreshes every 5s while a backfill is in progress. Verified live in
preview with seeded data (count auto-climbed 37 -> 50 without manual refresh).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Keysat
2026-06-06 12:29:01 -05:00
parent 1850bc4431
commit 2cb476e36b
5 changed files with 60 additions and 12 deletions
+24 -8
View File
@@ -9964,7 +9964,7 @@
} catch (err) {
// Integration off on the server -> render the informative disabled state, not an error.
if (err?.status === 503 || /disabl/i.test(err?.payload?.error || '')) {
setStatus({ enabled: false, accounts_summary: {}, matched_emails: 0 });
setStatus({ enabled: false, accounts_summary: {}, matched_emails: 0, captured_emails: 0, backfilling: false, last_run: null });
setAccounts([]);
return;
}
@@ -9992,6 +9992,16 @@
return () => { cancelled = true; };
}, [load]);
// While a backfill is in progress the captured-email count climbs page by page,
// so poll the status to show live progress without a manual refresh.
const backfilling = !!status && (status.backfilling || status.last_run?.status === 'running' || status.running);
useEffect(() => {
if (!backfilling) return undefined;
let cancelled = false;
const iv = setInterval(async () => { try { if (!cancelled) await load(); } catch (_) { /* transient */ } }, 5000);
return () => { cancelled = true; clearInterval(iv); };
}, [backfilling, load]);
const runAction = async (key, endpoint, successMsg, confirmMsg, body) => {
if (busy) return;
if (confirmMsg && !window.confirm(confirmMsg)) return;
@@ -10018,8 +10028,11 @@
const nAccounts = sum.n_accounts || 0;
const nError = sum.n_error || 0;
const capturing = enabled && nAccounts > 0;
const lastSync = status.last_run_unix ? formatDate(new Date(status.last_run_unix * 1000).toISOString()) : '—';
const statusWord = capturing ? 'Capturing' : enabled ? 'Not enrolled' : 'Disabled';
const captured = status.captured_emails ?? 0;
const lastRun = status.last_run;
const lastSync = lastRun ? formatDate(lastRun.finished_at || lastRun.started_at)
: (status.last_run_unix ? formatDate(new Date(status.last_run_unix * 1000).toISOString()) : '—');
const statusWord = backfilling ? 'Backfilling' : capturing ? 'Capturing' : enabled ? 'Not enrolled' : 'Disabled';
return (
<div className="page-container">
@@ -10031,10 +10044,12 @@
<span className="index-job-pill idle"><span className="index-job-dot" /> Integration disabled — no Gmail service-account key on the server.</span>
) : nAccounts === 0 ? (
<span className="index-job-pill idle"><span className="index-job-dot" /> Enabled, but no mailbox is enrolled yet, so nothing is being captured.</span>
) : backfilling ? (
<span className="index-job-pill running"><span className="index-job-dot" /> Backfilling history… {captured} emails captured so far ({status.matched_emails || 0} matched to investors). Updates live; first backfill can take a while.</span>
) : nError ? (
<span className="index-job-pill running"><span className="index-job-dot" /> {nAccounts} mailbox(es), {nError} in error — see below.</span>
) : (
<span className="index-job-pill"><span className="index-job-dot" /> Capturing: {nAccounts} mailbox(es), {status.matched_emails || 0} emails matched to investors.</span>
<span className="index-job-pill"><span className="index-job-dot" /> Capturing: {nAccounts} mailbox(es), {captured} emails captured, {status.matched_emails || 0} matched to investors.</span>
)}
</div>
</div>
@@ -10050,13 +10065,14 @@
{nError ? <div className="kpi-subtitle">{nError} error</div> : null}
</div>
<div className="kpi-card">
<div className="kpi-label">Emails matched to investors</div>
<div className="kpi-value">{status.matched_emails ?? 0}</div>
<div className="kpi-label">Emails captured</div>
<div className="kpi-value">{captured}{backfilling ? '…' : ''}</div>
<div className="kpi-subtitle">{status.matched_emails ?? 0} matched to investors</div>
</div>
<div className="kpi-card">
<div className="kpi-label">Last sync</div>
<div className="kpi-value" style={{ fontSize: '16px' }}>{lastSync}</div>
<div className="kpi-subtitle">{status.interval_sec ? `every ${Math.round(status.interval_sec / 60)} min` : ''}</div>
<div className="kpi-value" style={{ fontSize: '16px' }}>{backfilling ? 'in progress' : lastSync}</div>
<div className="kpi-subtitle">{lastRun?.status ? `last run: ${lastRun.status}` : (status.interval_sec ? `every ${Math.round(status.interval_sec / 60)} min` : '')}</div>
</div>
</div>