[object Object]

← back to Dw Pitch Followup

Fix sticking Send button (reset #pSend/#pRegen/#pCopy on panel open); stamp emailed date/time in panel+card+sendstat; keep permanent sent-log.jsonl of every email + /api/sent + 'Sent log' viewer

b6e57378db34c77e2b528afd498cd9d936ff07c3 · 2026-07-07 15:44:58 -0700 · Steve Abrams

Files touched

Diff

commit b6e57378db34c77e2b528afd498cd9d936ff07c3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jul 7 15:44:58 2026 -0700

    Fix sticking Send button (reset #pSend/#pRegen/#pCopy on panel open); stamp emailed date/time in panel+card+sendstat; keep permanent sent-log.jsonl of every email + /api/sent + 'Sent log' viewer
---
 public/index.html | 38 +++++++++++++++++++++++++++++++++-----
 server.js         | 22 ++++++++++++++++++++--
 2 files changed, 53 insertions(+), 7 deletions(-)

diff --git a/public/index.html b/public/index.html
index f3898b4..a6a1864 100644
--- a/public/index.html
+++ b/public/index.html
@@ -85,6 +85,7 @@
   .inv{margin:2px 0} .inv a{color:var(--gold)}
   .lastcorr{color:var(--mut)} .lastcorr.found{color:var(--green)}
   .lifebig{font-size:15px;color:var(--green);font-weight:700}
+  body.log-mode .pletter{display:none}
   .pletter .subj{font-size:12.5px;color:var(--gold);margin:6px 0 5px}
   textarea{width:100%;min-height:250px;background:#15120f;color:var(--ink);border:1px solid var(--line);border-radius:7px;padding:9px;font:13px/1.5 ui-monospace,Menlo,monospace;resize:vertical}
   .ctxnote{font-size:11.5px;color:var(--mut);margin:5px 0}
@@ -114,6 +115,7 @@
   <span><label title="OFF hides manufacturer numbers"><input type="checkbox" id="showMfr" /> mfr #</label></span>
   <span class="spacer"></span>
   <span class="selcount" id="selcount">0 selected</span>
+  <button class="btn" id="sentBtn">📤 Sent log</button>
   <button class="btn" id="copyBtn">Copy selected</button>
   <button class="btn gold" id="exportBtn">Export ▾</button>
   <span class="meta" id="built"></span>
@@ -286,6 +288,7 @@ function closePanel(){document.getElementById('panel').classList.remove('open');
 
 function openPanelFor(card,{generate=false}={}){
   const r=card._row,list=card._list;
+  document.body.classList.remove('log-mode');
   document.querySelectorAll('.card.active').forEach(c=>c.classList.remove('active')); card.classList.add('active');
   active={card,row:r,list,seed:1};
   const nm=list==='list3'?r.vendor:r.company;
@@ -294,7 +297,14 @@ function openPanelFor(card,{generate=false}={}){
   document.getElementById('pCtx').innerHTML=panelContextHTML(list,r);
   // reset letter area
   document.getElementById('pSubj').textContent=''; document.getElementById('pNote').textContent=''; document.getElementById('pNote').className='ctxnote';
-  document.getElementById('pBody').value=''; document.getElementById('pVariant').textContent=''; document.getElementById('pSendStat').textContent='';
+  document.getElementById('pBody').value=''; document.getElementById('pVariant').textContent=''; const pStat=document.getElementById('pSendStat'); pStat.textContent=''; pStat.style.color='var(--mut)';
+  // reset every action button so state never sticks from the previous client
+  const pSend=document.getElementById('pSend'); pSend.disabled=false; pSend.textContent='✉ Send now (info@)';
+  const pCopy=document.getElementById('pCopy'); pCopy.textContent='Copy';
+  const pRegen=document.getElementById('pRegen'); pRegen.disabled=false; pRegen.textContent='↻ Regenerate';
+  // if already emailed, show WHEN in the panel
+  const prevSent=LS('sent:'+list+':'+rowKey(list,r),null);
+  if(prevSent){ pStat.style.color='var(--green)'; pStat.textContent=`✓ emailed ${fmtWhen(new Date(prevSent.at).toISOString())}`; }
   const reorder=needsReorder(r); const pGen=document.getElementById('pGen'); pGen.disabled=reorder;
   if(reorder){ document.getElementById('pSubj').innerHTML='<span class="reorder-note">Reorder Samples?? — nothing has shipped yet, so there’s nothing to follow up on.</span>'; }
   openPanel();
@@ -351,10 +361,11 @@ 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:[]})});
+    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);
-    btn.textContent='✓ Sent'; stat.style.color='var(--green)'; stat.textContent=`sent via info@${j.inThread?' · in-thread':''}`;
-    setLS('sent:'+list+':'+k,{at:Date.now(),messageId:j.messageId||''});
+    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||''});
     if(active.card){ const nc=cardHTML(list,r); const tmp=document.createElement('div'); tmp.innerHTML=nc; const fresh=tmp.firstElementChild; fresh._row=r; fresh._list=list; fresh.classList.add('active'); active.card.replaceWith(fresh); active.card=fresh; }
   }catch(e){btn.disabled=false;btn.textContent=old;stat.style.color='var(--red)';stat.textContent='send failed: '+e.message}
 }
