[object Object]

← back to Draft Viewer

draft-viewer: make the board honest — verify triage against live Gmail — TK-11231

20c1dc8887bd9f7684c7bb6f932220f3c2b52a5e · 2026-09-10 08:06:23 -0700 · Steve Abrams

The board was serving a frozen 2026-09-04 snapshot and had started to LIE: one
listed draft (info@ 'Newmor 2026 Cost List') had already been permanently
deleted by com.steve.george-drain-old-drafts, but rendered as still actionable.

refresh.mjs was broken two ways: it called /api/gmail/list (no such route — the
real ones are /api/search and /api/drafts) and grepped GEORGE_BASIC_AUTH_PASS
out of secrets-manager/.env, where that key does not exist (verified) — hence
the 401 the prior session hit and could not explain.

It also had a design flaw worth more than the bugs: it rebuilt drafts.json from
a flat API list, which would have OVERWRITTEN the READY-vs-DELETE triage with
raw data. That classification is judgement and is not derivable from Gmail — it
is the only thing the viewer is for. Now it PRESERVES the triage and refreshes
LIVENESS instead, and on a fetch failure it exits without touching the file
rather than marking everything gone (a false all-clear beats no board — never
show a false empty).

Viewer renders x.live===false as a struck-through, dimmed card with a GONE
badge, plus a 'verified <time>' stamp — and says NOT VERIFIED in red when the
data has never been checked, so staleness is visible rather than implied.

Verified live: 5 ready, 21 delete, 1 GONE, auth gate returns 401 unauthenticated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTxE4MgnygQ9EY2rPZvtcK

Files touched

Diff

commit 20c1dc8887bd9f7684c7bb6f932220f3c2b52a5e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 10 08:06:23 2026 -0700

    draft-viewer: make the board honest — verify triage against live Gmail — TK-11231
    
    The board was serving a frozen 2026-09-04 snapshot and had started to LIE: one
    listed draft (info@ 'Newmor 2026 Cost List') had already been permanently
    deleted by com.steve.george-drain-old-drafts, but rendered as still actionable.
    
    refresh.mjs was broken two ways: it called /api/gmail/list (no such route — the
    real ones are /api/search and /api/drafts) and grepped GEORGE_BASIC_AUTH_PASS
    out of secrets-manager/.env, where that key does not exist (verified) — hence
    the 401 the prior session hit and could not explain.
    
    It also had a design flaw worth more than the bugs: it rebuilt drafts.json from
    a flat API list, which would have OVERWRITTEN the READY-vs-DELETE triage with
    raw data. That classification is judgement and is not derivable from Gmail — it
    is the only thing the viewer is for. Now it PRESERVES the triage and refreshes
    LIVENESS instead, and on a fetch failure it exits without touching the file
    rather than marking everything gone (a false all-clear beats no board — never
    show a false empty).
    
    Viewer renders x.live===false as a struck-through, dimmed card with a GONE
    badge, plus a 'verified <time>' stamp — and says NOT VERIFIED in red when the
    data has never been checked, so staleness is visible rather than implied.
    
    Verified live: 5 ready, 21 delete, 1 GONE, auth gate returns 401 unauthenticated.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01VTxE4MgnygQ9EY2rPZvtcK
---
 data/drafts.json | 268 +++++++++++++++++++++++++++++++++++++++++++++++++------
 refresh.mjs      |  90 ++++++++++++++-----
 server.js        |   9 +-
 3 files changed, 315 insertions(+), 52 deletions(-)

