[object Object]

← back to Tools Dw Hub

tools-dw-hub: href-drill — card category is a filter-link + URL-addressable state

1b68235060f534cdd872ed084a3a1dc9daac4ab6 · 2026-08-01 01:57:09 -0700 · Steve Abrams

Each card's category is now an <a href="?category=…"> to its filtered view (href-drill
HARD rule); left-panel facets (category/status/type) + search are URL-addressable. Click
filters in place + reflects into the URL, cmd/ctrl-click opens the shareable href,
back-button + deep-link restore via popstate/readURL.

Verified (Playwright + basic auth, live :9950): click category 'Room Settings' -> ?category
+ grid 29->3 all-matching; deep-link restores; back-button->29; canary FLAT->DONE; 0 err.
href-drill backlog grid 7/92. TK-10089.

Files touched

Diff

commit 1b68235060f534cdd872ed084a3a1dc9daac4ab6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Aug 1 01:57:09 2026 -0700

    tools-dw-hub: href-drill — card category is a filter-link + URL-addressable state
    
    Each card's category is now an <a href="?category=…"> to its filtered view (href-drill
    HARD rule); left-panel facets (category/status/type) + search are URL-addressable. Click
    filters in place + reflects into the URL, cmd/ctrl-click opens the shareable href,
    back-button + deep-link restore via popstate/readURL.
    
    Verified (Playwright + basic auth, live :9950): click category 'Room Settings' -> ?category
    + grid 29->3 all-matching; deep-link restores; back-button->29; canary FLAT->DONE; 0 err.
    href-drill backlog grid 7/92. TK-10089.
---
 public/index.html | 20 +++++++++++++++-----
 1 file changed, 15 insertions(+), 5 deletions(-)

