[object Object]

← back to Sample Followup Sweep

watch-sent-stamp: fast Sent-poller stamps FileMaker 2nd Request (+Date-Email-Sent) when a follow-up actually leaves

9f0c85d81f8ceed2e325837a99c5902412d040c4 · 2026-08-20 11:19:28 -0700 · Steve

Files touched

Diff

commit 9f0c85d81f8ceed2e325837a99c5902412d040c4
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Aug 20 11:19:28 2026 -0700

    watch-sent-stamp: fast Sent-poller stamps FileMaker 2nd Request (+Date-Email-Sent) when a follow-up actually leaves
---
 scripts/watch-sent-stamp.mjs | 76 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 76 insertions(+)

diff --git a/scripts/watch-sent-stamp.mjs b/scripts/watch-sent-stamp.mjs
new file mode 100644
index 0000000..f4aca9c
--- /dev/null
+++ b/scripts/watch-sent-stamp.mjs
@@ -0,0 +1,76 @@
+#!/usr/bin/env node
+// FAST Sent-poller (Steve 8/20): watch info@ Sent for sample follow-ups that ACTUALLY LEFT, and
+// stamp FileMaker "2nd Request" (+ the proven "Date Email Sent to Vendor after 10 Days") on that
+// vendor's outstanding memos. Drafting never stamps — only a real send does. Idempotent via a ledger.
+//   node scripts/watch-sent-stamp.mjs           # one pass
+//   node scripts/watch-sent-stamp.mjs --loop    # poll every POLL_SECS (default 45s)
+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 LEDGER = join(ROOT, 'data/stamp-ledger.json');
+const DB = 'WALLPAPER', LAYOUT = 'Report for old memo samples';
+const FIELD_SENT = 'Date Email Sent to Vendor after 10 Days';
+const FIELD_2ND = '2nd Request';
+const WIN = process.env.WIN || '08/06/2026...08/10/2026';
+const POLL_SECS = Number(process.env.POLL_SECS || 45);
+
+// recipient (or a fragment) → the FileMaker vids that vendor's memos live under
+const TO_VIDS = [
+  ['bmwallpaper.com', ['BM', 'GREEN']], ['romo.com', ['ROMO']], ['brewp.com', ['BRE']],
+  ['oalusa.com', ['OSB']], ['yorkwall.com', ['YOR']], ['kravet.com', ['KRA']],
+  ['gimmersta.com', ['SANDB']], ['wallquest.com', ['WQ']], ['thibautdesign.com', ['THIB']],
+  ['egg-and-dart.com', ['ARTE']],
+];
+
+const cfg = JSON.parse(readFileSync(join(homedir(), '.claude.json'), 'utf8'));
+const fenv = cfg?.mcpServers?.filemaker?.env || {};
+for (const k of ['FM_CLOUD_HOST', 'FM_CLARIS_EMAIL', 'FM_CLARIS_PASSWORD']) process.env[k] = fenv[k];
+process.env.FM_READONLY = '0';
+const fm = await import('/Users/macstudio3/Projects/filemaker-mcp/src/fm-client.js');
+
+const readJSON = (f, d) => { try { return JSON.parse(readFileSync(f, 'utf8')); } catch { return d; } };
+const pad = n => String(n).padStart(2, '0'); const today = (d => `${pad(d.getMonth() + 1)}/${pad(d.getDate())}/${d.getFullYear()}`)(new Date());
+
+// George /api/search (HTTP) — find follow-ups that actually left info@
+const genv = k => { for (const f of ['/Users/macstudio3/Projects/Designer-Wallcoverings/DW-MCP/.env', '/Users/macstudio3/Projects/george-gmail/.env']) { try { const m = readFileSync(f, 'utf8').match(new RegExp('^' + k + '=(.+)$', 'm')); if (m) return m[1].trim(); } catch {} } return ''; };
+let auth = genv('GEORGE_BASIC_AUTH'); if (!auth.includes(':')) auth = 'admin:' + genv('GEORGE_BASIC_AUTH_PASS');
+auth = 'Basic ' + Buffer.from(auth).toString('base64');
+function gsearch(q) { return new Promise(res => { const p = '/api/search?account=info&maxResults=30&q=' + encodeURIComponent(q); const rq = http.request({ host: '127.0.0.1', port: 9850, path: p, method: 'GET', headers: { Authorization: auth } }, r => { let d = ''; r.on('data', x => d += x); r.on('end', () => { try { res(JSON.parse(d).messages || JSON.parse(d) || []); } catch { res([]); } }); }); rq.on('error', () => res([])); rq.end(); }); }
+
+async function stampVids(vids) {
+  // outstanding (Date WP Sample Sent empty) for these vids in the window → recordIds → stamp
+  let n = 0;
+  for (const vid of vids) {
+    const rows = (await fm.findRecords(DB, 'REPORT ON SAMPLES ORDERED', { 'Date WP Sample Sent': '=', vid, 'today for client': WIN }, { limit: 200 }).catch(() => ({ records: [] }))).records;
+    for (const r of rows) {
+      // primary: 2nd Request; also stamp the proven Date-Email-Sent field. Separate calls so one bad field can't block the other.
+      await fm.updateRecord(DB, LAYOUT, r.recordId, { [FIELD_2ND]: today }, { dryRun: false }).catch(() => {});
+      await fm.updateRecord(DB, LAYOUT, r.recordId, { [FIELD_SENT]: today }, { dryRun: false }).catch(() => {});
+      n++;
+    }
+  }
+  return n;
+}
+
+async function pass() {
+  const seen = readJSON(LEDGER, {});
+  const msgs = await gsearch('in:sent subject:"Sample Follow-Up — Outstanding Memos" newer_than:1d');
+  let acted = 0;
+  for (const m of msgs) {
+    if (seen[m.id]) continue;
+    const to = String(m.to || m.snippet || '').toLowerCase();
+    const hit = TO_VIDS.find(([frag]) => to.includes(frag.toLowerCase()));
+    if (!hit) { seen[m.id] = { skip: 'no vendor match', to: m.to || '' }; continue; }
+    const stamped = await stampVids(hit[1]);
+    seen[m.id] = { stamped, vids: hit[1], to: m.to || '', at: new Date().toISOString() };
+    acted += stamped;
+    console.log(`  ✓ SENT → ${m.to || hit[0]} → stamped ${stamped} memo(s) [${hit[1].join('+')}] 2nd Request=${today}`);
+  }
+  writeFileSync(LEDGER, JSON.stringify(seen, null, 2));
+  if (!acted) console.log(`  (no new sent follow-ups to stamp) — ${new Date().toLocaleTimeString()}`);
+  return acted;
+}
+
+const LOOP = process.argv.includes('--loop');
+console.log(`watch-sent-stamp — window ${WIN} — ${LOOP ? `LOOP every ${POLL_SECS}s` : 'one pass'}`);
+if (!LOOP) { await pass(); process.exit(0); }
+for (;;) { try { await pass(); } catch (e) { console.error('pass err:', e.message); } await new Promise(r => setTimeout(r, POLL_SECS * 1000)); }

← 5df1ebe reconciled drafter: window 8/6-8/10 (fill-in since last send  ·  back to Sample Followup Sweep  ·  auto-data-snapshot: 2026-08-20T11:21:15 (1 data files) — dat 55d6a33 →