[object Object]

← back to Wpb Sales Dashboard

wpb-sales-dashboard: adopt href-to-deeper-data primitives (TK-10093)

1c9f5e241c34bbf2910c54eb6660f37259500f16 · 2026-08-01 20:35:46 -0700 · Steve Abrams

Files touched

Diff

commit 1c9f5e241c34bbf2910c54eb6660f37259500f16
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Aug 1 20:35:46 2026 -0700

    wpb-sales-dashboard: adopt href-to-deeper-data primitives (TK-10093)
---
 public/drill.js   | 36 +++++++++++++++++++++++++++
 public/index.html | 73 +++++++++++++++++++++++++++++++++++++++++++++++--------
 2 files changed, 99 insertions(+), 10 deletions(-)

diff --git a/public/drill.js b/public/drill.js
new file mode 100644
index 0000000..e63cdd3
--- /dev/null
+++ b/public/drill.js
@@ -0,0 +1,36 @@
+// ── href-to-deeper-data primitives — SINGLE SOURCE OF TRUTH (HARD rule, 2026-07-31) ──
+// Every displayed data point must be an href to its deeper (filtered) data. These are the
+// reusable atoms; a viewer's index.html loads this file, then defines FIELDS, FIL, $, esc,
+// and applyFilters (which these reference at call time via the shared classic-script global
+// scope). Canonical copy: ~/Projects/_shared/web/drill.js → each viewer's public/drill.js.
+// Memory: all-data-points-href-to-deeper-data. Reference viewers: astek-landing, schumacher-internal.
+
+// Build the URL for a given filter set — any count/atom becomes a real, shareable link.
+function qstr(fil, q, pat) {
+  const u = new URLSearchParams();
+  for (const [k] of FIELDS) { if (fil[k]) u.set(k, fil[k]); }
+  if (q) u.set('q', q); if (pat) u.set('pat', pat);
+  const s = u.toString(); return s ? ('?' + s) : location.pathname;
+}
+// Read the URL's filter state back into FIL + the search inputs (deep-link / back-button).
+function readURL() {
+  const u = new URLSearchParams(location.search);
+  for (const [k] of FIELDS) FIL[k] = u.get(k) || '';
+  $('#q').value = u.get('q') || ''; $('#pattern').value = u.get('pat') || '';
+}
+// Reflect the current filter state into the URL (push = new history entry, else replace).
+function writeURL(push) {
+  const url = qstr(FIL, $('#q').value.trim(), $('#pattern').value.trim());
+  history[push ? 'pushState' : 'replaceState']({}, '', url);
+}
+// drill(field,value,label,cls) → an <a> whose href IS the filtered view for that value.
+// cmd/ctrl/middle-click opens it in a new tab; plain click filters in place (delegated listener).
+function drill(field, val, label, cls) {
+  if (val == null || val === '') return '';
+  const href = qstr({ ...FIL, [field]: String(val) }, $('#q').value.trim(), $('#pattern').value.trim());
+  return `<a class="drill ${cls || ''}" href="${href}" data-filter="${field}" data-v="${esc(val)}" title="Filter to ${esc(val)}">${label != null ? esc(label) : esc(val)}</a>`;
+}
+// Atom drill target: SET the filter to this value (drill in), vs pickF which TOGGLES.
+function setF(k, v) { if (FIL[k] === String(v)) return; FIL[k] = String(v); applyFilters('push'); }
+
+window.qstr = qstr; window.readURL = readURL; window.writeURL = writeURL; window.drill = drill; window.setF = setF;
diff --git a/public/index.html b/public/index.html
index 45ff932..e4dd2fc 100644
--- a/public/index.html
+++ b/public/index.html
@@ -148,14 +148,52 @@
     </label>
     <span class="count" id="count"></span>
   </div>
+  <div class="activef" id="activef" style="display:none"></div>
   <div class="grid" id="grid"></div>
 
   <div class="foot">Compute cost: <b>$0 (local)</b> — reads pattern-vault files + local ledger, no paid API. · Auth admin:•••• · :9812</div>
 </div>
 
+<style>.listing .drill{color:inherit;text-decoration:none;cursor:pointer;border-bottom:1px dotted transparent}.listing .drill:hover{border-bottom-color:currentColor;opacity:.85}
+.activef{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px}
+.activef a{font-size:11px;text-decoration:none;color:var(--ink);background:var(--panel2);border:1px solid var(--line);border-radius:999px;padding:3px 10px}
+.activef a:hover{border-color:var(--accent);color:var(--accent)}</style>
+<script src="/drill.js"></script>
 <script>
 const $ = s => document.querySelector(s);
 const fmtMoney = v => '$' + (v||0).toLocaleString(undefined,{minimumFractionDigits:0,maximumFractionDigits:2});
