[object Object]

← back to Dw Photo Capture

Add-mode visual dedup: new-SKU pattern photo → fine-tuned visual-ID; if it confidently matches an existing catalog pattern, warn (non-blocking) + offer one-tap 'Update it instead' (resolves via mfr then dw_sku). Prevents duplicate SKUs

cf2ea4a12d3b264eecef27398e010f8ac3267335 · 2026-07-07 10:12:51 -0700 · Steve Abrams

Files touched

Diff

commit cf2ea4a12d3b264eecef27398e010f8ac3267335
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jul 7 10:12:51 2026 -0700

    Add-mode visual dedup: new-SKU pattern photo → fine-tuned visual-ID; if it confidently matches an existing catalog pattern, warn (non-blocking) + offer one-tap 'Update it instead' (resolves via mfr then dw_sku). Prevents duplicate SKUs
---
 public/index.html | 22 +++++++++++++++++++++-
 1 file changed, 21 insertions(+), 1 deletion(-)

diff --git a/public/index.html b/public/index.html
index 84dbd27..bfbaf51 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1552,6 +1552,26 @@ async function addCommit(){ const b=$('#addCommit'); b.disabled=true; $('#addNot
 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; _updateNeedsConfirm=!confirmed; return r; } }catch(e){} return null; }
+// ADD-MODE DEDUP: before creating a new SKU, check the pattern photo against the catalog (fine-tuned
+// visual-ID). If it strongly matches an existing product, WARN (non-blocking) + offer "Update instead".
+async function addDedupCheck(){ const photos=addPhotosList(); if(!photos.length || _addMode!=='add') return;
+  let r; try{ r=await(await fetch('/api/identify-multi',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({front:photos[photos.length-1]})})).json(); }catch(e){ return; }
+  const prim=r&&r.primary; if(!prim || !r.confidence || r.confidence==='low' || _addMode!=='add') return;   // only warn on a confident match
+  const label=prim.pattern || prim.dw_sku || prim.mfr_sku || 'an existing item';
+  // WARN using the visual match itself (the pattern exists in the catalog even if it doesn't map to a live product_id)
+  $('#addTarget').innerHTML=`⚠ This pattern may already exist as <b>${label}</b>${prim.dw_sku?(' <small>('+prim.dw_sku+')</small>'):''} <small>· ${r.confidence}</small>`;
+  // try to resolve it to a LIVE product (mfr then dw_sku) so we can offer a one-tap "Update instead"
+  let hit=null;
+  for(const qs of [prim.mfr_sku&&('mfr='+encodeURIComponent(prim.mfr_sku)), prim.dw_sku&&('sku='+encodeURIComponent(prim.dw_sku))].filter(Boolean)){
+    try{ const h=await(await fetch('/api/resolve-product?'+qs)).json(); if(h.found){ hit=h; break; } }catch(e){} }
+  if(_addMode!=='add') return;
+  if(hit){ $('#addTarget').innerHTML+=` <button class="vid-chip" id="addSwitchUpd">↻ Update it instead</button>`;
+    const el=document.getElementById('addSwitchUpd'); if(el) el.addEventListener('click',()=>switchToUpdate(hit)); } }
+// user chose to update the existing match instead of creating a dup — bind it (explicit → no extra confirm)
+function switchToUpdate(hit){ _addMode='update'; _updatePid=hit.product_id; _updateSku=hit.dw_sku; _updateNeedsConfirm=false;
+  $('#addTitle').textContent='Update SKU on Shopify'; $('#addPreview').textContent='⬆ Push media to SKU (LIVE)';
+  $('#addTarget').innerHTML=`✓ Updating <b>${hit.title||hit.dw_sku}</b> <small>(${hit.dw_sku}) — your photos will attach here</small>`;
+  $('#addResult').innerHTML=''; toast('Switched to Update — media attaches to '+hit.dw_sku); }
 async function addResolveUpdate(){ const mfr=$('#addMfr').value.trim(); if(!mfr) return;
   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>`; }
@@ -1610,7 +1630,7 @@ async function addExtract(){ if(!_addPhoto)return; const mySession=_addSession;
     $('#addNote').textContent='✓ Imported from label';
     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();
+    else { addPromptMissing(); addDedupCheck(); }   // warn (non-blocking) if this pattern already exists
   }catch(e){ $('#addNote').textContent=''; }
 }
 // speak a prompt out loud, then listen for the answer (the on-screen field stays usable too)

← 7d4ccb6 Device-test checklist for the iPhone-Safari real-device pass  ·  back to Dw Photo Capture  ·  Contrarian gate fix (dedup): add _addSession guard before ev 828996f →