← back to Flock Fix Viewer

public/index.html

133 lines

<!doctype html><html><head><meta charset="utf-8">
<script>
// Credential-safe fetch guard (fleet drop-in) — if this page is opened with
// credentials in the URL (a saved bookmark / Chrome-remembered basic-auth:
// https://user:pass@host/…), the browser poisons document.baseURI, so any
// relative or root-relative fetch('/api/…') throws "Request cannot be constructed
// from a URL that includes credentials" and callers silently fall to an empty
// state. Resolve every non-absolute request URL against the credential-free
// location instead of baseURI. Placed first so it wraps window.fetch before any
// app script runs. Ref: creds-in-url-fetch-guard-fleet-pattern.
(function () {
  var _fetch = window.fetch.bind(window);
  var cleanBase = function () { return location.origin + location.pathname; };
  window.fetch = function (input, init) {
    try {
      if (typeof input === 'string' && !/^[a-z]+:\/\//i.test(input) && input.indexOf('//') !== 0) {
        input = new URL(input, cleanBase()).href;
      } else if (input instanceof Request && !/^[a-z]+:\/\//i.test(input.url)) {
        input = new Request(new URL(input.url, cleanBase()).href, input);
      }
    } catch (_) { /* fall through to native */ }
    return _fetch(input, init);
  };
})();
</script>
<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;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}
.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">'
      +'<a class="ttl" data-drill="record" 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)
        +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)
        +chip(r.f_sqft,(r.sqftSR?('S/R '+r.sqftSR.replace(' Sq Ft','')+' · D/R '+(r.sqftDR||'').replace(' Sq Ft','')+' sqft'):'sqft set'),'no sqft',true)
        +chip(r.f_specs,'specs verified','no specs',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 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();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>