[object Object]

← back to Approval Command Center

href-drill: add drill primitives to approval-command-center (TK-10903)

da2d754b2181d2c7ccf912076b537ec9ac9cb1a6 · 2026-08-27 04:46:00 -0700 · Steve

Files touched

Diff

commit da2d754b2181d2c7ccf912076b537ec9ac9cb1a6
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Aug 27 04:46:00 2026 -0700

    href-drill: add drill primitives to approval-command-center (TK-10903)
---
 public/drill.js   | 36 ++++++++++++++++++++++++++++++++++++
 public/index.html | 20 ++++++++++++++++----
 2 files changed, 52 insertions(+), 4 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 1ca8b8e..e3a616b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -94,12 +94,14 @@
   .card.soft .risk{color:#8fcabb}
 </style>
   <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
+  <script src="/drill.js"></script>
 </head>
 <body>
 <header>
   <h1>Approval Command Center <span class="sub">— the 109-item pending-approval digest · 2026-08-09</span></h1>
   <div class="sub">Your click is the human gate. <b>Approve</b> logs your go and queues the item for the live officer-yolo pass (DTD → officer → contrarian → verify). Nothing irreversible fires from this page unattended.</div>
   <div class="bar">
+    <input type="hidden" id="pattern" value="">
     <input type="search" id="q" placeholder="Search title / body / slug…"/>
     <select id="sort">
       <option value="sev">Sort: Risk (hard first)</option>
@@ -134,6 +136,11 @@
 </div>
 <div class="toast" id="toast"></div>
 <script>
+// href-drill primitives (TK-10903)
+var FIELDS=[['gate','Gate'],['sev','Severity'],['state','State'],['officer','Officer']];
+var FIL={gate:'',sev:'',state:'',officer:''};
+function applyFilters(push){if(push)writeURL(true);render();}
+
 let ITEMS=[], SEL=new Set();
 const activeGates=new Set(), activeSev=new Set(), activeState=new Set();
 const $=s=>document.querySelector(s);
@@ -162,6 +169,10 @@ function load(){
   fetch('/api/items').then(r=>r.json()).then(d=>{ ITEMS=d.items; render(); });
 }
 function passesFilter(it){
+  if(FIL.gate && it.gate!==FIL.gate) return false;
+  if(FIL.sev && it.severity!==FIL.sev) return false;
+  if(FIL.state && it.where!==FIL.state) return false;
+  if(FIL.officer && (it.officer||'')!==FIL.officer) return false;
   if(activeGates.size && !activeGates.has(it.gate)) return false;
   if(activeSev.size && !activeSev.has(it.severity)) return false;
   if(activeState.size && !activeState.has(it.where)) return false;
@@ -236,10 +247,10 @@ function cardEl(it){
     <div class="ctitle">${esc(it.title)}</div>
     <div class="when">🕓 ${fmtWhen(it.created)}</div>
     <div class="meta">
-      <span class="badge gate">${esc(it.gate)}</span>
+      ${drill('gate',it.gate,`<span class="badge gate">${esc(it.gate)}</span>`,'')}
       <span class="badge ${it.reversible?'rev':'irrev'}">${it.reversible?'reversible':'IRREVERSIBLE'}</span>
-      <span class="badge state">${it.where}</span>
-      ${it.officer&&it.officer!=='unassigned'?`<span class="officer">${esc(it.officer)}</span>`:''}
+      ${drill('state',it.where,`<span class="badge state">${it.where}</span>`,'')}
+      ${it.officer&&it.officer!=='unassigned'?drill('officer',it.officer,`<span class="officer">${esc(it.officer)}</span>`,''):''}
     </div>
     <div class="risk"><span class="rk">${it.severity==='soft'?'✓':'⚠'}</span><span>${esc(riskLine(it))}</span></div>
     <div class="row">
@@ -281,7 +292,8 @@ function post(url,b){ return fetch(url,{method:'POST',headers:{'Content-Type':'a
 function toast(m){ const t=$('#toast'); t.textContent=m; t.style.display='block'; setTimeout(()=>t.style.display='none',2200); }
 function esc(s){ return (s||'').replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c])); }
 
-// density + sort persistence
+// density + sort persistence — readURL restores filter state from URL (href-drill, TK-10903)
+readURL();
 const dens=$('#dens'); dens.value=localStorage.acc_dens||3; document.documentElement.style.setProperty('--cols',6-dens.value);
 dens.oninput=()=>{ localStorage.acc_dens=dens.value; document.documentElement.style.setProperty('--cols',6-dens.value); };
 $('#sort').value=localStorage.acc_sort||'sev'; $('#sort').onchange=()=>{ localStorage.acc_sort=$('#sort').value; render(); };

← 72c3664 creds-safe fetch guard: resolve relative fetch vs credential  ·  back to Approval Command Center  ·  (newest)