← back to Wallco Ai
cactus-curator: haiku-vision chips + Fix-now heal pipeline w/ rollback
f6e31885231fa19451221ec6f9dfd35707aef749 · 2026-05-29 12:33:12 -0700 · Steve Abrams
new in cactus-curator (Steve 2026-05-29 — built while the haiku-weekend
full sweep was finishing):
1. Haiku verdict chip on every card the weekend pass flagged. read from
data/haiku-weekend-*.jsonl via new GET /api/admin/cactus/haiku-verdicts.
per-defect color-coding (ghost=red, fuzzy_seam/tone_break=amber,
mural_misclass=orange, ok=green, etc.). reason in hover-title.
2. Fix button now ALSO triggers the seam-heal pipeline in-place — not just
stamp needs_fixing_at. POST /api/design/:id/fix runs scripts/fix-seam.py,
promotes the healed PNG to canonical, backs up original alongside as
__preseam.png. result lands in-card via cache-busted img refresh.
3. New POST /api/design/:id/restore-preseam — swaps __preseam.png backup
back to canonical to roll back a heal that made things worse.
4. In-card 'chat panel' below each fixed card shows: what fix-seam did
(fixed/already-seamless/no-change + duration), the haiku reason that
triggered it, and a Restore button when a backup exists. honors the
Round-1-sacred rule: in-place pixel surgery only, no SDXL re-roll.
dtd-ratified path A. local verified: endpoints respond correctly,
haiku-verdicts returns 847 entries from the completed weekend sweep,
HTML embeds new code chunks. NOT yet pushed to prod (will likely
auto-sync via same mechanism that propagated unpublish button earlier;
if not, surgical-port + scp).
Files touched
M public/admin/cactus-curator.htmlM server.js
Diff
commit f6e31885231fa19451221ec6f9dfd35707aef749
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri May 29 12:33:12 2026 -0700
cactus-curator: haiku-vision chips + Fix-now heal pipeline w/ rollback
new in cactus-curator (Steve 2026-05-29 — built while the haiku-weekend
full sweep was finishing):
1. Haiku verdict chip on every card the weekend pass flagged. read from
data/haiku-weekend-*.jsonl via new GET /api/admin/cactus/haiku-verdicts.
per-defect color-coding (ghost=red, fuzzy_seam/tone_break=amber,
mural_misclass=orange, ok=green, etc.). reason in hover-title.
2. Fix button now ALSO triggers the seam-heal pipeline in-place — not just
stamp needs_fixing_at. POST /api/design/:id/fix runs scripts/fix-seam.py,
promotes the healed PNG to canonical, backs up original alongside as
__preseam.png. result lands in-card via cache-busted img refresh.
3. New POST /api/design/:id/restore-preseam — swaps __preseam.png backup
back to canonical to roll back a heal that made things worse.
4. In-card 'chat panel' below each fixed card shows: what fix-seam did
(fixed/already-seamless/no-change + duration), the haiku reason that
triggered it, and a Restore button when a backup exists. honors the
Round-1-sacred rule: in-place pixel surgery only, no SDXL re-roll.
dtd-ratified path A. local verified: endpoints respond correctly,
haiku-verdicts returns 847 entries from the completed weekend sweep,
HTML embeds new code chunks. NOT yet pushed to prod (will likely
auto-sync via same mechanism that propagated unpublish button earlier;
if not, surgical-port + scp).
---
public/admin/cactus-curator.html | 122 ++++++++++++++++++++++++++++++++++++++-
server.js | 80 +++++++++++++++++++++++++
2 files changed, 199 insertions(+), 3 deletions(-)
diff --git a/public/admin/cactus-curator.html b/public/admin/cactus-curator.html
index a91f998..6fa744b 100644
--- a/public/admin/cactus-curator.html
+++ b/public/admin/cactus-curator.html
@@ -107,6 +107,30 @@
.acts .live { border-color:#1f4d2a; color:#9ff0a6; }
.acts .etsy { border-color:#5a3f17; color:#ffb86b; }
.acts .unpub { border-color:#3a3f44; color:#a8b3bc; }
+
+ /* Haiku-vision verdict chip — appears on cards where the haiku-weekend pass flagged a defect */
+ .b.haiku { background:#4a1f5a; color:#f2c8ff; font-weight:700; }
+ .b.haiku.ok { background:#1f4d2a; color:#9ff0a6; }
+ .b.haiku.ghost { background:#5a2a2a; color:#ff9a9a; }
+ .b.haiku.fuzzy_seam { background:#5a4410; color:#ffd98a; }
+ .b.haiku.tone_break { background:#5a4410; color:#ffd98a; }
+ .b.haiku.mural_misclass{ background:#5a3f17; color:#ffb86b; }
+ .b.haiku.vendor_leak { background:#5a2a2a; color:#ff9a9a; }
+ .b.haiku.bleed { background:#2a3f5a; color:#a8d8ff; }
+ .b.haiku.other { background:#2a3f5a; color:#a8d8ff; }
+
+ /* In-card "chat" panel that appears after Fix runs — explains what the heal pipeline did + the haiku reason */
+ .fix-panel { margin-top:6px; padding:8px 10px; border:1px solid var(--line); border-radius:6px;
+ background:#14171a; font-size:12px; }
+ .fix-row { display:flex; gap:8px; padding:3px 0; align-items:flex-start; }
+ .fix-row .ico { flex:0 0 18px; font-size:14px; line-height:1.3; }
+ .fix-row .msg { flex:1; color:var(--ink); line-height:1.4; word-wrap:break-word; }
+ .fix-row .msg b { color:#9ff0a6; font-weight:700; }
+ .fix-row.haiku .msg b { color:#f2c8ff; }
+ .fix-row.err .msg, .fix-row.err .msg b { color:#ff9a9a; }
+ .restore-btn { margin-top:6px; padding:5px 11px; font:600 11px inherit;
+ background:#1d2023; color:#a8b3bc; border:1px solid #3a3f44; border-radius:5px; cursor:pointer; }
+ .restore-btn:hover { filter:brightness(1.25); }
.card.moved-to-etsy { opacity:0; transform:scale(.92); transition:opacity .3s ease, transform .3s ease; }
.acts button kbd { font:inherit; opacity:.55; }
@@ -393,6 +417,9 @@ function card(d, idx){
${roomBadgeHTML(d)}
<span class="b ${verdictClass(d.seam_verdict)}">${d.seam_verdict||'no scan'}</span>
${failReason(d).map(r=>`<span class="b reason" title="failed: ${r}">${r}</span>`).join('')}
+ ${(window.HAIKU_VERDICTS && window.HAIKU_VERDICTS[d.id]) ?
+ `<span class="b haiku ${(window.HAIKU_VERDICTS[d.id].verdict||'').toLowerCase()}" title="Haiku 4.5 vision verdict: ${(window.HAIKU_VERDICTS[d.id].reason||'').replace(/"/g,'"')}">🤖 ${window.HAIKU_VERDICTS[d.id].verdict}</span>`
+ : ''}
</div>
<img loading="lazy" src="/designs/img/by-id/${d.id}" alt="cactus ${d.id}">
${roomImgHTML(d)}
@@ -635,6 +662,12 @@ async function decide(id, action, el){
};
document.body.appendChild(t);
setTimeout(()=>t.remove(), 10000);
+ } else if (action === 'fix' && el) {
+ // Steve 2026-05-29: the Fix button now ALSO runs the seam-heal pipeline
+ // in-place (not just stamp needs_fixing_at). Result chat panel + Restore
+ // button land inside the card. Original PNG is backed up alongside as
+ // __preseam.png — restore-preseam reverses the swap.
+ runFixAndDisplay(id, el);
}
} catch(e){
el?.classList.remove('decided');
@@ -644,6 +677,79 @@ async function decide(id, action, el){
}
}
+// Run /api/design/:id/fix → shift-and-blend seam heal. On promotion (an actual
+// fix landed), refresh the card thumb with a cache-bust and append a chat-style
+// panel showing what the pipeline did + the Haiku verdict reason + a Restore
+// button that swaps the __preseam.png backup back into canonical.
+async function runFixAndDisplay(id, el){
+ if (!el) return;
+ let panel = el.querySelector('.fix-panel');
+ if (panel) panel.remove();
+ panel = document.createElement('div');
+ panel.className = 'fix-panel';
+ panel.innerHTML = `<div class="fix-row"><span class="ico">🔧</span><div class="msg">Running fix-seam… <span class="spin">⏳</span></div></div>`;
+ el.appendChild(panel);
+ try {
+ const r = await fetch(q(`/api/design/${id}/fix`), {
+ method:'POST', headers:{'Content-Type':'application/json'}, body:'{}'
+ });
+ const j = await r.json().catch(()=>({}));
+ if(!r.ok || j.ok===false) throw new Error(j.error || ('HTTP '+r.status));
+
+ if (j.promoted) {
+ const img = el.querySelector('img');
+ if (img) {
+ const u = new URL(img.src, location.origin);
+ u.searchParams.set('t', String(Date.now()));
+ img.src = u.toString();
+ }
+ }
+
+ const haiku = (window.HAIKU_VERDICTS||{})[id];
+ let html = '';
+ if (j.promoted) {
+ html += `<div class="fix-row"><span class="ico">🔧</span><div class="msg"><b>Seam-healed in place.</b> fixed=${j.fixed} · ${j.duration_ms}ms<br>${escapeHtml(j.note||'')}</div></div>`;
+ } else if (j.already_seamless > 0) {
+ html += `<div class="fix-row"><span class="ico">✓</span><div class="msg"><b>No seam defect detected.</b> already-seamless n=${j.already_seamless}.</div></div>`;
+ } else {
+ html += `<div class="fix-row"><span class="ico">·</span><div class="msg"><b>No change applied.</b> ${escapeHtml(j.note||'fix-seam reported no actionable seam.')}</div></div>`;
+ }
+ if (haiku) {
+ html += `<div class="fix-row haiku"><span class="ico">🤖</span><div class="msg"><b>Haiku ${haiku.verdict}:</b> ${escapeHtml(haiku.reason||'')}</div></div>`;
+ }
+ if (j.promoted) {
+ html += `<button class="restore-btn">↶ Restore original</button>`;
+ }
+ panel.innerHTML = html;
+ const rb = panel.querySelector('.restore-btn');
+ if (rb) rb.addEventListener('click', e => { e.stopPropagation(); runRestore(id, el); });
+ } catch(e){
+ panel.innerHTML = `<div class="fix-row err"><span class="ico">⚠</span><div class="msg"><b>Fix failed:</b> ${escapeHtml(e.message)}</div></div>`;
+ }
+}
+
+async function runRestore(id, el){
+ const panel = el.querySelector('.fix-panel');
+ try {
+ const r = await fetch(q(`/api/design/${id}/restore-preseam`), {
+ method:'POST', headers:{'Content-Type':'application/json'}, body:'{}'
+ });
+ const j = await r.json().catch(()=>({}));
+ if(!r.ok || j.ok===false) throw new Error(j.error || ('HTTP '+r.status));
+ const img = el.querySelector('img');
+ if (img) {
+ const u = new URL(img.src, location.origin);
+ u.searchParams.set('t', String(Date.now()));
+ img.src = u.toString();
+ }
+ if (panel) panel.innerHTML = `<div class="fix-row"><span class="ico">↶</span><div class="msg"><b>Restored original.</b> ${escapeHtml(j.note||'')}</div></div>`;
+ } catch(e){
+ if (panel) panel.innerHTML = `<div class="fix-row err"><span class="ico">⚠</span><div class="msg"><b>Restore failed:</b> ${escapeHtml(e.message)}</div></div>`;
+ }
+}
+
+function escapeHtml(s){ return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
+
async function bulk(action){
const ids = [...selected];
if(!ids.length) return;
@@ -866,10 +972,20 @@ function applySelection(next){
async function load(){
try {
- const r = await fetch(q('/api/admin/cactus/list?include=removed&category=' + encodeURIComponent(CATEGORY)));
- if(!r.ok) throw new Error('list '+r.status+(r.status===404?' (admin gate — append ?admin=TOKEN)':''));
- const j = await r.json();
+ // Fan-out: pull the catalog list AND the Haiku-vision verdicts in parallel.
+ // Haiku verdicts populate window.HAIKU_VERDICTS so renderCard can show a
+ // "🤖 <verdict>" chip on each card the weekend scan flagged. Verdicts failure
+ // is non-fatal — chips just won't render.
+ const [listR, haikuR] = await Promise.all([
+ fetch(q('/api/admin/cactus/list?include=removed&category=' + encodeURIComponent(CATEGORY))),
+ fetch(q('/api/admin/cactus/haiku-verdicts')).catch(() => null),
+ ]);
+ if(!listR.ok) throw new Error('list '+listR.status+(listR.status===404?' (admin gate — append ?admin=TOKEN)':''));
+ const j = await listR.json();
ALL = j.items || [];
+ if (haikuR && haikuR.ok) {
+ try { const hj = await haikuR.json(); if (hj.ok) window.HAIKU_VERDICTS = hj.verdicts || {}; } catch {}
+ }
// Reflect the loaded collection in the title.
const h = document.querySelector('h1'); if(h && CATEGORY!=='cactus') h.textContent = '❖ ' + CATEGORY + ' Curator';
render();
diff --git a/server.js b/server.js
index 8f8b782..0c667b2 100644
--- a/server.js
+++ b/server.js
@@ -1823,6 +1823,44 @@ app.get('/api/admin/defect-registry/list', (req, res) => {
}
});
+// GET /api/admin/cactus/haiku-verdicts — return the latest Haiku-vision verdict
+// per design ID, aggregated across every data/haiku-weekend-*.jsonl file. Used
+// by the cactus-curator frontend to render a per-card defect chip ("🤖 GHOST",
+// "🤖 FUZZY_SEAM" etc.) with the model's one-sentence reason as the tooltip.
+// Cheap to compute (~35K rows max) — read once per curator page-load.
+app.get('/api/admin/cactus/haiku-verdicts', (req, res) => {
+ if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
+ try {
+ const dataDir = path.join(__dirname, 'data');
+ if (!fs.existsSync(dataDir)) return res.json({ ok: true, total: 0, verdicts: {} });
+ const files = fs.readdirSync(dataDir).filter(f => /^haiku-weekend-.*\.jsonl$/.test(f));
+ const verdicts = {}; // id → { verdict, reason, bucket, ts, via } (newest wins on ts)
+ for (const f of files) {
+ const fp = path.join(dataDir, f);
+ for (const line of fs.readFileSync(fp, 'utf8').split('\n')) {
+ if (!line.trim()) continue;
+ try {
+ const r = JSON.parse(line);
+ if (!r.id || !r.verdict) continue;
+ const existing = verdicts[r.id];
+ if (!existing || (r.ts && existing.ts && r.ts > existing.ts)) {
+ verdicts[r.id] = {
+ verdict: r.verdict,
+ reason: (r.reason || '').slice(0, 240),
+ bucket: r.bucket || null,
+ ts: r.ts || null,
+ via: r.via || 'api',
+ };
+ }
+ } catch {}
+ }
+ }
+ res.json({ ok: true, total: Object.keys(verdicts).length, verdicts });
+ } catch (e) {
+ res.status(500).json({ ok: false, error: e.message });
+ }
+});
+
// JSON list of every cactus design joined to its rank sidecar. rank_score is
// computed in SQL so it always reflects the latest scores: 0.6*vision +
// 0.4*seam when both present, else whichever exists. Default hides already-
@@ -8962,6 +9000,48 @@ app.post('/api/design/:id/fix', express.json({ limit: '4kb' }), (req, res) => {
}
});
+// POST /api/design/:id/restore-preseam — undo the most-recent seam-heal by
+// swapping the `__preseam.png` backup back to be the canonical PNG. Steve's
+// "↶ Restore original" button on the cactus-curator card after a Fix run.
+// Idempotent-ish: if no backup exists, 404. After restore the backup is gone
+// (renamed onto the canonical), so a subsequent fix re-creates a new backup.
+app.post('/api/design/:id/restore-preseam', express.json({ limit: '1kb' }), (req, res) => {
+ if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
+ const id = parseInt(req.params.id, 10);
+ if (!Number.isFinite(id) || id < 1) return res.status(400).json({ error: 'bad id' });
+ try {
+ // Resolve canonical path the same way /api/design/:id/fix does (PG → DESIGNS cache fallback)
+ let lp = null;
+ try {
+ const pgRaw = psqlQuery(`SELECT local_path FROM spoon_all_designs WHERE id=${id} LIMIT 1;`);
+ if (pgRaw && pgRaw.trim()) lp = pgRaw.trim();
+ } catch {}
+ if (!lp) {
+ const cached = DESIGNS.find(x => x.id === id);
+ if (cached && cached.image_url && cached.image_url.startsWith('/designs/img/')) {
+ const fn = cached.image_url.replace(/^\/designs\/img\//, '');
+ const candidate = path.join(__dirname, 'data', 'generated', fn);
+ if (fs.existsSync(candidate)) lp = candidate;
+ }
+ }
+ if (!lp) return res.status(404).json({ ok: false, error: 'design not found' });
+
+ const backupPath = lp.replace(/\.png$/i, '__preseam.png');
+ if (!fs.existsSync(backupPath)) {
+ return res.status(404).json({ ok: false, error: 'no __preseam.png backup — nothing to restore' });
+ }
+ // Atomic-ish swap: delete current, rename backup onto canonical.
+ if (fs.existsSync(lp)) fs.unlinkSync(lp);
+ fs.renameSync(backupPath, lp);
+ res.json({
+ ok: true, id, restored: true,
+ note: 'Restored original from __preseam.png backup. Run Fix again to create a new backup.',
+ });
+ } catch (e) {
+ res.status(500).json({ ok: false, error: e.message });
+ }
+});
+
app.post('/api/design/:id/regenerate-with-comment', express.json({ limit: '8kb' }), (req, res) => {
if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
const id = parseInt(req.params.id, 10);
← 2c77613 Add Theme Gallery link to admin-nav hamburger (sibling of Ro
·
back to Wallco Ai
·
harden(db): psqlExecLocal throws on psql error (ON_ERROR_STO fe9624d →