diff --git a/backend/email_integration/routes.py b/backend/email_integration/routes.py
index e1e6d22..bba89e1 100644
--- a/backend/email_integration/routes.py
+++ b/backend/email_integration/routes.py
@@ -162,6 +162,22 @@ def _h_list_accounts(handler):
"FROM email_accounts ORDER BY email_address"
)
rows = [dict(r) for r in cur.fetchall()]
+ # Per-mailbox counts: emails are de-duplicated globally, so "captured per
+ # mailbox" comes from the per-account sighting table; "matched" joins to emails.
+ captured, matched = {}, {}
+ try:
+ captured = {r["account_id"]: r["n"] for r in cur.execute(
+ "SELECT account_id, COUNT(*) AS n FROM email_account_messages "
+ "WHERE deleted_at IS NULL GROUP BY account_id")}
+ matched = {r["account_id"]: r["n"] for r in cur.execute(
+ "SELECT eam.account_id AS account_id, COUNT(*) AS n FROM email_account_messages eam "
+ "JOIN emails e ON e.id = eam.email_id "
+ "WHERE eam.deleted_at IS NULL AND e.is_matched = 1 GROUP BY eam.account_id")}
+ except sqlite3.OperationalError:
+ pass
+ for r in rows:
+ r["captured"] = captured.get(r["id"], 0)
+ r["matched"] = matched.get(r["id"], 0)
finally:
conn.close()
# Non-admins only see their own row
diff --git a/frontend/index.html b/frontend/index.html
index 8063de1..c4e41b9 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -10197,6 +10197,7 @@
{a.email_address}
{a.sync_status || 'pending'}{a.backfill_complete ? '' : ' · backfilling'}
+
{(a.captured ?? 0)} captured · {(a.matched ?? 0)} matched
{a.last_synced_at ? `last ${formatDate(a.last_synced_at)}` : 'never synced'}{a.sync_error ? ` · ${a.sync_error}` : ''}
))}
diff --git a/start9/0.4/startos/utils.ts b/start9/0.4/startos/utils.ts
index 28b062e..3130be4 100644
--- a/start9/0.4/startos/utils.ts
+++ b/start9/0.4/startos/utils.ts
@@ -29,8 +29,9 @@ export const PACKAGE_TITLE = 'Ten31 Database'
// * 0.1.0:61 (Email Capture: live backfill progress + auto-refresh)
// * 0.1.0:62 (fix backfill crash on no-Reply-To emails; Sync now retries errored mailboxes)
// * 0.1.0:63 (System Status: storage usage — DB, attachments, backups, disk free)
-// * Current: 0.1.0:64 (email-activity agent: propose->review->approve grid notes; sync ~15 min)
-export const PACKAGE_VERSION = '0.1.0:64'
+// * 0.1.0:64 (email-activity agent: propose->review->approve grid notes; sync ~15 min)
+// * Current: 0.1.0:65 (Email Capture: per-mailbox captured/matched counts)
+export const PACKAGE_VERSION = '0.1.0:65'
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 97cf82d..d51faa9 100644
--- a/start9/0.4/startos/versions/index.ts
+++ b/start9/0.4/startos/versions/index.ts
@@ -25,8 +25,9 @@ import { v_0_1_0_61 } from './v0.1.0.61'
import { v_0_1_0_62 } from './v0.1.0.62'
import { v_0_1_0_63 } from './v0.1.0.63'
import { v_0_1_0_64 } from './v0.1.0.64'
+import { v_0_1_0_65 } from './v0.1.0.65'
export const versionGraph = VersionGraph.of({
- current: v_0_1_0_64,
- 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, v_0_1_0_61, v_0_1_0_62, v_0_1_0_63],
+ current: v_0_1_0_65,
+ 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, v_0_1_0_61, v_0_1_0_62, v_0_1_0_63, v_0_1_0_64],
})
diff --git a/start9/0.4/startos/versions/v0.1.0.65.ts b/start9/0.4/startos/versions/v0.1.0.65.ts
new file mode 100644
index 0000000..9250741
--- /dev/null
+++ b/start9/0.4/startos/versions/v0.1.0.65.ts
@@ -0,0 +1,16 @@
+import { VersionInfo } from '@start9labs/start-sdk'
+
+// Email Capture: per-mailbox counts. Each enrolled mailbox card now shows how many
+// emails that mailbox captured and how many matched to investors (from the per-account
+// sighting table; emails are de-duplicated globally, so an email seen by two mailboxes
+// counts for each). Read-only. No schema migration.
+export const v_0_1_0_65 = VersionInfo.of({
+ version: '0.1.0:65',
+ releaseNotes: {
+ en_US: [
+ 'Email Capture now shows captured and matched-to-investor counts per Ten31 mailbox,',
+ 'so you can see each user’s email coverage, not just the totals.',
+ ].join(' '),
+ },
+ migrations: { up: async () => {}, down: async () => {} },
+})