[object Object]

← back to Fix Live Board

board UX upgrade: day/night toggle, click-to-drill detail drawer, Hootsuite-style column/stream view (group by state/status/vendor), green→red usage meter, group-by + sort — nav-bar-standard style

edfc58b624c2cd61656dcd048f7b60bc0e6ed468 · 2026-09-02 14:33:34 -0700 · Steve

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkjHuJKiariXaTtFujZxWC

Files touched

Diff

commit edfc58b624c2cd61656dcd048f7b60bc0e6ed468
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Sep 2 14:33:34 2026 -0700

    board UX upgrade: day/night toggle, click-to-drill detail drawer, Hootsuite-style column/stream view (group by state/status/vendor), green→red usage meter, group-by + sort — nav-bar-standard style
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01KkjHuJKiariXaTtFujZxWC
---
 public/board.html | 226 ++++++++++++++++++++++++++++++++++++++----------------
 1 file changed, 158 insertions(+), 68 deletions(-)

diff --git a/public/board.html b/public/board.html
index 4845640..d9ebee2 100644
--- a/public/board.html
+++ b/public/board.html
@@ -1,106 +1,196 @@
 <!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
 <title>Fix Live Board</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}
+/* ---- light base (day) ---- */
+:root{
+  --bg:#f4f5f8;--card:#ffffff;--line:#e2e5ee;--txt:#1b1e28;--mut:#5c6274;
+  --ok:#1f9d57;--bad:#d63d4c;--warn:#c98a12;--accent:#4a5bd6;--head:#ffffffee;--shadow:0 1px 3px #0000001a;
+}
+/* ---- dark (night): system default unless toggled to light ---- */
+@media (prefers-color-scheme: dark){:root:not([data-theme="light"]){
+  --bg:#0e0f13;--card:#171922;--line:#252838;--txt:#e7e9f0;--mut:#8b90a6;
+  --ok:#25c26e;--bad:#ff5c6c;--warn:#f2b134;--accent:#7c8cff;--head:#0e0f13ee;--shadow:0 1px 3px #0000;
+}}
+:root[data-theme="dark"]{
+  --bg:#0e0f13;--card:#171922;--line:#252838;--txt:#e7e9f0;--mut:#8b90a6;
+  --ok:#25c26e;--bad:#ff5c6c;--warn:#f2b134;--accent:#7c8cff;--head:#0e0f13ee;--shadow:0 1px 3px #0000;
+}
+*{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:var(--head);backdrop-filter:blur(6px);padding:12px 18px;border-bottom:1px solid var(--line);z-index:20}
+h1{margin:0;font-size:16px;letter-spacing:.3px;font-weight:600;display:flex;align-items:center;gap:8px}
 .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)}
