← 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
M public/admin/cactus-curator.html
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
-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 →