[object Object]

← back to Wallco Ai

drunk-curator (Hot or Not): fix votes silently dropped + same item looping — decide() now SAVES FIRST and only advances on res.ok (was optimistically advancing i++ before the POST + swallowing errors in catch{}, so saves failing during server restarts left the DB unchanged and unsaved items reappeared on reload). Codex-confirmed root cause; surfaces save failures + busy-guard against double-vote

7462d55d1115f7d79718dbc2bd0c30bc95ea531c · 2026-05-27 14:11:43 -0700 · Steve Abrams

Files touched

Diff

commit 7462d55d1115f7d79718dbc2bd0c30bc95ea531c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed May 27 14:11:43 2026 -0700

    drunk-curator (Hot or Not): fix votes silently dropped + same item looping — decide() now SAVES FIRST and only advances on res.ok (was optimistically advancing i++ before the POST + swallowing errors in catch{}, so saves failing during server restarts left the DB unchanged and unsaved items reappeared on reload). Codex-confirmed root cause; surfaces save failures + busy-guard against double-vote
---
 public/admin/drunk-curator.html | 18 +++++++++++++++---
 1 file changed, 15 insertions(+), 3 deletions(-)

diff --git a/public/admin/drunk-curator.html b/public/admin/drunk-curator.html
index 44d2675..e8d52cf 100644
--- a/public/admin/drunk-curator.html
+++ b/public/admin/drunk-curator.html
@@ -94,11 +94,23 @@ function render(){
     <div class="btns"><button class="not" onclick="decide('not')">👎 NOT</button><button class="hot" onclick="decide('hot')">🔥 HOT</button></div>
     <div class="hint">← / N = not · → / H = hot · left = flat repeat, right = on a wall</div>`;
 }
+let busy=false;
 async function decide(v){
-  if(i>=QUEUE.length) return; const d=QUEUE[i];
+  if(busy || i>=QUEUE.length) return; const d=QUEUE[i];
+  busy=true;
+  // SAVE FIRST, then advance — never advance on a failed/unconfirmed save, or the
+  // unsaved item loops back on reload (Codex-confirmed root cause 2026-05-27).
+  try{
+    const res = await fetch(q('/api/admin/drunk/decide'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:d.id,verdict:v})});
+    if(!res.ok){ const j=await res.json().catch(()=>({})); throw new Error(j.error||('HTTP '+res.status)); }
+  }catch(err){
+    busy=false; flash('⚠'); console.error('decide failed',d.id,v,err);
+    $('#stage').insertAdjacentHTML('afterbegin',`<div id="saveerr" style="position:fixed;top:62px;left:50%;transform:translateX(-50%);background:#a13030;color:#fff;padding:8px 16px;border-radius:8px;z-index:99;font-size:13px">Save failed for #${d.id}: ${err.message} — vote NOT recorded, try again</div>`);
+    setTimeout(()=>document.getElementById('saveerr')?.remove(),4000);
+    return;   // do NOT advance — the vote didn't persist
+  }
   if(v==='hot'){hot++;streak++;flash('🔥');}else{not++;streak=0;flash('👎');}
-  i++; render();
-  try{ await fetch(q('/api/admin/drunk/decide'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:d.id,verdict:v})}); }catch{}
+  i++; busy=false; render();
 }
 document.addEventListener('keydown',e=>{ const k=e.key.toLowerCase();
   if(e.target.tagName==='SELECT')return;

← ed7d4f3 Hot-or-not + room setting generalized to ALL wallco.ai lines  ·  back to Wallco Ai  ·  Add flat-color vectorize pipeline (potrace) + edge-fix compa 040d453 →