[object Object]

โ† back to Commercialrealestate

feat(index): per-card ๐Ÿ“‹ Copy summary (DTD verdict C, UNANIMOUS 3/3 Codex+Qwen+Claude) โ€” copies a clean multi-line text block (address, type/units/price, cap/$-unit/$-SF, DSCR/CoC/cash needed/cash flow, est rent/deal score/warrantability, listing URL) to the clipboard for email/CRM; honest fields only (omits missing values); โœ“ confirmation. Contrarian-gated, 0 errors.

a8fedb13031b4007524f0a5eae9761823b15c1b3 ยท 2026-07-02 16:13:52 -0700 ยท Steve Abrams

Officer: vp-engineering
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit a8fedb13031b4007524f0a5eae9761823b15c1b3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 2 16:13:52 2026 -0700

    feat(index): per-card ๐Ÿ“‹ Copy summary (DTD verdict C, UNANIMOUS 3/3 Codex+Qwen+Claude) โ€” copies a clean multi-line text block (address, type/units/price, cap/$-unit/$-SF, DSCR/CoC/cash needed/cash flow, est rent/deal score/warrantability, listing URL) to the clipboard for email/CRM; honest fields only (omits missing values); โœ“ confirmation. Contrarian-gated, 0 errors.
    
    Officer: vp-engineering
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 public/index.html | 19 ++++++++++++++++++-
 1 file changed, 18 insertions(+), 1 deletion(-)

diff --git a/public/index.html b/public/index.html
index 734ae22..f5f842a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -187,6 +187,8 @@
   /* shortlist stars */
   .starbtn { background:none; border:0; color:var(--mut); font-size:18px; line-height:1; cursor:pointer; padding:0; }
   .starbtn:hover, .starbtn.on { color:var(--warn); }
+  .copybtn { background:none; border:0; color:var(--mut); font-size:14px; line-height:1; cursor:pointer; padding:0; }
+  .copybtn:hover { color:var(--blue); }
   .listtbl .starbtn { font-size:14px; margin-right:5px; vertical-align:middle; }
   #shortbtn.shorton { background:rgba(210,153,34,.14); border-color:var(--warn); color:var(--warn); }
   /* aggregate stats strip */
