[object Object]

← 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

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 →