[object Object]

← back to Commercialrealestate

5x sweep 3 (contrarian-gated): fix medAll ÷0 → $NaN tile; guard F.money/F.num/countUp + grad() undefined-color crash (was swallowed on empty data); make Refresh/toggle verifier-discoverable; add strong deals-assert.js (charts-drew-data + empty-data + no-NaN gate)

292b7fb70b4ebecb0a451c301c497e82c8bf2648 · 2026-06-28 22:11:26 -0700 · Steve

Files touched

Diff

commit 292b7fb70b4ebecb0a451c301c497e82c8bf2648
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun Jun 28 22:11:26 2026 -0700

    5x sweep 3 (contrarian-gated): fix medAll ÷0 → $NaN tile; guard F.money/F.num/countUp + grad() undefined-color crash (was swallowed on empty data); make Refresh/toggle verifier-discoverable; add strong deals-assert.js (charts-drew-data + empty-data + no-NaN gate)
---
 5x/deals-assert.js | 40 ++++++++++++++++++++++++++++++++++++++++
 5x/sweep-2-ct.raw  |  4 ++++
 5x/sweep-2.raw     |  7 +++++++
 public/deals.html  | 15 ++++++++-------
 4 files changed, 59 insertions(+), 7 deletions(-)

diff --git a/5x/deals-assert.js b/5x/deals-assert.js
new file mode 100644
index 0000000..ae316fc
--- /dev/null
+++ b/5x/deals-assert.js
@@ -0,0 +1,40 @@
+// Strong deals.html gate (what /3x's --selector cannot do): assert the 6 charts actually drew DATA,
+// no tile renders NaN/$NaN, and the EMPTY-DATA path degrades without crashing or showing NaN.
+const pw = require('/Users/stevestudio2/.claude/skills/browserbase/node_modules/playwright-core');
+const EXEC = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
+const CHARTS = ['cPipe','cType','cCap','cMed','cGeo','cBand'];
+
+async function run(label, mockEmpty){
+  const errors=[];
+  const b = await pw.chromium.launch({ executablePath: EXEC, headless: true });
+  const pg = await b.newPage({ viewport:{width:1320,height:1000} });
+  pg.on('pageerror', e=>errors.push('PAGEERROR: '+e.message));
+  pg.on('console', m=>{ if(m.type()==='error' && !/Failed to load resource/.test(m.text())) errors.push('CONSOLE: '+m.text()); });
+  if (mockEmpty){
+    await pg.route('**/api/segments', r=>r.fulfill({status:200,contentType:'application/json',body:'{"segments":[]}'}));
+    await pg.route('**/api/crcp/stats', r=>r.fulfill({status:200,contentType:'application/json',body:'{}'}));
+    await pg.route('**/data/ranked.json', r=>r.fulfill({status:200,contentType:'application/json',body:'{"ranked":[]}'}));
+    await pg.route('**/data/map-points.json', r=>r.fulfill({status:200,contentType:'application/json',body:'{"points":[]}'}));
+  }
+  await pg.goto('http://127.0.0.1:9911/deals.html', { waitUntil:'networkidle' });
+  await pg.waitForTimeout(2200);
+  const r = await pg.evaluate((CHARTS)=>{
+    const charts = CHARTS.map(id=>{ const c=Chart.getChart(id); return { id, exists:!!c, points: c?(c.data.datasets[0]?.data?.length||0):0 }; });
+    const tiles = [...document.querySelectorAll('.tile .v')].map(v=>v.textContent);
+    const nanTiles = tiles.filter(t=>/nan|undefined/i.test(t));
+    return { charts, tiles, nanTiles };
+  }, CHARTS);
+  await b.close();
+  const chartsWithData = r.charts.filter(c=>c.points>0).length;
+  return { label, ...r, chartsWithData, jsErrors:errors };
+}
+
+(async()=>{
+  const normal = await run('NORMAL', false);
+  const empty  = await run('EMPTY-DATA', true);
+  // Pass criteria:
+  const passNormal = normal.charts.every(c=>c.exists) && normal.chartsWithData>=5 && normal.nanTiles.length===0 && normal.jsErrors.length===0;
+  const passEmpty  = empty.nanTiles.length===0 && empty.jsErrors.length===0;   // empty allowed; NaN/crash NOT
+  console.log(JSON.stringify({ normal, empty, passNormal, passEmpty, VERDICT: (passNormal&&passEmpty)?'PASS':'FAIL' }, null, 2));
+  process.exit((passNormal&&passEmpty)?0:1);
+})().catch(e=>{ console.error('FAIL', e.message); process.exit(1); });
diff --git a/5x/sweep-2-ct.raw b/5x/sweep-2-ct.raw
index 9c07957..15ba57e 100644
--- a/5x/sweep-2-ct.raw
+++ b/5x/sweep-2-ct.raw
@@ -9,3 +9,7 @@ click-through agent → http://127.0.0.1:9911/deals.html
   [FAIL] verdict button: skip — locator.click: Timeout 30000ms exceeded.
   [FAIL] Next → — locator.click: Timeout 30000ms exceeded.
   [FAIL] ← Prev — locator.click: Timeout 30000ms exceeded.
