← back to Draft Viewer

refresh.mjs

74 lines

#!/usr/bin/env node
// 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 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++;
  }
}
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`);