← back to Wallco Ai
edges-review + seam-debug + PDP: status filter + per-surface Fix (in-place seam-heal) & Redo (seam-debug regenerate) buttons wired to existing skill endpoints
2b09de864a6d06e065c319c7e01173ed49677541 · 2026-06-02 08:25:56 -0700 · Steve Abrams
Files touched
M public/admin/edges-review.htmlM public/admin/seam-debug.htmlM server.js
Diff
commit 2b09de864a6d06e065c319c7e01173ed49677541
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Jun 2 08:25:56 2026 -0700
edges-review + seam-debug + PDP: status filter + per-surface Fix (in-place seam-heal) & Redo (seam-debug regenerate) buttons wired to existing skill endpoints
---
public/admin/edges-review.html | 121 ++++++++++++++++++++++++++++++++++++++++-
public/admin/seam-debug.html | 21 ++++++-
server.js | 25 +++++++++
3 files changed, 165 insertions(+), 2 deletions(-)
diff --git a/public/admin/edges-review.html b/public/admin/edges-review.html
index ef32225..b2aabc3 100644
--- a/public/admin/edges-review.html
+++ b/public/admin/edges-review.html
@@ -166,6 +166,35 @@
}
.ctl-btn:hover { border-color:var(--gold); }
+ /* Per-card action bar (Fix / Redo) — hover-revealed, sits above the tooltip */
+ .tile .acts {
+ position:absolute; left:0; right:0; bottom:0; z-index:4;
+ display:flex; gap:4px; justify-content:center; padding:6px 5px;
+ background:linear-gradient(transparent, rgba(0,0,0,.85) 45%);
+ opacity:0; transition:opacity .12s; pointer-events:none;
+ }
+ .tile:hover .acts { opacity:1; pointer-events:auto; }
+ .tile .acts button, .tile .acts a {
+ font:700 10px var(--sans); letter-spacing:.05em; text-transform:uppercase;
+ padding:4px 9px; border-radius:4px; border:0; cursor:pointer; text-decoration:none;
+ line-height:1.2;
+ }
+ .tile .acts .a-fix { background:#143b1d; color:#9be7a0; }
+ .tile .acts .a-redo { background:#1d2c3b; color:#7ec8ff; }
+ .tile .acts button:hover, .tile .acts a:hover { filter:brightness(1.3); }
+ .tile .acts button:disabled { opacity:.5; cursor:wait; }
+
+ /* Toast */
+ #toast {
+ position:fixed; bottom:74px; left:50%; transform:translateX(-50%);
+ background:var(--ink); color:#fff; padding:9px 16px; border-radius:8px;
+ font:13px var(--sans); z-index:50; box-shadow:0 6px 20px rgba(0,0,0,.35);
+ opacity:0; transition:opacity .2s; pointer-events:none; max-width:80vw;
+ }
+ #toast.show { opacity:1; }
+
+ .bulkbar .b-fix { background:#143b1d; color:#9be7a0; }
+
/* Bulk action bar */
.bulkbar {
position:fixed; left:50%; transform:translateX(-50%); bottom:18px; z-index:20;
@@ -218,7 +247,7 @@
<option value="active_first">Status: active first</option>
</select>
- <label for="filter">Filter</label>
+ <label for="filter">Defect</label>
<select id="filter">
<option value="all">All defects</option>
<option value="mids_only">SDXL grid-leak (mids only)</option>
@@ -227,6 +256,14 @@
<option value="err">Scan error / missing file</option>
</select>
+ <label for="status-filter">Status</label>
+ <select id="status-filter">
+ <option value="all">All statuses</option>
+ <option value="active">Active (published)</option>
+ <option value="inactive">Not active (flagged)</option>
+ <option value="removed">Removed</option>
+ </select>
+
<label for="density">Density</label>
<input id="density" type="range" min="120" max="380" step="20" value="200">
<span class="density-val" id="density-val">200</span>
@@ -250,11 +287,13 @@
<div class="bulkbar" id="bulkbar" hidden>
<span><b id="sel-count">0</b> selected</span>
+ <button class="b-fix" id="b-fix" title="Seam-heal each selected in place (fix-seam.py — preserves composition, reversible)">⚕ Fix selected</button>
<button class="b-reject" id="b-reject">Reject (keep unpublished)</button>
<button class="b-delete" id="b-delete">Delete (quarantine)</button>
<button class="b-clear" id="b-clear">Clear</button>
<span class="prod-note" id="prod-note" hidden>⚠ read-only on prod — curate on localhost:9905</span>
</div>
+<div id="toast"></div>
<script>
(function(){
@@ -262,6 +301,7 @@
const state = {
sort: localStorage.getItem('edges-review-sort') || 'max_desc',
filter: localStorage.getItem('edges-review-filter') || 'all',
+ status: localStorage.getItem('edges-review-status') || 'all',
density: parseInt(localStorage.getItem('edges-review-density') || '200', 10),
source: localStorage.getItem('edges-review-source') || 'snapshot',
page: 1,
@@ -303,6 +343,11 @@
const items = state.items.slice();
// Filter
const filtered = items.filter(it => {
+ // Status gate (active/published · not-active/flagged · removed)
+ if (state.status === 'active' && !it.published) return false;
+ if (state.status === 'inactive' && (it.published || it.removed)) return false;
+ if (state.status === 'removed' && !it.removed) return false;
+ // Defect gate
if (state.filter === 'all') return true;
const g = classifyGrid(it);
return (state.filter === 'mids_only' && g === 'mids')
@@ -378,6 +423,10 @@
<div class="why">${reason}</div>
${lensRows || `<div class="row"><span class="k">error</span><span class="v fail">${item.error || 'unknown'}</span></div>`}
</div>
+ <div class="acts">
+ ${IS_PROD ? '' : `<button class="a-fix" data-fix="${item.id}" title="Seam-heal in place (fix-seam.py — preserves composition, reversible via __preseam backup)">⚕ Fix</button>`}
+ <a class="a-redo" href="/admin/seam-debug/${item.id}" target="_blank" rel="noopener" title="Open the seam debugger — box-heal · regenerate-from-prompt (redo) · preview & approve">⟲ Redo →</a>
+ </div>
</div>`;
}).join('');
}
@@ -407,9 +456,17 @@
// Wire controls
document.getElementById('sort').value = state.sort;
document.getElementById('filter').value = state.filter;
+ document.getElementById('status-filter').value = state.status;
document.getElementById('density').value = state.density;
applyDensity(state.density);
+ document.getElementById('status-filter').addEventListener('change', e => {
+ state.status = e.target.value;
+ localStorage.setItem('edges-review-status', state.status);
+ state.page = 1;
+ render();
+ });
+
document.getElementById('sort').addEventListener('change', e => {
state.sort = e.target.value;
localStorage.setItem('edges-review-sort', state.sort);
@@ -464,10 +521,72 @@
if (!IS_PROD) return;
document.getElementById('b-reject').disabled = true;
document.getElementById('b-delete').disabled = true;
+ document.getElementById('b-fix').disabled = true;
document.getElementById('prod-note').hidden = false;
}
gateProd();
+ // ── Toast ──
+ function toast(msg, ms) {
+ const t = document.getElementById('toast');
+ t.textContent = msg; t.classList.add('show');
+ clearTimeout(t._t); t._t = setTimeout(() => t.classList.remove('show'), ms || 2800);
+ }
+
+ // ── Per-card Fix (seam-heal in place — fix-seam.py via /api/design/:id/fix) ──
+ // Free + local + reversible (original kept alongside as __preseam.png). Mirrors
+ // the cactus-curator Fix button. Redo lives on /admin/seam-debug/:id (regenerate
+ // from prompt + preview/approve), reached via the ⟲ Redo link.
+ function fixDesign(id, btn) {
+ if (IS_PROD) { toast('⚠ Fix runs on localhost:9905 only'); return; }
+ const orig = btn ? btn.textContent : '';
+ if (btn) { btn.disabled = true; btn.textContent = '⚕ …'; }
+ return fetch('/api/design/' + id + '/fix', {
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}'
+ })
+ .then(r => r.json())
+ .then(j => {
+ if (!j.ok) { toast('Fix #' + id + ' failed: ' + (j.error || '?')); return false; }
+ if (j.fixed > 0 && j.promoted) {
+ toast('⚕ #' + id + ' seam-healed in place');
+ const img = document.querySelector('.tile[data-id="' + id + '"] img');
+ if (img) img.src = '/designs/img/by-id/' + id + '?t=' + Date.now();
+ return true;
+ }
+ toast('#' + id + (j.already_seamless > 0 ? ': no seam defect — untouched' : ': no change applied'));
+ return false;
+ })
+ .catch(e => { toast('Fix #' + id + ' error: ' + e.message); return false; })
+ .finally(() => { if (btn) { btn.disabled = false; btn.textContent = orig; } });
+ }
+
+ // Per-card Fix click (event-delegated). Redo is a plain <a> → seam-debug, no JS.
+ document.getElementById('grid').addEventListener('click', e => {
+ const fb = e.target.closest('[data-fix]');
+ if (fb) { e.preventDefault(); e.stopPropagation(); fixDesign(parseInt(fb.dataset.fix, 10), fb); }
+ });
+
+ // ── Bulk Fix selected (concurrency-limited pool of 3) ──
+ document.getElementById('b-fix').addEventListener('click', async () => {
+ if (IS_PROD) return;
+ const ids = [...selected];
+ if (!ids.length) return;
+ if (!confirm(`Seam-heal ${ids.length} selected design(s) in place?\nReversible (originals kept as __preseam.png). Then redeploy the snapshot.`)) return;
+ const btns = bulkbar.querySelectorAll('button'); btns.forEach(b => b.disabled = true);
+ let done = 0, healed = 0, i = 0;
+ async function worker() {
+ while (i < ids.length) {
+ const id = ids[i++];
+ const ok = await fixDesign(id, null);
+ done++; if (ok) healed++;
+ toast(`⚕ fixing ${done}/${ids.length} — ${healed} healed`);
+ }
+ }
+ await Promise.all([worker(), worker(), worker()]);
+ toast(`⚕ Fix done: ${healed}/${ids.length} seam-healed`, 4000);
+ btns.forEach(b => b.disabled = false); gateProd();
+ });
+
function bulkAction(verdict, label) {
if (IS_PROD) return;
const ids = [...selected];
diff --git a/public/admin/seam-debug.html b/public/admin/seam-debug.html
index 2c21b52..6abb09b 100644
--- a/public/admin/seam-debug.html
+++ b/public/admin/seam-debug.html
@@ -61,7 +61,8 @@
<label class="toggle"><input type="checkbox" id="t_seam" checked>seam-lines</label>
<button class="heal" id="healbtn" disabled title="click boxes to select, then heal only those regions">⚕ Heal selected (0)</button>
<button class="heal" id="healall" style="background:#3b2e08;color:#f0b400;border-color:#5a4710" title="heal every detected defect">⚕ Heal all</button>
- <button class="heal" id="regenbtn" style="background:#1d2c3b;color:#7ec8ff;border-color:#365778" title="regenerate from the original prompt with a new seed — preview before approve">⟲ Regenerate from prompt</button>
+ <button class="heal" id="autofixbtn" style="background:#143b1d;color:#9be7a0;border-color:#2a6b35" title="auto seam-heal the whole tile in place (fix-seam.py — preserves composition, reversible via __preseam backup). No box selection needed.">⚕ Fix (auto seam-heal)</button>
+ <button class="heal" id="regenbtn" style="background:#1d2c3b;color:#7ec8ff;border-color:#365778" title="REDO — regenerate from the original prompt with a new seed — preview before approve">⟲ Redo (regenerate)</button>
<a id="openraw" style="font-size:12px;color:var(--mut);text-decoration:none" target="_blank" rel="noopener noreferrer">open raw image ↗</a>
</header>
<div id="err" style="display:none"></div>
@@ -292,8 +293,26 @@ async function regenerate() {
} catch (e) { alert('Regenerate failed: ' + e.message); }
finally { btn.disabled = false; btn.textContent = orig; }
}
+// === Auto-fix handler (whole-tile seam-heal in place — no box selection) =====
+// Hits /api/design/:id/fix (fix-seam.py). Preserves composition, backs the
+// original up as __preseam.png, then re-runs the scan so the verdict refreshes.
+async function autoFix() {
+ const id = $('idin').value.trim(); if (!id) return;
+ const btn = $('autofixbtn'); const orig = btn.textContent;
+ btn.disabled = true; btn.textContent = '⚕ healing…';
+ try {
+ const r = await fetch(q(`/api/design/${id}/fix`), { method:'POST', headers:{'Content-Type':'application/json'}, body:'{}' });
+ const j = await r.json();
+ if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status));
+ btn.textContent = j.fixed > 0 ? '⚕ healed ✓' : (j.already_seamless > 0 ? '⚕ already clean' : '⚕ no change');
+ if (j.fixed > 0) setTimeout(load, 400); // re-scan to refresh verdict + boxes
+ setTimeout(() => { btn.textContent = orig; }, 1600);
+ } catch (e) { alert('Auto-fix failed: ' + e.message); btn.textContent = orig; }
+ finally { btn.disabled = false; }
+}
$('healbtn').addEventListener('click',()=>healSelected([...SEL]));
$('healall').addEventListener('click',()=>healSelected((CUR?.boxes||[]).map((_,i)=>i)));
+$('autofixbtn').addEventListener('click',autoFix);
$('regenbtn').addEventListener('click',regenerate);
if(ID0) load();
</script>
diff --git a/server.js b/server.js
index cc7ef67..2c5a8ea 100644
--- a/server.js
+++ b/server.js
@@ -13121,6 +13121,16 @@ ${_isAdmin ? `
style="padding:7px 14px;border:1px solid #c98a00;border-radius:5px;background:transparent;color:#c98a00;font:600 12px var(--sans);cursor:pointer;letter-spacing:.04em">
🔧 Smart-fix
</button>
+ <button type="button" id="atb-seamfix" data-id="${design.id}"
+ style="padding:7px 14px;border:1px solid #2a6b35;border-radius:5px;background:transparent;color:#9be7a0;font:600 12px var(--sans);cursor:pointer;letter-spacing:.04em"
+ title="Seam-heal in place (fix-seam.py — preserves composition, reversible via __preseam backup). Does NOT create a child.">
+ ⚕ Fix (seam-heal)
+ </button>
+ <a id="atb-redo" href="/admin/seam-debug/${design.id}" target="_blank" rel="noopener"
+ style="padding:7px 14px;border:1px solid #365778;border-radius:5px;background:transparent;color:#7ec8ff;font:600 12px var(--sans);cursor:pointer;letter-spacing:.04em;text-decoration:none"
+ title="Open the seam debugger — box-heal · regenerate-from-prompt (redo) · preview & approve">
+ ⟲ Redo →
+ </a>
<button type="button" id="atb-etsy" data-id="${design.id}"
style="padding:7px 14px;border:1px solid #6c8a4c;border-radius:5px;background:transparent;color:#9bc46b;font:600 12px var(--sans);cursor:pointer;letter-spacing:.04em"
title="Add to wallco_etsy_bucket with eligibility check + DPI/upscale plan. Does NOT call the Etsy API.">
@@ -13181,6 +13191,21 @@ try {
if (r.ok && j.ok) setStatus('✓ smart-fix → #' + (j.new_id || j.id || '?') + (j.kind ? ' (' + j.kind + ')' : ''), 'ok');
else setStatus('smart-fix failed: ' + (j.error || j.reason || r.status), 'err');
});
+ onClick('atb-seamfix', async function(b){
+ // In-place seam-heal (fix-seam.py via /api/design/:id/fix) — preserves the
+ // Round-1 composition (CLAUDE.md sacred-output rule), backs the original up
+ // as __preseam.png, does NOT create a child. The Fix complement to the
+ // child-spawning Smart-fix. Same skill as Edges Review / seam-debug ⚕ Fix.
+ if (!confirm('Seam-heal #' + b.dataset.id + ' in place? Preserves composition; original backed up as __preseam.png (reversible).')) return;
+ setStatus('⚕ Seam-healing…');
+ var _o = location.protocol + '//' + location.host;
+ var r = await fetch(_o + '/api/design/' + b.dataset.id + '/fix', { method:'POST', credentials:'include', headers:{'Content-Type':'application/json'}, body:'{}' });
+ var j = await r.json();
+ if (r.ok && j.ok) {
+ if (j.fixed > 0 && j.promoted) { setStatus('✓ seam-healed in place — reloading…', 'ok'); setTimeout(function(){ location.reload(); }, 900); }
+ else setStatus(j.already_seamless > 0 ? 'no seam defect — untouched' : 'no change applied', 'ok');
+ } else setStatus('fix failed: ' + (j.error || r.status), 'err');
+ });
onClick('atb-etsy', async function(b){
// From the design page, the admin has DELIBERATELY picked this design to
// sell on Etsy. The seam gate is advisory here, not gating — digital
← d668131 Add pilot-build-tifs.py (PG-drift-proof TIF builder via by-i
·
back to Wallco Ai
·
generator: every new design self-archives TIF+SVG via build- c198fff →