β back to Commercialrealestate
feat(mls): heat cues + copy summary parity (DTD verdict D, UNANIMOUS 3/3 Codex+Qwen+Claude) β color-by-metric Heat (Cap rate / Score) tints table rows red->green over the filtered set (only real values), legend; per-row π Copy summary (address/type/units/price/cap/$-unit/$-SF/year/beds-baths/warrantability/URL) to clipboard. Closes the index<->mls consistency gap. Contrarian-gated, 0 errors.
9b10d00ca5153776f21faf2645c6fe7bfb777a39 Β· 2026-07-02 16:43:32 -0700 Β· Steve Abrams
Officer: vp-engineering
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
Diff
commit 9b10d00ca5153776f21faf2645c6fe7bfb777a39
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 2 16:43:32 2026 -0700
feat(mls): heat cues + copy summary parity (DTD verdict D, UNANIMOUS 3/3 Codex+Qwen+Claude) β color-by-metric Heat (Cap rate / Score) tints table rows red->green over the filtered set (only real values), legend; per-row π Copy summary (address/type/units/price/cap/$-unit/$-SF/year/beds-baths/warrantability/URL) to clipboard. Closes the index<->mls consistency gap. Contrarian-gated, 0 errors.
Officer: vp-engineering
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
public/mls.html | 35 ++++++++++++++++++++++++++++++++---
1 file changed, 32 insertions(+), 3 deletions(-)
diff --git a/public/mls.html b/public/mls.html
index f356dc3..48a0ebd 100644
--- a/public/mls.html
+++ b/public/mls.html
@@ -76,7 +76,13 @@
.statsbar:empty { display:none; }
.statpill { font-size:12px; color:var(--ink); background:var(--card); border:1px solid var(--line); border-radius:8px; padding:4px 10px; }
.statpill .sl { color:var(--mut); } .statpill b { font-variant-numeric:tabular-nums; }
- table.mls td.startd, table.mls th.starth { width:26px; text-align:center; padding:5px 4px; }
+ table.mls td.startd, table.mls th.starth { width:46px; white-space:nowrap; text-align:center; padding:5px 4px; }
+ .copybtn { background:none; border:0; color:var(--mut); font-size:14px; line-height:1; cursor:pointer; padding:0; }
+ .copybtn:hover { color:var(--blue); }
+ #heat { background:var(--card); color:var(--ink); border:1px solid var(--line); border-radius:9px; padding:8px 10px; font-size:13px; }
+ .heatleg { display:none; align-items:center; gap:5px; font-size:11px; color:var(--mut); }
+ .heatleg.on { display:inline-flex; }
+ .heatleg .hb { width:52px; height:8px; border-radius:4px; background:linear-gradient(90deg,hsl(0,70%,45%),hsl(60,70%,45%),hsl(120,70%,45%)); }
</style>
</head>
<body>
@@ -87,6 +93,8 @@
<input class="search" id="q" type="text" placeholder="search anything β address, city, firm, type, yearβ¦">
<div class="toggle" id="view"><button data-v="table" class="on">β¦ Table</button><button data-v="grid">β€ Grid</button></div>
<button class="csvbtn" id="shortbtn" title="Show only your shortlisted (starred) listings β shared with the Explorer">β Shortlist (0)</button>
+ <select id="heat" title="Color rows red (low) β green (high) by a metric"><option value="none">Heat: off</option><option value="cap">Heat: Cap rate</option><option value="score">Heat: Score</option></select>
+ <span id="heatleg" class="heatleg">low <span class="hb"></span> high Β· <span class="hl"></span></span>
<button class="csvbtn" id="csv" title="Export current rows to CSV">β¬ CSV</button>
<span class="count" id="count">β¦</span>
</header>
@@ -142,6 +150,25 @@ const SHORT=new Set((function(){ try{ return JSON.parse(localStorage.getItem('cr
function saveShort(){ try{ localStorage.setItem('cre_shortlist', JSON.stringify([...SHORT])); }catch(e){} }
function toggleStar(id){ SHORT.has(id)?SHORT.delete(id):SHORT.add(id); saveShort(); }
function updateShortBtn(){ const sb=$('#shortbtn'); if(sb){ sb.textContent='β Shortlist ('+SHORT.size+')'; sb.classList.toggle('shorton',F.shortlist); } }
+// heat cues + copy summary (parity with the Explorer)
+let heat=localStorage.getItem('cre_heat_mls')||'none';
+function heatVal(r){ return heat==='cap'?(r.cap_rate!=null&&r.cap_rate!==''?+r.cap_rate:null):heat==='score'?(r.composite!=null?+r.composite:null):null; }
+function heatColor(t){ t=Math.max(0,Math.min(1,t)); return `hsl(${Math.round(t*120)},70%,45%)`; }
+function applyHeat(rows){
+ const lg=$('#heatleg'); if(lg){ lg.classList.toggle('on',heat!=='none'); if(heat!=='none') lg.querySelector('.hl').textContent={cap:'Cap rate',score:'Score'}[heat]; }
+ if(heat==='none') return;
+ const vals=rows.map(heatVal).filter(v=>v!=null&&isFinite(v)); if(!vals.length) return;
+ const min=Math.min(...vals),max=Math.max(...vals),span=(max-min)||1;
+ rows.forEach(r=>{ const v=heatVal(r); const cell=document.querySelector(`.startd [data-star="${r.id}"]`)?.closest('td'); if(!cell) return; cell.style.boxShadow=(v==null||!isFinite(v))?'':'inset 5px 0 0 '+heatColor((v-min)/span); });
+}
+function rowSummary(r){ const L=[];
+ L.push(`${r.address||''}, ${r.city||''} CA ${r.zip||''}`.trim());
+ const l2=[r.type, r.units?`${r.units} unit${r.units>1?'s':''}`:null, r.price?money(r.price):null].filter(Boolean).join(' Β· '); if(l2)L.push(l2);
+ const m=[]; if(r.cap_rate!=null&&r.cap_rate!=='') m.push('Cap '+r.cap_rate+'%'); if(r.units&&r.price) m.push('$'+Math.round(r.price/r.units).toLocaleString()+'/unit'); if(r.sqft&&r.price) m.push('$'+Math.round(r.price/r.sqft).toLocaleString()+'/SF'); if(m.length)L.push(m.join(' Β· '));
+ const ex=[]; if(r.year_built) ex.push('Built '+r.year_built); if(r.beds!=null) ex.push((r.beds)+'bd/'+(r.baths??'β')+'ba'); if(r.warrantable_status) ex.push('Warrantability: '+r.warrantable_status); if(ex.length)L.push(ex.join(' Β· '));
+ if(r.source) L.push(r.source); return L.join('\n');
+}
+function copySummary(id,btn){ const r=DATA.find(x=>x.id===id); if(!r) return; const done=()=>{ if(btn){ btn.textContent='β'; setTimeout(()=>btn.textContent='π',1200); } }; try{ navigator.clipboard.writeText(rowSummary(r)).then(done).catch(done); }catch(e){ done(); } }
function median(a){ if(!a.length) return null; const s=a.slice().sort((x,y)=>x-y); const m=Math.floor(s.length/2); return s.length%2?s[m]:(s[m-1]+s[m])/2; }
function renderStats(rows){ const sb=$('#statsbar'); if(!sb) return; if(!rows.length){ sb.innerHTML=''; return; }
const prices=rows.map(r=>+r.price).filter(v=>v>0);
@@ -233,7 +260,7 @@ function render(){
const rows=filtered(); const cols=visCols();
renderStats(rows);
$('#count').textContent=`${rows.length} of ${DATA.length} properties`;
- const star=r=>`<button class="starbtn ${SHORT.has(r.id)?'on':''}" data-star="${r.id}" title="Shortlist">β
</button>`;
+ const star=r=>`<button class="starbtn ${SHORT.has(r.id)?'on':''}" data-star="${r.id}" aria-label="${SHORT.has(r.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(r.id)}" title="Shortlist">β
</button><button class="copybtn" data-copy="${r.id}" aria-label="Copy summary" title="Copy summary">π</button>`;
// table head
$('#thead').innerHTML='<tr><th class="starth">β</th>'+cols.map(c=>`<th data-k="${c.k}" class="${sortKey===c.k?(sortDir>0?'asc':'desc'):''}">${esc(c.l)}</th>`).join('')+'</tr>';
if(view==='table'){
@@ -250,6 +277,7 @@ function render(){
+bodyCols.map(c=>`<div class="row"><span class="k">${esc(c.l)}</span><span class="v">${cellHTML(r,c)}</span></div>`).join('')
+`</div>`).join('');
}
+ applyHeat(rows);
}
$('#thead').addEventListener('click',e=>{ const th=e.target.closest('th'); if(!th) return; const k=th.dataset.k; if(!k) return;
if(sortKey===k) sortDir*=-1; else { sortKey=k; sortDir=(k==='address'||k==='city'||k==='firm'||k==='type')?1:-1; } render(); });
@@ -270,7 +298,7 @@ async function propHistory(address,city){
if(!a && !d.sales.length && !(d.permits||[]).length) h+='<div class="ph-mut">No history on file yet for this address. The 2.43M-parcel assessor roll is still ingesting β year built, beds/baths, and assessed value will appear here once it lands.</div>';
mb.innerHTML=h;
}
-document.addEventListener('click',e=>{ const st=e.target.closest('[data-star]'); if(st){ toggleStar(st.dataset.star); st.classList.toggle('on',SHORT.has(st.dataset.star)); updateShortBtn(); if(F.shortlist) render(); return; }
+document.addEventListener('click',e=>{ const cp=e.target.closest('[data-copy]'); if(cp){ copySummary(cp.dataset.copy,cp); return; } const st=e.target.closest('[data-star]'); if(st){ toggleStar(st.dataset.star); st.classList.toggle('on',SHORT.has(st.dataset.star)); updateShortBtn(); if(F.shortlist) render(); return; }
const row=e.target.closest('.prow'); if(row && e.target.tagName!=='A'){ propHistory(row.dataset.addr,row.dataset.city); }
if(e.target.id==='ov'||e.target.classList.contains('ph-x')) $('#ov').classList.remove('on'); });
$('#q').addEventListener('input',()=>{ q=$('#q').value.trim(); render(); });
@@ -279,6 +307,7 @@ function exportCSV(){ const rows=filtered(); const cols=visCols(); const cv=(r,c
const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([csv],{type:'text/csv'})); a.download='cre-mls-'+rows.length+'.csv'; a.click(); URL.revokeObjectURL(a.href); }
$('#csv').addEventListener('click',exportCSV);
$('#shortbtn').addEventListener('click',()=>{ F.shortlist=!F.shortlist; render(); });
+$('#heat').value=heat; $('#heat').addEventListener('change',()=>{ heat=$('#heat').value; localStorage.setItem('cre_heat_mls',heat); render(); });
$('#view').addEventListener('click',e=>{ const b=e.target.closest('button'); if(!b) return;
document.querySelectorAll('#view button').forEach(x=>x.classList.toggle('on',x===b)); view=b.dataset.v; render(); });
// left-rail facets + column toggles
β a8fedb1 feat(index): per-card π Copy summary (DTD verdict C, UNANIM
Β·
back to Commercialrealestate
Β·
perf(mls): paginated table (DTD verdict B, UNANIMOUS 3/3 Cod 8ceee47 β