[object Object]

← back to Commercialrealestate

crcp graphics.html: make every datum drillable (TK-10091)

f5e7a1a61780b8c4066aaf14eb657def1d2afb45 · 2026-07-31 10:53:45 -0700 · Steve

Every chart bar/slice, stat count, and expiring-table cell now hrefs to its
filtered/detail view (Steve's all-data-points-drillable rule):
- broker bars → /crcp.html?broker=<id> (broker modal); firm slices → ?firm=<name>
- warrantability doughnuts + city bars → /condos.html?status=&city=
- expiring rows: project→?status=fha_expired&q=, city→?city=, zip→?q=
- stat-bar counts → matching filtered condos/crcp views
Adds ?city=/?q= deep-links to condos.html (city resolves case-insensitively,
FHA-list UPPERCASE vs scraped Title-Case) and ?broker=/?firm= boot to crcp.html.
Verified: 5x/graphics-drill-assert.js — 14/14 headless drill assertions pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit f5e7a1a61780b8c4066aaf14eb657def1d2afb45
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Jul 31 10:53:45 2026 -0700

    crcp graphics.html: make every datum drillable (TK-10091)
    
    Every chart bar/slice, stat count, and expiring-table cell now hrefs to its
    filtered/detail view (Steve's all-data-points-drillable rule):
    - broker bars → /crcp.html?broker=<id> (broker modal); firm slices → ?firm=<name>
    - warrantability doughnuts + city bars → /condos.html?status=&city=
    - expiring rows: project→?status=fha_expired&q=, city→?city=, zip→?q=
    - stat-bar counts → matching filtered condos/crcp views
    Adds ?city=/?q= deep-links to condos.html (city resolves case-insensitively,
    FHA-list UPPERCASE vs scraped Title-Case) and ?broker=/?firm= boot to crcp.html.
    Verified: 5x/graphics-drill-assert.js — 14/14 headless drill assertions pass.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 5x/graphics-drill-assert.js | 81 +++++++++++++++++++++++++++++++++++++++++++++
 public/condos.html          | 14 ++++++--
 public/graphics.html        | 48 ++++++++++++++++++++-------
 3 files changed, 129 insertions(+), 14 deletions(-)

diff --git a/5x/graphics-drill-assert.js b/5x/graphics-drill-assert.js
new file mode 100644
index 0000000..a85da41
--- /dev/null
+++ b/5x/graphics-drill-assert.js
@@ -0,0 +1,81 @@
+// TK-10091 — prove every datum on graphics.html drills to a filtered/detail view.
+// Verifies: 6 charts each have an onClick handler; stat-bar + expiring-table cells are real
+// <a> links with correct hrefs; and the drill targets actually resolve (condos ?status/?city,
+// crcp ?broker modal). Basic-auth aware (fetch() 401s on URL-only creds, so use httpCredentials).
+const pw = require('/Users/macstudio3/.claude/skills/browserbase/node_modules/playwright-core');
+const EXEC = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
+const BASE = 'http://127.0.0.1:9943';
+const CRED = { username: 'admin', password: 'DW2024!' };
+const fail = [], pass = [];
+const ok = (c, m) => (c ? pass : fail).push(m);
+
+(async () => {
+  const b = await pw.chromium.launch({ executablePath: EXEC, headless: true });
+  const ctx = await b.newContext({ httpCredentials: CRED, viewport: { width: 1320, height: 1200 } });
+  const pg = await ctx.newPage();
+  const perr = []; pg.on('pageerror', e => perr.push(e.message));
+
+  // ---- graphics.html: charts wired + anchors correct ----
+  await pg.goto(BASE + '/graphics.html', { waitUntil: 'networkidle' });
+  await pg.waitForTimeout(1200);
+  const g = await pg.evaluate(() => {
+    const inst = window.Chart ? Object.values(window.Chart.instances) : [];
+    const onClicks = inst.map(c => typeof (c.config.options.onClick));
+    const statHrefs = [...document.querySelectorAll('#stats a.statlink')].map(a => a.getAttribute('href'));
+    const expLinks = [...document.querySelectorAll('#expTable tbody a.dl')].map(a => a.getAttribute('href'));
+    const loadErr = /load error/.test(document.querySelector('#stats')?.textContent || '');
+    return { charts: inst.length, onClicks, statHrefs, expLinks, loadErr };
+  });
+  ok(!g.loadErr, 'graphics stats loaded (no load error)');
+  ok(g.charts === 6, `6 charts rendered (got ${g.charts})`);
+  ok(g.onClicks.length === 6 && g.onClicks.every(t => t === 'function'), `all 6 charts have onClick (${g.onClicks.join(',')})`);
+  ok(g.statHrefs.includes('/condos.html?status=fha_expired'), 'stat "FHA-expired" → condos?status=fha_expired');
+  ok(g.statHrefs.includes('/condos.html?status=fha_approved'), 'stat "FHA-approved" → condos?status=fha_approved');
+  ok(g.statHrefs.includes('/crcp.html'), 'stat "brokers/firms" → crcp.html');
+  ok(g.statHrefs.includes('#expCard'), 'stat "lapsing <12mo" → #expCard anchor');
+  ok(g.expLinks.length >= 5 && g.expLinks.some(h => h.includes('status=fha_expired&q=')), 'expiring rows link project → condos?status=fha_expired&q=');
+  ok(g.expLinks.some(h => h.startsWith('/condos.html?city=')), 'expiring rows link city → condos?city=');
+
+  // ---- drill 1: condos ?status=fha_expired lands filtered ----
+  await pg.goto(BASE + '/condos.html?status=fha_expired', { waitUntil: 'networkidle' });
+  await pg.waitForTimeout(1000);
+  const c1 = await pg.evaluate(() => ({
+    activeChip: document.querySelector('#fWarr .chip.active')?.dataset.warr || null,
+    count: document.querySelector('#count')?.textContent || ''
+  }));
+  ok(c1.activeChip === 'fha_expired', `condos status deep-link active (chip=${c1.activeChip})`);
+
+  // ---- drill 2: condos ?city=Torrance resolves case-insensitively + filters ----
+  await pg.goto(BASE + '/condos.html?city=TORRANCE', { waitUntil: 'networkidle' });
+  await pg.waitForTimeout(1000);
+  const c2 = await pg.evaluate(() => {
+    const active = [...document.querySelectorAll('#fCity .chip.active')].map(x => x.dataset.cy);
+    return { active, count: document.querySelector('#count')?.textContent || '' };
+  });
+  ok(c2.active.length === 1 && /torrance/i.test(c2.active[0]), `condos ?city=TORRANCE resolved to real chip (${c2.active.join('|')})`);
+
+  // ---- drill 3: crcp ?broker=293 opens the broker modal ----
+  await pg.goto(BASE + '/crcp.html?broker=293&name=Errol%20Spiro', { waitUntil: 'networkidle' });
+  await pg.waitForTimeout(1400);
+  const c3 = await pg.evaluate(() => ({
+    open: document.querySelector('#ov')?.classList.contains('on'),
+    heading: document.querySelector('#mbody h2')?.textContent || ''
+  }));
+  ok(c3.open && /Errol Spiro/.test(c3.heading), `crcp ?broker=293 opened modal (${c3.heading.slice(0, 40)})`);
+
+  // ---- drill 4: crcp ?firm=... opens the firm modal ----
+  await pg.goto(BASE + '/crcp.html?firm=' + encodeURIComponent('Lyon Stahl Investment real Estate'), { waitUntil: 'networkidle' });
+  await pg.waitForTimeout(1400);
+  const c4 = await pg.evaluate(() => ({
+    open: document.querySelector('#ov')?.classList.contains('on'),
+    heading: document.querySelector('#mbody h2')?.textContent || ''
+  }));
+  ok(c4.open && /Lyon Stahl/i.test(c4.heading), `crcp ?firm deep-link opened firm modal (${c4.heading.slice(0, 40)})`);
+
+  ok(perr.length === 0, `no page errors (${perr.join(' | ') || 'clean'})`);
+
+  await b.close();
+  console.log('\n  PASS (' + pass.length + '):'); pass.forEach(p => console.log('   ✓ ' + p));
+  if (fail.length) { console.log('\n  FAIL (' + fail.length + '):'); fail.forEach(f => console.log('   ✗ ' + f)); process.exit(1); }
+  console.log('\n  ✅ all drill assertions passed');
+})().catch(e => { console.error('HARNESS ERROR:', e.message); process.exit(2); });
diff --git a/public/condos.html b/public/condos.html
index d43c018..0878b26 100644
--- a/public/condos.html
+++ b/public/condos.html
@@ -355,9 +355,19 @@ async function boot(){
     try{ const r=await (await fetch('/api/fha-condos?status=all')).json(); DATA=(r.condos||[]).map(c=>({...c,warrantable_status:c.warrant_signal})); bannerDefault='⚖️ '+(( DATA[0]&&DATA[0].label)||PROXY)+' — HUD FHA-approved project directory (no live condo listings loaded; full residential scrape is gated).'; }catch(_){}
   }
   if(!bannerDefault) bannerDefault='⚖️ '+PROXY;
-  // deep-link: /condos?status=unwarrantable (or any warrantability value)
-  const qs=new URLSearchParams(location.search).get('status');
+  // deep-link (drill-through from the graphics dashboard): ?status=…&city=…&q=…
+  const P=new URLSearchParams(location.search);
+  const qs=P.get('status');
   if(qs && ['all','fha_approved','fha_expired','not_listed','heuristic_flag','unwarrantable'].includes(qs)) F.warr=qs;
+  const cityParam=P.get('city');
+  if(cityParam){
+    // FHA-list cities are UPPERCASE, scraped-listing cities are Title Case → resolve case-insensitively
+    const want=cityParam.trim().toLowerCase();
+    const real=[...new Set(DATA.map(c=>c.city).filter(Boolean))].find(c=>String(c).toLowerCase()===want);
+    if(real) F.cities.add(real);
+  }
+  const qParam=P.get('q');
+  if(qParam){ const box=$('#q'); if(box) box.value=qParam; }
   buildRail(); apply();
 }
 
diff --git a/public/graphics.html b/public/graphics.html
index eb3d6a2..97948a9 100644
--- a/public/graphics.html
+++ b/public/graphics.html
@@ -29,6 +29,14 @@
   .pill{display:inline-block;padding:1px 7px;border-radius:10px;font-size:11px;font-weight:600;}
   .pill.soon{background:rgba(248,81,73,.15);color:var(--red);}
   .pill.ok{background:rgba(63,185,80,.15);color:var(--acc);}
+  /* every datum is drillable (Steve rule: no dead-end text) */
+  .stats a.statlink{color:var(--mut);text-decoration:none;cursor:pointer;}
+  .stats a.statlink:hover b{color:var(--blue);}
+  a.dl{color:var(--blue);text-decoration:none;}
+  td a.dl{color:var(--ink);border-bottom:1px dotted rgba(88,166,255,.45);}
+  td a.dl:hover{color:var(--blue);border-bottom-color:var(--blue);}
+  #expTable tbody tr:hover td{background:rgba(88,166,255,.05);}
+  .chartwrap canvas{cursor:default;}
   @media(max-width:820px){ main{grid-template-columns:1fr;} }
 </style>
 </head>
@@ -82,7 +90,7 @@
     <div class="chartwrap"><canvas id="cityChart"></canvas></div>
   </div>
 
-  <div class="card wide">
+  <div class="card wide" id="expCard">
     <h2>FHA approval expiring within 12 months ⚠️</h2>
     <p class="sub">Approved projects whose FHA certification lapses soon — a buyer using FHA financing should confirm re-certification. (Proxy signal, not lender-verified.)</p>
     <div style="overflow:auto;max-height:340px">
@@ -98,6 +106,12 @@ Chart.defaults.color=GH.mut; Chart.defaults.borderColor=GH.line; Chart.defaults.
 const PALETTE=['#58a6ff','#3fb950','#d29922','#bc8cff','#f85149','#39c5cf','#db61a2','#7ee787','#ffa657','#a5d6ff','#ff7b72','#56d364'];
 
 async function j(u){ const r=await fetch(u); return r.json(); }
+// --- drill helpers (every datum hrefs to its filtered/detail view) ---
+const enc=encodeURIComponent;
+const go=u=>{ if(u) location.href=u; };
+const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
+// flip the canvas cursor to a pointer while hovering a clickable element
+const HOVER=(evt,els)=>{ const t=evt&&evt.native&&evt.native.target; if(t) t.style.cursor=els.length?'pointer':'default'; };
 
 (async()=>{
   // --- top brokers ---
@@ -109,6 +123,7 @@ async function j(u){ const r=await fetch(u); return r.json(); }
     labels: brokers.map(b=>b.name),
     datasets:[{ label:'listings', data:brokers.map(b=>b.listings), backgroundColor:GH.acc }] },
     options:{ indexAxis:'y', responsive:true, maintainAspectRatio:false,
+      onHover:HOVER, onClick:(e,els)=>{ if(els.length){ const b=brokers[els[0].index]; if(b&&b.id!=null) go('/crcp.html?broker='+enc(b.id)+'&name='+enc(b.name||'')); } },
       plugins:{ legend:{display:false}, tooltip:{ callbacks:{ afterLabel:(c)=>brokers[c.dataIndex].firm||'' } } },
       scales:{ x:{ grid:{color:GH.line} }, y:{ grid:{display:false} } } } });
 
