← back to Wallco Ai
cactus-curator: park gate-failed designs by default, require explicit human override to publish (DTD verdict)
98f6c51e64425147ec177fdc6a83f6d1ffa2b210 · 2026-05-30 23:16:50 -0700 · Steve Abrams
Reverts the 2026-05-29 auto-force-publish behavior in /admin/cactus-curator
that silently shipped triple-gate-failed designs live (e.g. #41813: seam ΔE
25.85, 12-color blowup, vision off-concept). Per the 2026-05-30 DTD panel
verdict (B = park-by-default), both the per-card Publish and bulk Publish
paths now PARK gate-held designs (saved server-side as needs-fixing) and
surface a one-click 'Publish anyway' override. Mirrors the sibling
drunk-curator forcePublish() pattern. Honors the standing
only-clean-designs-welcome default.
Files touched
M public/admin/cactus-curator.html
Diff
commit 98f6c51e64425147ec177fdc6a83f6d1ffa2b210
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat May 30 23:16:50 2026 -0700
cactus-curator: park gate-failed designs by default, require explicit human override to publish (DTD verdict)
Reverts the 2026-05-29 auto-force-publish behavior in /admin/cactus-curator
that silently shipped triple-gate-failed designs live (e.g. #41813: seam ΔE
25.85, 12-color blowup, vision off-concept). Per the 2026-05-30 DTD panel
verdict (B = park-by-default), both the per-card Publish and bulk Publish
paths now PARK gate-held designs (saved server-side as needs-fixing) and
surface a one-click 'Publish anyway' override. Mirrors the sibling
drunk-curator forcePublish() pattern. Honors the standing
only-clean-designs-welcome default.
---
public/admin/cactus-curator.html | 100 ++++++++++++++++++++++++++-------------
1 file changed, 68 insertions(+), 32 deletions(-)
diff --git a/public/admin/cactus-curator.html b/public/admin/cactus-curator.html
index c3e002e..0cae7a4 100644
--- a/public/admin/cactus-curator.html
+++ b/public/admin/cactus-curator.html
@@ -689,35 +689,44 @@ 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){
- // 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.
+ // DTD verdict 2026-05-30 (panel B, park-by-default): a Publish click on a
+ // gate-failed design PARKS it (the verdict is saved server-side with
+ // needs_fixing + curator-loved tags) and surfaces the reasons + an explicit
+ // "Publish anyway" link. It does NOT auto-force past the gate. Only a
+ // deliberate human click on that link publishes a triple-gate-failed design.
+ // This reverts the 2026-05-29 auto-force behavior that silently shipped
+ // #41813 (seam ΔE 25.85 · 12-color blowup · vision off-concept) live.
+ // Mirrors the sibling drunk-curator forcePublish() pattern. Honors the
+ // standing [[feedback_only_clean_designs_welcome]] default.
+ decided.set(id,'held');
+ if(st){ st.textContent='🔧 HELD — review'; st.style.display='block'; }
+ el?.classList.remove('decided');
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.textContent=`#${id} gate held (${reasons}) — auto-force-publishing…`;
+ t.id='cactus-gatetoast';
+ t.style.cssText='position:fixed;top:60px;left:50%;transform:translateX(-50%);background:#8a6d1f;color:#fff;padding:8px 16px;border-radius:8px;z-index:99;font-size:13px;max-width:90vw;box-shadow:0 4px 18px rgba(0,0,0,.5)';
+ t.innerHTML=`🔧 #${id} held — not auto-published: ${escapeHtml(reasons)}. <a href="#" id="cactus-forcepub" style="color:#ffe9a8;font-weight:600;text-decoration:underline">Publish anyway</a>`;
+ document.getElementById('cactus-gatetoast')?.remove();
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';}
- 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;}
- // Steve 2026-05-29: animate card out on successful publish so it leaves
- // the Undecided filter view (matches bad/digital/unpublish behavior).
- // Without this the card sits there with ✓ PUBLISHED and Steve sees
- // 'auto-force-publishing…' but no visible action — feels broken.
- if(el){ el.classList.add('moved-to-etsy'); setTimeout(()=>el.remove(),300); }
- } else {
- flash(`#${id} force-publish failed: ${j2.error||r2.status}`);
- }
- } catch(e){ flash(`#${id} force-publish failed: ${e.message}`); }
+ setTimeout(()=>document.getElementById('cactus-gatetoast')?.remove(), 9000);
+ // The override is a deliberate human action — only a click fires force:true.
+ document.getElementById('cactus-forcepub')?.addEventListener('click', async ev=>{
+ ev.preventDefault();
+ 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';}
+ 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;}
+ document.getElementById('cactus-gatetoast')?.remove();
+ if(el){ el.classList.add('moved-to-etsy'); setTimeout(()=>el.remove(),300); }
+ } 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
@@ -837,12 +846,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 {
- // 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};
+ // DTD verdict 2026-05-30 (panel B, park-by-default): bulk Publish does NOT
+ // auto-force past the publish gate. Gate-failed designs come back parked
+ // (server reports them in j.results[].gate_held + j.gate_held count); the
+ // post-response handler re-flags them NEEDS FIXING and a toast then offers
+ // an explicit "Publish N held anyway" override. Reverts the 2026-05-29
+ // unconditional force that silently shipped gate-failed designs live.
+ const body = {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));
}
@@ -878,6 +888,32 @@ async function bulk(action){
if(j.dropped_from_index!=null) msg += ` · ${j.dropped_from_index} dropped from live index`;
if(j.errored) msg += ` · ${j.errored} errored`;
flash(msg);
+
+ // DTD park-by-default override: if any were parked at the gate, offer an
+ // explicit one-click "Publish N held anyway" — the only path to publishing
+ // gate-failed designs in bulk. Re-POSTs only the held ids with force:true.
+ if(action==='live' && j.gate_held>0){
+ const heldIds=(j.results||[]).filter(rr=>rr&&rr.gate_held).map(rr=>rr.id);
+ document.getElementById('cactus-gatetoast')?.remove();
+ const t=document.createElement('div'); t.id='cactus-gatetoast';
+ t.style.cssText='position:fixed;top:60px;left:50%;transform:translateX(-50%);background:#8a6d1f;color:#fff;padding:8px 16px;border-radius:8px;z-index:99;font-size:13px;max-width:90vw;box-shadow:0 4px 18px rgba(0,0,0,.5)';
+ t.innerHTML=`🔧 ${heldIds.length} held at the publish gate. <a href="#" id="cactus-bulkforce" style="color:#ffe9a8;font-weight:600;text-decoration:underline">Publish ${heldIds.length} anyway</a>`;
+ document.body.appendChild(t);
+ setTimeout(()=>document.getElementById('cactus-gatetoast')?.remove(), 12000);
+ document.getElementById('cactus-bulkforce')?.addEventListener('click', async ev=>{
+ ev.preventDefault();
+ try{
+ const r=await fetch(q('/api/cactus-decision/bulk'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ids:heldIds,action:'live',force:true})});
+ const jf=await r.json().catch(()=>({})); if(!r.ok||jf.ok===false) throw new Error(jf.error||('HTTP '+r.status));
+ heldIds.forEach(id=>{ const el=grid.querySelector(`.card[data-id="${id}"]`); const d=ALL.find(x=>x.id===id);
+ decided.set(id,'live'); if(d){d.is_published=true;d.web_viewer=true;d.user_removed=false;d.needs_fixing_at=null;d.digital_file_at=null;}
+ el?.classList.add('moved-to-etsy'); setTimeout(()=>el?.remove(),300); });
+ document.getElementById('cactus-gatetoast')?.remove();
+ flash(`${jf.applied||heldIds.length} force-published`);
+ updateStat();
+ }catch(e){ flash(`Force-publish failed: ${e.message}`); }
+ });
+ }
}catch(e){
ids.forEach(id=>{ const el=grid.querySelector(`.card[data-id="${id}"]`); el?.classList.remove('decided');
const st=el?.querySelector('.state'); if(st) st.style.display='none'; decided.delete(id); });
← c812cf6 elements: backend POST /api/admin/elements/compose — real Ge
·
back to Wallco Ai
·
TODO: mark /admin/elements drawer CTA wiring DONE (was stale e83be1d →