← back to Dw Pitch Followup
yolo/contrarian fixes: build-time TODAY (fix aging bug) + 30-day double-email frequency guard at send (list1∩list2 dedup) with 'Send anyway' override
ff1955812b58fa79950c4def3308d8057a2a66af · 2026-07-07 16:43:52 -0700 · Steve Abrams
Files touched
M public/index.htmlM scripts/build-lists.jsM server.js
Diff
commit ff1955812b58fa79950c4def3308d8057a2a66af
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Jul 7 16:43:52 2026 -0700
yolo/contrarian fixes: build-time TODAY (fix aging bug) + 30-day double-email frequency guard at send (list1∩list2 dedup) with 'Send anyway' override
---
public/index.html | 11 +++++++++--
scripts/build-lists.js | 2 +-
server.js | 14 ++++++++++++++
3 files changed, 24 insertions(+), 3 deletions(-)
diff --git a/public/index.html b/public/index.html
index ca26ae7..4085cd9 100644
--- a/public/index.html
+++ b/public/index.html
@@ -375,8 +375,15 @@ async function sendActive(btn){
if(!confirm(`Send this letter NOW?\n\nFROM: info@designerwallcoverings.com\nTO: ${to}\nSUBJ: ${subj}\n\nThis emails the recipient immediately.`))return;
const old=btn.textContent; btn.disabled=true; btn.textContent='sending…'; stat.style.color='var(--mut)'; stat.textContent='';
try{
- const res=await fetch(API('/api/send'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({to,subject:subj,body,samples:Array.isArray(r.samples_sent)?r.samples_sent:[],list,account:r.account,company:(list==='list3'?r.vendor:r.company)})});
- const j=await res.json(); if(j.error)throw new Error(j.blocked?`blocked by send-guard: ${j.error}`:j.error);
+ const payloadBase={to,subject:subj,body,samples:Array.isArray(r.samples_sent)?r.samples_sent:[],list,account:r.account,company:(list==='list3'?r.vendor:r.company)};
+ let res=await fetch(API('/api/send'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payloadBase)});
+ let j=await res.json();
+ if(j.alreadySent){ // double-email guard tripped — let Steve override
+ btn.disabled=false; if(!confirm(`This client was already emailed ${new Date(j.lastSent).toLocaleDateString()} (within 30 days).\n\nSend AGAIN anyway?`)){ btn.textContent=old; stat.style.color='var(--amber)'; stat.textContent='skipped — already emailed recently'; return; }
+ btn.disabled=true; btn.textContent='sending…';
+ res=await fetch(API('/api/send'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({...payloadBase,force:true})}); j=await res.json();
+ }
+ if(j.error)throw new Error(j.blocked?`blocked by send-guard: ${j.error}`:j.error);
const when=j.sentAt||new Date().toISOString();
btn.textContent='✓ Sent'; stat.style.color='var(--green)'; stat.textContent=`✓ emailed ${fmtWhen(when)}${j.inThread?' · in-thread':''}`;
setLS('sent:'+list+':'+k,{at:Date.parse(when)||Date.now(),messageId:j.messageId||''});
diff --git a/scripts/build-lists.js b/scripts/build-lists.js
index 8c51ec7..81a1b08 100644
--- a/scripts/build-lists.js
+++ b/scripts/build-lists.js
@@ -17,7 +17,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DATA_DIR = path.join(__dirname, '..', 'data');
fs.mkdirSync(DATA_DIR, { recursive: true });
-const TODAY = new Date('2026-06-30T12:00:00');
+const TODAY = process.env.PITCH_TODAY ? new Date(process.env.PITCH_TODAY) : new Date(); // build time, not a hardcode (fixes negative-age bug)
const LOOKBACK_SENT_DAYS = Number(process.env.LOOKBACK_SENT_DAYS || 120); // List 1 recency window
const INVOICE_SINCE = process.env.INVOICE_SINCE || '03/01/2026'; // List 2: last ~4 months
const FIRM_ORDER_MIN = Number(process.env.FIRM_ORDER_MIN || 50); // List 2: TOTAL 1 > this = firm order
diff --git a/server.js b/server.js
index df59d32..b9e2d96 100644
--- a/server.js
+++ b/server.js
@@ -374,6 +374,20 @@ app.post('/api/send', async (req, res) => {
if (!to || !subject || !body) return res.status(400).json({ error: 'to, subject and body are required' });
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(String(to).trim())) return res.status(400).json({ error: `not a valid recipient email: ${to}` });
+ // FREQUENCY GUARD: the same client can appear in both list1 and list2, so guard against
+ // double-emailing — block a repeat send to the same address within 30 days unless force:true.
+ try {
+ if (!req.body.force && fs.existsSync(SENT_LOG)) {
+ const cutoff = Date.now() - 30 * 86400000, toLC = String(to).trim().toLowerCase();
+ for (const l of fs.readFileSync(SENT_LOG, 'utf8').split('\n')) {
+ if (!l.trim()) continue;
+ try { const o = JSON.parse(l); if (String(o.to || '').toLowerCase() === toLC && Date.parse(o.ts) > cutoff) {
+ return res.status(409).json({ error: `already emailed ${new Date(o.ts).toLocaleDateString()} (within 30 days) — use “Send anyway” to override`, alreadySent: true, lastSent: o.ts });
+ } } catch { /* skip bad line */ }
+ }
+ }
+ } catch { /* guard must never crash a send */ }
+
// Best-effort: find the newest message in the info@ mailbox for this address to thread into.
let replyToMessageId = null;
try {
← d191c5c Auto-swap to a thank-you letter when client has a BOOKED ord
·
back to Dw Pitch Followup
·
Stop leaking internal 'From job: dw-pitch-followup' banner o cc4f468 →