[object Object]

← 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

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 →