[object Object]

← 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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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 →