[object Object]

← back to Flock Fix Viewer

flock-fix live viewer: per-SKU field status board (live Shopify, sort+density, created-time chips)

c01bb175286d22a2a6d2daba1b7009cc2fb32d87 · 2026-09-02 13:08:52 -0700 · Steve

Files touched

Diff

commit c01bb175286d22a2a6d2daba1b7009cc2fb32d87
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Sep 2 13:08:52 2026 -0700

    flock-fix live viewer: per-SKU field status board (live Shopify, sort+density, created-time chips)
---
 .gitignore        |  5 +++
 .port             |  1 +
 public/index.html | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 server.js         | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 199 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d80cb52
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+node_modules/
+.env*
+.token
+*.log
+.DS_Store
diff --git a/.port b/.port
new file mode 100644
index 0000000..c3e7ce5
--- /dev/null
+++ b/.port
@@ -0,0 +1 @@
+61438
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..f70898e
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,94 @@
+<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Flock Fix — Live</title><style>
+:root{--bg:#0e0f13;--card:#171922;--line:#252838;--txt:#e7e9f0;--mut:#8b90a6;--ok:#25c26e;--bad:#ff5c6c;--warn:#f2b134;--accent:#7c8cff}
+*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--txt);font:14px/1.4 -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}
+header{position:sticky;top:0;background:linear-gradient(#0e0f13,#0e0f13ee);padding:14px 20px;border-bottom:1px solid var(--line);z-index:5}
+h1{margin:0;font-size:17px;letter-spacing:.3px;font-weight:600}
+.sub{color:var(--mut);font-size:12px;margin-top:3px}
+.bar{height:8px;background:var(--line);border-radius:9px;margin-top:10px;overflow:hidden}
+.bar>i{display:block;height:100%;background:linear-gradient(90deg,#25c26e,#7c8cff);width:0;transition:width .5s}
+.controls{display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin-top:10px}
+.controls label{color:var(--mut);font-size:12px}
+select,input[type=range]{background:var(--card);color:var(--txt);border:1px solid var(--line);border-radius:7px;padding:5px 8px}
+.grid{display:grid;grid-template-columns:repeat(var(--cols,3),1fr);gap:12px;padding:16px 20px}
+.card{background:var(--card);border:1px solid var(--line);border-radius:12px;overflow:hidden;display:flex;flex-direction:column}
+.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}
+.meta{display:flex;justify-content:space-between;color:var(--mut);font-size:11px}
+.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}
+.chip.ok{color:var(--ok);border-color:#1f7a48;background:#0f2a1c}
+.chip.bad{color:var(--bad);border-color:#5a2730;background:#2a1216}
+.chip.warn{color:var(--warn);border-color:#5a4a1e;background:#2a2410}
+.when{color:var(--mut);font-size:10.5px}
+.pill{font-size:10px;padding:1px 7px;border-radius:6px;border:1px solid var(--line)}
+.pill.active{color:var(--ok);border-color:#1f7a48}.pill.draft{color:var(--warn);border-color:#5a4a1e}
+.dot{width:7px;height:7px;border-radius:50%;display:inline-block;margin-right:5px}
+</style></head><body>
+<header>
+  <h1>🪶 Flock Fix — Live Board</h1>
+  <div class="sub" id="sub">loading…</div>
+  <div class="bar"><i id="prog"></i></div>
+  <div class="controls">
+    <span><label>Sort</label>
+      <select id="sort">
+        <option value="broken">Broken first</option>
+        <option value="fixed">Fixed first</option>
+        <option value="sku">SKU</option>
+        <option value="price">Price ↓</option>
+        <option value="created">Newest</option>
+      </select></span>
+    <span><label>Density</label> <input type="range" id="dens" min="2" max="6" value="3"></span>
+    <span class="sub" id="upd"></span>
+  </div>
+</header>
+<div class="grid" id="grid"></div>
+<script>
+let DATA=[];
+const g=document.getElementById('grid');
+function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));}
+function chip(ok,txtOk,txtBad,warn){var c=ok?'ok':(warn?'warn':'bad');return '<span class="chip '+c+'"><span class="dot" style="background:currentColor"></span>'+(ok?txtOk:txtBad)+'</span>';}
+function fmtWhen(iso){try{return new Date(iso).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'})}catch(e){return iso||''}}
+function render(){
+  const s=document.getElementById('sort').value;
+  let d=DATA.slice();
+  const rank=r=>r.fixed?1:0;
+  if(s==='broken')d.sort((a,b)=>rank(a)-rank(b)||a.sku.localeCompare(b.sku));
+  else if(s==='fixed')d.sort((a,b)=>rank(b)-rank(a)||a.sku.localeCompare(b.sku));
+  else if(s==='sku')d.sort((a,b)=>a.sku.localeCompare(b.sku));
+  else if(s==='price')d.sort((a,b)=>b.price-a.price);
+  else if(s==='created')d.sort((a,b)=>new Date(b.created)-new Date(a.created));
+  g.innerHTML=d.map(function(r){
+    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>'
+      +'<div class="chips">'
+        +'<span class="pill '+r.status.toLowerCase()+'">'+r.status+'</span>'
+        +chip(r.f_quotes,'no quote tag','QUOTE tag',false)
+        +chip(r.f_price,'has price','no price',false)
+        +chip(r.f_badge,'badge ok','54&quot;/yard badge',false)
+        +chip(r.f_buyable,'buyable','not buyable',false)
+        +chip(r.f_min,'min set','no min',true)
+        +chip(r.f_width,'width set','no width',true)
+        +(r.colorwayDup?'<span class="chip warn"><span class="dot" style="background:currentColor"></span>colorway: rename</span>':'')
+      +'</div>'
+      +'<div class="when" title="'+esc(r.created)+'">🕓 '+fmtWhen(r.created)+'</div>'
+      +'</div></div>';
+  }).join('');
+  const fixed=DATA.filter(r=>r.fixed).length;
+  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){}}
+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);}})();
+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
new file mode 100644
index 0000000..28b76d3
--- /dev/null
+++ b/server.js
@@ -0,0 +1,99 @@
+#!/usr/bin/env node
+// Flock Fix Viewer — live board showing each flock SKU's fields going broken -> fixed
+// Reads LIVE Shopify (not the unreliable mirror). Basic auth admin/DW2024!.
+const http = require('http');
+const https = require('https');
+const fs = require('fs');
+const path = require('path');
+
+const SHOP = 'designer-laboratory-sandbox.myshopify.com';
+const TOKEN = fs.readFileSync(path.join(__dirname, '.token'), 'utf8').trim();
+const AUTH = 'Basic ' + Buffer.from('admin:DW2024!').toString('base64');
+// The flock set we are fixing (Phillipe Romano flocked velvet line)
+const QUERY = "vendor:'Phillipe Romano' status:active (tag:'Flock Velvet' OR handle:flock OR title:flock)";
+
+function gql(query, variables) {
+  return new Promise((resolve, reject) => {
+    const body = JSON.stringify({ query, variables: variables || {} });
+    const req = https.request({
+      host: SHOP, path: '/admin/api/2024-10/graphql.json', method: 'POST',
+      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }
+    }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(e); } }); });
+    req.on('error', reject); req.write(body); req.end();
+  });
+}
+
+const PRODUCT_Q = `query($c:String){
+  products(first:100, query:${JSON.stringify(QUERY)}, after:$c){
+    pageInfo{hasNextPage endCursor}
+    edges{node{
+      handle title status createdAt
+      featuredImage{url}
+      tags
+      variants(first:8){edges{node{sku title price}}}
+      mMin: metafield(namespace:"global", key:"v_prods_quantity_order_min"){value}
+      mWidth: metafield(namespace:"global", key:"width"){value}
+      mUnit: metafield(namespace:"global", key:"unit_of_measure"){value}
+    }}
+  }
+}`;
+
+let cache = { at: 0, rows: [] };
+async function fetchStatus() {
+  if (Date.now() - cache.at < 4000) return cache.rows; // light throttle
+  let cur = null, all = [];
+  do {
+    const r = await gql(PRODUCT_Q, { c: cur });
+    const p = r && r.data && r.data.products;
+    if (!p) break;
+    all = all.concat(p.edges);
+    cur = p.pageInfo.hasNextPage ? p.pageInfo.endCursor : null;
+  } while (cur);
+  const rows = all.map(e => {
+    const n = e.node;
+    const tags = n.tags || [];
+    const hasQuotes = tags.some(t => String(t).replace(/["{}]/g, '').trim().toLowerCase() === 'quotes');
+    const vs = (n.variants.edges || []).map(v => v.node);
+    const roll = vs.filter(v => !/sample/i.test(v.title || ''));
+    const rollPrice = roll.reduce((m, v) => Math.max(m, parseFloat(v.price) || 0), 0);
+    const sku = (roll[0] && roll[0].sku) || (vs[0] && vs[0].sku) || '?';
+    const handle = n.handle;
+    const isDup = handle.startsWith('copy-of-') || /-\d+$/.test(handle);
+    const hasPrice = rollPrice > 10;
+    const buyable = !hasQuotes && hasPrice && n.status === 'ACTIVE';
+    return {
+      sku, handle, title: n.title, status: n.status, price: rollPrice,
+      img: (n.featuredImage && n.featuredImage.url) || null,
+      created: n.createdAt,
+      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
+      f_buyable: buyable,
+      f_min: !!(n.mMin && n.mMin.value),
+      f_width: !!(n.mWidth && n.mWidth.value),
+      colorwayDup: isDup,
+      fixed: !hasQuotes && hasPrice, // primary fix complete
+    };
+  });
+  cache = { at: Date.now(), rows };
+  return rows;
+}
+
+const PAGE = fs.readFileSync(path.join(__dirname, 'public', 'index.html'), 'utf8');
+
+const server = http.createServer(async (req, res) => {
+  if ((req.headers.authorization || '') !== AUTH) {
+    res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="flock"' }); return res.end('auth required');
+  }
+  if (req.url.startsWith('/api/status')) {
+    try { const rows = await fetchStatus(); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(rows)); }
+    catch (e) { res.writeHead(500); res.end(JSON.stringify({ error: String(e) })); }
+    return;
+  }
+  res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(PAGE);
+});
+server.listen(0, '127.0.0.1', () => {
+  const port = server.address().port;
+  fs.writeFileSync(path.join(__dirname, '.port'), String(port));
+  console.log('FLOCK-FIX-VIEWER listening on http://127.0.0.1:' + port + '  (admin / DW2024!)');
+});

(oldest)  ·  back to Flock Fix Viewer  ·  flock quotes-removal batch tool (idempotent, reversible, led 682f69a →