[object Object]

← back to Commercialrealestate

5x sweep 4 (contrarian follow-up): sort+density on broker-grid + condos grids; fix pre-existing favicon 404 on condos+graphics

a19ee81b24bdbcc3efc93b71f0466496ae708dbd · 2026-07-10 11:30:57 -0700 · Steve Abrams

Files touched

Diff

commit a19ee81b24bdbcc3efc93b71f0466496ae708dbd
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Jul 10 11:30:57 2026 -0700

    5x sweep 4 (contrarian follow-up): sort+density on broker-grid + condos grids; fix pre-existing favicon 404 on condos+graphics
---
 5x/REPORT.md            |  8 ++++++++
 5x/sweep-4.md           | 24 ++++++++++++++++++++++++
 public/broker-grid.html | 32 +++++++++++++++++++++++++++++---
 public/condos.html      | 28 ++++++++++++++++++++++++++++
 public/graphics.html    |  1 +
 5 files changed, 90 insertions(+), 3 deletions(-)

diff --git a/5x/REPORT.md b/5x/REPORT.md
index e56438a..0813534 100644
--- a/5x/REPORT.md
+++ b/5x/REPORT.md
@@ -27,3 +27,11 @@
 
 ## Stop condition: CLEAN TWICE (sweeps 2 & 3). No open defects.
 ## Scope note: verified the 3 actively-changed pages; did NOT deploy (local only).
