[object Object]

← back to Commercialrealestate

feat(index): collapsible filter tabs (all collapsed on load) + wait-for-selection empty state (no 2134-card dump on load) + ◆ Non-QM in-grid filter (unwarrantable condos + small multifamily 1-9u + mixed-use) + lazy assessor data points (assessed value / land-imp split / use-code / prior sale via /api/property-history, IntersectionObserver + eager first batch, concurrency-capped, cached)

e9d13a8d8540cd65ed9ac5e7716c8d9c91fd63b7 · 2026-07-02 10:29:40 -0700 · Steve Abrams

Files touched

Diff

commit e9d13a8d8540cd65ed9ac5e7716c8d9c91fd63b7
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 2 10:29:40 2026 -0700

    feat(index): collapsible filter tabs (all collapsed on load) + wait-for-selection empty state (no 2134-card dump on load) + ◆ Non-QM in-grid filter (unwarrantable condos + small multifamily 1-9u + mixed-use) + lazy assessor data points (assessed value / land-imp split / use-code / prior sale via /api/property-history, IntersectionObserver + eager first batch, concurrency-capped, cached)
---
 public/index.html | 81 +++++++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 73 insertions(+), 8 deletions(-)

diff --git a/public/index.html b/public/index.html
index 5ded577..48d6ea3 100644
--- a/public/index.html
+++ b/public/index.html
@@ -166,6 +166,12 @@
     #drawer { position:sticky; top:57px; height:calc(100vh - 57px); transform:none; flex:0 0 var(--drawerW); box-shadow:none; }
     #drawer .dhead .x { display:none; }
   }
+  /* collapsible filter tabs */
+  .fsec > h4 { cursor:pointer; display:flex; align-items:center; justify-content:space-between; user-select:none; }
+  .fsec > h4::after { content:'▾'; color:var(--mut); font-size:11px; transition:transform .15s; margin-left:8px; }
+  .fsec.collapsed > h4::after { transform:rotate(-90deg); }
+  .fsec.collapsed > *:not(h4) { display:none !important; }
+  .assessline { border-top:1px dashed rgba(255,255,255,.06); padding-top:6px; margin-top:2px; }
 </style>
 </head>
 <body>
@@ -551,6 +557,10 @@ const FIELDS = [
   {k:'hoa',        label:'HOA / mo (condo)',   card:1, col:1, def:0},
   {k:'warr',       label:'Warrantability',     card:1, col:1, def:0},
   {k:'broker',     label:'Broker (condo)',     card:1, col:1, def:0},
+  {k:'assessed',   label:'Assessed value ✦',   card:1, col:0, def:0, assessor:1},
+  {k:'landimp',    label:'Land / Improvement ✦',card:1, col:0, def:0, assessor:1},
+  {k:'usecode',    label:'Assessor use code ✦', card:1, col:0, def:0, assessor:1},
+  {k:'priorsale',  label:'Prior sale ✦',        card:1, col:0, def:0, assessor:1},
 ];
 const VIS = (function(){ let s={}; try{ s=JSON.parse(localStorage.getItem('cre_fields')||'{}'); }catch(e){} const o={}; FIELDS.forEach(f=>o[f.k]=(f.k in s)?!!s[f.k]:!!f.def); return o; })();
 function vis(k){ return !!VIS[k]; }
