[object Object]

← 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

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 →