+  [FAIL] switch to Board — locator.click: Timeout 30000ms exceeded.
+  [FAIL] density slider — locator.fill: Timeout 30000ms exceeded.
+
+  CLICK-THROUGH → 0 passed, 10 failed, 0 skipped
diff --git a/5x/sweep-2.raw b/5x/sweep-2.raw
index 84ce958..cd331bd 100644
--- a/5x/sweep-2.raw
+++ b/5x/sweep-2.raw
@@ -23,3 +23,10 @@ click-through agent → http://127.0.0.1:9911/deals.html
   [FAIL] verdict button: skip — locator.click: Timeout 30000ms exceeded.
   [FAIL] Next → — locator.click: Timeout 30000ms exceeded.
   [FAIL] ← Prev — locator.click: Timeout 30000ms exceeded.
+  [FAIL] switch to Board — locator.click: Timeout 30000ms exceeded.
+  [FAIL] density slider — locator.fill: Timeout 30000ms exceeded.
+
+  CLICK-THROUGH → 0 passed, 10 failed, 0 skipped
+
+  RESULT → 6/7 passed, 1 FAILED
+  artifacts: /var/folders/2n/jmw9_4v56_x4m3539tr4v0lr0000gn/T/3x-nqf9lO
diff --git a/public/deals.html b/public/deals.html
index 23c0be2..7ca9514 100644
--- a/public/deals.html
+++ b/public/deals.html
@@ -82,8 +82,8 @@
       <p id="asof">LA County commercial + residential pipeline · live</p>
     </div>
     <div class="ctrls">
-      <button id="refresh" class="ctrl-btn" title="Re-pull live data + redraw all charts"><span class="ico">↻</span> Refresh</button>
-      <span class="seg-toggle" id="metric" title="Switch the pipeline chart metric">
+      <button id="refresh" class="ctrl-btn btn" title="Re-pull live data + redraw all charts"><span class="ico">↻</span> Refresh</button>
+      <span class="seg-toggle seg" id="metric" title="Switch the pipeline chart metric">
         <button data-m="count" class="on">Deals</button><button data-m="med">Median $</button>
       </span>
     </div>
@@ -119,18 +119,18 @@
 </div>
 
 <script>
-const F = { money:n=>n==null?'—':'$'+(n>=1e9?(n/1e9).toFixed(1)+'B':n>=1e6?(n/1e6).toFixed(2)+'M':n>=1e3?Math.round(n/1e3)+'k':n),
-  num:n=>Number(n||0).toLocaleString() };
+const F = { money:n=>(n==null||!isFinite(n))?'—':'$'+(n>=1e9?(n/1e9).toFixed(1)+'B':n>=1e6?(n/1e6).toFixed(2)+'M':n>=1e3?Math.round(n/1e3)+'k':n),
+  num:n=>{ n=Number(n); return isFinite(n)?n.toLocaleString():'—'; } };
 const PAL = ['#e9c46a','#2dd4bf','#8b7cf6','#60a5fa','#f472b6','#fb923c','#34d399','#f87171','#a78bfa','#fbbf24'];
 // translucent gradient fill for glass-matched bars
-function grad(ctx, c, vertical=true){ const a=ctx.chart.chartArea; if(!a) return c+'cc';
+function grad(ctx, c, vertical=true){ if(typeof c!=='string'||!c) c='#888888'; const a=ctx.chart.chartArea; if(!a) return c+'cc';
   const g=ctx.chart.ctx.createLinearGradient(vertical?0:a.left,vertical?a.bottom:0,vertical?0:a.right,vertical?a.top:0);
   g.addColorStop(0,c+'33'); g.addColorStop(1,c+'ee'); return g; }
 Chart.defaults.color='#9aa3b4'; Chart.defaults.borderColor='rgba(255,255,255,.07)';
 Chart.defaults.font.family="-apple-system,'SF Pro Display',Segoe UI,Roboto,sans-serif";
 const glassTip={backgroundColor:'rgba(16,20,28,.92)',borderColor:'rgba(255,255,255,.14)',borderWidth:1,padding:11,cornerRadius:12,titleColor:'#fff',bodyColor:'#cfd6e4',displayColors:true};
 
-function countUp(el,to,fmt){ const dur=900,t0=performance.now();
+function countUp(el,to,fmt){ if(!isFinite(to))to=0; const dur=900,t0=performance.now();
   (function step(t){ const k=Math.min(1,(t-t0)/dur), e=1-Math.pow(1-k,3); el.textContent=fmt(Math.round(to*e)); if(k<1)requestAnimationFrame(step); })(t0); }
 
 let LAST=null, METRIC='count';
@@ -156,7 +156,8 @@ function draw(){
   const totalProps=(stats.listings||0)+(stats.sfr||rows.length*0||0)+ (pts.filter(p=>p.kind==='condo').length||0) || pts.length;
   const screened = pts.length || (segs.reduce((s,x)=>s+x.props,0));
   const agents=(stats.brokers||stats.agents_commercial||0);
-  const medAll = segs.length? Math.round(segs.reduce((s,x)=>s+x.med,0)/segs.filter(x=>x.med).length):0;
+  const priced = segs.filter(x=>x.med>0);                                // guard ÷0 → no "$NaN" tile
+  const medAll = priced.length ? Math.round(priced.reduce((s,x)=>s+x.med,0)/priced.length) : 0;
   const tiles=[
     {k:'Properties screened',v:screened,cls:'gold',ic:'🏙️',s:'commercial · condo · SFR, geocoded'},
     {k:'Loan products live',v:segs.length,cls:'teal',ic:'◆',s:'Arcstone call segments'},

← 022582f 5x: sweep logs + REPORT (deals.html clean twice; clickthroug  ·  back to Commercialrealestate  ·  5x: final REPORT — clean twice (sweeps 4-5) with strengthene 2f5592e →