+
+## Sweep 4 (contrarian gate) — additional fixes
+- broker-grid.html: added sort select + density slider (was missing both in grid)
+- condos.html: added sort select (had density, lacked sort)
+- condos.html + graphics.html: fixed pre-existing /favicon.ico 404 (missing inline icon link)
+- Verified: all 6 touched pages 0 JS errors; sort/density functional + persistent.
+- Re-gate: contrarian's scope critique addressed. Remaining app grids not rendered
+  as card-grids (deals/sales/rent-rolls/brokers are tables) — no density applies.
diff --git a/5x/sweep-4.md b/5x/sweep-4.md
new file mode 100644
index 0000000..bbb992d
--- /dev/null
+++ b/5x/sweep-4.md
@@ -0,0 +1,24 @@
+# /5x — Sweep 4 (contrarian gate follow-up)
+/contrarian returned NOT-CLEAN: validated my 2 dismissals as correct, but caught
+that I scoped to only 3 touched pages while Steve's grid rule is "every grid".
+
+Real gaps it found + fixed this sweep:
+1. broker-grid.html — grid view had NO sort + NO density (twin of the mls bug).
+   FIX: #sortsel (from COLS) + #density slider (--card-min 400→150), persisted,
+   thead-sync. Verified: density visible, 372px@2 vs 178px@9, both persist, 0 err.
+2. condos.html — had density (#cols) but NO sort. FIX: #csort select (Newest/
+   Price↑↓/$SF/Year/HOA/Project/City) + sortRows() in apply(), persisted.
+   Verified: re-orders rows (price↓ vs ↑ differ), persists across reload, 0 err.
+
+Bonus real defect surfaced while verifying (PRE-EXISTING, A/B-proven against the
+original file — not caused by my edit):
+3. condos.html + graphics.html threw "1 JS error" = /favicon.ico 404. Root cause:
+   both lacked the inline data-URI <link rel=icon> that the other 12 pages have,
+   so Chrome fell back to requesting /favicon.ico (which the server 404s).
+   FIX: added the inline icon link to both. condos M3 now 0 errors x2.
+
+Gotcha logged: system Chrome (runner's engine) reports resource 404s as console
+errors; bundled Chromium does not — why the bug only showed via the /3x runner.
+Also fixed a TDZ risk: hoisted condos SORTKEY to top-level state.
+
+All 6 touched pages: 0 JS errors. CLEAN.
diff --git a/public/broker-grid.html b/public/broker-grid.html
index 520fc8b..b4c9413 100644
--- a/public/broker-grid.html
+++ b/public/broker-grid.html
@@ -42,7 +42,10 @@
   #modal .litem{display:flex;justify-content:space-between;gap:10px;padding:4px 0;border-bottom:1px solid #20262f;font-size:13px;} #modal .litem .m{color:var(--mut);}
   .mbtn{background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:9px;padding:7px 12px;font-size:13px;cursor:pointer;} .mbtn:hover{border-color:var(--blue);}
   .pitch{background:#0a0d13;border:1px solid var(--line);border-radius:10px;padding:12px;font-size:13px;white-space:pre-wrap;margin-top:10px;} .pitch .subj{color:var(--gold);font-weight:700;margin-bottom:6px;}
-  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:20px;}
+  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--card-min,280px),1fr));gap:20px;}
+  #sortsel{background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:7px;padding:5px 8px;font-size:12px}
+  #denswrap{display:none;align-items:center;gap:6px;font-size:13px;color:var(--mut)}
+  #denswrap input[type=range]{width:92px;accent-color:var(--acc)}
   .gc{background:var(--card);border:2px solid #fff;border-radius:11px;padding:18px 20px;}
   .gc .nm{font-weight:700;} .gc .fm{color:var(--mut);font-size:12px;margin-bottom:6px;}
   .gc .row{display:flex;justify-content:space-between;gap:8px;font-size:12.5px;padding:2px 0;border-top:1px solid #20262f;}
@@ -82,6 +85,8 @@
   <span class="spacer"></span>
   <input class="search" id="q" type="text" placeholder="search name, firm, phone, email, specialty…">
   <div class="toggle" id="view"><button data-v="table" class="on">▦ List</button><button data-v="grid">▤ Grid</button></div>
+  <select id="sortsel" title="Sort brokers"></select>
+  <label id="denswrap" title="Card density — grid view">⤢<input type="range" id="density" min="1" max="10" step="1"></label>
   <button class="csvbtn" id="csv" title="Export current rows to CSV">⬇ CSV</button>
   <span class="count" id="count">…</span>
 </header>
@@ -233,14 +238,35 @@ async function genPitch(name,id){
 }
 document.addEventListener('click',e=>{ const row=e.target.closest('.brow'); if(row && e.target.tagName!=='A'){ openContact(row.dataset.name,row.dataset.id); } if(e.target.id==='ov') $('#ov').classList.remove('on'); });
 $('#thead').addEventListener('click',e=>{const th=e.target.closest('th');if(!th)return;const k=th.dataset.k;
-  if(sortKey===k)sortDir*=-1;else{sortKey=k;sortDir=(k==='listings'||k==='total_assets'||k==='created_at')?-1:1;}render();});
+  if(sortKey===k)sortDir*=-1;else{sortKey=k;sortDir=(k==='listings'||k==='total_assets'||k==='created_at')?-1:1;}
+  localStorage.setItem('brokSortKey',sortKey);localStorage.setItem('brokSortDir',sortDir);if(window.__syncSortSel)window.__syncSortSel();render();});
 $('#q').addEventListener('input',()=>{q=$('#q').value.trim();render();});
 function exportCSV(){ const rows=filtered(); const cols=visCols(); const cv=(r,c)=>{let v=r[c.k]; v=(v==null?'':String(v)); return '"'+v.replace(/"/g,'""')+'"';};
   const csv=cols.map(c=>'"'+c.l+'"').join(',')+'\n'+rows.map(r=>cols.map(c=>cv(r,c)).join(',')).join('\n');
   const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([csv],{type:'text/csv'})); a.download='cre-brokers-'+rows.length+'.csv'; a.click(); URL.revokeObjectURL(a.href); }
 $('#csv').addEventListener('click',exportCSV);
 $('#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();});
+  document.querySelectorAll('#view button').forEach(x=>x.classList.toggle('on',x===b));view=b.dataset.v;localStorage.setItem('brokView',view);$('#denswrap').style.display=(view==='grid')?'inline-flex':'none';render();});
+// ── sort <select> + density slider (Steve rule: every grid gets sort + density) ──
+(function(){
+  const sel=$('#sortsel'),dz=$('#density');
+  sel.innerHTML=COLS.filter(c=>c.t==='s'||c.t==='n'||c.t==='date'||c.t==='atype').map(c=>`<option value="${c.k}">Sort: ${esc(c.l)}</option>`).join('');
+  const sk=localStorage.getItem('brokSortKey'),sd=localStorage.getItem('brokSortDir');
+  if(sk&&COLS.some(c=>c.k===sk))sortKey=sk; if(sd==='1'||sd==='-1')sortDir=+sd;
+  if(!Array.from(sel.options).some(o=>o.value===sortKey))sortKey='listings';
+  sel.value=sortKey;
+  sel.addEventListener('change',()=>{sortKey=sel.value;const c=COLS.find(x=>x.k===sortKey);
+    sortDir=(c&&(c.t==='n'||c.t==='date'))?-1:1;
+    localStorage.setItem('brokSortKey',sortKey);localStorage.setItem('brokSortDir',sortDir);render();});
+  const widthFor=v=>Math.round(400-(Math.max(1,Math.min(10,v))-1)*(400-150)/9);
+  const applyDensity=v=>document.documentElement.style.setProperty('--card-min',widthFor(v)+'px');
+  const dv=parseInt(localStorage.getItem('brokDensity'),10),dval=(dv>=1&&dv<=10)?dv:5;
+  dz.value=dval;applyDensity(dval);
+  dz.addEventListener('input',()=>{const v=+dz.value;applyDensity(v);localStorage.setItem('brokDensity',v);});
+  const pv=localStorage.getItem('brokView'); if(pv==='grid'||pv==='table'){view=pv;document.querySelectorAll('#view button').forEach(x=>x.classList.toggle('on',x.dataset.v===view));}
+  $('#denswrap').style.display=(view==='grid')?'inline-flex':'none';
+  window.__syncSortSel=()=>{if(Array.from(sel.options).some(o=>o.value===sortKey)&&sel.value!==sortKey)sel.value=sortKey;};
+})();
 // left-rail facets + column toggles
 $('#fType').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.types,c.dataset.ty);c.classList.toggle('active');render();});
 $('#fFirm').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.firms,c.dataset.fm);c.classList.toggle('active');render();});
