← back to Flock Fix Viewer
href-drill-canary: fix flock-fix-viewer flat grid — per-card drill to Shopify admin, URL-persist sort/density
fc17f1609d4f5f9940299ea10b20897dc4f61811 · 2026-09-02 16:47:30 -0700 · Steve Abrams
Cards' SKU and title were inert text with no deeper-data drill target, and the
sort/density controls only persisted to localStorage, not the URL. Fixed per
the approval-viewer c64e08d pattern (TK-11131 cycle 3/4):
- server.js: request the product's numeric Shopify id via GraphQL and emit
adminUrl (https://admin.shopify.com/store/designer-laboratory-sandbox/
products/<id>) per row — the genuinely deeper record (full variants,
metafields, media) behind each summary card.
- public/index.html: wrap the card title and SKU in real <a> hrefs to that
admin product page (target=_blank, noopener noreferrer). Add syncUrl()/
applyUrlState() so sort + density persist to the URL via
history.replaceState, matching approval-viewer's own convention, so a
filtered/sorted view is now a shareable/bookmarkable link.
Verified live: restarted the plain-node process (no pm2 entry, PID replaced),
curled /api/status — adminUrl present on all 97 rows — and curled / — served
HTML contains the new ttl/sku-link href markup.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Files touched
M public/index.htmlM server.js
Diff
commit fc17f1609d4f5f9940299ea10b20897dc4f61811
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 2 16:47:30 2026 -0700
href-drill-canary: fix flock-fix-viewer flat grid — per-card drill to Shopify admin, URL-persist sort/density
Cards' SKU and title were inert text with no deeper-data drill target, and the
sort/density controls only persisted to localStorage, not the URL. Fixed per
the approval-viewer c64e08d pattern (TK-11131 cycle 3/4):
- server.js: request the product's numeric Shopify id via GraphQL and emit
adminUrl (https://admin.shopify.com/store/designer-laboratory-sandbox/
products/<id>) per row — the genuinely deeper record (full variants,
metafields, media) behind each summary card.
- public/index.html: wrap the card title and SKU in real <a> hrefs to that
admin product page (target=_blank, noopener noreferrer). Add syncUrl()/
applyUrlState() so sort + density persist to the URL via
history.replaceState, matching approval-viewer's own convention, so a
filtered/sorted view is now a shareable/bookmarkable link.
Verified live: restarted the plain-node process (no pm2 entry, PID replaced),
curled /api/status — adminUrl present on all 97 rows — and curled / — served
HTML contains the new ttl/sku-link href markup.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---
public/index.html | 25 ++++++++++++++++++-------
server.js | 5 ++++-
2 files changed, 22 insertions(+), 8 deletions(-)
diff --git a/public/index.html b/public/index.html
index ef01b4b..f13fcd2 100644
--- a/public/index.html
+++ b/public/index.html
@@ -15,8 +15,11 @@ select,input[type=range]{background:var(--card);color:var(--txt);border:1px soli
.card.fixed{border-color:#1f7a48;box-shadow:0 0 0 1px #1f7a4855 inset}
.thumb{height:130px;background:#0b0c10 center/cover no-repeat;border-bottom:1px solid var(--line)}
.body{padding:10px 11px;display:flex;flex-direction:column;gap:7px}
-.ttl{font-size:12.5px;font-weight:600;line-height:1.25;max-height:2.5em;overflow:hidden}
+.ttl{font-size:12.5px;font-weight:600;line-height:1.25;max-height:2.5em;overflow:hidden;color:var(--txt);text-decoration:none;display:block}
+.ttl:hover{color:var(--accent);text-decoration:underline}
.meta{display:flex;justify-content:space-between;color:var(--mut);font-size:11px}
+.sku-link{color:var(--mut);text-decoration:none}
+.sku-link:hover{color:var(--accent);text-decoration:underline}
.price{color:#dfe3ff;font-weight:600}
.chips{display:flex;flex-wrap:wrap;gap:5px}
.chip{font-size:10.5px;padding:2px 7px;border-radius:999px;border:1px solid var(--line);display:inline-flex;gap:4px;align-items:center}
@@ -65,8 +68,8 @@ function render(){
return '<div class="card '+(r.fixed?'fixed':'')+'">'
+'<div class="thumb" style="background-image:url(\''+(r.img||'')+'\')"></div>'
+'<div class="body">'
- +'<div class="ttl" title="'+esc(r.title)+'">'+esc(r.title||r.handle)+'</div>'
- +'<div class="meta"><span>'+esc(r.sku)+'</span><span class="price">'+(r.price>0?('$'+r.price.toFixed(2)):'—')+'</span></div>'
+ +'<a class="ttl" href="'+esc(r.adminUrl||('https://admin.shopify.com/store/designer-laboratory-sandbox/products?query='+encodeURIComponent(r.handle||'')))+'" target="_blank" rel="noopener noreferrer" title="Open '+esc(r.title)+' in Shopify admin">'+esc(r.title||r.handle)+'</a>'
+ +'<div class="meta"><a class="sku-link" href="'+esc(r.adminUrl||'#')+'" target="_blank" rel="noopener noreferrer" title="Open in Shopify admin">'+esc(r.sku)+'</a><span class="price">'+(r.price>0?('$'+r.price.toFixed(2)):'—')+'</span></div>'
+'<div class="chips">'
+'<span class="pill '+r.status.toLowerCase()+'">'+r.status+'</span>'
+chip(r.f_quotes,'no quote tag','QUOTE tag',false)
@@ -86,11 +89,19 @@ function render(){
document.getElementById('sub').textContent=DATA.length+' flock products · '+fixed+' fixed · '+(DATA.length-fixed)+' still broken';
document.getElementById('prog').style.width=(DATA.length?(fixed/DATA.length*100):0)+'%';
}
-function setCols(v){document.documentElement.style.setProperty('--cols',v);try{localStorage.setItem('flockCols',v);}catch(e){}}
+function syncUrl(){var u=new URL(location.href),sv=document.getElementById('sort').value,dv=document.getElementById('dens').value;
+ sv&&sv!=='broken'?u.searchParams.set('sort',sv):u.searchParams.delete('sort');
+ dv&&dv!=='3'?u.searchParams.set('cols',dv):u.searchParams.delete('cols');
+ history.replaceState(null,'',u);}
+function setCols(v,skipSync){document.documentElement.style.setProperty('--cols',v);try{localStorage.setItem('flockCols',v);}catch(e){}if(!skipSync)syncUrl();}
document.getElementById('dens').addEventListener('input',e=>setCols(e.target.value));
-document.getElementById('sort').addEventListener('change',()=>{try{localStorage.setItem('flockSort',document.getElementById('sort').value);}catch(e){}render();});
-(function(){try{const c=localStorage.getItem('flockCols');if(c){document.getElementById('dens').value=c;setCols(c);}else setCols(3);
- const s=localStorage.getItem('flockSort');if(s)document.getElementById('sort').value=s;}catch(e){setCols(3);}})();
+document.getElementById('sort').addEventListener('change',()=>{try{localStorage.setItem('flockSort',document.getElementById('sort').value);}catch(e){}render();syncUrl();});
+(function(){var p=new URLSearchParams(location.search);
+ try{
+ var c=p.get('cols')||localStorage.getItem('flockCols');if(c){document.getElementById('dens').value=c;setCols(c,true);}else setCols(3,true);
+ var s=p.get('sort')||localStorage.getItem('flockSort');if(s)document.getElementById('sort').value=s;
+ }catch(e){setCols(3,true);}
+})();
async function poll(){try{const r=await fetch('api/status');DATA=await r.json();render();document.getElementById('upd').textContent='updated '+new Date().toLocaleTimeString();}catch(e){document.getElementById('upd').textContent='refresh error';}}
poll();setInterval(poll,8000);
</script></body></html>
diff --git a/server.js b/server.js
index 3d7c2ba..d129ff9 100644
--- a/server.js
+++ b/server.js
@@ -27,7 +27,7 @@ const PRODUCT_Q = `query($c:String){
products(first:100, query:${JSON.stringify(QUERY)}, after:$c){
pageInfo{hasNextPage endCursor}
edges{node{
- handle title status createdAt
+ id handle title status createdAt
featuredImage{url}
tags
variants(first:8){edges{node{sku title price}}}
@@ -65,10 +65,13 @@ async function fetchStatus() {
const isDup = handle.startsWith('copy-of-') || /-\d+$/.test(handle);
const hasPrice = rollPrice > 10;
const buyable = !hasQuotes && hasPrice && n.status === 'ACTIVE';
+ const numericId = (n.id || '').split('/').pop();
+ const adminUrl = 'https://admin.shopify.com/store/designer-laboratory-sandbox/products/' + numericId;
return {
sku, handle, title: n.title, status: n.status, price: rollPrice,
img: (n.featuredImage && n.featuredImage.url) || null,
created: n.createdAt,
+ adminUrl,
f_quotes: !hasQuotes, // fixed when quotes gone
f_price: hasPrice, // has a real roll price
f_badge: !hasQuotes, // wrong 54"/yard badge gone when quotes gone
← 9290c2e auto-data-snapshot: 2026-09-02T16:45:26 (2 data files) — sad
·
back to Flock Fix Viewer
·
Fix normalized flock dimension verification 78a3bee →