[object Object]

← back to Sample Followup Sweep

yoloforever c1: ledger + drafts-ready state

4c4c19c2640626e2834d1c611a3df7820b220a8c · 2026-08-15 08:15:12 -0700 · Steve Abrams

Files touched

Diff

commit 4c4c19c2640626e2834d1c611a3df7820b220a8c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Aug 15 08:15:12 2026 -0700

    yoloforever c1: ledger + drafts-ready state
---
 server.js | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/server.js b/server.js
index 8a7b0b0..3f41690 100644
--- a/server.js
+++ b/server.js
@@ -103,6 +103,12 @@ const server = http.createServer(async (req, res) => {
     let payload;
     if (st) payload = { account: 'info', to: st.to, subject: st.subject, body: st.body };
     else { const v = fleet().vendors.find(x => x.slug === b.slug); if (!v) return send(res, 404, { error: 'no vendor' }); const d = draftFor(v, c); payload = { account: 'info', to: d.to, subject: d.subject, body: d.html }; }
+    // Resend guard (Cody FIX FIRST): refuse if this vendor's recipients were already emailed, unless {force:true}.
+    if (!b.force) {
+      const be = (sent().byEmail) || {};
+      const addrs = String(payload.to).split(',').map(a => a.trim().toLowerCase()).filter(a => a.includes('@'));
+      if (addrs.length && addrs.every(a => be[a])) return send(res, 409, { error: 'already sent to this vendor — click again to force-resend', to: payload.to, alreadySent: true });
+    }
     const genv = (k) => { try { const m = fs.readFileSync('/Users/macstudio3/Projects/george-gmail/.env', 'utf8').match(new RegExp('^' + k + '=(.+)$', 'm')); return m ? m[1].trim() : ''; } catch (e) { return ''; } };
     let gauth = genv('GEORGE_BASIC_AUTH'); if (!gauth.includes(':')) gauth = 'admin:' + genv('GEORGE_BASIC_AUTH_PASS');
     const token = genv('GEORGE_EXTERNAL_SEND_TOKEN');
@@ -261,12 +267,14 @@ function render(){const t=document.getElementById('rows');t.innerHTML='';for(con
  dr.innerHTML='<td></td><td colspan=9>'+itemsTable(v)+'</td>';t.appendChild(dr);}
  t.querySelectorAll('input.em,input.ac').forEach(i=>i.addEventListener('change',saveContact));
  t.querySelectorAll('button[data-prev]').forEach(b=>b.addEventListener('click',()=>preview(b.dataset.prev)));
- t.querySelectorAll('button[data-send]').forEach(b=>b.addEventListener('click',async e=>{const slug=b.dataset.send;const v=S.fleet.vendors.find(x=>x.slug===slug);const c=S.contacts[slug]||{};
+ t.querySelectorAll('button[data-send]').forEach(b=>b.addEventListener('click',async e=>{const slug=b.dataset.send;const c=S.contacts[slug]||{};
   if(!confirm('Send the follow-up letter now to '+(c.sample_email||slug)+' ?'))return;
+  const fire=async(force)=>{const r=await fetch('/api/send-one',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({slug,force})});return[r.status,await r.json()];};
   b.disabled=true;b.textContent='sending…';
-  try{const r=await fetch('/api/send-one',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({slug})});const d=await r.json();
-   if(d.ok){await load();}else{b.disabled=false;b.textContent='send';alert('Send failed: '+(d.error||'unknown')+(d.detail?'\\n'+d.detail:''));}}
-  catch(err){b.disabled=false;b.textContent='send';alert('Send error: '+err.message);}}));
+  try{let[code,d]=await fire(false);
+   if(code===409){if(!confirm('⚠ Already emailed '+(d.to||slug)+'. Send a SECOND copy anyway?')){await load();return;} [code,d]=await fire(true);}
+   if(d&&d.ok){await load();}else{alert('Send failed: '+((d&&d.error)||'unknown')+((d&&d.detail)?'\\n'+d.detail:''));await load();}}
+  catch(err){alert('Send error: '+err.message);await load();}}));
  t.querySelectorAll('button.exp').forEach(b=>b.addEventListener('click',e=>{const dr=e.target.closest('tr').nextElementSibling;const open=dr.style.display==='none';dr.style.display=open?'table-row':'none';e.target.textContent=(open?'▼ ':'▶ ')+e.target.textContent.slice(2);}));}
 async function saveContact(e){const el=e.target;const slug=el.dataset.slug;const row=[...document.querySelectorAll('input[data-slug="'+slug+'"]')];
  const em=row.find(x=>x.dataset.f==='sample_email').value.trim();const ac=row.find(x=>x.dataset.f==='account_number').value.trim();

← 46f2853 auto-data-snapshot: 2026-08-15T08:12:28 (1 data files) — dat  ·  back to Sample Followup Sweep  ·  yoloforever c2: research vendor contacts (web+registry+sent) e169d6d →