diff --git a/public/condos.html b/public/condos.html
index 2c4fb59..7412a73 100644
--- a/public/condos.html
+++ b/public/condos.html
@@ -3,6 +3,7 @@
 <head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
+<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%230e1116'/%3E%3Ctext x='8' y='12' font-size='10' text-anchor='middle' fill='%233fb950'%3E%F0%9F%8F%A2%3C/text%3E%3C/svg%3E">
 <title>LA Condos — Warrantability (FHA-approval proxy)</title>
 <style>
   :root{ --bg:#0e1116; --card:#161b22; --line:#2a313c; --ink:#e6edf3; --mut:#8b949e; --acc:#3fb950; --blue:#58a6ff; --gold:#d29922; --red:#f85149; --purp:#bc8cff; }
@@ -108,6 +109,16 @@
     <input type="text" id="q" placeholder="search project / city / zip / address / broker / firm…">
     <span class="viewseg" id="viewseg"><button data-view="grid" class="active">▦ Grid</button><button data-view="list">☰ List</button></span>
     <button id="shortbtn" title="Show only your shortlisted (starred) listings — shared with the Explorer & MLS">⭐ Shortlist (0)</button>
+    <select id="csort" title="Sort condos" style="background:var(--card,#141925);color:var(--ink,#E6E8EC);border:1px solid var(--line,#232a38);border-radius:7px;padding:5px 8px;font-size:12px">
+      <option value="">Sort: Newest</option>
+      <option value="price_desc">Sort: Price ↓</option>
+      <option value="price_asc">Sort: Price ↑</option>
+      <option value="pps_desc">Sort: $/SF ↓</option>
+      <option value="year_desc">Sort: Year (newest)</option>
+      <option value="hoa_asc">Sort: HOA ↑</option>
+      <option value="proj_az">Sort: Project A→Z</option>
+      <option value="city_az">Sort: City A→Z</option>
+    </select>
     <label class="dens">Density <input type="range" id="cols" min="1" max="20" step="1" value="3"></label>
     <span class="count" id="count">…</span>
   </div>
@@ -123,6 +134,7 @@ const esc=s=>String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').rep
 const PROXY='FHA/VA-approval-based proxy, NOT lender-verified Fannie/Freddie warrantability.';
 const NOW_YEAR=new Date().getFullYear();
 let DATA=[], MODE='db', VIEW=localStorage.getItem('condoView')||'grid', bannerDefault='';
+let SORTKEY=localStorage.getItem('condoSort')||'';   // hoisted so early apply() during boot never hits TDZ
 
 function statusBadge(s){
   const txt={ fha_approved:'FHA-APPROVED · proxy warrantable', fha_expired:'FHA-EXPIRED · re-cert', heuristic_flag:'FLAGGED · review', not_listed:'NOT LISTED · verify' }[s]||s;
@@ -248,6 +260,7 @@ function apply(){
   let rows=DATA.filter(passFacets);
   const ql=($('#q').value||'').trim().toLowerCase();
   if(ql) rows=rows.filter(c=>((c.project_name||'')+' '+(c.city||'')+' '+(c.zip||'')+' '+(c.address||'')+' '+(c.broker_name||'')+' '+(c.firm_name||'')).toLowerCase().includes(ql));
+  rows=sortRows(rows);
   renderStats(rows); updateShortBtn();
   const unw=F.warr==='unwarrantable';
   $('#count').innerHTML=`<b>${rows.length}</b> of ${DATA.length} ${unw?'unwarrantable (non-FHA) condos':(MODE==='fha'?'FHA-list projects':'condos')}`;
@@ -287,6 +300,21 @@ $('#fAll').addEventListener('click',()=>{FIELDS.forEach(f=>VIS[f.k]=true);saveVi
 $('#fNone').addEventListener('click',()=>{FIELDS.forEach(f=>VIS[f.k]=false);saveVis();buildFieldToggles();apply();});
 $('#fDef').addEventListener('click',()=>{FIELDS.forEach(f=>VIS[f.k]=!!f.def);saveVis();buildFieldToggles();apply();});
 $('#q').oninput=()=>{ clearTimeout(window._t); window._t=setTimeout(apply,180); };
+// ── sort (Steve rule: every grid gets sort + density; condos already had density) ──
+function sortRows(rows){
+  const num=(v)=>{const n=+v;return isFinite(n)?n:null;};
+  const cmp={
+    price_desc:(a,b)=>(num(b.price)??-1)-(num(a.price)??-1),
+    price_asc:(a,b)=>(num(a.price)??Infinity)-(num(b.price)??Infinity),
+    pps_desc:(a,b)=>((num(b.price)&&num(b.sqft)?b.price/b.sqft:-1))-((num(a.price)&&num(a.sqft)?a.price/a.sqft:-1)),
+    year_desc:(a,b)=>(num(b.year_built)??-1)-(num(a.year_built)??-1),
+    hoa_asc:(a,b)=>(num(a.hoa)??Infinity)-(num(b.hoa)??Infinity),
+    proj_az:(a,b)=>String(a.project_name||a.address||'').localeCompare(String(b.project_name||b.address||'')),
+    city_az:(a,b)=>String(a.city||'').localeCompare(String(b.city||'')),
+  }[SORTKEY];
+  return cmp?rows.slice().sort(cmp):rows;
+}
+(function(){ const s=$('#csort'); if(s){ s.value=SORTKEY; s.addEventListener('change',()=>{ SORTKEY=s.value; localStorage.setItem('condoSort',SORTKEY); apply(); }); } })();
 $('#cols').oninput=e=>{ document.documentElement.style.setProperty('--cols',e.target.value); localStorage.setItem('condoCols',e.target.value); };
 const savedCols=localStorage.getItem('condoCols'); if(savedCols){ $('#cols').value=savedCols; document.documentElement.style.setProperty('--cols',savedCols); }
 $$('#viewseg button').forEach(b=>b.classList.toggle('active',b.dataset.view===VIEW));
diff --git a/public/graphics.html b/public/graphics.html
index f8f7d49..a26aaf6 100644
--- a/public/graphics.html
+++ b/public/graphics.html
@@ -3,6 +3,7 @@
 <head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
+<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%230e1116'/%3E%3Ctext x='8' y='12' font-size='10' text-anchor='middle' fill='%233fb950'%3E%F0%9F%93%8A%3C/text%3E%3C/svg%3E">
 <title>CRE Graphics — Brokers, Firms & Warrantability · LA County</title>
 <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
 <style>

← 9e62037 5x: sweeps 2-3 clean + REPORT (clean twice, done)  ·  back to Commercialrealestate  ·  5x: correct report scope note — verified deals/fha/rent-roll 65dbb60 →