[object Object]

← back to Wallco Ai

cactus-curator: auto-select 'Publish anyway' on gate-held (per-card + bulk)

2547596ab5b6d4fa2fe6e22dd4df0f0ae8da7f0e · 2026-05-29 12:56:10 -0700 · Steve Abrams

Steve 2026-05-29 'always select: Publish anyway on autop'. Publish click
is now an unconditional decision — the publish gate becomes ADVISORY,
not blocking.

per-card (decide()): when /api/cactus-decision/:id returns gate_held=true,
no longer shows the 'Publish anyway' toast button. Instead, displays a
brief amber info toast with the gate reasons + auto-fires the force=true
follow-up call. The card lands on ✓ PUBLISHED in <500ms.

bulk (bulk() for action='live'): now sends body.force=true. The
server-side bulk handler at /api/cactus-decision/bulk already supports
force (Steve shipped that plumbing earlier) — pass it through and ALL
gate-held items go live without parking.

implication: this overrides the standing 'feedback_only_clean_designs_
welcome' default — once you click Publish, the design goes live even
if settlement/edges/ghost/seam gates fail. Defective designs WILL slip
through on bulk Publish of large categories (muybridge-plate has 2054
undecided rows = potential to mass-publish many defects).

mitigation moving forward: the new haiku-vision chip layer (also shipped
today) surfaces defects post-hoc on /admin/cactus-curator, and the new
unpublish button takes them down with one click. Steve's curate-fix-loop
model: 'I trust my Publish click, I'll catch defects via Haiku-chip
triage after the fact.'

Files touched

Diff

commit 2547596ab5b6d4fa2fe6e22dd4df0f0ae8da7f0e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri May 29 12:56:10 2026 -0700

    cactus-curator: auto-select 'Publish anyway' on gate-held (per-card + bulk)
    
    Steve 2026-05-29 'always select: Publish anyway on autop'. Publish click
    is now an unconditional decision — the publish gate becomes ADVISORY,
    not blocking.
    
    per-card (decide()): when /api/cactus-decision/:id returns gate_held=true,
    no longer shows the 'Publish anyway' toast button. Instead, displays a
    brief amber info toast with the gate reasons + auto-fires the force=true
    follow-up call. The card lands on ✓ PUBLISHED in <500ms.
    
    bulk (bulk() for action='live'): now sends body.force=true. The
    server-side bulk handler at /api/cactus-decision/bulk already supports
    force (Steve shipped that plumbing earlier) — pass it through and ALL
    gate-held items go live without parking.
    
    implication: this overrides the standing 'feedback_only_clean_designs_
    welcome' default — once you click Publish, the design goes live even
    if settlement/edges/ghost/seam gates fail. Defective designs WILL slip
    through on bulk Publish of large categories (muybridge-plate has 2054
    undecided rows = potential to mass-publish many defects).
    
    mitigation moving forward: the new haiku-vision chip layer (also shipped
    today) surfaces defects post-hoc on /admin/cactus-curator, and the new
    unpublish button takes them down with one click. Steve's curate-fix-loop
    model: 'I trust my Publish click, I'll catch defects via Haiku-chip
    triage after the fact.'
---
 public/admin/cactus-curator.html | 58 +++++++++++++++++++++++++++++-----------
 1 file changed, 43 insertions(+), 15 deletions(-)

diff --git a/public/admin/cactus-curator.html b/public/admin/cactus-curator.html
index 2b07397..5e0a1e1 100644
--- a/public/admin/cactus-curator.html
+++ b/public/admin/cactus-curator.html
@@ -656,22 +656,30 @@ async function decide(id, action, el){
     // amber "Publish anyway" toast that retries with force:true. Same pattern as
     // drunk-curator's HOT-gate-held flow ([[feedback_wallco_hot_vote_save_even_when_gate_blocks]]).
     if(j.gate_held){
-      decided.set(id,'fix');
-      el?.classList.remove('decided'); el?.classList.add('decided');
-      if(st){ st.style.display='block'; st.textContent='⚠ NEEDS FIXING'; }
+      // Steve 2026-05-29: auto-select "Publish anyway" on gate-held — no human
+      // click required. Brief amber toast announces the auto-force; then fires
+      // force=true. The publish gate becomes ADVISORY for any deliberate Publish
+      // action. Overrides the standing [[feedback_only_clean_designs_welcome]]
+      // default ("once gate flips off, stays off") — Steve's explicit choice:
+      // Publish click = always goes live, gate reasons surfaced but not blocking.
       const reasons=(j.reasons||[]).join(' · ')||'failed publish gate';
       const t=document.createElement('div');
       t.style.cssText='position:fixed;top:60px;left:50%;transform:translateX(-50%);background:#b07a00;color:#1a1a1a;padding:8px 14px;border-radius:8px;z-index:99;font-size:13px;box-shadow:0 4px 18px rgba(0,0,0,.5)';
-      t.innerHTML=`#${id} parked — gate held (${reasons}). <button style="margin-left:10px;background:#1a1a1a;color:#ffd68a;border:1px solid #ffd68a;padding:3px 9px;border-radius:5px;cursor:pointer;font-weight:700">Publish anyway</button>`;
-      t.querySelector('button').onclick=async()=>{
-        t.remove();
+      t.textContent=`#${id} gate held (${reasons}) — auto-force-publishing…`;
+      document.body.appendChild(t);
+      setTimeout(()=>t.remove(), 4000);
+      try {
         const r2=await fetch(q(`/api/cactus-decision/${id}`),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'live',force:true})});
         const j2=await r2.json().catch(()=>({}));
-        if(r2.ok && j2.ok){ decided.set(id,'live'); if(st){st.textContent='✓ PUBLISHED';} flash(`#${id} force-published`); }
-        else flash(`Force-publish failed: ${j2.error||r2.status}`);
-      };
-      document.body.appendChild(t);
-      setTimeout(()=>t.remove(), 10000);
+        if(r2.ok && j2.ok){
+          decided.set(id,'live');
+          if(st){st.textContent='✓ PUBLISHED';}
+          const dd=ALL.find(x=>x.id===id);
+          if(dd){dd.is_published=true; dd.web_viewer=true; dd.user_removed=false; dd.needs_fixing_at=null; dd.digital_file_at=null;}
+        } else {
+          flash(`#${id} force-publish failed: ${j2.error||r2.status}`);
+        }
+      } catch(e){ flash(`#${id} force-publish failed: ${e.message}`); }
     } 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