diff --git a/data/drafts.json b/data/drafts.json
index 8ce9241..adeabb7 100644
--- a/data/drafts.json
+++ b/data/drafts.json
@@ -2,33 +2,245 @@
   "generated": "2026-09-04",
   "note": "Snapshot verified via in:draft search. Bodies live in Gmail — cards deep-link there.",
   "ready": [
-    {"id":"1a06d5e2baaf3149","account":"steve-office","to":"Customerservice@nationalsolutions.com","vendor":"Command Wallcovering","subject":"Net cost list request — full Command Wallcovering line","items":2120,"gap":"2,120 rows · 0 trade · 0 retail","created":"2026-09-04T10:01:22"},
-    {"id":"1a06d5e530b2ec40","account":"steve-office","to":"info@vahallan.com","vendor":"Vahallan","subject":"Net cost list request — full Vahallan line","items":390,"gap":"390 rows · no cost column","created":"2026-09-04T10:01:33"},
-    {"id":"1a06d55e4f511b8e","account":"steve-office","to":"samples@rwltd.com","vendor":"Reid Witlin","subject":"Net cost list request — full Reid Witlin line","items":1470,"gap":"1,470 rows · 0 cost (1,070 price only)","created":"2026-09-04T09:52:20"},
-    {"id":"1a06d2ea705dddd3","account":"steve-office","to":"tokiwausa@tokiwa.net","vendor":"Fentucci Naturals","subject":"Net cost list request — Fentucci Naturals grasscloth","items":148,"gap":"148 rows · no cost column","created":"2026-09-04T09:09:29"},
-    {"id":"1a06d1ef99f846d1","account":"info","to":"JCarol@lbiboyd.com · cc stephanie@","vendor":"Newmor / LBI Boyd","subject":"Newmor pricing — 3 quick questions","items":1294,"gap":"1,294 rows · 0 priced · 305 products dark","created":"2026-09-04T08:52:21"}
+    {
+      "id": "1a06d5e2baaf3149",
+      "account": "steve-office",
+      "to": "Customerservice@nationalsolutions.com",
+      "vendor": "Command Wallcovering",
+      "subject": "Net cost list request — full Command Wallcovering line",
+      "items": 2120,
+      "gap": "2,120 rows · 0 trade · 0 retail",
+      "created": "2026-09-04T10:01:22",
+      "live": true
+    },
+    {
+      "id": "1a06d5e530b2ec40",
+      "account": "steve-office",
+      "to": "info@vahallan.com",
+      "vendor": "Vahallan",
+      "subject": "Net cost list request — full Vahallan line",
+      "items": 390,
+      "gap": "390 rows · no cost column",
+      "created": "2026-09-04T10:01:33",
+      "live": true
+    },
+    {
+      "id": "1a06d55e4f511b8e",
+      "account": "steve-office",
+      "to": "samples@rwltd.com",
+      "vendor": "Reid Witlin",
+      "subject": "Net cost list request — full Reid Witlin line",
+      "items": 1470,
+      "gap": "1,470 rows · 0 cost (1,070 price only)",
+      "created": "2026-09-04T09:52:20",
+      "live": true
+    },
+    {
+      "id": "1a06d2ea705dddd3",
+      "account": "steve-office",
+      "to": "tokiwausa@tokiwa.net",
+      "vendor": "Fentucci Naturals",
+      "subject": "Net cost list request — Fentucci Naturals grasscloth",
+      "items": 148,
+      "gap": "148 rows · no cost column",
+      "created": "2026-09-04T09:09:29",
+      "live": true
+    },
+    {
+      "id": "1a06d1ef99f846d1",
+      "account": "info",
+      "to": "JCarol@lbiboyd.com · cc stephanie@",
+      "vendor": "Newmor / LBI Boyd",
+      "subject": "Newmor pricing — 3 quick questions",
+      "items": 1294,
+      "gap": "1,294 rows · 0 priced · 305 products dark",
+      "created": "2026-09-04T08:52:21",
+      "live": true
+    }
   ],
   "delete": [
-    {"id":"19f3e20f58596b3e","account":"steve-office","subject":"Command Wallcovering — full price list request","why":"superseded by today's fresh draft","age":"Jul 7"},
-    {"id":"19f3e1c8c8a01b4d","account":"steve-office","subject":"Request: Full Command Wallcovering price list","why":"DUPLICATE — written 3h after the one above, same day","age":"Jul 7"},
-    {"id":"19f906c4a8f0e8fe","account":"steve-office","subject":"Trade / wholesale pricing request — Vahallan","why":"superseded by today's fresh draft","age":"Jul 23"},
-    {"id":"19f3d1a86a49b8c6","account":"steve-office","subject":"Pricing request — Dr. Feelgood Henna","why":"WRONG SCOPE — asks 1 SKU, catalog needs 1,470","age":"Jul 7"},
-    {"id":"1a059915205816a2","account":"steve-office","subject":"Net Cost Request — Fentucci Naturals (118 SKUs)","why":"BROKEN — promises an attachment that isn't there; count wrong (118 vs 148)","age":"Aug 31"},
-    {"id":"1a015db4fe26802c","account":"info","subject":"Request: current 2026 wholesale cost list — Newmor","why":"superseded — bundled 5 lines into one cold ask","age":"Aug 18"},
-    {"id":"1a0691afe4c69d0d","account":"info","subject":"Newmor 2026 Cost List (was: RE: Samples sm Marcus)","why":"superseded — missing all 3 binding questions","age":"Sep 3"},
-    {"id":"1a059922f619b980","account":"info","subject":"Width spec request — 2 discontinued Maya Romanoff patterns","why":"DUPLICATE of the steve-office copy, 2 min apart","age":"Aug 31"},
-    {"id":"19ff677a456e1e61","account":"steve-office","subject":"Willem — Lee Jofa Levens Velvet quote ($476.85/yd)","why":"customer reply, 23 days stale — your call: delete","age":"Aug 12","customer":true},
-    {"id":"19ff67c7ca93d3ee","account":"steve-office","subject":"Dena — Graham & Brown Weave Emerald stock check","why":"customer reply, 23 days stale","age":"Aug 12","customer":true},
-    {"id":"19ff677c9748dd70","account":"steve-office","subject":"Jane — yes you can buy the Merlot pillows","why":"customer reply, 23 days stale","age":"Aug 12","customer":true},
-    {"id":"19ff677d1895b6f4","account":"steve-office","subject":"Laura — ready to place Wolf Gordon Forge order","why":"customer reply, 23 days stale","age":"Aug 12","customer":true},
-    {"id":"19ff67c4e29d46c2","account":"steve-office","subject":"David — Roberto Cavalli RC21003 pricing","why":"customer reply, 23 days stale","age":"Aug 12","customer":true},
-    {"id":"19ff67c3b9d347b7","account":"steve-office","subject":"Nancy — Lee Jofa Lagoon sample","why":"customer reply, 23 days stale","age":"Aug 12","customer":true},
-    {"id":"19ff67c334844485","account":"steve-office","subject":"Leslie — Fleur de Lis coverage + pricing","why":"customer reply, 23 days stale","age":"Aug 12","customer":true},
-    {"id":"19ff67c288b79265","account":"steve-office","subject":"Rita — roll count for 368 sq ft bedroom","why":"customer reply, 23 days stale","age":"Aug 12","customer":true},
-    {"id":"19ff67c1ef9c846b","account":"steve-office","subject":"Shani — expedited samples","why":"customer reply, 23 days stale","age":"Aug 12","customer":true},
-    {"id":"19ff67c543906d8f","account":"steve-office","subject":"Andrew — Fidelity Flora NT65 / Glamour CM178","why":"customer reply, 23 days stale","age":"Aug 12","customer":true},
-    {"id":"19ff67c5f7a58198","account":"steve-office","subject":"Jeanette — glass beaded for art project","why":"customer reply, 23 days stale","age":"Aug 12","customer":true},
-    {"id":"19ff67c6cc0dc4e4","account":"steve-office","subject":"Bonnee — Brand McKenzie Happy Hour colorway","why":"customer reply, 23 days stale","age":"Aug 12","customer":true},
-    {"id":"19ff67c7686cc8dc","account":"steve-office","subject":"Michael — bamboo mural identification","why":"customer reply, 23 days stale","age":"Aug 12","customer":true}
-  ]
-}
+    {
+      "id": "19f3e20f58596b3e",
+      "account": "steve-office",
+      "subject": "Command Wallcovering — full price list request",
+      "why": "superseded by today's fresh draft",
+      "age": "Jul 7",
+      "live": true
+    },
+    {
+      "id": "19f3e1c8c8a01b4d",
+      "account": "steve-office",
+      "subject": "Request: Full Command Wallcovering price list",
+      "why": "DUPLICATE — written 3h after the one above, same day",
+      "age": "Jul 7",
+      "live": true
+    },
+    {
+      "id": "19f906c4a8f0e8fe",
+      "account": "steve-office",
+      "subject": "Trade / wholesale pricing request — Vahallan",
+      "why": "superseded by today's fresh draft",
+      "age": "Jul 23",
+      "live": true
+    },
+    {
+      "id": "19f3d1a86a49b8c6",
+      "account": "steve-office",
+      "subject": "Pricing request — Dr. Feelgood Henna",
+      "why": "WRONG SCOPE — asks 1 SKU, catalog needs 1,470",
+      "age": "Jul 7",
+      "live": true
+    },
+    {
+      "id": "1a059915205816a2",
+      "account": "steve-office",
+      "subject": "Net Cost Request — Fentucci Naturals (118 SKUs)",
+      "why": "BROKEN — promises an attachment that isn't there; count wrong (118 vs 148)",
+      "age": "Aug 31",
+      "live": true
+    },
+    {
+      "id": "1a015db4fe26802c",
+      "account": "info",
+      "subject": "Request: current 2026 wholesale cost list — Newmor",
+      "why": "superseded — bundled 5 lines into one cold ask",
+      "age": "Aug 18",
+      "live": true
+    },
+    {
+      "id": "1a0691afe4c69d0d",
+      "account": "info",
+      "subject": "Newmor 2026 Cost List (was: RE: Samples sm Marcus)",
+      "why": "superseded — missing all 3 binding questions",
+      "age": "Sep 3",
+      "live": false
+    },
+    {
+      "id": "1a059922f619b980",
+      "account": "info",
+      "subject": "Width spec request — 2 discontinued Maya Romanoff patterns",
+      "why": "DUPLICATE of the steve-office copy, 2 min apart",
+      "age": "Aug 31",
+      "live": true
+    },
+    {
+      "id": "19ff677a456e1e61",
+      "account": "steve-office",
+      "subject": "Willem — Lee Jofa Levens Velvet quote ($476.85/yd)",
+      "why": "customer reply, 23 days stale — your call: delete",
+      "age": "Aug 12",
+      "customer": true,
+      "live": true
+    },
+    {
+      "id": "19ff67c7ca93d3ee",
+      "account": "steve-office",
+      "subject": "Dena — Graham & Brown Weave Emerald stock check",
+      "why": "customer reply, 23 days stale",
+      "age": "Aug 12",
+      "customer": true,
+      "live": true
+    },
+    {
+      "id": "19ff677c9748dd70",
+      "account": "steve-office",
+      "subject": "Jane — yes you can buy the Merlot pillows",
+      "why": "customer reply, 23 days stale",
+      "age": "Aug 12",
+      "customer": true,
+      "live": true
+    },
+    {
+      "id": "19ff677d1895b6f4",
+      "account": "steve-office",
+      "subject": "Laura — ready to place Wolf Gordon Forge order",
+      "why": "customer reply, 23 days stale",
+      "age": "Aug 12",
+      "customer": true,
+      "live": true
+    },
+    {
+      "id": "19ff67c4e29d46c2",
+      "account": "steve-office",
+      "subject": "David — Roberto Cavalli RC21003 pricing",
+      "why": "customer reply, 23 days stale",
+      "age": "Aug 12",
+      "customer": true,
+      "live": true
+    },
+    {
+      "id": "19ff67c3b9d347b7",
+      "account": "steve-office",
+      "subject": "Nancy — Lee Jofa Lagoon sample",
+      "why": "customer reply, 23 days stale",
+      "age": "Aug 12",
+      "customer": true,
+      "live": true
+    },
+    {
+      "id": "19ff67c334844485",
+      "account": "steve-office",
+      "subject": "Leslie — Fleur de Lis coverage + pricing",
+      "why": "customer reply, 23 days stale",
+      "age": "Aug 12",
+      "customer": true,
+      "live": true
+    },
+    {
+      "id": "19ff67c288b79265",
+      "account": "steve-office",
+      "subject": "Rita — roll count for 368 sq ft bedroom",
+      "why": "customer reply, 23 days stale",
+      "age": "Aug 12",
+      "customer": true,
+      "live": true
+    },
+    {
+      "id": "19ff67c1ef9c846b",
+      "account": "steve-office",
+      "subject": "Shani — expedited samples",
+      "why": "customer reply, 23 days stale",
+      "age": "Aug 12",
+      "customer": true,
+      "live": true
+    },
+    {
+      "id": "19ff67c543906d8f",
+      "account": "steve-office",
+      "subject": "Andrew — Fidelity Flora NT65 / Glamour CM178",
+      "why": "customer reply, 23 days stale",
+      "age": "Aug 12",
+      "customer": true,
+      "live": true
+    },
+    {
+      "id": "19ff67c5f7a58198",
+      "account": "steve-office",
+      "subject": "Jeanette — glass beaded for art project",
+      "why": "customer reply, 23 days stale",
+      "age": "Aug 12",
+      "customer": true,
+      "live": true
+    },
+    {
+      "id": "19ff67c6cc0dc4e4",
+      "account": "steve-office",
+      "subject": "Bonnee — Brand McKenzie Happy Hour colorway",
+      "why": "customer reply, 23 days stale",
+      "age": "Aug 12",
+      "customer": true,
+      "live": true
+    },
+    {
+      "id": "19ff67c7686cc8dc",
+      "account": "steve-office",
+      "subject": "Michael — bamboo mural identification",
+      "why": "customer reply, 23 days stale",
+      "age": "Aug 12",
+      "customer": true,
+      "live": true
+    }
+  ],
+  "verified_at": "2026-09-10T15:05:13.996Z",
+  "liveness_note": "26 entries re-checked against live George; 1 no longer exist (sent or permanently deleted)."
+}
\ No newline at end of file
diff --git a/refresh.mjs b/refresh.mjs
index 934612c..d4f42fd 100644
--- a/refresh.mjs
+++ b/refresh.mjs
@@ -1,27 +1,73 @@
 #!/usr/bin/env node
