[object Object]

← 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

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 &amp; 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 &amp; 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 →