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
Files touched
M public/index.htmlM server.js
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 });
}