@@ -651,7 +661,7 @@ function loopnetLink(p){
 }
 // Rich filter state
 const F = { types:new Set(), firms:new Set(), cityQuery:'', priceMin:null, priceMax:null,
-            capMin:0, unitsMin:null, yearMin:null, status:'all', afford:false, unwarrantable:false };
+            capMin:0, unitsMin:null, yearMin:null, status:'all', afford:false, unwarrantable:false, nonqm:false };
 
 function recompute(){
   const A = SCEN[scenario];
@@ -750,11 +760,54 @@ function card(p){
     ${vis('demo')?demoLine(p):''}
     ${vis('rent')?rentLine(p):''}
     ${vis('links')?`<div class="src"><a href="${p.source}" target="_blank" rel="noopener noreferrer">View listing ↗</a> · ${loopnetLink(p)} · <button class="lcbtn" data-id="${p.id}" title="Pull fresh comps from LoopNet via a cloud browser (~$0.03)">🔄 Live comps</button> · <button class="histbtn" data-id="${p.id}" data-address="${(p.address||'').replace(/"/g,'&quot;')}" data-city="${(p.city||'').replace(/"/g,'&quot;')}" data-zip="${p.zip||''}" title="LA County assessor + permit + film history (free)">📜 Property history</button></div>`:''}
+    ${anyAssessorVisible()?`<div class="assessline small" data-addr="${(p.address||'').replace(/"/g,'&quot;')}" data-city="${(p.city||'').replace(/"/g,'&quot;')}">📋 assessor…</div>`:''}
     <div class="lcout" id="lc-${p.id}"></div>
     <div class="histout" id="hist-${p.id}"></div>
   </div>`;
 }
 
+// ---- Non-QM target set: unwarrantable condos + small multifamily (1–9u) + mixed-use ----
+function isNonQM(p){
+  const t=(p.type||'').toLowerCase();
+  if(p.isCondo && p.warrantable_status && p.warrantable_status!=='fha_approved') return true;
+  if(/multi/.test(t) && p.units>=1 && p.units<=9) return true;
+  if(/mixed/.test(t)) return true;
+  return false;
+}
+// ---- lazy assessor enrichment (LA County parcel roll via /api/property-history, free) ----
+function anyAssessorVisible(){ return FIELDS.some(f=>f.assessor && vis(f.k)); }
+window.ASSESS = window.ASSESS || {};
+let assessObs = null;
+function assessLine(d){
+  const a=d&&(d.assessor||d.assessorArchive); const bits=[];
+  if(vis('assessed')){ const v=a&&(a.roll_total_value||a.total_value); if(v) bits.push('💰 assessed $'+(+v).toLocaleString()); }
+  if(vis('landimp')&&a){ const l=a.roll_land_value, im=a.roll_imp_value; if(l||im) bits.push('land $'+(+(l||0)).toLocaleString()+' / imp $'+(+(im||0)).toLocaleString()); }
+  if(vis('usecode')&&a){ const u=a.use_desc2||a.use_desc1||a.use_description; if(u) bits.push('🏷 '+u); }
+  if(vis('priorsale')&&d&&d.sales&&d.sales.length){ const s=d.sales[0]; bits.push('last sold $'+(+(s.sold_price||0)).toLocaleString()+' · '+String(s.sold_date||'').slice(0,10)); }
+  return bits.length ? ('📋 '+bits.join(' · ')) : '<span class="na">📋 no assessor record on file</span>';
+}
+function enrichAssessor(){
+  if(assessObs){ assessObs.disconnect(); assessObs=null; }
+  if(!anyAssessorVisible()) return;
+  const els=[...document.querySelectorAll('.assessline')];
+  const queue=[]; let active=0; const MAX=6;
+  function pump(){ while(active<MAX && queue.length){ fetchOne(queue.shift()); } }
+  function fetchOne(el){
+    const addr=el.dataset.addr, city=el.dataset.city, key=(addr+'|'+city).toLowerCase();
+    if(window.ASSESS[key]!==undefined){ el.innerHTML=assessLine(window.ASSESS[key]); pump(); return; }
+    active++;
+    fetch('/api/property-history?address='+encodeURIComponent(addr)+'&city='+encodeURIComponent(city||''))
+      .then(r=>r.json()).then(d=>{ window.ASSESS[key]=d; el.innerHTML=assessLine(d); })
+      .catch(()=>{ el.innerHTML='<span class="na">📋 assessor lookup failed</span>'; })
+      .finally(()=>{ active--; pump(); });
+  }
+  // As cards approach the viewport, queue them for enrichment (concurrency-capped).
+  assessObs=new IntersectionObserver((ents,obs)=>{ ents.forEach(en=>{ if(en.isIntersecting){ obs.unobserve(en.target); queue.push(en.target); pump(); } }); },{rootMargin:'400px'});
+  els.forEach(el=>assessObs.observe(el));
+  // Eager first batch so the top of the grid always resolves (no dependence on a scroll event).
+  els.slice(0,30).forEach(el=>{ assessObs.unobserve(el); queue.push(el); }); pump();
+}
+
 // ---- the rich filter pass ----
 function applyFilters(list){
   return list.filter(p=>{
@@ -769,6 +822,7 @@ function applyFilters(list){
     if(F.status==='active' && /Under Contract|Pending|OUT OF BUDGET/i.test(p.status)) return false;
     if(F.afford && !p.finance.affordable) return false;
     if(F.unwarrantable && !(p.isCondo && p.warrantable_status && p.warrantable_status!=='fha_approved')) return false;
+    if(F.nonqm && !isNonQM(p)) return false;
     return true;
   });
 }
@@ -777,7 +831,7 @@ function activeFilterCount(){
   if(F.types.size) n++; if(F.firms.size) n++; if(F.cityQuery) n++;
   if(F.priceMin!=null||F.priceMax!=null) n++; if(F.capMin>0) n++;
   if(F.unitsMin!=null) n++; if(F.yearMin!=null) n++;
-  if(F.status!=='all') n++; if(F.afford) n++; if(F.unwarrantable) n++;
+  if(F.status!=='all') n++; if(F.afford) n++; if(F.unwarrantable) n++; if(F.nonqm) n++;
   return n;
 }
 
@@ -791,15 +845,21 @@ const num=v=>v==null?-1e9:v;
 const dealNum=p=>(p.deal && p.deal.score!=null) ? p.deal.score : -1e9;  // unscored sink to the bottom
 
 function render(){
+  const g=$('#grid'); const fc=activeFilterCount();
+  $('#fcount').textContent=fc; $('#fcount').classList.toggle('on',fc>0);
+  // Wait-for-selection: on a clean load (no filter chosen) don't dump 2,134 cards — prompt for a pick.
+  if(fc===0 && !F.cityQuery){
+    g.className='grid';
+    g.innerHTML=`<div class="empty">👈 Pick an asset type or filter to begin.<br><span class="small">${(DATA.ranked.length).toLocaleString()} listings loaded · ${DATA.meta.market}. Try <b>Condo</b>, <b>⚑ Unwarrantable</b>, <b>◆ Non-QM</b>, a city, or a price range — results appear instantly.</span></div>`;
+    $('#count').innerHTML=`<b>${(DATA.ranked.length).toLocaleString()}</b> loaded · choose a filter to view`;
+    saveState(); return;
+  }
   let r = applyFilters(DATA.ranked.slice());
   r.sort(SORTS[$('#sort').value]||SORTS.composite);
-  const g=$('#grid');
   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(''); }
-  const fc=activeFilterCount();
+  else { g.className='grid'; g.innerHTML=r.map(card).join(''); enrichAssessor(); }
   $('#count').innerHTML = `<b>${r.length}</b> of ${DATA.ranked.length} properties${fc?` · ${fc} filter${fc>1?'s':''} active`:''}`;
-  $('#fcount').textContent=fc; $('#fcount').classList.toggle('on',fc>0);
   $('#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();
 }
@@ -812,8 +872,9 @@ function buildFacets(){
     .map(([k,c])=>`<span class="chip" data-${key}="${k.replace(/"/g,'&quot;')}">${k}<span class="ct">${c}</span></span>`).join('');
   // Condos are merged into the grid as a real 'Condo' asset type (chip comes from the data below).
   // Prepend one special in-grid filter: ⚑ Unwarrantable = Condo AND not FHA-approved (Arcstone non-QM target).