-// refresh.mjs — pull the CURRENT unsent agent-composed drafts from George into data/drafts.json.
-// George exposes create_draft only over MCP, but the local HTTP API can list. If it can't,
-// we keep the last snapshot rather than blanking the viewer (never show a false empty).
-import { writeFileSync, existsSync, readFileSync } from 'node:fs';
+// refresh.mjs — re-verify the curated draft triage against LIVE George. TK-11231.
+//
+// Design note: this deliberately does NOT rebuild data/drafts.json from the API. The value of
+// this file is the READY-vs-DELETE *classification*, which is human/agent judgement and is not
+// derivable from Gmail. The old version fetched a flat list and overwrote the triage with it —
+// that would have destroyed the only thing the viewer is for. Instead we keep the triage and
+// refresh LIVENESS, so an entry that has been sent or permanently deleted shows as GONE rather
+// than the viewer quietly lying about a draft that no longer exists.
+//
+// Auth: George's basic auth, same credential the george MCP already uses. Resolution order
+// mirrors george-mcp/index.js. The previous version grepped GEORGE_BASIC_AUTH_PASS out of
+// secrets-manager/.env — that key is not there (verified), which is why it 401'd and the
+// viewer froze on a 2026-09-04 snapshot.
+import { writeFileSync, readFileSync, existsSync } from 'node:fs';
 import { execSync } from 'node:child_process';