@@ -860,6 +862,7 @@ function card(p){
     <div class="top">
       <div><div class="rank">#${p.rank} ยท score</div><div class="addr">${p.address}</div><div class="city">${p.city}, CA ${p.zip||''}</div></div>
       <div style="display:flex;align-items:flex-start;gap:8px">
+        <button class="copybtn" data-copy="${p.id}" aria-label="Copy listing summary" title="Copy summary to clipboard">๐Ÿ“‹</button>
         <button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" aria-label="${SHORT.has(p.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(p.id)}" title="Shortlist this listing">โ˜…</button>
         ${vis('score')?`<div class="score">${p.composite}<small>fin ${p.financeScore}${p.qwenScore!=null?' ยท ai '+p.qwenScore:''}</small></div>`:''}
       </div>
@@ -1143,6 +1146,20 @@ function buildSavedList(){ const el=$('#savedList'); if(!el) return;
     ? SAVED.map((s,i)=>`<div class="savedrow"><button class="savedname" data-qs="${(s.qs||'').replace(/"/g,'&quot;')}" title="Restore this saved view">๐Ÿ“ ${esc(s.name)}</button><button class="saveddel" data-del="${i}" aria-label="Delete saved search">โœ•</button></div>`).join('')
     : '<div class="savedempty">None yet โ€” filter, then โ€œSave current viewโ€.</div>';
 }
+// ---- per-listing clipboard summary (honest fields only) ----
+function cardSummary(p){ const f=p.finance||{}; const L=[];
+  L.push(`${p.address}, ${p.city} CA ${p.zip||''}`.trim());
+  const l2=[p.type, p.units?`${p.units} unit${p.units>1?'s':''}`:null, p.price?fmt(p.price):null].filter(Boolean).join(' ยท '); if(l2) L.push(l2);
+  const m=[]; if(p.cap_rate!=null) m.push('Cap '+p.cap_rate+'%'); if(ppu(p)&&!p.isCondo) m.push('$'+ppu(p).toLocaleString()+'/unit'); if(ppsf(p)) m.push('$'+ppsf(p).toLocaleString()+'/SF'); if(m.length) L.push(m.join(' ยท '));
+  const fin=[]; if(f.dscr!=null) fin.push('DSCR '+f.dscr); if(f.coc!=null) fin.push('CoC '+f.coc+'%'); if(f.cashNeeded!=null) fin.push('Cash needed '+fmt(f.cashNeeded)); if(f.cashFlow!=null) fin.push('Cash flow/yr '+fmt(f.cashFlow)); if(fin.length) L.push(fin.join(' ยท '));
+  const r=rentEstimate(p); const ex=[]; if(r) ex.push('Est. rent $'+r.std.toLocaleString()+'/mo'); if(p.deal&&p.deal.score!=null) ex.push('Deal score '+p.deal.score+'/100'); if(p.warrantable_status) ex.push('Warrantability: '+p.warrantable_status); if(ex.length) L.push(ex.join(' ยท '));
+  if(p.source) L.push(p.source);
+  return L.join('\n');
+}
+function copySummary(id, btn){ const p=DATA.ranked.find(x=>x.id===id); if(!p) return;
+  const done=()=>{ if(btn){ btn.textContent='โœ“'; setTimeout(()=>btn.textContent='๐Ÿ“‹',1200); } };
+  try{ navigator.clipboard.writeText(cardSummary(p)).then(done).catch(done); }catch(e){ done(); }
+}
 function applyQuick(k){
   if(k==='condo'){ F.types=new Set(['Condo']); }
   else if(k==='mf'){ F.types=new Set(['Multifamily']); }
@@ -1447,7 +1464,7 @@ fetch('data/ranked.json').then(r=>r.json()).then(async d=>{
   $('#examples').innerHTML = EX.map(x=>`<button class="ex">${x}</button>`).join('');
   $('#examples').onclick=e=>{ if(e.target.classList.contains('ex')){ $('#q').value=e.target.textContent; $('#q').focus(); } };
   $('#chatform').onsubmit=e=>{ e.preventDefault(); const t=$('#q').value.trim(); if(!t) return; $('#q').value=''; ask(t); };
-  $('#grid').onclick=e=>{ const qk=e.target.closest('[data-quick]'); if(qk){ applyQuick(qk.dataset.quick); return; } const more=e.target.closest('#showmore'); if(more){ _shown=Math.min(_shown+PAGE(), _rows.length); paintPage(); return; } const st=e.target.closest('[data-star]'); if(st){ toggleStar(st.dataset.star); st.classList.toggle('on',SHORT.has(st.dataset.star)); const sb=$('#shortbtn'); if(sb) sb.textContent='โญ Shortlist ('+SHORT.size+')'; if(F.shortlist) render(); return; } const th=e.target.closest('.listtbl th[data-sortk]'); if(th){ const k=th.dataset.sortk; if(colSort&&colSort.key===k) colSort.dir*=-1; else colSort={key:k,dir:['type','firm','warr','broker','status','zip','addr','rec'].includes(k)?1:-1}; render(); return; } const b=e.target.closest('.lcbtn'); if(b){ fetchLiveComps(b.dataset.id); return; } const h=e.target.closest('.histbtn'); if(h) fetchHistory(h); };
+  $('#grid').onclick=e=>{ const cp=e.target.closest('[data-copy]'); if(cp){ copySummary(cp.dataset.copy, cp); return; } const qk=e.target.closest('[data-quick]'); if(qk){ applyQuick(qk.dataset.quick); return; } const more=e.target.closest('#showmore'); if(more){ _shown=Math.min(_shown+PAGE(), _rows.length); paintPage(); return; } const st=e.target.closest('[data-star]'); if(st){ toggleStar(st.dataset.star); st.classList.toggle('on',SHORT.has(st.dataset.star)); const sb=$('#shortbtn'); if(sb) sb.textContent='โญ Shortlist ('+SHORT.size+')'; if(F.shortlist) render(); return; } const th=e.target.closest('.listtbl th[data-sortk]'); if(th){ const k=th.dataset.sortk; if(colSort&&colSort.key===k) colSort.dir*=-1; else colSort={key:k,dir:['type','firm','warr','broker','status','zip','addr','rec'].includes(k)?1:-1}; render(); return; } const b=e.target.closest('.lcbtn'); if(b){ fetchLiveComps(b.dataset.id); return; } const h=e.target.closest('.histbtn'); if(h) fetchHistory(h); };
   $('#shortbtn').onclick=()=>{ F.shortlist=!F.shortlist; render(); };
   $('#cmpbtn').onclick=openCompare;
   $('#heat').value=heat; $('#heat').onchange=()=>{ heat=$('#heat').value; localStorage.setItem('cre_heat',heat); render(); };

โ† 45fe54b feat(index): saved searches (DTD verdict D, 2/3 Codex+Claude  ยท  back to Commercialrealestate  ยท  feat(mls): heat cues + copy summary parity (DTD verdict D, U 9b10d00 โ†’