diff --git a/public/index.html b/public/index.html
index e680711..1b142dc 100644
--- a/public/index.html
+++ b/public/index.html
@@ -127,6 +127,12 @@ const LS={get:(k,d)=>{try{return JSON.parse(localStorage.getItem('dwtools_'+k))?
 // facet fields — each a collapsed table in the left panel
 const FIELDS=[['category','Category'],['status','Status'],['type','Type']];
 const FIL={category:'',status:'',type:''};
+// ── href-drill: FIL/search state is URL-addressable (shareable + deep-link + back-button) ──
+const escU=s=>encodeURIComponent(String(s==null?'':s));
+function qstr(over){ const f=Object.assign({},FIL,over||{}); const u=[]; for(const [k] of FIELDS) if(f[k]) u.push(k+'='+escU(f[k])); const q=($('#q')&&$('#q').value.trim())||''; if(q)u.push('q='+escU(q)); return u.length?('?'+u.join('&')):location.pathname; }
+function drill(field,val,label){ if(val==null||val===''||val==='—')return esc(label!=null?label:val); return `<a class="drill" data-filter="${field}" data-v="${esc(val)}" href="${esc(qstr({[field]:String(val)}))}" title="Filter to ${esc(val)}">${esc(label!=null?label:val)}</a>`; }
+function readURL(){ const u=new URLSearchParams(location.search); for(const [k] of FIELDS) FIL[k]=u.get(k)||''; if($('#q'))$('#q').value=u.get('q')||''; }
+function writeURL(push){ history[push?'pushState':'replaceState']({},'',qstr()); }
 const typeOf=t=>t.missingSource?'Missing source':t.cli?'CLI':t.static?'Static':t.onDemand?'On-demand':'Web app';
 const statusOf=t=>{const s=STATUS[t.slug];return s==='up'?'Up':s==='down'?'Down':'Idle';};
 const valOf=(t,k)=>k==='status'?statusOf(t):k==='type'?typeOf(t):(t[k]||'—');
@@ -186,7 +192,7 @@ function render(){
     return `<div class="card${launchable?' launchable':''}" ${launchable?`data-href="${url}"`:''}>
       <div class="ct"><span class="name">${esc(t.name)}</span><span class="dot ${cls}" title="${st}"></span></div>
       <div class="desc">${esc(t.desc)}</div>
-      <div class="meta"><span class="cat">${esc(t.category)}</span><span title="${t.created}">🕓 ${fmtDate(t.created)}</span>${t.port?`<span>:${t.port}</span>`:''}${badges}</div>
+      <div class="meta"><span class="cat">${drill('category',t.category)}</span><span title="${t.created}">🕓 ${fmtDate(t.created)}</span>${t.port?`<span>:${t.port}</span>`:''}${badges}</div>
       <div class="actions">${go}${sb}<button class="act copy" data-cmd="${esc(t.start||'')}">Copy start ⌘</button></div>
     </div>`;
   }).join('');
@@ -218,8 +224,12 @@ function toast(m){const el=$('#toast');el.textContent=m;el.classList.add('on');c
 
 // facet click (delegated)
 $('#facetbox').addEventListener('click',e=>{const row=e.target.closest('.frow');if(!row)return;
-  const{k,v}=row.dataset; FIL[k]=FIL[k]===v?'':v; renderSide(); render();});
-$('#clearf').onclick=()=>{FIELDS.forEach(([k])=>FIL[k]='');$('#q').value='';renderSide();render();};
+  const{k,v}=row.dataset; FIL[k]=FIL[k]===v?'':v; writeURL(true); renderSide(); render();});
+$('#clearf').onclick=()=>{FIELDS.forEach(([k])=>FIL[k]='');$('#q').value='';writeURL(true);renderSide();render();};
+// a card's category is an href-drill atom → filter in place + reflect into the URL (cmd/ctrl-click opens the shareable href)
+document.addEventListener('click',e=>{ const d=e.target.closest('a.drill[data-filter]'); if(!d)return; if(e.metaKey||e.ctrlKey||e.shiftKey||e.button===1)return; e.preventDefault(); e.stopPropagation();
+  const k=d.dataset.filter,v=d.dataset.v; FIL[k]=(FIL[k]===v?'':v); writeURL(true); renderSide(); render(); });
+addEventListener('popstate',()=>{ readURL(); renderSide(); render(); });   // back/forward restores filter state
 
 async function loadStatus(){try{STATUS=await(await fetch(O+'/api/status')).json();renderSide();render();}catch{}}
 
@@ -230,13 +240,13 @@ async function init(){
   $('#sort').value=LS.get('sort','newest');
   const dens=LS.get('density',5); $('#density').value=dens; document.documentElement.style.setProperty('--cols',dens); setFs(dens);
   if(LS.get('imgonly',false)){$('#imgonly').checked=true;document.body.classList.add('imgonly');}
-  $('#q').oninput=()=>{renderSide();render();};
+  $('#q').oninput=()=>{writeURL(false);renderSide();render();};
   $('#sort').onchange=()=>{LS.set('sort',$('#sort').value);render();};
   $('#density').oninput=()=>{const v=+$('#density').value;document.documentElement.style.setProperty('--cols',v);setFs(v);LS.set('density',v);};
   $('#imgonly').onchange=()=>{document.body.classList.toggle('imgonly',$('#imgonly').checked);LS.set('imgonly',$('#imgonly').checked);};
   $('#burger').onclick=()=>document.body.classList.add('filters');
   $('#fclose').onclick=()=>document.body.classList.remove('filters');
-  renderSide(); render(); await loadStatus(); setInterval(loadStatus,20000);
+  readURL(); renderSide(); render(); await loadStatus(); setInterval(loadStatus,20000);   // land in URL's filter state
   $('#foot').textContent='Live status refreshes every 20s. Green = running (marker-verified over tailnet) — click a card or Launch to open it. Red = stopped; use Start.';
 }
 // density → type scale (fewer/larger cards → bigger type)

← 3e7e390 security: remove DW2024! hardcoded creds from pm2 WRAP — now  ·  back to Tools Dw Hub  ·  nav-agent: universal grid-controls drop-in on internal dashb 8c962db →