+/* green->red usage meter: fill width = fixed%, hue = green(fixed) -> red(backlog) */
+.meter{height:9px;background:var(--line);border-radius:9px;margin-top:9px;overflow:hidden;position:relative}
+.meter>i{display:block;height:100%;width:0;border-radius:9px;transition:width .5s,background .5s}
+.controls{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:10px}
+.controls label{color:var(--mut);font-size:11px;margin-right:4px}
+select,input[type=range],button.tg{background:var(--card);color:var(--txt);border:1px solid var(--line);border-radius:7px;padding:5px 8px;font-size:12px}
+button.tg{cursor:pointer}button.tg.on{border-color:var(--accent);color:var(--accent)}
+.seg{display:inline-flex;border:1px solid var(--line);border-radius:7px;overflow:hidden}
+.seg button{background:var(--card);color:var(--mut);border:0;padding:5px 10px;font-size:12px;cursor:pointer}
+.seg button.on{background:var(--accent);color:#fff}
+/* grid */
+.grid{display:grid;grid-template-columns:repeat(var(--cols,3),1fr);gap:12px;padding:16px 18px}
+.ghead{grid-column:1/-1;color:var(--mut);font-size:12px;font-weight:600;padding:6px 2px;border-bottom:1px solid var(--line);margin-top:6px}
+/* hootsuite-style columns */
+.streams{display:flex;gap:14px;padding:16px 18px;overflow-x:auto;align-items:flex-start}
+.stream{flex:0 0 300px;background:color-mix(in srgb,var(--card) 60%,transparent);border:1px solid var(--line);border-radius:12px;max-height:calc(100vh - 190px);display:flex;flex-direction:column}
+.stream>h3{margin:0;padding:10px 12px;font-size:12.5px;font-weight:600;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;position:sticky;top:0;background:var(--card);border-radius:12px 12px 0 0;z-index:1}
+.stream>h3 .cnt{color:var(--mut);font-weight:500}
+.stream>.col-body{overflow-y:auto;padding:10px;display:flex;flex-direction:column;gap:10px}
+/* card */
+.card{background:var(--card);border:1px solid var(--line);border-radius:12px;overflow:hidden;display:flex;flex-direction:column;box-shadow:var(--shadow);cursor:pointer;transition:border-color .15s,transform .05s}
+.card:hover{border-color:var(--accent)}.card:active{transform:scale(.997)}
+.card.fixed{border-color:color-mix(in srgb,var(--ok) 55%,var(--line))}
+.thumb{height:120px;background:#0000000d 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}
+.price{color:var(--txt);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}
+.chip.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,var(--line))}
+.chip.bad{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 45%,var(--line))}
+.chip.warn{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,var(--line))}
 .when{color:var(--mut);font-size:10.5px}
 .pill{font-size:10px;padding:1px 7px;border-radius:6px;border:1px solid var(--line)}
