← back to Dw Photo Capture
Contrarian gate fix: _updateNeedsConfirm invariant — ANY ambiguous visual bind (background OR mid-Push) requires a 2nd Push before a LIVE write; label OCR + chip-tap are explicit (no gate). Plus session checks after resolve/visual awaits in addExtract
5673cadd1ab07609e9ce6f5ba166439b4d167898 · 2026-07-06 22:15:46 -0700 · Steve Abrams
Files touched
Diff
commit 5673cadd1ab07609e9ce6f5ba166439b4d167898
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jul 6 22:15:46 2026 -0700
Contrarian gate fix: _updateNeedsConfirm invariant — ANY ambiguous visual bind (background OR mid-Push) requires a 2nd Push before a LIVE write; label OCR + chip-tap are explicit (no gate). Plus session checks after resolve/visual awaits in addExtract
---
public/index.html | 25 +++++++++++++++----------
1 file changed, 15 insertions(+), 10 deletions(-)
diff --git a/public/index.html b/public/index.html
index 069f5f2..84dbd27 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1476,7 +1476,7 @@ $('#fbGo').addEventListener('click',fbIdentify);
const MAX_PHOTOS=10, MAX_VIDEOS=6;
let _addPhoto=null,_vendorsLoaded=false,_addExtracted={};
let _media=[]; // {type:'photo'|'video', dataUrl, name}
-let _addMode='add', _updatePid=null, _updateSku=null, _extracted=false, _addSession=0;
+let _addMode='add', _updatePid=null, _updateSku=null, _extracted=false, _addSession=0, _updateNeedsConfirm=false;
function mediaCounts(){ return { p:_media.filter(m=>m.type==='photo').length, v:_media.filter(m=>m.type==='video').length }; }
function renderMedia(){ const c=mediaCounts();
$('#cPhoto').textContent=c.p+'/'+MAX_PHOTOS; $('#cVideo').textContent=c.v+'/'+MAX_VIDEOS;
@@ -1485,7 +1485,7 @@ function renderMedia(){ const c=mediaCounts();
? `<div class="media-cell"><video src="${m.dataUrl}" muted playsinline preload="metadata"></video><span class="badge">🎥</span><button class="rm" data-rm="${i}">✕</button></div>`
: `<div class="media-cell" style="background-image:url('${m.dataUrl}')"><button class="rm" data-rm="${i}">✕</button></div>`).join('');
$('#addMedia').querySelectorAll('.rm').forEach(b=>b.addEventListener('click',()=>{ _media.splice(+b.dataset.rm,1); renderMedia(); })); }
-function addResetMedia(){ _media=[]; _addPhoto=null; _addExtracted={}; _extracted=false; _updatePid=null; _updateSku=null; $('#addSpecs').innerHTML=''; renderMedia(); }
+function addResetMedia(){ _media=[]; _addPhoto=null; _addExtracted={}; _extracted=false; _updatePid=null; _updateSku=null; _updateNeedsConfirm=false; $('#addSpecs').innerHTML=''; renderMedia(); }
function fileToDataUrl(f){ return new Promise(r=>{ const fr=new FileReader(); fr.onload=()=>r(fr.result); fr.readAsDataURL(f); }); }
async function addMediaAdd(type,file){ if(!file)return; const c=mediaCounts();
if(type==='photo'){ if(c.p>=MAX_PHOTOS){ toast('Max '+MAX_PHOTOS+' photos'); return; }
@@ -1547,11 +1547,13 @@ async function addCommit(){ const b=$('#addCommit'); b.disabled=true; $('#addNot
}
// UPDATE mode: resolve the existing product from the scanned mfr#, then push media LIVE.
// resolve an mfr# / sku to a live Shopify product_id and bind it as the Update target
-async function bindProduct(mfr,sku){ const qs=mfr?('mfr='+encodeURIComponent(mfr)):('sku='+encodeURIComponent(sku||''));
+// confirmed=true only for EXPLICIT binds (label OCR match, or a user tapping a candidate).
+// A visual auto-bind is confirmed=false → addUpdate demands a 2nd Push before writing LIVE.
+async function bindProduct(mfr,sku,confirmed){ const qs=mfr?('mfr='+encodeURIComponent(mfr)):('sku='+encodeURIComponent(sku||''));
try{ const r=await(await fetch('/api/resolve-product?'+qs)).json();
- if(r.found){ _updatePid=r.product_id; _updateSku=r.dw_sku; return r; } }catch(e){} return null; }
+ if(r.found){ _updatePid=r.product_id; _updateSku=r.dw_sku; _updateNeedsConfirm=!confirmed; return r; } }catch(e){} return null; }
async function addResolveUpdate(){ const mfr=$('#addMfr').value.trim(); if(!mfr) return;
- const r=await bindProduct(mfr,null);
+ const r=await bindProduct(mfr,null,true); // label OCR match = explicit → no extra confirm needed
if(r){ $('#addTarget').innerHTML=`✓ Updating <b>${r.title||r.dw_sku}</b> <small>(${r.status||''} · ${r.dw_sku})</small>`; }
else { _updatePid=null; _updateSku=null; $('#addTarget').innerHTML='⚠ No SKU matched “'+mfr+'”. Try a photo of the pattern, fix the mfr#, or use Add New SKU.'; } }
// REALLY ID by DESIGN: front pattern (+ label) photo → /api/identify-multi (OCR+QR+vendor+CLIP visual)
@@ -1568,14 +1570,16 @@ async function visualIdentify(){ const photos=addPhotosList(); if(!photos.length
$('#addTarget').innerHTML=`🔍 Visually matched <b>${bound.title||_updateSku}</b> <small>(${r.confidence} · ${_updateSku})</small>`; }
else if(cands.length){ _vidCands=cands;
$('#addTarget').innerHTML='🔍 Closest patterns — <b>tap to confirm</b>:<div class="vid-cands">'+cands.map((c,i)=>`<button class="vid-chip" data-i="${i}">${((c.pattern||c.mfr_sku||'?')+'').slice(0,20)}</button>`).join('')+'</div>';
- document.querySelectorAll('#addTarget .vid-chip').forEach(b=>b.addEventListener('click',async()=>{ const c=_vidCands[+b.dataset.i]; const bd=await bindProduct(c.mfr_sku,c.dw_sku); if(bd){ if(c.mfr_sku)$('#addMfr').value=c.mfr_sku; $('#addTarget').innerHTML=`🔍 Using <b>${bd.title||_updateSku}</b>`; } else $('#addTarget').innerHTML='That pattern has no live product — use Add New SKU.'; })); }
+ document.querySelectorAll('#addTarget .vid-chip').forEach(b=>b.addEventListener('click',async()=>{ const c=_vidCands[+b.dataset.i]; const bd=await bindProduct(c.mfr_sku,c.dw_sku,true); if(bd){ if(c.mfr_sku)$('#addMfr').value=c.mfr_sku; $('#addTarget').innerHTML=`🔍 Using <b>${bd.title||_updateSku}</b>`; } else $('#addTarget').innerHTML='That pattern has no live product — use Add New SKU.'; })); }
else $('#addTarget').innerHTML='No confident match — fix the mfr# or use Add New SKU.'; }
let _vidCands=[];
async function addUpdate(){ if(!_updatePid) await addResolveUpdate();
- if(!_updatePid){ await visualIdentify(); // last resort: match by the pattern itself
- if(_updatePid){ // a bind just happened silently during Push → REQUIRE a 2nd press to confirm the LIVE write
- $('#addNote').innerHTML=`⚠ Matched <b>${_updateSku}</b> by pattern — tap Push again to confirm the LIVE update.`; return; } }
+ if(!_updatePid) await visualIdentify(); // last resort: match by the pattern itself
if(!_updatePid){ $('#addNote').textContent='No product to update — scan the label/pattern or fix the mfr#.'; return; }
+ // INVARIANT: never write LIVE to a product bound by an ambiguous visual match without an explicit
+ // 2nd Push — covers BOTH the mid-Push bind AND the background bind set earlier during addExtract.
+ if(_updateNeedsConfirm){ _updateNeedsConfirm=false;
+ $('#addNote').innerHTML=`⚠ Matched <b>${_updateSku}</b> by pattern — tap Push again to confirm the LIVE update.`; return; }
const photos=addPhotosList(); if(!photos.length && !addVideosList().length){ $('#addNote').textContent='Add at least one photo or video.'; return; }
$('#addNote').textContent='⬆ Pushing to Shopify (LIVE)…'; let added=0;
if(photos.length){ const r=await(await fetch('/api/photos',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dw_sku:_updateSku,product_id:_updatePid,dataUrls:photos})})).json(); added=r.added||0; }
@@ -1604,7 +1608,8 @@ async function addExtract(){ if(!_addPhoto)return; const mySession=_addSession;
const specs=[['Material',f.material],['Collection',f.collection],['Width',f.width],['Roll length',f.roll_length],['Repeat',f.repeat],['Match',f.pattern_match],['Substrate / contents',f.substrate],['How sold',f.how_sold],['Price code',f.price_code]].filter(x=>x[1]);
$('#addSpecs').innerHTML = specs.length? ('<div class="samp-meta" style="margin:10px 0 2px">Captured specs:</div>'+specs.map(s=>`<div class="add-spec"><span>${s[0]}</span><b>${s[1]}</b></div>`).join('')) : '';
$('#addNote').textContent='✓ Imported from label';
- if(_addMode==='update'){ await addResolveUpdate(); if(!_updatePid) await visualIdentify(); } // label first, then match by design
+ if(_addMode==='update'){ await addResolveUpdate(); if(_addSession!==mySession) return; // discard if modal reset mid-resolve
+ if(!_updatePid){ await visualIdentify(); if(_addSession!==mySession) return; } } // label first, then match by design
else addPromptMissing();
}catch(e){ $('#addNote').textContent=''; }
}
← 1570feb yolo hardening (8 fixes from adversarial review): C1 visual
·
back to Dw Photo Capture
·
Wire in fine-tuned DW CLIP: sanity-check picks best epoch (e ec02bd7 →