+
 const OUT = new URL('./data/drafts.json', import.meta.url).pathname;
-const pass = (() => {
-  try { return execSync(`grep -m1 '^GEORGE_BASIC_AUTH_PASS=' ~/Projects/secrets-manager/.env | cut -d= -f2-`,
-    {shell:'/bin/bash'}).toString().trim(); } catch { return ''; }
-})();
-const accounts = ['info','steve-office'];
-const out = [];
-for (const acct of accounts) {
-  for (const q of ['in:draft newer_than:90d']) {
-    try {
-      const url = `http://127.0.0.1:9850/api/gmail/list?account=${acct}&q=${encodeURIComponent(q)}&max=100`;
-      const r = await fetch(url, { headers: { Authorization: 'Basic ' + Buffer.from('admin:'+pass).toString('base64') } });
-      if (!r.ok) throw new Error('HTTP '+r.status);
-      const j = await r.json();
-      for (const m of (j.messages||j||[])) out.push({ ...m, account: acct });
-    } catch (e) { console.error(`[warn] ${acct}: ${e.message} — keeping existing snapshot`); }
+const BASE = process.env.GEORGE_URL || 'http://127.0.0.1:9850';
+
+function resolveAuth() {
+  if (process.env.GEORGE_BASIC_AUTH) return process.env.GEORGE_BASIC_AUTH;
+  try {
+    const j = JSON.parse(readFileSync(`${process.env.HOME}/.claude.json`, 'utf8'));
+    const v = j?.mcpServers?.george?.env?.GEORGE_BASIC_AUTH;
+    if (v) return v;
+  } catch { /* fall through */ }
+  try {
+    const pw = execSync('security find-generic-password -s dw-agents -a admin -w', { encoding: 'utf8' }).trim();
+    if (pw) return Buffer.from(`admin:${pw}`).toString('base64');
+  } catch { /* fall through */ }
+  return null;
+}
+
+const AUTH = resolveAuth();
+if (!AUTH) { console.error('no George auth resolvable — snapshot retained, viewer unchanged'); process.exit(1); }
+
+async function liveMessageIds(account) {
+  const url = `${BASE}/api/drafts?maxResults=500${account === 'info' ? '&account=info' : ''}`;
+  const r = await fetch(url, { headers: { Authorization: `Basic ${AUTH}` } });
+  if (!r.ok) throw new Error(`HTTP ${r.status} on ${account}`);
+  const arr = await r.json();
+  return new Set((arr || []).map((d) => d?.message?.id).filter(Boolean));
+}
+
+if (!existsSync(OUT)) { console.error(`no triage file at ${OUT} — nothing to verify`); process.exit(1); }
+const snap = JSON.parse(readFileSync(OUT, 'utf8'));
+
+let live;
+try {
+  const [so, info] = await Promise.all([liveMessageIds('steve-office'), liveMessageIds('info')]);
+  live = { 'steve-office': so, info };
+} catch (e) {
+  // Never blank the viewer on a fetch failure, and never silently mark everything GONE —
+  // a false "all clear" is worse than a stale board.
+  console.error(`live check failed (${e.message}) — snapshot retained, liveness NOT updated`);
+  process.exit(1);
+}
+
+let checked = 0, gone = 0;
+for (const cls of ['ready', 'delete']) {
+  for (const item of snap[cls] || []) {
+    const set = live[item.account];
+    if (!set) continue;               // unknown account: leave untouched rather than guess
+    item.live = set.has(item.id);
+    checked++;
+    if (!item.live) gone++;
   }
 }
-if (out.length) { writeFileSync(OUT, JSON.stringify({ fetched_at: new Date().toISOString(), live:true, drafts: out }, null, 2)); console.log(`wrote ${out.length} drafts`); }
-else if (existsSync(OUT)) { console.log('live fetch failed — snapshot retained'); }
-else { console.error('no live fetch and no snapshot'); process.exit(1); }
+snap.verified_at = new Date().toISOString();
+snap.liveness_note = `${checked} entries re-checked against live George; ${gone} no longer exist (sent or permanently deleted).`;
+writeFileSync(OUT, JSON.stringify(snap, null, 2));
+console.log(`verified ${checked} drafts — ${checked - gone} still live, ${gone} gone`);
diff --git a/server.js b/server.js
index 3457af3..99822e4 100644
--- a/server.js
+++ b/server.js
@@ -4,8 +4,12 @@ const DATA=path.join(__dirname,'data','drafts.json');
 const gm=(acct,id)=>`https://mail.google.com/mail/u/${acct==='info'?'1':'0'}/#drafts/${id}`;
 const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
 function page(d){
-  const card=(x,kind)=>`<article class="c ${kind}${x.customer?' cust':''}">
+  // x.live is set by refresh.mjs. `false` means the draft no longer exists in Gmail — sent, or
+  // PERMANENTLY deleted by com.steve.george-drain-old-drafts (info@ >30d, no Trash). A board that
+  // renders a destroyed draft as if it were still actionable is worse than no board.
+  const card=(x,kind)=>`<article class="c ${kind}${x.customer?' cust':''}${x.live===false?' gone':''}">
     <header><span class="v">${esc(x.vendor||x.subject.split('—')[0])}</span>
+    ${x.live===false?'<span class="gonebadge" title="No longer in Gmail — sent or permanently deleted">GONE</span>':''}
     <span class="acct">${esc(x.account)}</span></header>
     <p class="s">${esc(x.subject)}</p>
     ${x.to?`<p class="to">→ ${esc(x.to)}</p>`:''}
@@ -24,7 +28,7 @@ header.top{padding:20px 24px;border-bottom:1px solid var(--line);display:flex;ga
 section{padding:20px 24px}h2{font-size:12px;letter-spacing:.16em;text-transform:uppercase;color:var(--dim);margin:0 0 14px}
 .grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fill,minmax(var(--w,320px),1fr))}
 .c{border:1px solid var(--line);border-radius:8px;padding:13px 15px;background:#141416;border-left-width:3px}
-.c.ready{border-left-color:var(--go)}.c.delete{border-left-color:var(--kill)}.c.cust{border-left-color:var(--cust)}
+.c.gone{opacity:.42;filter:grayscale(.7)}.c.gone .s{text-decoration:line-through}.gonebadge{font:700 10px/1.6 ui-monospace,Menlo,monospace;letter-spacing:.12em;background:#4a4a4a;color:#eee;padding:1px 6px;border-radius:2px;margin-left:6px}.c.ready{border-left-color:var(--go)}.c.delete{border-left-color:var(--kill)}.c.cust{border-left-color:var(--cust)}
 .c header{display:flex;justify-content:space-between;gap:8px;align-items:baseline;margin-bottom:6px}
 .v{font-weight:650}.acct{font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--dim);border:1px solid var(--line);border-radius:99px;padding:1px 7px}
 .s{margin:0 0 6px;font-size:13px}.to{margin:0 0 4px;font-size:12px;color:var(--dim);word-break:break-all}
@@ -39,6 +43,7 @@ input[type=range]{width:130px}
 <span class=k><b>${d.ready.length}</b> ready to send</span>
 <span class=k><b>${d.delete.length}</b> to delete</span>
 <span class=k style="color:var(--cust)"><b>${d.delete.filter(x=>x.customer).length}</b> stale customer replies</span>
+${d.verified_at?`<span class=k title="${esc(d.liveness_note||'')}">verified ${esc(new Date(d.verified_at).toLocaleString(undefined,{month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}))}</span>`:'<span class=k style="color:var(--kill)">NOT verified against live Gmail — run refresh.mjs</span>'}
 <span class=controls>density <input type=range min=240 max=520 value=320 id=dn></span></header>
 <section><h2 style="color:var(--go)">▸ Ready to send — ${d.ready.length}</h2>
 <div class="grid" id=g1>${d.ready.map(x=>card(x,'ready')).join('')}</div></section>

← e93ef88 Draft triage viewer — 5 ready to send, 21 to delete, deep-li  ·  back to Draft Viewer  ·  Snapshot every tracked draft body to disk — deletion is no l b443b7c →