-.dot{width:7px;height:7px;border-radius:50%;display:inline-block;margin-right:5px}
-.err{color:var(--bad);padding:14px 20px}
+.dot{width:7px;height:7px;border-radius:50%;display:inline-block;margin-right:4px}
 .fixers{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
 .fx{font-size:12px;padding:5px 10px;border-radius:8px;border:1px solid var(--line);background:var(--card);color:var(--txt);cursor:pointer}
 .fx.gated{color:var(--mut);cursor:not-allowed;border-style:dashed}
-.fx:hover:not(.gated){border-color:var(--accent)}
+.err{color:var(--bad);padding:12px 18px}
+/* detail drawer */
+.scrim{position:fixed;inset:0;background:#0006;z-index:40;opacity:0;pointer-events:none;transition:opacity .2s}
+.scrim.open{opacity:1;pointer-events:auto}
+.drawer{position:fixed;top:0;right:0;height:100%;width:min(440px,92vw);background:var(--card);border-left:1px solid var(--line);z-index:41;transform:translateX(100%);transition:transform .22s;overflow-y:auto;box-shadow:-8px 0 30px #0003}
+.drawer.open{transform:translateX(0)}
+.drawer .dh{position:sticky;top:0;background:var(--card);padding:12px 16px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:center;gap:10px}
+.drawer .dh b{font-size:14px}.drawer .x{cursor:pointer;color:var(--mut);font-size:20px;line-height:1;border:0;background:none}
+.drawer .dimg{width:100%;height:200px;background:#0000000d center/cover no-repeat}
+.drawer .dbody{padding:14px 16px;display:flex;flex-direction:column;gap:12px}
+.kv{display:grid;grid-template-columns:110px 1fr;gap:4px 10px;font-size:12.5px}
+.kv .k{color:var(--mut)}
+.fieldrow{display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid var(--line);font-size:12.5px}
+.alink{color:var(--accent);text-decoration:none;font-size:12px}
 </style></head><body>
 <header>
-  <h1 id="h1">🛠️ Fix Live Board</h1>
+  <h1><span id="h1">🛠️ Fix Live Board</span></h1>
   <div class="sub" id="sub">loading…</div>
-  <div class="bar"><i id="prog"></i></div>
+  <div class="meter"><i id="meter"></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="id">ID</option>
-        <option value="price">Price ↓</option>
-        <option value="created">Newest</option>
+        <option value="broken">Broken first</option><option value="fixed">Fixed first</option>
+        <option value="id">ID</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><label>Group</label>
+      <select id="group"><option value="none">None</option><option value="state">State</option><option value="status">Status</option><option value="vendor">Vendor</option></select></span>
+    <span><label>View</label>
+      <span class="seg"><button id="vGrid" data-v="grid">▦ Grid</button><button id="vCols" data-v="columns">▤ Columns</button></span></span>
+    <span id="densWrap"><label>Density</label> <input type="range" id="dens" min="2" max="6" value="3"></span>
+    <button class="tg" id="theme">🌙 Night</button>
     <span class="sub" id="upd"></span>
   </div>
   <div class="fixers" id="fixers"></div>
 </header>
 <div class="grid" id="grid"></div>
+<div class="streams" id="streams" hidden></div>
 <div class="err" id="err" style="display:none"></div>
+<div class="scrim" id="scrim"></div>
+<aside class="drawer" id="drawer" aria-hidden="true"></aside>
 <script>
-let META={fields:[],fixers:[],name:'Fix Live Board'}, DATA=[];
-const g=document.getElementById('grid');
-const KEY='flb_'+location.port; // per-board localStorage namespace
+let META={fields:[],fixers:[],name:'Fix Live Board'}, DATA=[], SEL=null;
+const KEY='flb_'+location.port;
+const $=id=>document.getElementById(id);
 function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));}
-function chip(f,val){var ok=!!val;var c=ok?'ok':(f.warn?'warn':'bad');var t=ok?(f.ok||f.key):(f.bad||('no '+f.key));return '<span class="chip '+c+'"><span class="dot" style="background:currentColor"></span>'+esc(t)+'</span>';}
+function get(k,d){try{const v=localStorage.getItem(KEY+'_'+k);return v==null?d:v}catch(e){return d}}
+function set(k,v){try{localStorage.setItem(KEY+'_'+k,v)}catch(e){}}
+function vendorOf(r){const m=String(r.title||'').match(/^\[([^\]]*)\]/);return m?m[1]:'—';}
 function fmtWhen(iso){if(!iso)return '';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;
+function chip(f,val){var ok=!!val;var c=ok?'ok':(f.warn?'warn':'bad');var t=ok?(f.ok||f.key):(f.bad||('no '+f.key));return '<span class="chip '+c+'"><span class="dot" style="background:currentColor"></span>'+esc(t)+'</span>';}
+function groupKey(r){const g=$('group').value;if(g==='state')return r.fixed?'Fixed':'To follow';if(g==='status')return r.status||'—';if(g==='vendor')return vendorOf(r);return '';}
+function sorted(){const s=$('sort').value;let d=DATA.slice();const rank=r=>r.fixed?1:0;
   if(s==='broken')d.sort((a,b)=>rank(a)-rank(b)||String(a.id).localeCompare(String(b.id)));
   else if(s==='fixed')d.sort((a,b)=>rank(b)-rank(a)||String(a.id).localeCompare(String(b.id)));
   else if(s==='id')d.sort((a,b)=>String(a.id).localeCompare(String(b.id)));
   else if(s==='price')d.sort((a,b)=>(b.price||0)-(a.price||0));
   else if(s==='created')d.sort((a,b)=>new Date(b.created||0)-new Date(a.created||0));
-  g.innerHTML=d.map(function(r){
-    const fields=(r.fields||{});
-    const chips=META.fields.map(f=>chip(f,fields[f.key])).join('');
-    return '<div class="card '+(r.fixed?'fixed':'')+'">'
-      +(r.img?'<div class="thumb" style="background-image:url(\''+esc(r.img)+'\')"></div>':'')
-      +'<div class="body">'
-      +'<div class="ttl" title="'+esc(r.title||r.id)+'">'+esc(r.title||r.id)+'</div>'
-      +'<div class="meta"><span>'+esc(r.sku||r.id)+'</span><span class="price">'+(r.price>0?('$'+Number(r.price).toFixed(2)):'—')+'</span></div>'
-      +'<div class="chips">'+(r.status?'<span class="pill">'+esc(r.status)+'</span>':'')+(r.note?'<span class="pill" style="border-color:#5a4a1e;color:var(--warn)">'+esc(r.note)+'</span>':'')+chips+'</div>'
-      +(r.created?'<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+' items · '+fixed+' fixed · '+(DATA.length-fixed)+' still to follow';
-  document.getElementById('prog').style.width=(DATA.length?(fixed/DATA.length*100):0)+'%';
-}
-function renderFixers(){
-  document.getElementById('fixers').innerHTML=(META.fixers||[]).map(f=>
-    '<button class="fx '+(f.launch?'':'gated')+'" data-id="'+esc(f.id)+'" '+(f.launch?'':'disabled title="Gated — run this fixer manually"')+'>'+esc(f.label||f.id)+(f.launch?'':' · gated')+'</button>').join('');
-  document.querySelectorAll('.fx:not(.gated)').forEach(b=>b.addEventListener('click',async()=>{
-    b.disabled=true;b.textContent='running…';
-    try{const r=await fetch('api/fix?id='+encodeURIComponent(b.dataset.id),{method:'POST'});const j=await r.json();b.textContent=(j.ok?'✓ ':'✗ ')+(b.dataset.id);}catch(e){b.textContent='error';}
-    setTimeout(poll,1200);setTimeout(()=>{renderFixers();},4000);
-  }));
+  return d;}
+function cardHTML(r){const fields=(r.fields||{});const chips=META.fields.map(f=>chip(f,fields[f.key])).join('');
+  return '<div class="card '+(r.fixed?'fixed':'')+'" data-id="'+esc(r.id)+'">'
+    +(r.img?'<div class="thumb" style="background-image:url(\''+esc(r.img)+'\')"></div>':'')
+    +'<div class="body"><div class="ttl" title="'+esc(r.title||r.id)+'">'+esc(r.title||r.id)+'</div>'
+    +'<div class="meta"><span>'+esc(r.sku||r.id)+'</span><span class="price">'+(r.price>0?('$'+Number(r.price).toFixed(2)):'—')+'</span></div>'
+    +'<div class="chips">'+(r.status?'<span class="pill">'+esc(r.status)+'</span>':'')+(r.note?'<span class="pill" style="border-color:var(--warn);color:var(--warn)">'+esc(r.note)+'</span>':'')+chips+'</div>'
+    +(r.created?'<div class="when">🕓 '+fmtWhen(r.created)+'</div>':'')+'</div></div>';}
+function render(){
+  const d=sorted(), g=$('group').value, view=$('view')&&0, cols=($('vCols').classList.contains('on'))?'columns':'grid';
+  const grid=$('grid'), streams=$('streams');
+  if(cols==='columns'){
+    grid.hidden=true;streams.hidden=false;$('densWrap').style.display='none';
+    // build groups (default group by state if none chosen, so columns are meaningful like Hootsuite streams)
+    const gb=(g==='none')?'state':g; const buckets={};
+    d.forEach(r=>{const k=(gb==='state')?(r.fixed?'Fixed':'To follow'):(gb==='status'?(r.status||'—'):vendorOf(r));(buckets[k]=buckets[k]||[]).push(r);});
+    const order=Object.keys(buckets).sort((a,b)=>{if(gb==='state')return a==='To follow'?-1:1;return buckets[b].length-buckets[a].length;});
+    streams.innerHTML=order.map(k=>'<div class="stream"><h3>'+esc(k)+' <span class="cnt">'+buckets[k].length+'</span></h3><div class="col-body">'+buckets[k].map(cardHTML).join('')+'</div></div>').join('');
+  }else{
+    streams.hidden=true;grid.hidden=false;$('densWrap').style.display='';
+    if(g==='none'){grid.innerHTML=d.map(cardHTML).join('');}
+    else{const buckets={};d.forEach(r=>{const k=groupKey(r);(buckets[k]=buckets[k]||[]).push(r);});
+      grid.innerHTML=Object.keys(buckets).map(k=>'<div class="ghead">'+esc(k)+' · '+buckets[k].length+'</div>'+buckets[k].map(cardHTML).join('')).join('');}
+  }
+  bindCards();
+  const fixed=DATA.filter(r=>r.fixed).length,total=DATA.length,frac=total?fixed/total:1;
+  $('sub').textContent=total+' items · '+fixed+' fixed · '+(total-fixed)+' to follow';
+  // usage meter: fill = fixed%, hue green(120,all fixed) -> red(0,all backlog "used")
+  const hue=Math.round(120*frac);$('meter').style.width=(frac*100)+'%';$('meter').style.background='hsl('+hue+',70%,45%)';
 }
-function setCols(v){document.documentElement.style.setProperty('--cols',v);try{localStorage.setItem(KEY+'_cols',v);}catch(e){}}
-document.getElementById('dens').addEventListener('input',e=>setCols(e.target.value));
-document.getElementById('sort').addEventListener('change',()=>{try{localStorage.setItem(KEY+'_sort',document.getElementById('sort').value);}catch(e){}render();});
-(function(){try{const c=localStorage.getItem(KEY+'_cols');if(c){document.getElementById('dens').value=c;setCols(c);}else setCols(3);const s=localStorage.getItem(KEY+'_sort');if(s)document.getElementById('sort').value=s;}catch(e){setCols(3);}})();
-async function loadMeta(){try{META=await(await fetch('api/meta')).json();document.getElementById('h1').textContent='🛠️ '+META.name;document.title=META.name;renderFixers();}catch(e){}}
-async function poll(){try{const j=await(await fetch('api/status')).json();const err=document.getElementById('err');
+function bindCards(){document.querySelectorAll('.card').forEach(c=>c.addEventListener('click',()=>openDrawer(c.dataset.id)));}
+function openDrawer(id){SEL=id;const r=DATA.find(x=>String(x.id)===String(id));if(!r)return;
+  const fields=(r.fields||{});
+  const rows=META.fields.map(f=>{const ok=!!fields[f.key];return '<div class="fieldrow"><span>'+esc(f.key)+'</span><span style="color:var(--'+(ok?'ok':(f.warn?'warn':'bad'))+')">'+(ok?(f.ok||'ok'):(f.bad||'missing'))+'</span></div>';}).join('');
+  const admin='https://admin.shopify.com/store/designer-laboratory-sandbox/products?query='+encodeURIComponent(r.handle||r.sku||'');
+  $('drawer').innerHTML='<div class="dh"><b>'+esc(r.title||r.id)+'</b><button class="x" onclick="closeDrawer()">×</button></div>'
+    +(r.img?'<div class="dimg" style="background-image:url(\''+esc(r.img)+'\')"></div>':'')
+    +'<div class="dbody">'
+    +'<div class="kv"><span class="k">SKU</span><span>'+esc(r.sku||'—')+'</span>'
+    +'<span class="k">Vendor</span><span>'+esc(vendorOf(r))+'</span>'
+    +'<span class="k">Status</span><span>'+esc(r.status||'—')+(r.note?' · '+esc(r.note):'')+'</span>'
+    +'<span class="k">Price</span><span>'+(r.price>0?('$'+Number(r.price).toFixed(2)):'—')+'</span>'
+    +'<span class="k">Fixed</span><span style="color:var(--'+(r.fixed?'ok':'bad')+')">'+(r.fixed?'yes':'no — to follow')+'</span>'
+    +'<span class="k">Created</span><span>'+esc(fmtWhen(r.created))+'</span>'
+    +'<span class="k">Handle</span><span>'+esc(r.handle||'—')+'</span></div>'
+    +'<div><div class="sub" style="margin-bottom:4px">Fields</div>'+rows+'</div>'
+    +'<a class="alink" href="'+admin+'" target="_blank" rel="noopener noreferrer">Open in Shopify admin ↗</a>'
+    +'</div>';
+  $('drawer').classList.add('open');$('drawer').setAttribute('aria-hidden','false');$('scrim').classList.add('open');}
+function closeDrawer(){SEL=null;$('drawer').classList.remove('open');$('drawer').setAttribute('aria-hidden','true');$('scrim').classList.remove('open');}
+$('scrim').addEventListener('click',closeDrawer);
+document.addEventListener('keydown',e=>{if(e.key==='Escape')closeDrawer();});
+// view toggle
+function setView(v){$('vGrid').classList.toggle('on',v==='grid');$('vCols').classList.toggle('on',v==='columns');set('view',v);render();}
+$('vGrid').addEventListener('click',()=>setView('grid'));$('vCols').addEventListener('click',()=>setView('columns'));
+// controls persistence
+['sort','group'].forEach(k=>$(k).addEventListener('change',()=>{set(k,$(k).value);render();}));
+function setCols(v){document.documentElement.style.setProperty('--cols',v);set('cols',v);}
+$('dens').addEventListener('input',e=>setCols(e.target.value));
+// theme (day/night) toggle
+function applyTheme(t){document.documentElement.setAttribute('data-theme',t);$('theme').textContent=t==='dark'?'☀️ Day':'🌙 Night';set('theme',t);}
+$('theme').addEventListener('click',()=>applyTheme(document.documentElement.getAttribute('data-theme')==='dark'?'light':'dark'));
+// fixers
+function renderFixers(){$('fixers').innerHTML=(META.fixers||[]).map(f=>'<button class="fx '+(f.launch?'':'gated')+'" data-id="'+esc(f.id)+'" '+(f.launch?'':'disabled title="Gated — run manually"')+'>'+esc(f.label||f.id)+(f.launch?'':' · gated')+'</button>').join('');
+  document.querySelectorAll('.fx:not(.gated)').forEach(b=>b.addEventListener('click',async e=>{e.stopPropagation();b.disabled=true;b.textContent='running…';try{const r=await fetch('api/fix?id='+encodeURIComponent(b.dataset.id),{method:'POST'});const j=await r.json();b.textContent=(j.ok?'✓ ':'✗ ')+b.dataset.id;}catch(x){b.textContent='error';}setTimeout(poll,1200);setTimeout(renderFixers,4000);}));}
+// init persisted state
+(function(){
+  applyTheme(get('theme', (window.matchMedia&&matchMedia('(prefers-color-scheme: dark)').matches)?'dark':'light'));
+  const c=get('cols','3');$('dens').value=c;setCols(c);
+  $('sort').value=get('sort','broken');$('group').value=get('group','none');
+  setView(get('view','grid'));
+})();
+async function loadMeta(){try{META=await(await fetch('api/meta')).json();$('h1').textContent='🛠️ '+META.name;document.title=META.name;renderFixers();}catch(e){}}
+async function poll(){try{const j=await(await fetch('api/status')).json();const err=$('err');
   if(j.err){err.style.display='block';err.textContent='probe error: '+j.err;}else{err.style.display='none';}
-  DATA=j.rows||[];render();document.getElementById('upd').textContent='updated '+new Date().toLocaleTimeString();
-}catch(e){document.getElementById('upd').textContent='refresh error';}}
+  DATA=j.rows||[];render();if(SEL)openDrawer(SEL);$('upd').textContent='updated '+new Date().toLocaleTimeString();
+}catch(e){$('upd').textContent='refresh error';}}
 loadMeta();poll();setInterval(poll,8000);
 </script></body></html>

← d014212 last5: treat showroom lines (Phillip Jeffries) as sample-onl  ·  back to Fix Live Board  ·  board: default to light (black-on-white) day theme; add cred 7bb00cf →