← back to Pitch Guard
PitchGuard: red 'already sent' alert + 4th Already-Sent column
1e95bac2e87bb17c3ff236f16281b33c7751f7f9 · 2026-06-18 15:48:31 -0700 · Steve Abrams
When a followup was already sent to the selected contact, show a red alert
(with the exact date+time it went out) in the Pitch verdict, and add a 4th
panel on the right listing every letter already sent to that contact —
date+time stamped, the matched followup flagged red. Layout bumped to v2 so
existing users pick up the new column.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
Diff
commit 1e95bac2e87bb17c3ff236f16281b33c7751f7f9
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jun 18 15:48:31 2026 -0700
PitchGuard: red 'already sent' alert + 4th Already-Sent column
When a followup was already sent to the selected contact, show a red alert
(with the exact date+time it went out) in the Pitch verdict, and add a 4th
panel on the right listing every letter already sent to that contact —
date+time stamped, the matched followup flagged red. Layout bumped to v2 so
existing users pick up the new column.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
public/index.html | 87 +++++++++++++++++++++++++++++++++++++++++++++++++------
1 file changed, 78 insertions(+), 9 deletions(-)
diff --git a/public/index.html b/public/index.html
index 42b053a..9f4542f 100644
--- a/public/index.html
+++ b/public/index.html
@@ -171,6 +171,23 @@
.verdict.clear{background:var(--greenbg);border-color:#b6d3c0}
.verdict h3{font-size:13px;font-family:inherit}
.verdict p{font-size:12px;margin-top:4px}
+ /* ── Already-Sent panel ── */
+ .sent-alert{margin:10px 12px;padding:10px 12px;background:var(--redbg);border:1px solid #d8b4ae;
+ border-left:4px solid var(--red);border-radius:7px;color:var(--red)}
+ .sent-alert h3{font-size:13px;font-weight:700;display:flex;align-items:center;gap:7px}
+ .sent-alert .when{margin-top:4px;font-size:12px;font-weight:600}
+ .sent-alert p{font-size:11.5px;margin-top:5px;color:#7a2a23}
+ .sent-list{padding:4px 12px 12px}
+ .sent-item{border:1px solid var(--line);border-radius:7px;padding:8px 10px;margin-bottom:8px;background:var(--paper)}
+ .sent-item.fu{border-color:#d8b4ae;background:var(--redbg)}
+ .sent-item .si-subj{font-size:12.5px;font-weight:600;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+ .sent-item .si-when{font-size:11px;color:var(--muted);margin-top:3px;display:flex;align-items:center;gap:6px}
+ .sent-item.fu .si-when{color:var(--red);font-weight:600}
+ .sent-item .si-snip{font-size:11px;color:#6b6456;margin-top:5px;line-height:1.5;
+ display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
+ .sent-item .si-tag{font-size:9px;font-weight:700;letter-spacing:.4px;background:var(--red);color:#fff;
+ border-radius:4px;padding:1px 6px;text-transform:uppercase}
+ .sent-item a.si-page{font-size:11px;color:var(--gold);text-decoration:none;margin-top:5px;display:inline-block}
.override{margin-top:9px;font-size:12px;display:flex;gap:7px;align-items:flex-start}
.override input{margin-top:2px}
.pitch-foot{padding:11px 14px;border-top:1px solid var(--line);background:var(--paper);
@@ -267,6 +284,21 @@
<div class="pitch-empty">No draft selected.<br>The followup waiting for this client will appear here.</div>
</div>
</div>
+
+ <!-- ALREADY SENT -->
+ <div class="panel" id="panel-sent" data-panel="sent">
+ <div class="pane-hd">
+ <span class="collapse-strip"><button class="hd-btn" data-act="collapse">▸</button><span class="vlabel">Already Sent</span></span>
+ <span class="grip">⠿</span>
+ <h2>Already Sent</h2><span class="count" id="sentCount" style="display:none"></span>
+ <span class="spacer"></span>
+ <button class="hd-btn" data-act="popout" title="Pop out into its own window">↗</button>
+ <button class="hd-btn" data-act="collapse" title="Collapse panel">–</button>
+ </div>
+ <div class="pane-body" id="sentBody">
+ <div class="empty">Pick a draft to see followups already sent to that contact.</div>
+ </div>
+ </div>
</div>
<div class="toast" id="toast"></div>
@@ -286,12 +318,14 @@ function toast(msg, err){ const t=$('#toast'); t.textContent=msg; t.className='t
function esc(s){ return String(s||'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); }
function fmtDate(d){ if(!d) return ''; const t=Date.parse(d); if(!t) return d; const dt=new Date(t);
return dt.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'2-digit'}); }
+function fmtDateTime(d){ if(!d) return ''; const t=Date.parse(d); if(!t) return d; const dt=new Date(t);
+ return dt.toLocaleString('en-US',{month:'short',day:'numeric',year:'numeric',hour:'numeric',minute:'2-digit'}); }
const riskWord = {high:'High risk',elevated:'Elevated',caution:'Caution',clear:'Clear'};
/* ════════ LAYOUT MANAGER — resize · collapse · reorder · pop-out ════════ */
-const PANELS = ['drafts','corr','pitch'];
-const LS = 'pg.layout.v1';
-const DEFAULT_LAYOUT = { order:['drafts','corr','pitch'], width:{drafts:336,corr:0,pitch:438}, collapsed:{} };
+const PANELS = ['drafts','corr','pitch','sent'];
+const LS = 'pg.layout.v2';
+const DEFAULT_LAYOUT = { order:['drafts','corr','pitch','sent'], width:{drafts:336,corr:0,pitch:438,sent:308}, collapsed:{} };
let layout = loadLayout();
function loadLayout(){
@@ -592,6 +626,8 @@ async function selectDraft(id){
if(!d.toEmail) corr.innerHTML = '<div class="empty">This draft has no recipient address — nothing to cross-check.</div>';
else { corr.innerHTML = '<div class="empty"><div class="spin"></div> pulling every thread with '+esc(d.toEmail)+'…</div>'; loadContact(d.toEmail); }
} else if(d.toEmail){ loadContact(d.toEmail); }
+ const sb=$('#sentBody');
+ if(sb){ sb.innerHTML = d.toEmail ? '<div class="empty"><div class="spin"></div> checking followups sent to '+esc(d.toEmail)+'…</div>' : '<div class="empty">This draft has no recipient address.</div>'; }
loadPitch(d);
}
@@ -600,8 +636,40 @@ async function loadContact(email){
let c = riskCache[email];
if(!c){ const r=await fetch('/api/contact?email='+encodeURIComponent(email)); c=await r.json();
if(c.error) throw new Error(c.error); riskCache[email]=c; renderDrafts(); }
- renderCorr(c); renderVerdict(c);
- }catch(e){ $('#corr').innerHTML='<div class="empty">⚠ '+esc(e.message)+'</div>'; }
+ renderCorr(c); renderVerdict(c); renderSent(c);
+ }catch(e){ if($('#corr')) $('#corr').innerHTML='<div class="empty">⚠ '+esc(e.message)+'</div>';
+ if($('#sentBody')) $('#sentBody').innerHTML='<div class="empty">⚠ '+esc(e.message)+'</div>'; }
+}
+// ── 4th column: followups/letters already sent to this contact ──
+function renderSent(c){
+ const body = $('#sentBody'); if(!body) return; // panel not in this window
+ const cnt = $('#sentCount');
+ const letters = (c.sentLetters||[]).slice().sort((a,b)=>(b.ts||0)-(a.ts||0));
+ const fu = c.recentFollowup; // the most-recent matched followup (last 21d)
+ if(cnt){ cnt.style.display='inline-block'; cnt.textContent=letters.length; }
+ let html='';
+ if(fu){
+ const days=fu.daysAgo;
+ html += '<div class="sent-alert"><h3>⚠ Followup already sent</h3>'+
+ '<div class="when">'+esc(fmtDateTime(fu.date))+' · '+(days===0?'today':days+' day'+(days===1?'':'s')+' ago')+'</div>'+
+ '<p>A standard followup already went out to this contact. Sending another now reads as spam — hold off unless you have a reason.</p></div>';
+ }
+ if(!letters.length){
+ html += '<div class="empty">'+(fu?'':'✓ ')+'No prior letters found to '+esc(c.email||'this contact')+(fu?'':' — clear to follow up.')+'</div>';
+ } else {
+ html += '<div class="sent-list">';
+ for(const m of letters){
+ const isFu = fu && Math.abs((m.ts||0)-(fu.ts||0))<60000; // same message as the matched followup
+ const page = (m.id && !String(m.id).startsWith('ledger:')) ? '<a class="si-page" href="/api/draft/'+encodeURIComponent(m.id)+'/html" target="_blank" rel="noopener noreferrer">↗ View letter</a>' : '';
+ html += '<div class="sent-item'+(isFu?' fu':'')+'">'+
+ '<div class="si-subj">'+(isFu?'<span class="si-tag">followup</span> ':'')+esc(m.subject||'(no subject)')+'</div>'+
+ '<div class="si-when">🕓 '+esc(fmtDateTime(m.date))+'</div>'+
+ (m.snippet?'<div class="si-snip">'+esc(m.snippet)+'</div>':'')+
+ page+'</div>';
+ }
+ html += '</div>';
+ }
+ body.innerHTML = html;
}
function renderCorr(c){
if(!$('#corr')) return; // correspondence panel not in this window
@@ -753,10 +821,11 @@ function renderVerdict(c){
let boxClass = c.risk;
if(c.recentFollowup){
const fw=c.recentFollowup.daysAgo;
- html+='<p style="margin-top:8px;padding:7px 9px;background:var(--amberbg);border-radius:6px;'+
- 'color:var(--amber);font-weight:600;font-size:12px;">⚠ A standard followup was already sent to '+
- 'this contact '+(fw===0?'today':fw+' day'+(fw===1?'':'s')+' ago')+
- '. Sending another now reads as spam — hold off.</p>';
+ html+='<p style="margin-top:8px;padding:8px 10px;background:var(--redbg);border:1px solid #d8b4ae;'+
+ 'border-left:4px solid var(--red);border-radius:6px;'+
+ 'color:var(--red);font-weight:700;font-size:12px;">⚠ A standard followup was already sent to '+
+ 'this contact on '+esc(fmtDateTime(c.recentFollowup.date))+' ('+(fw===0?'today':fw+' day'+(fw===1?'':'s')+' ago')+
+ '). Sending another now reads as spam — hold off. See <b>Already Sent →</b></p>';
if(c.risk==='clear'||c.risk==='caution') boxClass='elevated';
}
if(gated) html+='<label class="override"><input type="checkbox" id="ovr">'+
← bcb6aaa Fix Send followup: pass George external-send approval token
·
back to Pitch Guard
·
PitchGuard: explicitly send/draft from info@designerwallcove 9b5953d →