← back to Pitch Guard
Pitch Guard: auto-remove a draft from the list once its followup sends successfully (trash + drop card)
ca01242b41c3b870c10f8ee347f9baec08d22458 · 2026-06-30 05:18:33 -0700 · Steve
Files touched
Diff
commit ca01242b41c3b870c10f8ee347f9baec08d22458
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Jun 30 05:18:33 2026 -0700
Pitch Guard: auto-remove a draft from the list once its followup sends successfully (trash + drop card)
---
public/index.html | 39 ++++++++++++++++++++++++++++++++++++---
1 file changed, 36 insertions(+), 3 deletions(-)
diff --git a/public/index.html b/public/index.html
index 9f4542f..8421e9c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -154,6 +154,11 @@
.msg.open .m-snip{display:none}
.sig-chip{font-size:9px;font-weight:700;letter-spacing:.4px;background:var(--redbg);color:var(--red);
border-radius:3px;padding:1px 5px}
+ /* warm-lead signals (invoice/samples WE sent) — these mean "follow up", not "stop" */
+ .warm-chip{font-size:9px;font-weight:700;letter-spacing:.4px;background:var(--greenbg);color:var(--green);
+ border-radius:3px;padding:1px 5px}
+ .cand.warm{box-shadow:inset 3px 0 0 var(--green)}
+ .cand .warms{margin-top:4px;display:flex;gap:5px;flex-wrap:wrap}
/* ── right / pitch ── */
.pitch-empty{padding:48px 24px;text-align:center;color:var(--muted);font-size:13px}
.pitch{display:flex;flex-direction:column;height:100%}
@@ -244,6 +249,8 @@
<select id="sortSel">
<option value="newest">Newest first</option>
<option value="oldest">Oldest first</option>
+ <option value="invoice">Invoice sent first</option>
+ <option value="samples">Samples sent first</option>
<option value="name">Recipient name A→Z</option>
<option value="subject">Subject A→Z</option>
<option value="risk">Risk (high → clear)</option>
@@ -492,6 +499,19 @@ function sortDrafts(arr){
else a.sort((x,y)=>ts(y)-ts(x)); // newest
return a;
}
+// Followup candidates carry warm signals (INVOICE SENT / SAMPLES SENT we sent). Default
+// order = warm leads first, then longest-quiet. The 'invoice'/'samples' sort options
+// pull that signal to the very top — the "find who needs a follow" criteria Steve asked for.
+function sortCandidates(arr){
+ const a = arr.slice();
+ const hasInv = c => (c.warmSignals||[]).includes('INVOICE SENT') ? 1 : 0;
+ const hasSam = c => (c.warmSignals||[]).includes('SAMPLES SENT') ? 1 : 0;
+ const warmN = c => (c.warmSignals||[]).length ? 1 : 0;
+ if(sortMode==='invoice') a.sort((x,y)=> hasInv(y)-hasInv(x) || (y.sentInvoiceTs||0)-(x.sentInvoiceTs||0) || y.daysSince-x.daysSince);
+ else if(sortMode==='samples') a.sort((x,y)=> hasSam(y)-hasSam(x) || (y.sentSamplesTs||0)-(x.sentSamplesTs||0) || y.daysSince-x.daysSince);
+ else a.sort((x,y)=> warmN(y)-warmN(x) || y.daysSince-x.daysSince); // warm-first, then longest-quiet
+ return a;
+}
function draftChip(d){
const c = verdictOf(d.toEmail);
let badge = '<span class="badge r-pending">·</span>';
@@ -509,13 +529,15 @@ function candidateSectionHTML(){
if(candScanning) body='<div class="empty" style="padding:24px"><div class="spin"></div> scanning sent mail for ghosted quotes…</div>';
else if(!candScanned) body='<div class="vgroup-note">Not scanned yet — click <b>Scan</b> to find quote/pricing threads we replied to that got no client response in 5+ days and have no followup drafted.</div>';
else if(!CANDIDATES.length) body='<div class="vgroup-note">No followup candidates — every ghosted quote in range already has a draft.'+(candScannedAt?' <span style="opacity:.7">· auto-scanned '+fmtDate(candScannedAt)+'</span>':'')+'</div>';
- else body=(candScannedAt?'<div class="vgroup-note">Auto-scans daily · last scan '+fmtDate(candScannedAt)+' — review, then ✎ Draft it.</div>':'')+CANDIDATES.map(c=>
- '<div class="cand" data-email="'+esc(c.contact)+'">'+
+ else body=(candScannedAt?'<div class="vgroup-note">Auto-scans daily · last scan '+fmtDate(candScannedAt)+' — review, then ✎ Draft it. <b>Green chips</b> = invoice/samples we sent (top follow-up priority).</div>':'')+sortCandidates(CANDIDATES).map(c=>{
+ const warms=(c.warmSignals||[]).map(w=>'<span class="warm-chip">'+esc(w)+'</span>').join(' ');
+ return '<div class="cand'+((c.warmSignals||[]).length?' warm':'')+'" data-email="'+esc(c.contact)+'">'+
'<button class="cand-draft" data-draft-cand="'+esc(c.contact)+'" title="Create the followup draft">✎ Draft it</button>'+
'<div class="who">'+esc(c.contactName)+(c.uncertain?' <span title="Reply/draft check was throttled — shown so it is not silently dropped; verify the thread before sending" style="color:#b8860b;font-size:11px">⚠ verify</span>':'')+'</div>'+
'<div class="subj">'+esc(c.subject)+'</div>'+
'<div class="meta">'+esc(c.contact)+' · '+c.daysSince+'d no reply</div>'+
- '</div>').join('');
+ (warms?'<div class="warms">'+warms+'</div>':'')+
+ '</div>';}).join('');
return '<div class="cgroup-hd'+(candOpen?'':' closed')+'" id="cgroupHd" title="Quotes with no client reply in 5+ days — the 5-day followup rule">'+
'<span class="arrow">▼</span> Followup Candidates '+
'<span class="vct">'+(candScanned?CANDIDATES.length:'—')+'</span>'+
@@ -856,6 +878,17 @@ async function sendPitch(d){
if(out.error) throw new Error(out.error);
$('#sendState').innerHTML='<span class="sent-stamp">✓ sent in-thread</span>';
toast('Followup sent to '+d.toEmail);
+ // Process finished successfully → auto-remove this draft (Steve 2026-06-30). The followup
+ // was sent, so the leftover Gmail draft is done: trash it (reversible → Gmail Trash, 30-day
+ // recovery) and drop it from the list immediately so it can't linger or be re-sent.
+ try{ const tr=await fetch('/api/draft/'+encodeURIComponent(d.id)+'/trash',{method:'POST'}); await tr.json(); }catch(_){}
+ DRAFTS = DRAFTS.filter(x=>x.id!==d.id);
+ if(CURRENT===d.id){
+ CURRENT=null;
+ const corr=$('#corr'); if(corr) corr.innerHTML='<div class="empty"><div class="big">✉</div>Pick a draft to see every thread<br>with that contact before you decide.</div>';
+ const pit=$('#pitch'); if(pit) pit.innerHTML='<div class="pitch-empty">No draft selected.<br>The followup waiting for this client will appear here.</div>';
+ }
+ renderDrafts();
}catch(e){ $('#sendState').textContent=''; sb.disabled=false; toast('Send failed: '+e.message,1); }
}
← bb14d71 snapshot before invoice/samples-sent followup-signal feature
·
back to Pitch Guard
·
feature: surface invoice-sent & samples-sent as warm followu aae4190 →