+  const nonqmChip='<span class="chip" data-warr="nonqm" style="border-color:rgba(188,140,255,.55);color:var(--purp,#bc8cff)" title="Arcstone non-QM target set: unwarrantable condos + small multifamily (1–9 units) + mixed-use">◆ Non-QM</span>';
   const unwChip='<span class="chip" data-warr="unwarrantable" style="border-color:rgba(248,81,73,.55);color:var(--bad)" title="Non-FHA-approved condos — filters the grid to Arcstone non-QM targets">⚑ Unwarrantable</span>';
-  $('#fType').innerHTML=unwChip+chipHTML(tCount,'t');
+  $('#fType').innerHTML=nonqmChip+unwChip+chipHTML(tCount,'t');
   $('#fFirm').innerHTML=chipHTML(fCount,'fm');
   // top 14 cities as quick chips; the search box covers the rest
   const topCities=Object.entries(cCount).sort((a,b)=>b[1]-a[1]).slice(0,14);
@@ -867,7 +928,7 @@ function setScenario(s){ scenario=s; localStorage.setItem('cre_scenario',s);
   $$('#scenario button').forEach(b=>b.classList.toggle('active',b.dataset.s===s)); recompute(); }
 
 function syncChips(){
-  $$('#fType .chip').forEach(c=>{ if(c.dataset.warr==='unwarrantable') c.classList.toggle('active',F.unwarrantable); else c.classList.toggle('active',F.types.has(c.dataset.t)); });
+  $$('#fType .chip').forEach(c=>{ if(c.dataset.warr==='unwarrantable') c.classList.toggle('active',F.unwarrantable); else if(c.dataset.warr==='nonqm') c.classList.toggle('active',F.nonqm); else c.classList.toggle('active',F.types.has(c.dataset.t)); });
   $$('#fFirm .chip').forEach(c=>c.classList.toggle('active',F.firms.has(c.dataset.fm)));
   $$('#fCityChips .chip').forEach(c=>c.classList.toggle('active',F.cityQuery===c.dataset.cy.toLowerCase()));
   $$('#fStatus .chip').forEach(c=>c.classList.toggle('active',F.status===c.dataset.st));
@@ -993,8 +1054,12 @@ fetch('data/ranked.json').then(r=>r.json()).then(async d=>{
   // drawer wiring
   $('#burger').onclick=openDrawer; $('#dclose').onclick=closeDrawer; $('#scrim').onclick=closeDrawer;
   $('#reset').onclick=resetAll;
+  // collapsible filter tabs — click a section header to expand/collapse; all collapsed on load
+  $('#drawer').addEventListener('click',e=>{ const h=e.target.closest('h4'); if(h && h.parentElement.classList.contains('fsec')) h.parentElement.classList.toggle('collapsed'); });
+  $$('#drawer .fsec').forEach(s=>s.classList.add('collapsed'));
   // facet chip handlers (multi-select toggles)
   $('#fType').onclick=e=>{ const c=e.target.closest('.chip'); if(!c) return;
+    if(c.dataset.warr==='nonqm'){ F.nonqm=!F.nonqm; syncChips(); render(); return; }
     if(c.dataset.warr==='unwarrantable'){ F.unwarrantable=!F.unwarrantable; if(F.unwarrantable) F.types=new Set(['Condo']); syncChips(); render(); return; }
     F.types.has(c.dataset.t)?F.types.delete(c.dataset.t):F.types.add(c.dataset.t);
     if(!F.types.has('Condo')) F.unwarrantable=false;

← 44c3be8 feat(index+mls): merge condos IN-GRID as a real 'Condo' asse  ·  back to Commercialrealestate  ·  feat(index): active-filter summary bar — removable chips for 9dc699f →