@@ -120,6 +135,7 @@ async function j(u){ const r=await fetch(u); return r.json(); }
     labels: firms.map(f=>f.firm),
     datasets:[{ data:firms.map(f=>f.listings), backgroundColor:PALETTE, borderColor:'#161b22', borderWidth:2 }] },
     options:{ responsive:true, maintainAspectRatio:false, cutout:'52%',
+      onHover:HOVER, onClick:(e,els)=>{ if(els.length){ const f=firms[els[0].index]; if(f) go('/crcp.html?firm='+enc(f.firm||'')); } },
       plugins:{ legend:{ position:'right', labels:{ boxWidth:10, font:{size:11} } },
         tooltip:{ callbacks:{ label:(c)=>`${c.label}: ${c.raw} listings (${firms[c.dataIndex].brokers} brokers)` } } } } });
 
@@ -132,6 +148,7 @@ async function j(u){ const r=await fetch(u); return r.json(); }
     labels: lwKeys.map(k=>lwLabels[k]||k),
     datasets:[{ data:lwKeys.map(k=>cs.byStatus[k]), backgroundColor:lwKeys.map(k=>k==='fha_approved'?GH.acc:k==='fha_expired'?GH.gold:k==='heuristic_flag'?GH.red:GH.mut), borderColor:'#161b22', borderWidth:2 }] },
     options:{ responsive:true, maintainAspectRatio:false, cutout:'55%',
+      onHover:HOVER, onClick:(e,els)=>{ if(els.length){ go('/condos.html?status='+enc(lwKeys[els[0].index])); } },
       plugins:{ legend:{ position:'bottom', labels:{ boxWidth:11, font:{size:11} } } } } });
 
   // --- scraped listings by city (stacked: approved vs other) ---
