[object Object]

← back to Commercialrealestate

feat(index): color-by-metric heat cues (DTD verdict D, 2/3 Qwen+Claude; Codex dissent C=rent-control OVERRIDDEN on honest-data grounds — rent_control is 98% 'verify' placeholder, real RSO needs a classification-data task) — Heat select (Deal score / Cap rate / Cash-on-cash) tints each card left-border / list row red→green over the filtered set's min-max; only real values colored; gradient legend; persisted + in shareable URL (?ht=). Contrarian-gated, 0 errors.

681a9b8a56d02d50e2f0b5d2d61ed73b7c13b6e7 · 2026-07-02 12:21:47 -0700 · Steve Abrams

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

Files touched

Diff

commit 681a9b8a56d02d50e2f0b5d2d61ed73b7c13b6e7
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 2 12:21:47 2026 -0700

    feat(index): color-by-metric heat cues (DTD verdict D, 2/3 Qwen+Claude; Codex dissent C=rent-control OVERRIDDEN on honest-data grounds — rent_control is 98% 'verify' placeholder, real RSO needs a classification-data task) — Heat select (Deal score / Cap rate / Cash-on-cash) tints each card left-border / list row red→green over the filtered set's min-max; only real values colored; gradient legend; persisted + in shareable URL (?ht=). Contrarian-gated, 0 errors.
    
    Officer: vp-engineering
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 public/index.html | 26 ++++++++++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/public/index.html b/public/index.html
index 8afe5ef..98bfb11 100644
--- a/public/index.html
+++ b/public/index.html
@@ -192,6 +192,9 @@
   .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; }
+  .heatlegend { display:none; align-items:center; gap:5px; font-size:11px; color:var(--mut); }
+  .heatlegend.on { display:inline-flex; }
+  .heatlegend .bar { width:56px; height:8px; border-radius:4px; background:linear-gradient(90deg,hsl(0,70%,45%),hsl(60,70%,45%),hsl(120,70%,45%)); }
   /* compare overlay */
   #cmpov { display:none; position:fixed; inset:0; background:rgba(0,0,0,.6); z-index:60; align-items:flex-start; justify-content:center; padding:40px 14px; overflow:auto; }
   #cmpov.on { display:flex; }
@@ -448,6 +451,8 @@
       <button id="cmpbtn" class="ex" title="Compare your shortlisted listings side by side">⚖ Compare</button>
       <button id="csvbtn" class="ex" title="Download the current filtered + sorted view (visible columns) as CSV">⬇ CSV</button>
       <button id="linkbtn" class="ex" title="Copy a shareable link to this exact filtered view">🔗 Copy link</button>
+      <select id="heat" title="Color cards/rows by a metric — red (low) → green (high) over the filtered set" style="width:auto"><option value="none">Heat: off</option><option value="deal">Heat: Deal score</option><option value="cap">Heat: Cap rate</option><option value="coc">Heat: Cash-on-cash</option></select>
+      <span id="heatlegend" class="heatlegend">low <span class="bar"></span> high · <span class="hl"></span></span>
       <label>Sort</label>
       <select id="sortTop" style="width:auto;min-width:180px"></select>
     </div>
@@ -608,6 +613,23 @@ let colSort = null;  // {key, dir} — click-to-sort on a List column; overrides
 const SHORT = new Set((function(){ try{ return JSON.parse(localStorage.getItem('cre_shortlist')||'[]'); }catch(e){ return []; } })());
 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(); }
+let heat = localStorage.getItem('cre_heat') || 'none';
+function heatVal(p){ const f=p.finance||{}; return heat==='deal'?(p.deal&&p.deal.score):heat==='cap'?p.cap_rate:heat==='coc'?f.coc: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=$('#heatlegend'); if(lg){ lg.classList.toggle('on',heat!=='none'); if(heat!=='none'){ lg.querySelector('.hl').textContent={deal:'Deal score',cap:'Cap rate',coc:'Cash-on-cash'}[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;
+  const listMode=$('#grid').classList.contains('listhost');
+  rows.forEach(p=>{ const v=heatVal(p);
+    const el=listMode?document.querySelector('.listtbl tr[data-id="'+p.id+'"] td.stick'):document.getElementById('card-'+p.id);
+    if(!el) return;
+    if(v==null||!isFinite(v)){ if(listMode) el.style.boxShadow=''; else el.style.borderLeft=''; return; }
+    const col=heatColor((v-min)/span);
+    if(listMode) el.style.boxShadow='inset 5px 0 0 '+col; else el.style.borderLeft='6px solid '+col;
+  });
+}
 function buildFieldToggles(){
   const host=$('#fFields'); if(!host) return;
   host.innerHTML = FIELDS.map(f=>`<label class="ftog"><input type="checkbox" data-fk="${f.k}" ${vis(f.k)?'checked':''}><span>${f.label}</span>${f.col?'<span class="colbadge" title="Also shows as a column in List view">▤</span>':''}</label>`).join('');
@@ -997,6 +1019,7 @@ function stateToURL(){
   if(scenario!=='25') p.set('sc',scenario);
   const so=$('#sort')&&$('#sort').value; if(so && so!=='deal') p.set('sort',so);
   if(colSort) p.set('cs',colSort.key+':'+(colSort.dir>0?'a':'d'));
+  if(heat!=='none') p.set('ht',heat);
   if(view!=='grid') p.set('v',view);
   const qs=p.toString();
   // NOTE: the app declares a local `history` (chat log) that shadows window.history — must qualify.
@@ -1020,6 +1043,7 @@ function urlToState(){
   if(p.get('sort')&&$('#sort')){ $('#sort').value=p.get('sort'); if($('#sortTop'))$('#sortTop').value=p.get('sort'); }
   if(p.get('v')){ view=p.get('v'); }
   if(p.get('cs')){ const [k,d]=p.get('cs').split(':'); colSort={key:k,dir:d==='a'?1:-1}; }
+  if(p.get('ht')){ heat=p.get('ht'); }
   return any;
 }
 function csvEsc(v){ v=(v==null?'':String(v)); return '"'+v.replace(/"/g,'""')+'"'; }
@@ -1099,6 +1123,7 @@ function render(){
   if(!r.length){ g.className='grid'; g.innerHTML=`<div class="empty">No properties match these filters.<br><span class="small">Loosen a filter or hit “Reset all”.</span></div>`; }
   else if(view==='list'){ g.className='listhost'; g.innerHTML=renderList(r); }
   else { g.className='grid'; g.innerHTML=r.map(card).join(''); enrichAssessor(); }
+  if(r.length) applyHeat(r);
   $('#count').innerHTML = `<b>${r.length}</b> of ${DATA.ranked.length} properties${fc?` · ${fc} filter${fc>1?'s':''} active`:''}`;
   $('#foot').textContent = `${scenario==='cash'?'all-cash':SCEN[scenario].downPct+'% down @ '+SCEN[scenario].ratePct+'%'} · * →X% = projected cap after value-add (not in-place). Cap rates broker-stated — verify NOI in DD.`;
   saveState();
@@ -1347,6 +1372,7 @@ fetch('data/ranked.json').then(r=>r.json()).then(async d=>{
   $('#grid').onclick=e=>{ 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(); };
   $('#cmpov').addEventListener('click',e=>{ if(e.target.id==='cmpov') e.currentTarget.classList.remove('on'); });
 
   window.DATA_READY = true;

← b5a7c9f feat(index): live facet counts (DTD verdict A, 2/3 Codex+Cla  ·  back to Commercialrealestate  ·  feat(mls): feature parity (DTD verdict B, 2/3 Codex+Claude; 7e7b8f4 →