← 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 →