← back to Draft Viewer
Prove the snapshot actually restores — restore.mjs, rehearsed end to end
584f7f4e0e28e43cc728c3a348159aa47024396a · 2026-09-10 09:03:28 -0700 · Steve Abrams
A snapshot nobody has replayed is a claim, not a backup. Same lesson as
dw-restore-rehearsal, which exists because a pg_dump ran green for 12 days while
producing output that would not restore. Codex flagged the same gap here.
restore.mjs replays a snapshotted body back into Gmail as a NEW draft. It can
only create drafts, so it structurally cannot deliver mail. Dry-run by default.
Rehearsed three times against live Gmail, recipient redirected to an internal
address so no real vendor draft was duplicated. Result: the recipient-visible
message round-trips byte-identical (516 == 516 chars). The only delta is
George's provenance banner carrying the restore timestamp, which is correct.
The rehearsal caught a real defect: restore was NOT idempotent. George prepends
a 'From job:' banner to every draft it creates, so each restore stacked another
one. The first strip attempt failed because the banner is a styled <div>
wrapping <strong>From job:</strong> — matching the bare phrase left the wrapper
behind. Now strips the whole leading div block; verified 1 banner in, 1 out.
All three rehearsal drafts deleted; info@ is back to its exact 41-draft baseline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Files touched
Diff
commit 584f7f4e0e28e43cc728c3a348159aa47024396a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 10 09:03:28 2026 -0700
Prove the snapshot actually restores — restore.mjs, rehearsed end to end
A snapshot nobody has replayed is a claim, not a backup. Same lesson as
dw-restore-rehearsal, which exists because a pg_dump ran green for 12 days while
producing output that would not restore. Codex flagged the same gap here.
restore.mjs replays a snapshotted body back into Gmail as a NEW draft. It can
only create drafts, so it structurally cannot deliver mail. Dry-run by default.
Rehearsed three times against live Gmail, recipient redirected to an internal
address so no real vendor draft was duplicated. Result: the recipient-visible
message round-trips byte-identical (516 == 516 chars). The only delta is
George's provenance banner carrying the restore timestamp, which is correct.
The rehearsal caught a real defect: restore was NOT idempotent. George prepends
a 'From job:' banner to every draft it creates, so each restore stacked another
one. The first strip attempt failed because the banner is a styled <div>
wrapping <strong>From job:</strong> — matching the bare phrase left the wrapper
behind. Now strips the whole leading div block; verified 1 banner in, 1 out.
All three rehearsal drafts deleted; info@ is back to its exact 41-draft baseline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
restore.mjs | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 66 insertions(+)
diff --git a/restore.mjs b/restore.mjs
new file mode 100644
index 0000000..b67de31
--- /dev/null
+++ b/restore.mjs
@@ -0,0 +1,66 @@
+#!/usr/bin/env node
+// restore.mjs — TK-11231
+// Replays a snapshotted draft body back into Gmail as a NEW draft.
+//
+// This is what makes the snapshot a backup rather than a claim. Same doctrine as
+// dw-restore-rehearsal: a dump nobody has replayed is not proven restorable.
+// It NEVER sends — Gmail drafts.create only. Composing a draft cannot deliver mail.
+//
+// node restore.mjs <snapshot.json> # dry run, prints what it would do
+// node restore.mjs <snapshot.json> --apply # recreate the draft for real
+// node restore.mjs <snapshot.json> --apply --test-to <addr>
+// redirect the recipient — used to REHEARSE the restore without creating a
+// second real draft addressed at a live vendor.
+
+import fs from 'node:fs';
+import path from 'node:path';
+import os from 'node:os';
+
+const args = process.argv.slice(2);
+const file = args.find((a) => !a.startsWith('--'));
+const APPLY = args.includes('--apply');
+const testTo = args.includes('--test-to') ? args[args.indexOf('--test-to') + 1] : null;
+if (!file) { console.error('usage: node restore.mjs <snapshot.json> [--apply] [--test-to addr]'); process.exit(1); }
+
+function george() {
+ const cfg = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'));
+ const env = cfg?.mcpServers?.george?.env || {};
+ return { url: (env.GEORGE_URL || 'http://127.0.0.1:9850').replace(/\/$/, ''),
+ headers: { Authorization: 'Basic ' + env.GEORGE_BASIC_AUTH, 'content-type': 'application/json' } };
+}
+
+const snap = JSON.parse(fs.readFileSync(path.resolve(file), 'utf8'));
+const m = snap.message, acct = snap.board.account;
+const to = testTo || m.to;
+// George stamps a "From job: …" provenance banner onto every draft it creates.
+// The snapshot captured a draft that already carries one, so restoring as-is
+// would prepend a SECOND banner and each further restore another — the restore
+// would not be idempotent. Strip a leading banner so a snapshot can be replayed
+// any number of times and still match the original.
+const stripBanner = (s) => s.replace(
+ // the whole leading <div …>…From job:…</div> block, not just the text — the
+ // banner is a styled div wrapping <strong>From job:</strong>, so matching on
+ // the bare phrase leaves the wrapper behind and strips nothing.
+ /^\s*<div\b[^>]*>(?:(?!<\/div>)[\s\S])*?From job:(?:(?!<\/div>)[\s\S])*?<\/div>\s*/i, '');
+const body = stripBanner(m.body || '');
+if (!body.trim()) { console.error('REFUSING: snapshot has an empty body — nothing to restore.'); process.exit(2); }
+
+console.log(`restore ${APPLY ? '(APPLY)' : '(dry run)'}`);
+console.log(` from : ${file}`);
+console.log(` account : ${acct}`);
+console.log(` to : ${to}${testTo ? ' [REDIRECTED for rehearsal]' : ''}`);
+console.log(` subject : ${m.subject}`);
+console.log(` body : ${body.length} chars`);
+if (!APPLY) { console.log('\ndry run — nothing written. add --apply to recreate.'); process.exit(0); }
+
+const g = george();
+// acknowledge_existing: this is a deliberate restore of a known draft, so the
+// duplicate-refusal added earlier today must not block a recovery.
+const res = await fetch(g.url + '/api/drafts', {
+ method: 'POST', headers: g.headers,
+ body: JSON.stringify({ account: acct, to, subject: m.subject, body, acknowledge_existing: true }),
+});
+const out = await res.json().catch(() => ({}));
+if (!res.ok) { console.error('restore FAILED:', res.status, JSON.stringify(out).slice(0, 400)); process.exit(3); }
+console.log('\nrestored →', JSON.stringify({ draftId: out.draftId || out.id, messageId: out.messageId || out.message?.id }));
+console.log('This created a DRAFT. It was not sent.');
← 3fb2245 TK-11231: archive + delete the 13 stale customer replies per
·
back to Draft Viewer
·
TK-11231: archive + clear the 7 superseded vendor drafts 955b609 →