@@ -142,6 +159,7 @@ async function j(u){ const r=await fetch(u); return r.json(); }
       { label:'FHA-approved (proxy)', data:lc.map(c=>c.approved), backgroundColor:GH.acc, stack:'s' },
       { label:'other / not-listed', data:lc.map(c=>c.n-c.approved), backgroundColor:GH.mut, stack:'s' } ] },
     options:{ responsive:true, maintainAspectRatio:false,
+      onHover:HOVER, onClick:(e,els)=>{ if(els.length){ const el=els[0], c=lc[el.index]; if(c) go('/condos.html?city='+enc(c.city)+(el.datasetIndex===0?'&status=fha_approved':'')); } },
       plugins:{ legend:{ position:'bottom', labels:{ boxWidth:11, font:{size:11} } } },
       scales:{ x:{ stacked:true, grid:{display:false} }, y:{ stacked:true, grid:{color:GH.line} } } } });
 
@@ -154,6 +172,7 @@ async function j(u){ const r=await fetch(u); return r.json(); }
     labels: wkeys.map(k=>wlabels[k]||k),
     datasets:[{ data:wkeys.map(k=>w.byStatus[k]), backgroundColor:wkeys.map(k=>k==='fha_approved'?GH.acc:k==='fha_expired'?GH.gold:GH.mut), borderColor:'#161b22', borderWidth:2 }] },
     options:{ responsive:true, maintainAspectRatio:false, cutout:'55%',
+      onHover:HOVER, onClick:(e,els)=>{ if(els.length){ const k=wkeys[els[0].index]; go('/condos.html?status='+enc(k==='fha_other'?'all':k)); } },
       plugins:{ legend:{ position:'bottom', labels:{ boxWidth:11, font:{size:11} } } } } });
 
   // --- approved by city ---
