← 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
A public/drill.jsM public/index.html
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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 →