← back to Draft Viewer

build-board.mjs

79 lines

#!/usr/bin/env node
// build-board.mjs — TK-11231
// Rebuilds data/board.json from the on-disk snapshots, grouped by the ACTION
// Steve decided for each class on 2026-09-10, not by the old ready/delete split.
//
// Ages come from Gmail's own internalDate captured in the snapshot, never from a
// hand-typed number — an age that drifts is how a fuse gets missed.

import fs from 'node:fs';
import path from 'node:path';

const HERE = path.dirname(new URL(import.meta.url).pathname);
const BODIES = path.join(HERE, 'data', 'bodies');

// info@ drafts older than 30 days are PERMANENTLY deleted (no Trash) by
// com.steve.george-drain-old-drafts, daily 04:15. steve-office is never touched.
const PURGE_DAYS = 30;

const CUSTOMER = new Set(['19ff677a456e1e61','19ff67c7ca93d3ee','19ff677c9748dd70','19ff677d1895b6f4',
  '19ff67c4e29d46c2','19ff67c3b9d347b7','19ff67c334844485','19ff67c288b79265','19ff67c1ef9c846b',
  '19ff67c543906d8f','19ff67c5f7a58198','19ff67c6cc0dc4e4','19ff67c7686cc8dc']);
const VENDOR_SEND = new Set(['1a06d5e2baaf3149','1a06d5e530b2ec40','1a06d55e4f511b8e','1a06d2ea705dddd3','1a06d1ef99f846d1']);
const FUSE_SEND   = new Set(['1a006aa890e66af3','1a005fc3ff6c56ff']);

const strip = (h) => String(h || '').replace(/<style[\s\S]*?<\/style>/gi, '').replace(/<[^>]+>/g, ' ')
  .replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/\s+/g, ' ').trim();

const rows = [];
for (const f of fs.readdirSync(BODIES).filter((x) => x.endsWith('.json'))) {
  const { board, message } = JSON.parse(fs.readFileSync(path.join(BODIES, f), 'utf8'));
  const created = message.internalDate ? new Date(Number(message.internalDate)) : null;
  const ageDays = created ? (Date.now() - created.getTime()) / 864e5 : null;
  // Only info@ has a fuse; a steve-office draft can sit forever.
  const daysLeft = board.account === 'info' && ageDays != null ? Math.floor(PURGE_DAYS - ageDays) : null;

  const bucket = FUSE_SEND.has(board.id) ? 'send_now'
    : VENDOR_SEND.has(board.id) ? 'send_now'
    : CUSTOMER.has(board.id) ? 'keep'
    : 'superseded';

  rows.push({
    id: board.id, account: board.account, bucket,
    fuse: FUSE_SEND.has(board.id),
    vendor: board.vendor || null,
    subject: message.subject || '(no subject)',
    to: message.to || board.to || null,
    gap: board.gap || null,
    why: board.why || null,
    created: created ? created.toISOString() : null,
    age_days: ageDays == null ? null : Math.round(ageDays * 10) / 10,
    days_left: daysLeft,
    preview: strip(message.body).slice(0, 260),
    snapshot_file: `data/bodies/${f}`,
  });
}

const order = { send_now: 0, keep: 1, superseded: 2 };
rows.sort((a, b) => (order[a.bucket] - order[b.bucket])
  || (a.fuse === b.fuse ? 0 : a.fuse ? -1 : 1)
  || ((a.days_left ?? 999) - (b.days_left ?? 999))
  || (b.age_days - a.age_days));

fs.writeFileSync(path.join(HERE, 'data', 'board.json'), JSON.stringify({
  built_at: new Date().toISOString(),
  decided_at: '2026-09-10',
  decisions: {
    send_now: 'Steve 2026-09-10: send both fuse drafts as-is + all 5 vendor asks. He clicks Send in Gmail himself — nothing is auto-fired.',
    keep: 'Steve 2026-09-10: "put in drafts" — the 13 stale customer replies are NOT deleted and NOT auto-sent. They stay as drafts; bodies are unedited.',
    superseded: 'No decision taken. Duplicates and wrong-scope asks already replaced by fresh drafts. Inert on steve-office, which the drainer never touches.',
  },
  purge_days: PURGE_DAYS,
  counts: rows.reduce((a, r) => ((a[r.bucket] = (a[r.bucket] || 0) + 1), a), {}),
  rows,
}, null, 2));

console.log('board.json built:', rows.length, 'rows',
  JSON.stringify(rows.reduce((a, r) => ((a[r.bucket] = (a[r.bucket] || 0) + 1), a), {})));
for (const r of rows.filter((x) => x.fuse)) console.log(`  FUSE ${r.days_left}d left — ${r.to}`);