← back to Wallco Live Viewer
viewer: surface the real gate-rejection reason on orphan cards
8312b956348b8206fdd2231a507ff7e5d23e5e6f · 2026-06-09 13:25:25 -0700 · SteveStudio2
Reads data/logs/gate-rejections.jsonl (mtime-cached), attaches {gate,reason} to
any orphan (no DB row) in /api/recent, and the badge + label now show e.g.
'✗ seamless-tile gate · no DB row' with the reason in the tooltip, instead of
the inferred 'orphaned at gen time'. Adds a client-side esc() (none existed).
Pre-2026-06-09 orphans with no logged reason keep the old label.
Files touched
Diff
commit 8312b956348b8206fdd2231a507ff7e5d23e5e6f
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Tue Jun 9 13:25:25 2026 -0700
viewer: surface the real gate-rejection reason on orphan cards
Reads data/logs/gate-rejections.jsonl (mtime-cached), attaches {gate,reason} to
any orphan (no DB row) in /api/recent, and the badge + label now show e.g.
'✗ seamless-tile gate · no DB row' with the reason in the tooltip, instead of
the inferred 'orphaned at gen time'. Adds a client-side esc() (none existed).
Pre-2026-06-09 orphans with no logged reason keep the old label.
---
server.js | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 68 insertions(+), 3 deletions(-)
diff --git a/server.js b/server.js
index 8f1fd13..bc7a1a4 100644
--- a/server.js
+++ b/server.js
@@ -16,6 +16,24 @@ const LOGS = {
};
const PORT = process.env.PORT || 9788;
const WALLCO = { host: '127.0.0.1', port: 9905 };
+// Gate-rejection audit log written by generate_designs.js (2026-06-09). Lets the
+// "orphaned · no DB row" cards name the ACTUAL gate (ghost/seamless/frame) +
+// reason instead of inferring it from absence. Cached, re-read only on mtime change.
+const GATE_LOG = path.join(process.env.HOME, 'Projects/wallco-ai/data/logs/gate-rejections.jsonl');
+let _gateCache = { mtime: 0, map: {} };
+function gateReasons() {
+ try {
+ const st = fs.statSync(GATE_LOG);
+ if (st.mtimeMs === _gateCache.mtime) return _gateCache.map;
+ const map = {};
+ for (const line of fs.readFileSync(GATE_LOG, 'utf8').split('\n')) {
+ if (!line.trim()) continue;
+ try { const o = JSON.parse(line); if (o.file) map[o.file] = { gate: o.gate, reason: o.reason, ts: o.ts }; } catch {}
+ }
+ _gateCache = { mtime: st.mtimeMs, map };
+ return map;
+ } catch { return _gateCache.map; }
+}
const NAME_RE = /^(\d{10,})_(\d+)\.png$/; // <ms>_<rand>.png, excludes *.original.png & derivatives
// Only these wallco-ai paths may be proxied — keeps /act from being an open relay.
@@ -252,6 +270,7 @@ async function act(path,body,method){
return d;
}
+function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));}
function stateBadge(d){
const s=states[d.dbId]||d.state; // optimistic overlay until next tick
if(s==='pub')return '<span class="state pub">published</span>';
@@ -260,8 +279,12 @@ function stateBadge(d){
// A file with no DB row after 30 min was REJECTED by a generation-time
// quality gate (ghost / seamless / frame-overlay) — the generator skips the
// INSERT on purpose. Without this check rejects show "awaiting" forever.
- if(d.created && (Date.now()-d.created) > 30*60*1000)
- return '<span class="state gone" title="Generated but never inserted — failed a quality gate (ghost/seamless/frame-overlay) at generation time. Not waiting on anything.">gate-rejected</span>';
+ if(d.created && (Date.now()-d.created) > 30*60*1000){
+ // If the generator logged WHICH gate killed it, name the gate on the badge.
+ if(d.gate&&d.gate.gate)
+ return '<span class="state gone" title="'+esc('Gate: '+d.gate.gate+(d.gate.reason?(' — '+d.gate.reason):''))+'">✗ '+esc(d.gate.gate)+'</span>';
+ return '<span class="state gone" title="Generated but never inserted — failed a quality gate (ghost/seamless/frame-overlay) at generation time. No reason was logged (pre-2026-06-09 run). Not waiting on anything.">gate-rejected</span>';
+ }
return '<span class="state">no row yet</span>';
}
@@ -366,12 +389,51 @@ function render(items){last=items;
'<button onclick="doAct(this,'+id+',\\'tif\\')" title="print master, up to 5 min">🏗 TIF</button>'+
'<button class="danger" onclick="doAct(this,'+id+',\\'remove\\')">🗑</button>')
: (d.created && (Date.now()-d.created) > 30*60*1000
- ? '<span style="color:var(--mut);font-size:11px" title="Generated but never inserted: timeout-killed or gate-skipped before the DB insert. Nothing retries orphans.">orphaned at gen time · no DB row</span>'
+ ? (d.gate&&d.gate.gate
+ ? '<span style="color:var(--mut);font-size:11px" title="'+esc('Rejected by '+d.gate.gate+' gate'+(d.gate.reason?(': '+d.gate.reason):'')+'. Nothing retries orphans.')+'">✗ '+esc(d.gate.gate)+' gate · no DB row</span>'
+ : '<span style="color:var(--mut);font-size:11px" title="Generated but never inserted: timeout-killed or gate-skipped before the DB insert. No reason logged (pre-2026-06-09 run). Nothing retries orphans.">orphaned at gen time · no DB row</span>')
: '<span style="color:var(--mut);font-size:11px">awaiting DB row…</span>'))+
'</div></div>';
}).join('');
items.forEach(d=>seen.add(d.file)); first=false;}
+/* ── drag-marquee multi-select ──────────────────────────────────────────
+ Drag across the grid to select 1-to-many cards (like the design-curator).
+ A plain click still opens the lightbox / fires the card buttons — the
+ marquee only engages past a 6px drag. Hold Shift to ADD to the current set.
+ Selection lives in the same `sel` Set, so live generator ticks that
+ re-render the grid preserve what you've dragged. */
+(function(){
+ const mq=document.createElement('div'); mq.id='marquee';
+ mq.style.cssText='position:fixed;border:1.5px solid var(--acc,#6cf);background:rgba(108,160,255,.16);'+
+ 'z-index:35;pointer-events:none;display:none;border-radius:3px';
+ document.body.appendChild(mq);
+ let sx=0,sy=0,active=false,pending=false,baseSel=new Set(),suppress=false; const TH=6;
+ function cardsIn(r){const out=[];grid.querySelectorAll('.card[data-id]').forEach(c=>{
+ const id=+c.getAttribute('data-id'); if(!id)return; const b=c.getBoundingClientRect();
+ if(b.left<r.right&&b.right>r.left&&b.top<r.bottom&&b.bottom>r.top) out.push(id);}); return out;}
+ function applyLive(){grid.querySelectorAll('.card[data-id]').forEach(c=>{
+ const id=+c.getAttribute('data-id'); const on=sel.has(id);
+ if(on!==c.classList.contains('sel'))c.classList.toggle('sel',on);
+ const cb=c.querySelector('.pick'); if(cb&&cb.checked!==on)cb.checked=on;}); syncBulk();}
+ grid.addEventListener('mousedown',e=>{
+ if(e.button!==0||e.target.closest('button,a,input'))return; // let controls/links/checkboxes work
+ sx=e.clientX;sy=e.clientY;pending=true;active=false;baseSel=e.shiftKey?new Set(sel):new Set();});
+ window.addEventListener('mousemove',e=>{
+ if(!pending)return;
+ if(!active){if(Math.abs(e.clientX-sx)<TH&&Math.abs(e.clientY-sy)<TH)return;
+ active=true;mq.style.display='block';document.body.style.userSelect='none';}
+ e.preventDefault();
+ const x=Math.min(sx,e.clientX),y=Math.min(sy,e.clientY),w=Math.abs(e.clientX-sx),h=Math.abs(e.clientY-sy);
+ mq.style.left=x+'px';mq.style.top=y+'px';mq.style.width=w+'px';mq.style.height=h+'px';
+ sel=new Set(baseSel); cardsIn({left:x,top:y,right:x+w,bottom:y+h}).forEach(id=>sel.add(id)); applyLive();});
+ window.addEventListener('mouseup',()=>{
+ if(!pending)return; pending=false;
+ if(active){active=false;mq.style.display='none';document.body.style.userSelect='';
+ suppress=true;setTimeout(()=>suppress=false,0);render(last);}});
+ document.addEventListener('click',e=>{if(suppress){e.stopPropagation();e.preventDefault();}},true);
+})();
+
function lb(src){document.getElementById('lbimg').src=src;document.getElementById('lightbox').style.display='flex';}
async function tick(){
@@ -395,10 +457,13 @@ const server = http.createServer((req, res) => {
if (u.pathname === '/api/recent') {
const items = listDesigns();
const rows = resolveDbRows(items);
+ const gates = gateReasons();
for (const d of items) {
const row = rows[d.file];
d.dbId = row ? row.id : (idCache.get(d.file) || null);
d.state = row ? (row.removed ? 'gone' : row.published ? 'pub' : 'unpub') : null;
+ // orphan (no DB row) — attach the recorded gate-rejection reason if we have one
+ if (!d.dbId && gates[d.file]) d.gate = gates[d.file];
}
res.writeHead(200, { 'content-type': 'application/json' });
return res.end(JSON.stringify({ items, live: liveSignal() }));
← 0bf0b7b badge wording: timeout-orphans not gate-rejects (debugger ve
·
back to Wallco Live Viewer
·
feat(live-viewer): drag-marquee multi-select across the grid 69eb692 →