โ 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,'"')}" 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 โ