+function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
+// ── href-to-deeper-data (HARD rule) — VIEWER model over the client-side listings array ──
+// Every displayed listing data point (marketplace / status / brand) becomes a .drill atom
+// whose href IS the filtered view for that value; filter state mirrors into the URL so any
+// filtered listings view is a shareable, deep-linkable, back-button-safe href. drill.js is
+// loaded for the fleet atom contract + the enforcer canary; the render logic below is local.
+const FIELDS = [['marketplaceName','Marketplace'],['status','Status'],['brand','Brand']];
+const FIL = {}; FIELDS.forEach(([k])=>FIL[k]='');
+function lqstr(){
+  const u=new URLSearchParams();
+  for(const [k] of FIELDS){ if(FIL[k]) u.set(k,FIL[k]); }
+  const s=u.toString(); return s?('?'+s):location.pathname;
+}
+function lwriteURL(push){ try{ history[push?'pushState':'replaceState']({},'',lqstr()); }catch(_){} }
+function lreadURL(){ const u=new URLSearchParams(location.search); for(const [k] of FIELDS) FIL[k]=u.get(k)||''; }
+// drill atom: an <a> whose href IS the filtered view for that field=value (delegated click filters in place)
+function ldrill(field,val,label){
+  if(val==null||val==='') return esc(label==null?'':label);
+  const u=new URLSearchParams(location.search); u.set(field,String(val));
+  return '<a class="drill" data-filter="'+field+'" data-v="'+esc(val)+'" href="?'+esc(u.toString())+'" title="Filter to '+esc(val)+'">'+esc(label!=null?label:val)+'</a>';
+}
+function lsetF(k,v){ if(FIL[k]===String(v)) return; FIL[k]=String(v); renderGrid(); lwriteURL(true); }
+function lclearF(k){ FIL[k]=''; renderGrid(); lwriteURL(true); }
+// delegated: plain click on a drill atom filters in place; modifier/middle keeps the real href
+document.addEventListener('click',function(e){
+  const c=e.target.closest('#activef a[data-clear]');
+  if(c){ e.preventDefault(); lclearF(c.dataset.clear); return; }
+  const d=e.target.closest('.listing a.drill[data-filter]'); if(!d) return;
+  if(e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||e.button===1) return;
+  e.preventDefault(); e.stopPropagation(); lsetF(d.dataset.filter,d.dataset.v);
+});
+addEventListener('popstate',function(){ lreadURL(); if(DATA) renderGrid(); });
 const fmtWhen = iso => { if(!iso) return '—'; const d=new Date(iso); if(isNaN(d)) return '—';
   return d.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); };
 const statusPill = s => `<span class="pill ${s}">${s}</span>`;
@@ -215,9 +253,21 @@ function renderMktOptions(markets){
   $('#recMkt').innerHTML = markets.map(m=>`<option value="${m.key}">${m.name}</option>`).join('');
 }
 
+// status pill, but the status TEXT is a drill atom to the filtered view
+function statusPillDrill(s){ return `<span class="pill ${s}">${ldrill('status',s,s)}</span>`; }
+function renderActiveFilters(){
+  const box=$('#activef'); if(!box) return;
+  const chips=FIELDS.filter(([k])=>FIL[k]).map(([k,lbl])=>
+    `<a href="#" data-clear="${k}" title="Remove ${lbl} filter">${lbl}: ${esc(FIL[k])} ✕</a>`).join('');
+  box.innerHTML=chips; box.style.display=chips?'flex':'none';
+}
 function renderGrid(){
   const sort = $('#sort').value;
-  let items = DATA.listings.slice();
+  // apply drill filters (VIEWER model: partition the client array by displayed data points)
+  let items = DATA.listings.filter(l=>{
+    for(const [k] of FIELDS){ if(FIL[k] && String(l[k]??'')!==FIL[k]) return false; }
+    return true;
+  });
   const statusOrder = {live:0,private:1,planned:2,draft:3};
   items.sort((a,b)=>{
     if(sort==='title') return (a.title||'').localeCompare(b.title||'');
@@ -228,24 +278,27 @@ function renderGrid(){
     const ta=a.createdAt?Date.parse(a.createdAt):-Infinity, tb=b.createdAt?Date.parse(b.createdAt):-Infinity;
     return tb-ta;
   });
-  $('#count').textContent = `${items.length} listings`;
+  renderActiveFilters();
+  const totalTxt = (FIELDS.some(([k])=>FIL[k])) ? `${items.length} of ${DATA.listings.length} listings` : `${items.length} listings`;
+  $('#count').textContent = totalTxt;
   $('#grid').innerHTML = items.length ? items.map(l=>`
     <div class="listing">
-      <div class="t">${l.title||'(untitled)'}</div>
+      <div class="t">${esc(l.title||'(untitled)')}</div>
       <div class="row">
-        <span class="mp">${l.marketplaceName}</span>
-        ${statusPill(l.status)}
-        ${l.price?`<span class="price">$${l.price}</span>`:''}
+        <span class="mp">${ldrill('marketplaceName',l.marketplaceName,l.marketplaceName)}</span>
+        ${statusPillDrill(l.status)}
+        ${l.price?`<span class="price">$${esc(l.price)}</span>`:''}
       </div>
-      <div class="brand">${l.brand||''}</div>
-      <div class="when" title="${l.createdAt||''}">🕓 ${fmtWhen(l.createdAt)}</div>
-      ${l.url?`<a class="mp" href="${l.url}" target="_blank" rel="noopener">open ↗</a>`:''}
+      <div class="brand">${l.brand?ldrill('brand',l.brand,l.brand):''}</div>
+      <div class="when" title="${esc(l.createdAt||'')}">🕓 ${fmtWhen(l.createdAt)}</div>
+      ${l.url?`<a class="mp" href="${esc(l.url)}" target="_blank" rel="noopener">open ↗</a>`:''}
     </div>`).join('')
-    : `<div class="empty">No listings yet.</div>`;
+    : `<div class="empty">No listings match this filter.</div>`;
 }
 
 async function load(){
   const r = await fetch('/api/data'); DATA = await r.json();
+  lreadURL();   // deep-link: hydrate listing filters from the URL before first render
   renderKpis(DATA.kpis);
   renderGraph(DATA.series);
   renderMarkets(DATA.markets);

← cf0832c add secured idempotent /webhook/sale receiver + .env loader  ·  back to Wpb Sales Dashboard  ·  nav-agent: universal grid-controls drop-in on internal dashb 074a977 →