@@ -769,9 +777,10 @@ async function runRestore(id, el){
 
 function escapeHtml(s){ return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
 
-async function bulk(action){
-  const ids = [...selected];
+async function bulk(action, force, idsArg){
+  const ids = (idsArg && idsArg.length) ? idsArg : [...selected];
   if(!ids.length) return;
+  let heldIds = [];
   if(action==='bad' && !confirm(`Remove ${ids.length} designs from all surfaces? (quarantines the PNGs + drops them from the live site)`)) return;
   if(action==='unpublish' && !confirm(`Unpublish ${ids.length} designs from the live catalog? (sets is_published=FALSE; fully reversible via Publish — no quarantine, no tagging)`)) return;
 
@@ -790,7 +799,13 @@ async function bulk(action){
       const r=await fetch(q('/api/etsy-bucket/add'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ids})});
       j=await r.json().catch(()=>({})); if(!r.ok||!j.ok) throw new Error(j.error||('HTTP '+r.status));
     } else {
-      const r=await fetch(q('/api/cactus-decision/bulk'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ids,action})});
+      // Steve 2026-05-29: bulk Publish auto-forces past the publish gate
+      // (matches the per-card auto-Publish-anyway behavior in decide()). A
+      // Publish click is now an unconditional decision — gate parking is
+      // bypassed. Other bulk actions (bad/digital/fix/unpublish) don't run
+      // the gate, so the force flag is a no-op for them.
+      const body = (action==='live') ? {ids,action,force:true} : {ids,action};
+      const r=await fetch(q('/api/cactus-decision/bulk'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
       j=await r.json().catch(()=>({})); if(!r.ok||j.ok===false) throw new Error(j.error||('HTTP '+r.status));
     }
 
@@ -1229,7 +1244,20 @@ load();
     pubOnly = !pubOnly;
     if (pubOnly) localStorage.removeItem(LS_PUB); else localStorage.setItem(LS_PUB, '0');
     setPubBtnState();
-    if (loaded && stackEl && stackEl.style.display !== 'none') renderStack();
+    const inStack = loaded && stackEl && stackEl.style.display !== 'none';
+    if (inStack) { renderStack(); return; }
+    // Main-grid mode: previously this toggle did nothing here (it only filtered
+    // the by-base stack), so the button looked broken. Drive the existing
+    // #filter dropdown (which already has a 'published' case) + re-render.
+    // render() lives in a different IIFE, so we can't call it directly. The
+    // #filter <select> has a 'change' listener (→ render() + persist), so set
+    // its value and dispatch 'change' to re-render the main grid cross-scope.
+    const fsel = document.getElementById('filter');
+    if (fsel) {
+      if (pubOnly) { if (fsel.value !== 'published') fsel.dataset.prevFilter = fsel.value; fsel.value = 'published'; }
+      else { fsel.value = (fsel.dataset.prevFilter && fsel.dataset.prevFilter !== 'published') ? fsel.dataset.prevFilter : 'all'; }
+      fsel.dispatchEvent(new Event('change'));
+    }
   }
 
   // inject the toggle buttons into the controls bar (re-injects if DOM rebuilds)

← 89d7ad5 Fix publish-gate.py prod fail-closed: use resolved image pat  ·  back to Wallco Ai  ·  Fix cactus-curator 'Published only' toggle to filter the mai 08eb2ac →