← back to Sample Followup Sweep
Sample follow-up: bound window to a rolling 3-day catch-up band at the 10-day mark (only memos that just crossed 10d, not the whole back-catalog)
3aa59b23665118d9ca5cdf90e0014c73b695e575 · 2026-08-20 10:22:42 -0700 · Steve Abrams
Files touched
A data/draft-ledger.jsonM data/runs/scheduled-08-20-2026.jsonA scripts/contact-research.mjsM scripts/scheduled-run.mjs
Diff
commit 3aa59b23665118d9ca5cdf90e0014c73b695e575
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Aug 20 10:22:42 2026 -0700
Sample follow-up: bound window to a rolling 3-day catch-up band at the 10-day mark (only memos that just crossed 10d, not the whole back-catalog)
---
data/draft-ledger.json | 17 +
data/runs/scheduled-08-20-2026.json | 640 +++---------------------------------
scripts/contact-research.mjs | 48 +++
scripts/scheduled-run.mjs | 77 +++--
4 files changed, 167 insertions(+), 615 deletions(-)
diff --git a/data/draft-ledger.json b/data/draft-ledger.json
new file mode 100644
index 0000000..e06c0e6
--- /dev/null
+++ b/data/draft-ledger.json
@@ -0,0 +1,17 @@
+{
+ "538974": "08/20/2026",
+ "538977": "08/20/2026",
+ "538983": "08/20/2026",
+ "538986": "08/20/2026",
+ "538996": "08/20/2026",
+ "538997": "08/20/2026",
+ "539014": "08/20/2026",
+ "539103": "08/20/2026",
+ "539105": "08/20/2026",
+ "539106": "08/20/2026",
+ "539283": "08/20/2026",
+ "539284": "08/20/2026",
+ "539285": "08/20/2026",
+ "539286": "08/20/2026",
+ "539288": "08/20/2026"
+}
\ No newline at end of file
diff --git a/data/runs/scheduled-08-20-2026.json b/data/runs/scheduled-08-20-2026.json
index f33ecc0..28aaa8e 100644
--- a/data/runs/scheduled-08-20-2026.json
+++ b/data/runs/scheduled-08-20-2026.json
@@ -1,633 +1,81 @@
{
- "ranAt": "2026-08-20T17:04:45.117Z",
- "mode": "DRY-RUN",
- "window": "08/21/2021...08/10/2026",
+ "ranAt": "2026-08-20T17:22:23.387Z",
+ "mode": "DRAFT",
+ "window": "08/08/2026...08/10/2026",
"stampDate": "08/20/2026",
- "vendorsSent": 22,
- "vendorsSkipped": 36,
- "vendorsNeedsConfirm": 6,
- "sent": [
- {
- "vid": "CON",
- "name": "Concertex",
- "to": "info@concertex.com",
- "n": 1,
- "skus": [
- "noc105"
- ],
- "dryRun": true
- },
- {
- "vid": "YOR",
- "name": "York",
- "to": "RiveraH@yorkwall.com, orders@yorkwall.com",
- "n": 52,
- "skus": [
- "DWJS15964",
- "DWJS13287",
- "DWJS14323",
- "DWJS14322",
- "DWJS13941",
- "DWC263505",
- "DWC279215",
- "DWC279217",
- "DWC279220",
- "DWC179237",
- "DWC182513",
- "DWC279224",
- "DWJS14643",
- "DWJS14640",
- "DWJS13140",
- "DWJS12243",
- "DWJS15198",
- "DWJS12002",
- "DWJS14580",
- "DWJS14873",
- "DWJS12956",
- "DWJS13305",
- "DWJS14652",
- "DWJS16168",
- "DWC285126",
- "DWC285129",
- "DWJS15440",
- "DWJS14769",
- "DWJS14454",
- "DWJS13319",
- "DWJS14600",
- "DWJS12002",
- "DWJS12535",
- "DWJS13376",
- "DWJS16892",
- "DWJS16168",
- "DWJS17237",
- "DWJS14115",
- "FLO28057",
- "DWC304149",
- "DWC304152",
- "DWJS13678",
- "DWJS14031",
- "DWJS13995",
- "DWJS14179",
- "DWJS15783",
- "PRC84025",
- "DWC322184",
- "DWC322186",
- "DWJS91574",
- "DWJS17387",
- "DWJS16618"
- ],
- "dryRun": true
- },
- {
- "vid": "THIB",
- "name": "Thibaut",
- "to": "cathy.dy@thibautdesign.com",
- "n": 24,
- "skus": [
- "DWC270781",
- "DWTT72120",
- "DWC273903",
- "DWTT72044",
- "DWTT72046",
- "DWTT71557",
- "DWTT71321",
- "DWC275223",
- "PRG71027",
- "DWTT70638",
- "DWTT71614",
- "DWTT71872",
- "CROC56551",
- "DWTT71348",
- "DWTT70435",
- "DWTT71906",
- "DWTT71908",
- "DAW73004",
- "DAW73002",
- "DWC278298",
- "DAW73053",
- "DWTT81049",
- "DWTT71766",
- "DWTT70093"
- ],
- "dryRun": true
- },
- {
- "vid": "WQ",
- "name": "WallQuest / Malibu",
- "to": "orders@wallquest.com",
- "n": 16,
- "skus": [
- "DWC271268",
- "DWC275134",
- "DWC275802",
- "DWQW57639",
- "DWQW56956",
- "DWQW56952",
- "DWQW57088",
- "DWQW56865",
- "DWQW56864",
- "DWQW57484",
- "DWQW58392",
- "DWQW57564",
- "DWQW58003",
- "DWQW58001",
- "DWQW57309",
- "DWQW60039"
- ],
- "dryRun": true
- },
- {
- "vid": "BRE",
- "name": "Brewster",
- "to": "brewsterorders@brewp.com",
- "n": 6,
- "skus": [
- "FFA82226",
- "CGS83463",
- "VVA84534",
- "DWJS91471",
- "DWJS90108",
- "DWJS90108"
- ],
- "dryRun": true
- },
- {
- "vid": "DGD",
- "name": "Designers Guild @ Osborne & Little",
- "to": "Polancol@oalusa.com",
- "n": 7,
- "skus": [
- "RALPH1011",
- "EUR90000",
- "EUR70844",
- "EUR70847",
- "EUR70846",
- "RALPH1036",
- "RALPH1096"
- ],
- "dryRun": true
- },
- {
- "vid": "PJ",
- "name": "Phillip Jeffries",
- "to": "jballard@phillipjeffries.com",
- "n": 6,
- "skus": [
- "DWC275494",
- "DWJP13372",
- "DWJP14274",
- "DWJP14266",
- "DWJP13466",
- "DWC279239"
- ],
- "dryRun": true
- },
+ "vendorsSent": 0,
+ "vendorsDrafted": 8,
+ "vendorsSkipped": 1,
+ "vendorsNeedsConfirm": 0,
+ "sent": [],
+ "drafted": [
{
"vid": "OSB",
"name": "Osborne & Little",
"to": "Polancol@oalusa.com",
- "n": 16,
- "skus": [
- "DWC275795",
- "EUR70503",
- "EUR71055",
- "EUR71130",
- "EUR71061",
- "EUR71059",
- "EUR71121",
- "EUR71120",
- "EUR70968",
- "EUR80261",
- "EUR80263",
- "EUR71256",
- "EUR71254",
- "EUR71252",
- "EUR90475",
- "EUR80387"
- ],
- "dryRun": true
- },
- {
- "vid": "ARTE",
- "name": "Arte (@ Egg & Dart)",
- "to": "Therese@egg-and-dart.com",
- "n": 8,
- "skus": [
- "RTE50062",
- "RTE50065",
- "RTE50016",
- "RTE50084",
- "RTE50110",
- "RTE50110",
- "RTE50111",
- "DWC-Arte72143"
- ],
- "dryRun": true
- },
- {
- "vid": "RBL",
- "name": "Rebel Walls (via Gimmersta US)",
- "to": "support@gimmersta.com",
- "n": 8,
- "skus": [
- "DWRW74200",
- "DWRW74939",
- "DWRW74940",
- "DWRW75460",
- "DWRW72275",
- "DWRW74200",
- "DWRW75898",
- "DWRW76496"
- ],
- "dryRun": true
- },
- {
- "vid": "KOR",
- "name": "Koroseal",
- "to": "samples@koroseal.com, sshortino@koroseal.com",
- "n": 7,
- "skus": [
- "DWC281919",
- "DWC281921",
- "DWC281923",
- "DWK29236",
- "DWK29238",
- "DWC 67680\r\r",
- "DWC67451\r\r"
- ],
- "dryRun": true
- },
- {
- "vid": "MAYA",
- "name": "Maya Romanoff",
- "to": "ari@quintushome.com",
- "n": 1,
- "skus": [
- "DWYA60003"
- ],
- "dryRun": true
- },
- {
- "vid": "SAN",
- "name": "Sancar (AS Création) — inferred",
- "to": "sales@sancarwallcovering.com",
"n": 2,
- "skus": [
- "DWRC140044",
- "DWRC140045"
- ],
- "dryRun": true
+ "draftId": "r7522150672371779640",
+ "confirm": false
},
{
- "vid": "ANNA",
- "name": "Thibaut",
- "to": "cathy.dy@thibautdesign.com",
- "n": 1,
- "skus": [
- "DWTA65396"
- ],
- "dryRun": true
- },
- {
- "vid": "SANDB",
- "name": "Sandberg (via Gimmersta US)",
- "to": "support@gimmersta.com",
- "n": 3,
- "skus": [
- "DWSS72759",
- "DWSS72760",
- "DWSS72675"
- ],
- "dryRun": true
+ "vid": "YOR",
+ "name": "York",
+ "to": "RiveraH@yorkwall.com, orders@yorkwall.com",
+ "n": 6,
+ "draftId": "r3080370794725730872",
+ "confirm": false
},
{
"vid": "KRA",
"name": "Kravet",
"to": "matt.schoffman@kravet.com",
- "n": 4,
- "skus": [
- "DWKK128364",
- "DWKK135711",
- "DWKK101625",
- "DWKK130319"
- ],
- "dryRun": true
- },
- {
- "vid": "AS CREATION",
- "name": "Sancar (AS Création) — inferred",
- "to": "sales@sancarwallcovering.com",
- "n": 1,
- "skus": [
- "DWAC1014925"
- ],
- "dryRun": true
- },
- {
- "vid": "ARTE INTERNATIONAL",
- "name": "Arte (@ Egg & Dart)",
- "to": "Therese@egg-and-dart.com",
- "n": 1,
- "skus": [
- "DWAR320016"
- ],
- "dryRun": true
- },
- {
- "vid": "ANNA FRENCH",
- "name": "Thibaut",
- "to": "cathy.dy@thibautdesign.com",
- "n": 3,
- "skus": [
- "DWAT65056",
- "DWAF74869",
- "DWAT65538"
- ],
- "dryRun": true
- },
- {
- "vid": "ASTSAND",
- "name": "Astek",
- "to": "amy.d@astek.com",
- "n": 1,
- "skus": [
- "DWPX500016"
- ],
- "dryRun": true
- },
- {
- "vid": "CHIN",
- "name": "China Seas by Quadrille — inferred",
- "to": "Rhaiza@quadrilleinc.com",
"n": 2,
- "skus": [
- "DWCS914048",
- "DWCS913548"
- ],
- "dryRun": true
+ "draftId": "r5458844388935871402",
+ "confirm": false
},
{
"vid": "GREEN",
"name": "Greenland (bmwallpaper)",
"to": "sales@bmwallpaper.com",
- "n": 2,
- "skus": [
- "PWV10040",
- "GRS800010"
- ],
- "dryRun": true
- }
- ],
- "needsConfirm": [
- {
- "vid": "MOM",
- "name": "Momentum (→Hollywood, INTERNAL chase only)",
- "to": "customerservice@momtex.com",
- "n": 11,
- "skus": [
- "DUR72411",
- "XPV48335",
- "XWC53248",
- "XWT53318",
- "XWC53245",
- "XA666454",
- "XG566927",
- "XVB73013",
- "LLN6641",
- "XWY53100",
- "XWY53104"
- ],
- "reason": "main-email fallback — CONFIRM person vs recent emails before send"
- },
- {
- "vid": "INN",
- "name": "Innovations",
- "to": "hmartinez@innovationsusa.com",
"n": 1,
- "skus": [
- "XCG34342"
- ],
- "reason": "main-email fallback — CONFIRM person vs recent emails before send"
+ "draftId": "r6173246691603703575",
+ "confirm": false
},
{
- "vid": "DTEX",
- "name": "Designtex",
- "to": "ddawson@designtex.com",
- "n": 2,
- "skus": [
- "SUR21562",
- "SUR22101"
- ],
- "reason": "main-email fallback — CONFIRM person vs recent emails before send"
+ "vid": "SANDB",
+ "name": "Sandberg (via Gimmersta US)",
+ "to": "support@gimmersta.com",
+ "n": 1,
+ "draftId": "r-84462059661524742",
+ "confirm": false
},
{
- "vid": "VER",
- "name": "Versace (via Sancar)",
- "to": "sales@sancarwallcovering.com",
+ "vid": "BRE",
+ "name": "Brewster",
+ "to": "brewsterorders@brewp.com",
"n": 1,
- "skus": [
- "VER46222"
- ],
- "reason": "main-email fallback — CONFIRM person vs recent emails before send"
+ "draftId": "r-4409334545407435014",
+ "confirm": false
},
{
- "vid": "WOL",
- "name": "Wolf Gordon",
- "to": "samples@wolfgordon.com",
- "n": 2,
- "skus": [
- "DWWG533039",
- "DWWG533038"
- ],
- "reason": "main-email fallback — CONFIRM person vs recent emails before send"
+ "vid": "WQ",
+ "name": "WallQuest / Malibu",
+ "to": "orders@wallquest.com",
+ "n": 1,
+ "draftId": "r3077839332594228811",
+ "confirm": false
},
{
- "vid": "ROMO",
- "name": "Romo",
- "to": "fawn.bowen@romo.com",
+ "vid": "THIB",
+ "name": "Thibaut",
+ "to": "cathy.dy@thibautdesign.com",
"n": 1,
- "skus": [
- "ROP74578"
- ],
- "reason": "main-email fallback — CONFIRM person vs recent emails before send"
+ "draftId": "r3922288234510569275",
+ "confirm": false
}
],
+ "needsConfirm": [],
"skipped": [
- {
- "vid": "RAD",
- "n": 1,
- "reason": "no-chase vendor"
- },
- {
- "vid": "GD",
- "n": 1,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "JF",
- "n": 4,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "MISS",
- "n": 2,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "SCH",
- "n": 12,
- "reason": "no-chase vendor"
- },
- {
- "vid": "ARM",
- "n": 3,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "PAUL",
- "n": 3,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "TBD",
- "n": 7,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "GRA",
- "n": 3,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "RCV",
- "n": 2,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "MINI",
- "n": 1,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "NLXL",
- "n": 1,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "REID",
- "n": 1,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "HOLLY",
- "n": 1,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "COW",
- "n": 8,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "CARLISLE",
- "n": 2,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "WAT",
- "n": 1,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "LAR",
- "n": 2,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "SIR",
- "n": 6,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "FTR",
- "n": 4,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "ULR",
- "n": 1,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "EGG",
- "n": 1,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "AMER",
- "n": 4,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "\r",
- "n": 1,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "COOR",
- "n": 1,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "FOL",
- "n": 2,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "HYG",
- "n": 1,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "TWI",
- "n": 3,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "CHRISTIAN LACROIX EUROPE",
- "n": 2,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "1838 WALLCOVERINGS",
- "n": 1,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "PHILLIPE ROMANO",
- "n": 1,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "SCHUMACHER",
- "n": 18,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "CARL",
- "n": 1,
- "reason": "no sample email/account on file"
- },
- {
- "vid": "SPN",
- "n": 11,
- "reason": "no-chase vendor"
- },
- {
- "vid": "POI",
- "n": 1,
- "reason": "no sample email/account on file"
- },
{
"vid": "BM",
"n": 3,
diff --git a/scripts/contact-research.mjs b/scripts/contact-research.mjs
new file mode 100644
index 0000000..6e813e4
--- /dev/null
+++ b/scripts/contact-research.mjs
@@ -0,0 +1,48 @@
+'use strict';
+// READ-ONLY Gmail domain research for skipped sample-followup vendor vids.
+// Searches info@ mailbox by vendor DOMAIN (not name), returns recent human counterparts.
+import http from 'node:http';
+import { readFileSync } from 'node:fs';
+
+const GEORGE = { host: '127.0.0.1', port: 9850 };
+function georgeAuth() {
+ const files = ['/Users/macstudio3/Projects/Designer-Wallcoverings/DW-MCP/.env', '/Users/macstudio3/Projects/george-gmail/.env'];
+ const genv = (k) => { for (const f of files) { try { const m = readFileSync(f, 'utf8').match(new RegExp('^' + k + '=(.+)$', 'm')); if (m) return m[1].trim(); } catch {} } return ''; };
+ let a = genv('GEORGE_BASIC_AUTH'); if (!a.includes(':')) a = 'admin:' + genv('GEORGE_BASIC_AUTH_PASS');
+ return 'Basic ' + Buffer.from(a).toString('base64');
+}
+function gGet(path) {
+ return new Promise((resolve, reject) => {
+ const req = http.request({ ...GEORGE, path, method: 'GET', headers: { Authorization: georgeAuth() } }, (r) => {
+ let b = ''; r.on('data', (d) => b += d); r.on('end', () => { try { resolve(JSON.parse(b)); } catch { resolve(null); } });
+ });
+ req.on('error', reject); req.end();
+ });
+}
+async function search(q) {
+ const res = await gGet(`/api/info/search?q=${encodeURIComponent(q)}`).catch(() => null);
+ const msgs = res && (res.messages || res.results || (Array.isArray(res) ? res : null));
+ return Array.isArray(msgs) ? msgs : [];
+}
+
+// vid -> {vendor, domain(s)} (domain from vendor_registry / known DW mappings)
+const MAP = JSON.parse(process.argv[2] || '{}');
+
+const run = async () => {
+ const out = {};
+ for (const [vid, info] of Object.entries(MAP)) {
+ const domains = info.domains || [];
+ const rec = { vendor: info.vendor, domains, from_hits: [], to_hits: [] };
+ for (const d of domains) {
+ // messages we RECEIVED from anyone @domain (who they route through)
+ const froms = await search(`from:@${d}`);
+ for (const m of froms.slice(0, 5)) rec.from_hits.push({ from: m.from || m.sender || '', subject: (m.subject || '').slice(0, 90), date: m.date || m.internalDate || '', snippet: (m.snippet || '').slice(0, 120) });
+ // messages we SENT to anyone @domain (who we chase)
+ const tos = await search(`to:@${d}`);
+ for (const m of tos.slice(0, 5)) rec.to_hits.push({ to: m.to || '', subject: (m.subject || '').slice(0, 90), date: m.date || m.internalDate || '' });
+ }
+ out[vid] = rec;
+ }
+ console.log(JSON.stringify(out, null, 2));
+};
+run().catch((e) => { console.error('research error:', e.message); process.exit(1); });
diff --git a/scripts/scheduled-run.mjs b/scripts/scheduled-run.mjs
index 1a81d9b..b432346 100644
--- a/scripts/scheduled-run.mjs
+++ b/scripts/scheduled-run.mjs
@@ -8,7 +8,7 @@
// run only sends the follow-ups that are NEW since the last run.
//
// Rules enforced:
-// • age ≥ 10 days, NO upper cap (Steve 8/20: chase everything 10+ days, not the 10–60 band) • Date WP Sample Sent empty (still outstanding)
+// • entered in a rolling 3-day band ending exactly at the 10-day mark (Steve 8/20 REVISED: "10 days old exactly, for the past 3 days" = only memos that JUST crossed 10d, NOT the whole back-catalog) • Date WP Sample Sent empty (still outstanding)
// • Date Email Sent to Vendor after 10 Days EMPTY → the "do not duplicate" guard
// • vendor must have a sample email + account (else skip + report — can't send)
//
@@ -29,13 +29,18 @@ const ROOT = join(__dir, '..');
const { compose } = require(join(ROOT, 'lib', 'compose.js'));
const SEND = process.argv.includes('--send');
+// Steve 8/20: DRAFT mode — create a Gmail draft per vendor in info@ Drafts for human review,
+// instead of sending directly. The draft IS the human gate; Steve sends from Drafts.
+const DRAFT = process.argv.includes('--draft');
const DB = 'WALLPAPER';
const LAYOUT = 'Report for old memo samples';
const FIELD_SENT = 'Date Email Sent to Vendor after 10 Days';
-// Steve 8/20: 10-day threshold, drop the 60-day dead-lead cap — chase everything ≥10d not arrived.
-// True unlimited breaks the FileMaker date-range find (a lo-date >~5yr old returns 0 records), so
-// 1825d (5yr) is the widest window that reliably returns data = effectively "no cap" for sample memos.
-const MIN_AGE_DAYS = 10, MAX_AGE_DAYS = Number(process.env.MAXAGE || 1825);
+// Steve 8/20 (REVISED): chase ONLY memos that JUST crossed the 10-day mark — a rolling 3-day
+// catch-up band, NOT the whole back-catalog. "10 days old exactly, for the past 3 days" → on 8/20
+// that is entered dates 8/8, 8/9, 8/10. The open-ended `...hi` was pulling every old memo; this
+// bounds it to [today-10-(CATCHUP-1) .. today-10]. Each daily run picks up the one new day; the
+// 3-day width is a safety net so a skipped run still catches the memos it would have missed.
+const MIN_AGE_DAYS = 10, CATCHUP_DAYS = Number(process.env.CATCHUP || 3);
const SHIP_TO = { name: 'Designer Wallcoverings', line1: '15442 Ventura Blvd. #102', city_state_zip: 'Sherman Oaks, CA 91403', phone: '1-888-373-4564' };
// --- FileMaker client (reuse the connector; creds from ~/.claude.json) ---
@@ -51,11 +56,11 @@ const fm = await import('/Users/macstudio3/Projects/filemaker-mcp/src/fm-client.
const pad = (n) => String(n).padStart(2, '0');
const fmtDate = (d) => `${pad(d.getMonth() + 1)}/${pad(d.getDate())}/${d.getFullYear()}`;
function windowRange(today = new Date()) {
+ // hi = the memo that hits 10 days old TODAY (entered today-10).
+ // lo = the memo that hit 10 days old CATCHUP_DAYS-1 days ago (entered today-10-(CATCHUP-1)).
+ // On 8/20 with CATCHUP=3 → hi=8/10, lo=8/8 → FileMaker range "08/08/2026...08/10/2026".
const hi = new Date(today); hi.setDate(hi.getDate() - MIN_AGE_DAYS);
- // Steve 8/20: no upper age cap. Use an OPEN-ENDED low bound (FileMaker `...hi`
- // = "on or before hi") so we chase EVERYTHING at least MIN_AGE_DAYS old.
- if (MAX_AGE_DAYS >= 3650) return `...${fmtDate(hi)}`;
- const lo = new Date(today); lo.setDate(lo.getDate() - MAX_AGE_DAYS);
+ const lo = new Date(today); lo.setDate(lo.getDate() - MIN_AGE_DAYS - (CATCHUP_DAYS - 1));
return `${fmtDate(lo)}...${fmtDate(hi)}`;
}
const norm = (s) => String(s || '').toLowerCase().trim();
@@ -103,6 +108,18 @@ function georgeSend(payload) {
req.write(body); req.end();
});
}
+// Create a Gmail draft in info@ Drafts (POST /api/drafts — no send-approval token needed).
+function georgeDraft(payload) {
+ const { auth } = georgeCreds();
+ const body = JSON.stringify(payload);
+ return new Promise((resolve) => {
+ const req = http.request({ host: '127.0.0.1', port: 9850, path: '/api/drafts', method: 'POST',
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), Authorization: auth } },
+ (r) => { let b = ''; r.on('data', (d) => b += d); r.on('end', () => { try { const j = JSON.parse(b); resolve({ ok: !!j.success, id: j.draftId || '', detail: b.slice(0, 200) }); } catch { resolve({ ok: false, detail: b.slice(0, 200) }); } }); });
+ req.on('error', (e) => resolve({ ok: false, detail: e.message }));
+ req.write(body); req.end();
+ });
+}
const run = async () => {
const win = windowRange();
@@ -130,14 +147,29 @@ const run = async () => {
}
const cmap = vidContactMap();
- const sent = [], skipped = [], needsConfirm = [];
+ // DRAFT dedup ledger — drafting ≠ sending, so we do NOT false-stamp FileMaker; we track which
+ // recordIds already have a draft locally so the daily run never piles up duplicate drafts.
+ const LEDGER = 'data/draft-ledger.json';
+ const draftLedger = DRAFT ? readJSON(LEDGER, {}) : {};
+ const sent = [], skipped = [], needsConfirm = [], drafted = [];
for (const [vid, rows] of Object.entries(byVid)) {
const c = cmap[vid];
if (!c || c.noChase) { skipped.push({ vid, n: rows.length, reason: c?.noChase ? 'no-chase vendor' : 'no sample email/account on file' }); continue; }
+ // In SEND mode, an unconfirmed (resolved/fallback) recipient is HELD — never fires. In DRAFT
+ // mode the draft itself IS the review, so everything is drafted for Steve to check + send.
+ if (c.needs_confirm && !DRAFT) { needsConfirm.push({ vid, name: c.name, to: (c.sample_email || c.main_email), n: rows.length, skus: rows.map((r) => r.sku), reason: 'main-email fallback — CONFIRM person vs recent emails before send' }); continue; }
+ if (DRAFT) {
+ const fresh = rows.filter((r) => !draftLedger[r.recordId]); // only newly-outstanding SKUs
+ if (!fresh.length) continue; // already drafted — no dup
+ const draft = compose({ name: c.name, account_number: c.account_number, sample_email: c.sample_email, main_email: c.main_email, ship_to: SHIP_TO }, fresh.map((r) => ({ mfr: r.mfr })));
+ const res = await georgeDraft({ account: 'info', to: draft.to, subject: draft.subject, body: draft.html });
+ if (res.ok) {
+ for (const r of fresh) draftLedger[r.recordId] = today; // dedup marker (NOT a FileMaker stamp)
+ drafted.push({ vid, name: c.name, to: draft.to, n: fresh.length, draftId: res.id, confirm: !!c.needs_confirm });
+ } else { skipped.push({ vid, n: fresh.length, reason: 'George draft failed: ' + res.detail }); }
+ continue;
+ }
const draft = compose({ name: c.name, account_number: c.account_number, sample_email: c.sample_email, main_email: c.main_email, ship_to: SHIP_TO }, rows.map((r) => ({ mfr: r.mfr })));
- // Steve 8/20: a fallback/resolved recipient (main_email, not a hard sample desk) NEVER
- // auto-sends — the person must be confirmed against recent emails first. Hold it out.
- if (c.needs_confirm) { needsConfirm.push({ vid, name: c.name, to: draft.to, n: rows.length, skus: rows.map((r) => r.sku), reason: 'main-email fallback — CONFIRM person vs recent emails before send' }); continue; }
if (!SEND) { sent.push({ vid, name: c.name, to: draft.to, n: rows.length, skus: rows.map((r) => r.sku), dryRun: true }); continue; }
const res = await georgeSend({ account: 'info', to: draft.to, subject: draft.subject, body: draft.html });
if (res.ok) {
@@ -147,16 +179,23 @@ const run = async () => {
skipped.push({ vid, n: rows.length, reason: 'George send failed: ' + res.detail });
}
}
+ if (DRAFT) writeFileSync(join(ROOT, LEDGER), JSON.stringify(draftLedger, null, 2));
- const report = { ranAt: new Date().toISOString(), mode: SEND ? 'SEND' : 'DRY-RUN', window: win, stampDate: today,
- vendorsSent: sent.length, vendorsSkipped: skipped.length, vendorsNeedsConfirm: needsConfirm.length, sent, needsConfirm, skipped };
+ const mode = DRAFT ? 'DRAFT' : SEND ? 'SEND' : 'DRY-RUN';
+ const report = { ranAt: new Date().toISOString(), mode, window: win, stampDate: today,
+ vendorsSent: sent.length, vendorsDrafted: drafted.length, vendorsSkipped: skipped.length, vendorsNeedsConfirm: needsConfirm.length, sent, drafted, needsConfirm, skipped };
mkdirSync(join(ROOT, 'data', 'runs'), { recursive: true });
writeFileSync(join(ROOT, 'data', 'runs', `scheduled-${today.replace(/\//g, '-')}.json`), JSON.stringify(report, null, 2));
- console.log(`Scheduled follow-up — ${report.mode} · window ${win} (${MIN_AGE_DAYS}-${MAX_AGE_DAYS}d) · dedup=FIELD empty\n`);
- console.log(`Would ${SEND ? 'HAVE SENT' : 'send'} to ${sent.length} vendor(s):`);
- sent.forEach((s) => console.log(` ${SEND ? '✓' : '·'} ${s.name.padEnd(30)} [${s.vid}] → ${s.to} (${s.n} SKU${s.n > 1 ? 's' : ''})`));
- if (needsConfirm.length) { console.log(`\n⚠ ${needsConfirm.length} main-email fallback(s) — HELD for person-confirmation (never auto-sent):`); needsConfirm.forEach((s) => console.log(` ? ${s.name.padEnd(30)} [${s.vid}] → ${s.to} (${s.n} SKU${s.n > 1 ? 's' : ''}) — ${s.reason}`)); }
+ console.log(`Scheduled follow-up — ${mode} · window ${win} (>=${MIN_AGE_DAYS}d, ${CATCHUP_DAYS}-day catch-up band)\n`);
+ if (DRAFT) {
+ console.log(`Created ${drafted.length} draft(s) in info@ Drafts for review:`);
+ drafted.forEach((s) => console.log(` ✎ ${s.name.padEnd(30)} [${s.vid}] → ${s.to} (${s.n} SKU${s.n > 1 ? 's' : ''})${s.confirm ? ' [CONFIRM recipient]' : ''}`));
+ } else {
+ console.log(`Would ${SEND ? 'HAVE SENT' : 'send'} to ${sent.length} vendor(s):`);
+ sent.forEach((s) => console.log(` ${SEND ? '✓' : '·'} ${s.name.padEnd(30)} [${s.vid}] → ${s.to} (${s.n} SKU${s.n > 1 ? 's' : ''})`));
+ if (needsConfirm.length) { console.log(`\n⚠ ${needsConfirm.length} main-email fallback(s) — HELD for person-confirmation (never auto-sent):`); needsConfirm.forEach((s) => console.log(` ? ${s.name.padEnd(30)} [${s.vid}] → ${s.to} (${s.n} SKU${s.n > 1 ? 's' : ''}) — ${s.reason}`)); }
+ }
if (skipped.length) { console.log(`\nSkipped ${skipped.length} (no contact / no-chase / error):`); skipped.forEach((s) => console.log(` - [${s.vid}] ${s.n} SKU(s): ${s.reason}`)); }
console.log(`\n→ data/runs/scheduled-${today.replace(/\//g, '-')}.json`);
};
← 40a48ce auto-data-snapshot: 2026-08-20T10:13:33 (2 data files) — dat
·
back to Sample Followup Sweep
·
sample-sender: draft mode + 3-day exact-10d catch-up window b62f3d9 →