← 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=>({'&':'&','<':'<','>':'>','"':'"'}[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 →