diff --git a/backend/email_integration/routes.py b/backend/email_integration/routes.py index b089e16..5d3234f 100644 --- a/backend/email_integration/routes.py +++ b/backend/email_integration/routes.py @@ -131,6 +131,18 @@ def _h_status(handler): counts = dict(cur.fetchone() or {}) cur.execute("SELECT COUNT(*) AS n FROM emails WHERE match_status = 'matched'") snap["matched_emails"] = cur.fetchone()["n"] + # Total captured climbs page-by-page during backfill (the sync_runs row only + # finalizes at the end), so it is the live progress signal. + cur.execute("SELECT COUNT(*) AS n FROM emails") + snap["captured_emails"] = cur.fetchone()["n"] + # Latest sync run (status running|ok|error|partial) for a human-readable line. + cur.execute("SELECT kind, status, started_at, finished_at, messages_seen, messages_stored " + "FROM email_sync_runs ORDER BY started_at DESC LIMIT 1") + lr = cur.fetchone() + snap["last_run"] = dict(lr) if lr else None + # An enrolled account whose backfill has not completed is still pulling history. + cur.execute("SELECT COUNT(*) AS n FROM email_accounts WHERE sync_enabled = 1 AND backfill_complete = 0") + snap["backfilling"] = (cur.fetchone()["n"] or 0) > 0 finally: conn.close() snap["accounts_summary"] = counts diff --git a/frontend/index.html b/frontend/index.html index d18c9b4..841d095 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -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 (
@@ -10031,10 +10044,12 @@ Integration disabled — no Gmail service-account key on the server. ) : nAccounts === 0 ? ( Enabled, but no mailbox is enrolled yet, so nothing is being captured. + ) : backfilling ? ( + Backfilling history… {captured} emails captured so far ({status.matched_emails || 0} matched to investors). Updates live; first backfill can take a while. ) : nError ? ( {nAccounts} mailbox(es), {nError} in error — see below. ) : ( - Capturing: {nAccounts} mailbox(es), {status.matched_emails || 0} emails matched to investors. + Capturing: {nAccounts} mailbox(es), {captured} emails captured, {status.matched_emails || 0} matched to investors. )}
@@ -10050,13 +10065,14 @@ {nError ?
{nError} error
: null}
-
Emails matched to investors
-
{status.matched_emails ?? 0}
+
Emails captured
+
{captured}{backfilling ? '…' : ''}
+
{status.matched_emails ?? 0} matched to investors
Last sync
-
{lastSync}
-
{status.interval_sec ? `every ${Math.round(status.interval_sec / 60)} min` : ''}
+
{backfilling ? 'in progress' : lastSync}
+
{lastRun?.status ? `last run: ${lastRun.status}` : (status.interval_sec ? `every ${Math.round(status.interval_sec / 60)} min` : '')}
diff --git a/start9/0.4/startos/utils.ts b/start9/0.4/startos/utils.ts index afaeffc..8840f89 100644 --- a/start9/0.4/startos/utils.ts +++ b/start9/0.4/startos/utils.ts @@ -25,8 +25,9 @@ export const PACKAGE_TITLE = 'Ten31 Database' // * 0.1.0:57 (redaction fix: magnitude regex no longer eats the word after an amount) // * 0.1.0:58 (seed 5 Architect positioning framings into the Workshop as candidate options) // * 0.1.0:59 (Email Capture admin panel + matched email into the grounding corpus) -// * Current: 0.1.0:60 (Email Capture: single-mailbox enroll field for testing) -export const PACKAGE_VERSION = '0.1.0:60' +// * 0.1.0:60 (Email Capture: single-mailbox enroll field for testing) +// * Current: 0.1.0:61 (Email Capture: live backfill progress + auto-refresh) +export const PACKAGE_VERSION = '0.1.0:61' export const DATA_MOUNT_PATH = '/data' export const WEB_PORT = 8080 diff --git a/start9/0.4/startos/versions/index.ts b/start9/0.4/startos/versions/index.ts index d630bbe..b74d717 100644 --- a/start9/0.4/startos/versions/index.ts +++ b/start9/0.4/startos/versions/index.ts @@ -21,8 +21,9 @@ import { v_0_1_0_57 } from './v0.1.0.57' import { v_0_1_0_58 } from './v0.1.0.58' import { v_0_1_0_59 } from './v0.1.0.59' import { v_0_1_0_60 } from './v0.1.0.60' +import { v_0_1_0_61 } from './v0.1.0.61' export const versionGraph = VersionGraph.of({ - current: v_0_1_0_60, - other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47, v_0_1_0_48, v_0_1_0_49, v_0_1_0_50, v_0_1_0_51, v_0_1_0_52, v_0_1_0_53, v_0_1_0_54, v_0_1_0_55, v_0_1_0_56, v_0_1_0_57, v_0_1_0_58, v_0_1_0_59], + current: v_0_1_0_61, + other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47, v_0_1_0_48, v_0_1_0_49, v_0_1_0_50, v_0_1_0_51, v_0_1_0_52, v_0_1_0_53, v_0_1_0_54, v_0_1_0_55, v_0_1_0_56, v_0_1_0_57, v_0_1_0_58, v_0_1_0_59, v_0_1_0_60], }) diff --git a/start9/0.4/startos/versions/v0.1.0.61.ts b/start9/0.4/startos/versions/v0.1.0.61.ts new file mode 100644 index 0000000..95c5a4e --- /dev/null +++ b/start9/0.4/startos/versions/v0.1.0.61.ts @@ -0,0 +1,18 @@ +import { VersionInfo } from '@start9labs/start-sdk' + +// Email Capture: show live backfill progress. The status endpoint now reports total +// emails captured (climbs page-by-page during backfill, since the sync-run row only +// finalizes at the end), the latest sync run, and whether a backfill is still in +// progress. The panel shows a "Backfilling… N captured so far" banner + an Emails +// Captured count, and auto-refreshes every 5s while a backfill runs. No schema migration. +export const v_0_1_0_61 = VersionInfo.of({ + version: '0.1.0:61', + releaseNotes: { + en_US: [ + 'Email Capture now shows live backfill progress: a count of emails captured so far', + 'that ticks up while the first sync pulls your history, plus a clear backfilling banner', + 'that refreshes on its own. No more staring at a static "pending" with no feedback.', + ].join(' '), + }, + migrations: { up: async () => {}, down: async () => {} }, +})