@@ -403,8 +414,25 @@ document.getElementById('board').addEventListener('click',e=>{
   if(e.target.matches('[data-gen]')) return openPanelFor(card,{generate:true});
   if(e.target.matches('[data-view]')) return openPanelFor(card,{generate:false});
 });
+// Sent-log viewer — the permanent record of every email sent.
+async function openSentLog(){
+  active=null; document.querySelectorAll('.card.active').forEach(c=>c.classList.remove('active'));
+  document.body.classList.add('log-mode');
+  document.getElementById('pName').textContent='Sent emails';
+  document.getElementById('pSub').textContent='every email sent from this tool';
+  document.getElementById('pCtx').innerHTML='<div class="drow">loading…</div>';
+  openPanel();
+  try{
+    const j=await (await fetch(API('/api/sent'))).json();
+    const rows=(j.sent||[]).map(s=>`<div class="drow" style="border-bottom:1px solid var(--line);padding:6px 0">
+      <div><b>${esc(fmtWhen(s.ts))}</b> → ${esc(s.to)}${s.inThread?' <span class="chip g">in-thread</span>':''}</div>
+      <div style="color:var(--mut);font-size:12px">${esc(s.company||'')}${s.company?' · ':''}${esc(s.subject||'')}</div></div>`).join('');
+    document.getElementById('pCtx').innerHTML=`<div class="drow"><b>${j.count||0} emails sent</b> (newest first)</div>`+(rows||'<div class="drow">None sent yet.</div>');
+  }catch(e){ document.getElementById('pCtx').innerHTML='<div class="drow">Could not load: '+esc(e.message)+'</div>'; }
+}
+document.getElementById('sentBtn').addEventListener('click',openSentLog);
 // panel buttons
-document.getElementById('pClose').addEventListener('click',closePanel);
+document.getElementById('pClose').addEventListener('click',()=>{document.body.classList.remove('log-mode');closePanel();});
 document.getElementById('pGen').addEventListener('click',()=>composeLetter({}));
 document.getElementById('pRegen').addEventListener('click',()=>composeLetter({regenerate:true}));
 document.getElementById('pCopy').addEventListener('click',e=>{const ta=document.getElementById('pBody');navigator.clipboard.writeText(`Subject: ${document.getElementById('pSubj').textContent.replace(/^Subject: /,'')}\n\n${ta.value}`);e.target.textContent='Copied ✓';setTimeout(()=>e.target.textContent='Copy',1200)});
diff --git a/server.js b/server.js
index 0ced87c..599af55 100644
--- a/server.js
+++ b/server.js
@@ -22,6 +22,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
 const PORT = Number(process.env.PORT || 9768);
 const DATA_DIR = path.join(__dirname, 'data');
 const LISTS = path.join(DATA_DIR, 'lists.json');
+const SENT_LOG = path.join(DATA_DIR, 'sent-log.jsonl'); // permanent record of every email we send
 const GEORGE = process.env.GEORGE_URL || 'http://127.0.0.1:9850';
 // Resolve George secrets: prefer our own env, else read them straight from george-gmail/.env
 // (same fallback George itself uses — launchd/pm2 don't load .env into process.env).
@@ -355,7 +356,7 @@ app.get('/api/lastcorr', async (req, res) => {
 // we pass that back verbatim. Each send is a deliberate per-recipient click in the UI.
 app.post('/api/send', async (req, res) => {
   try {
-    const { to, subject, body, samples } = req.body || {};
+    const { to, subject, body, samples, list, account: acctNo, company } = req.body || {};
     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}` });
 
@@ -379,7 +380,24 @@ app.post('/api/send', async (req, res) => {
     });
     const j = await r.json().catch(() => ({}));
     if (!r.ok || j.error) return res.status(r.status === 403 ? 403 : (r.status || 500)).json({ error: j.error || 'send failed', blocked: !!j.blocked, external: j.external });
-    res.json({ success: true, account: 'info', messageId: j.messageId, threadId: j.threadId, inThread: !!(j.inThread || replyToMessageId), cost: '$0 (Gmail relay via George)' });
+    // KEEP a permanent record of every email we send (append-only log; the full letter too).
+    const sentAt = new Date().toISOString();
+    try {
+      fs.appendFileSync(SENT_LOG, JSON.stringify({ ts: sentAt, to: String(to).trim(), subject, list: list || '', account: acctNo || '', company: company || '', messageId: j.messageId || '', threadId: j.threadId || '', inThread: !!(j.inThread || replyToMessageId), body }) + '\n');
+    } catch (e) { /* logging must never block a successful send */ }
+    res.json({ success: true, account: 'info', messageId: j.messageId, threadId: j.threadId, inThread: !!(j.inThread || replyToMessageId), sentAt, cost: '$0 (Gmail relay via George)' });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// GET /api/sent — the permanent record of every email sent from this tool (newest first).
+app.get('/api/sent', (_req, res) => {
+  try {
+    if (!fs.existsSync(SENT_LOG)) return res.json({ count: 0, sent: [] });
+    const lines = fs.readFileSync(SENT_LOG, 'utf8').split('\n').filter((l) => l.trim());
+    const sent = lines.slice(-800).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean).reverse();
+    res.json({ count: lines.length, sent });
   } catch (e) {
     res.status(500).json({ error: e.message });
   }

← a4a77d7 Make panel 'Send now (info@)' prominent — filled gold, bold,  ·  back to Dw Pitch Followup  ·  Flag invoices placed SINCE samples sent (possible conversion 4f8a57a →