@@ -161,26 +180,31 @@ async function j(u){ const r=await fetch(u); return r.json(); }
     labels: w.cities.map(c=>c.city),
     datasets:[{ label:'approved projects', data:w.cities.map(c=>c.n), backgroundColor:GH.blue }] },
     options:{ indexAxis:'y', responsive:true, maintainAspectRatio:false,
+      onHover:HOVER, onClick:(e,els)=>{ if(els.length){ const c=w.cities[els[0].index]; if(c) go('/condos.html?status=fha_approved&city='+enc(c.city)); } },
       plugins:{ legend:{display:false} }, scales:{ x:{grid:{color:GH.line}}, y:{grid:{display:false}} } } });
 
   // --- expiring table ---
+  const EXP='/condos.html?status=fha_expired';
+  const A=(href,txt)=>`<a class="dl" href="${href}">${txt}</a>`;
   $('#expTable tbody').innerHTML = (w.expiring||[]).map(e=>`
-    <tr><td>${e.project}</td><td>${e.city||'—'}</td><td>${e.zip||''}</td>
-        <td class="r">${e.expiration}</td>
-        <td class="r"><span class="pill ${e.days<90?'soon':'ok'}">${e.days}d</span></td></tr>`).join('')
+    <tr><td>${A(EXP+'&q='+enc(e.project||''), esc(e.project))}</td>
+        <td>${e.city?A('/condos.html?city='+enc(e.city), esc(e.city)):'—'}</td>
+        <td>${e.zip?A('/condos.html?q='+enc(e.zip), esc(e.zip)):''}</td>
+        <td class="r">${A(EXP, esc(e.expiration))}</td>
+        <td class="r">${A(EXP, '<span class="pill '+(e.days<90?'soon':'ok')+'">'+e.days+'d</span>')}</td></tr>`).join('')
     || '<tr><td colspan="5" style="color:var(--mut)">none expiring within 12 months</td></tr>';
 
   // --- stats bar ---
   const approved=w.byStatus.fha_approved||0, expired=w.byStatus.fha_expired||0;
   $('#stats').innerHTML =
-    `<span><b>${brokers.length?'1,256':'—'}</b> brokers tracked</span>`+
-    `<span><b>${firms.length}</b> firms charted</span>`+
-    `<span><b>${(cs.total||0).toLocaleString()}</b> real condos scraped</span>`+
-    `<span><b>${cs.byStatus&&cs.byStatus.fha_approved||0}</b> listings FHA-approved (proxy)</span>`+
-    `<span><b>${approved}</b> FHA-approved condo projects</span>`+
-    `<span><b>${expired}</b> FHA-expired</span>`+
-    `<span><b>${(w.expiring||[]).length}</b> approvals lapsing &lt;12mo</span>`+
-    `<span style="color:var(--mut)">source: HUD FHA list · cre broker graph</span>`;
+    `<a class="statlink" href="/crcp.html"><b>${brokers.length?'1,256':'—'}</b> brokers tracked</a>`+
+    `<a class="statlink" href="/crcp.html"><b>${firms.length}</b> firms charted</a>`+
+    `<a class="statlink" href="/condos.html"><b>${(cs.total||0).toLocaleString()}</b> real condos scraped</a>`+
+    `<a class="statlink" href="/condos.html?status=fha_approved"><b>${cs.byStatus&&cs.byStatus.fha_approved||0}</b> listings FHA-approved (proxy)</a>`+
+    `<a class="statlink" href="/condos.html?status=fha_approved"><b>${approved}</b> FHA-approved condo projects</a>`+
+    `<a class="statlink" href="/condos.html?status=fha_expired"><b>${expired}</b> FHA-expired</a>`+
+    `<a class="statlink" href="#expCard"><b>${(w.expiring||[]).length}</b> approvals lapsing &lt;12mo</a>`+
+    `<span style="color:var(--mut)">source: HUD FHA list · cre broker graph · <em>click any bar, slice, row or number to drill in</em></span>`;
 })().catch(e=>{ $('#stats').innerHTML='<span style="color:var(--red)">load error: '+e.message+'</span>'; });
 </script>
   <script src="/col-resize.js" defer></script>

← ed0652f Fix broker-grid column resize (adjust column not working)  ·  back to Commercialrealestate  ·  crcp: drilldown on Warrantability (FHA proxy) — bars + tile e25aad3 →