← back to Sample Followup Sweep
send-reconciled: rolling catch-up window + draft-dedup ledger (seeded w/ today's handled batch) — make it daily-safe as the authoritative morning drafter
55eebeaaef34b664c3ded0c43b20ef6cda036dd0 · 2026-08-20 13:48:54 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A data/reconciled-draft-ledger.jsonM scripts/send-reconciled.mjs
Diff
commit 55eebeaaef34b664c3ded0c43b20ef6cda036dd0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Aug 20 13:48:54 2026 -0700
send-reconciled: rolling catch-up window + draft-dedup ledger (seeded w/ today's handled batch) — make it daily-safe as the authoritative morning drafter
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
data/reconciled-draft-ledger.json | 32 ++++++++++++++++++++++++++++++++
scripts/send-reconciled.mjs | 25 +++++++++++++++++++++----
2 files changed, 53 insertions(+), 4 deletions(-)
diff --git a/data/reconciled-draft-ledger.json b/data/reconciled-draft-ledger.json
new file mode 100644
index 0000000..1404830
--- /dev/null
+++ b/data/reconciled-draft-ledger.json
@@ -0,0 +1,32 @@
+{
+ "OSB:EUR71256": "08/20/2026",
+ "OSB:EUR71254": "08/20/2026",
+ "OSB:EUR71252": "08/20/2026",
+ "OSB:EUR90475": "08/20/2026",
+ "OSB:EUR80387": "08/20/2026",
+ "POI:DWPN100258": "08/20/2026",
+ "ASTSAND:DWPX500016": "08/20/2026",
+ "CHIN:DWCS914048": "08/20/2026",
+ "CHIN:DWCS913548": "08/20/2026",
+ "KRA:DWKK135711": "08/20/2026",
+ "KRA:DWKK101625": "08/20/2026",
+ "KRA:DWKK130319": "08/20/2026",
+ "ARTE:DWC-Arte72143": "08/20/2026",
+ "BRE:DWJS91471": "08/20/2026",
+ "BRE:DWJS90108": "08/20/2026",
+ "BMGREEN:PWV10040": "08/20/2026",
+ "BMGREEN:GRS800010": "08/20/2026",
+ "ROMO:ROP74578": "08/20/2026",
+ "BMGREEN:HLW73095": "08/20/2026",
+ "BMGREEN:GRS20323": "08/20/2026",
+ "BMGREEN:HLW73145": "08/20/2026",
+ "YOR:PRC84025": "08/20/2026",
+ "YOR:DWC322184": "08/20/2026",
+ "YOR:DWC322186": "08/20/2026",
+ "YOR:DWJS91574": "08/20/2026",
+ "YOR:DWJS17387": "08/20/2026",
+ "YOR:DWJS16618": "08/20/2026",
+ "SANDB:DWSS72675": "08/20/2026",
+ "WQ:DWQW60039": "08/20/2026",
+ "THIB:DWTT70093": "08/20/2026"
+}
\ No newline at end of file
diff --git a/scripts/send-reconciled.mjs b/scripts/send-reconciled.mjs
index 6d41f5a..2d91764 100644
--- a/scripts/send-reconciled.mjs
+++ b/scripts/send-reconciled.mjs
@@ -6,7 +6,15 @@
// WIN env overrides the FileMaker date range (default = today's fill-in window).
import { readFileSync, writeFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { join } from 'node:path'; import http from 'node:http';
const ROOT = join(homedir(), 'Projects/sample-followup-sweep');
-const WIN = process.env.WIN || '08/06/2026...08/10/2026'; // entered 8/6..8/10 (since 8/15 send, 10-day floor)
+// Rolling window: memos that crossed the 10-day follow-up floor within the last CATCHUP days (default 7),
+// so a daily scheduled run always covers any gap. Env WIN overrides for a manual one-off. (Draft-dedup
+// below stops the same memo being drafted twice across runs.)
+const _pad = (n) => String(n).padStart(2, '0');
+const _fmt = (d) => `${_pad(d.getMonth() + 1)}/${_pad(d.getDate())}/${d.getFullYear()}`;
+const _MIN_AGE = 10, _CATCHUP = Number(process.env.CATCHUP) || 7;
+const _hi = new Date(); _hi.setDate(_hi.getDate() - _MIN_AGE);
+const _lo = new Date(); _lo.setDate(_lo.getDate() - _MIN_AGE - (_CATCHUP - 1));
+const WIN = process.env.WIN || `${_fmt(_lo)}...${_fmt(_hi)}`;
// --- FileMaker: pull outstanding (Date WP Sample Sent empty) in WIN, group by vid ---
const cfg = JSON.parse(readFileSync(join(homedir(), '.claude.json'), 'utf8'));
@@ -62,11 +70,20 @@ auth = 'Basic ' + Buffer.from(auth).toString('base64');
function draft(to, subject, body) { return new Promise(res => { const b = JSON.stringify({ account: 'info', to, subject, body }); const rq = http.request({ host: '127.0.0.1', port: 9850, path: '/api/drafts', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(b), Authorization: auth } }, r => { let d = ''; r.on('data', x => d += x); r.on('end', () => { try { const j = JSON.parse(d); res(j.success ? 'ok' : d.slice(0, 80)); } catch { res(d.slice(0, 80)); } }); }); rq.on('error', e => res(e.message)); rq.write(b); rq.end(); }); }
const DO = process.argv.includes('--draft');
+// Draft-dedup: never re-draft a memo (keyed by vendor+combo-sku) that a prior run already drafted,
+// so a daily scheduled run over a rolling window can't pile up duplicate drafts.
+const LEDGER = join(ROOT, 'data/reconciled-draft-ledger.json');
+let ledger = {}; try { ledger = JSON.parse(readFileSync(LEDGER, 'utf8')); } catch {}
+const stamp = _fmt(new Date());
console.log(`Reconciled follow-up — window ${WIN} — ${DO ? 'DRAFT into info@' : 'DRY-RUN (add --draft to create)'}\n`);
for (const [key, rows] of Object.entries(items)) {
const v = V[key]; if (!v) { console.log(` ? no vendor map for ${key} (${rows.length})`); continue; }
+ const fresh = rows.filter(r => !ledger[`${key}:${r.sku}`]);
+ if (!fresh.length) { console.log(` ⊘ ${v.name.padEnd(26)} — all ${rows.length} already drafted (skip)`); continue; }
const subj = `Sample Follow-Up — Outstanding Memos (Acct ${v.acct}) — Designer Wallcoverings`;
- if (!DO) { console.log(` · ${v.name.padEnd(26)} → ${v.to.padEnd(40)} (${rows.length}) refs: ${rows.map(r => r.sku).join(', ')}`); continue; }
- const r = await draft(v.to, subj, letter(v, rows));
- console.log(` ${r === 'ok' ? '✎' : '✗'} ${v.name.padEnd(26)} → ${v.to.padEnd(40)} (${rows.length}) ${r === 'ok' ? '' : r}`);
+ if (!DO) { console.log(` · ${v.name.padEnd(26)} → ${v.to.padEnd(40)} (${fresh.length}) refs: ${fresh.map(r => r.sku).join(', ')}`); continue; }
+ const r = await draft(v.to, subj, letter(v, fresh));
+ console.log(` ${r === 'ok' ? '✎' : '✗'} ${v.name.padEnd(26)} → ${v.to.padEnd(40)} (${fresh.length}) ${r === 'ok' ? '' : r}`);
+ if (r === 'ok') for (const row of fresh) ledger[`${key}:${row.sku}`] = stamp;
}
+if (DO) writeFileSync(LEDGER, JSON.stringify(ledger, null, 2));
← b6a575a auto-data-snapshot: 2026-08-20T13:34:43 (1 data files) — dat
·
back to Sample Followup Sweep
·
auto-data-snapshot: 2026-08-20T14:07:04 (2 data files) — dat 660caf0 →