[object Object]

← back to Dw Photo Capture

Add 'Any SKU' lookup: search all 555 Fentucci products by SKU/model#/name to add or replace any product's photo (in-memory catalog index + /api/lookup)

a2403b91753f98051cfebd6bc5c7d01179401e27 · 2026-06-24 17:18:15 -0700 · steve@designerwallcoverings.com

Files touched

Diff

commit a2403b91753f98051cfebd6bc5c7d01179401e27
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date:   Wed Jun 24 17:18:15 2026 -0700

    Add 'Any SKU' lookup: search all 555 Fentucci products by SKU/model#/name to add or replace any product's photo (in-memory catalog index + /api/lookup)
---
 data/build.json   |  5 +++--
 public/index.html | 55 ++++++++++++++++++++++++++++++++++++-----------------
 server.js         | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 97 insertions(+), 20 deletions(-)

diff --git a/data/build.json b/data/build.json
index 99abb3d..282c61a 100644
--- a/data/build.json
+++ b/data/build.json
@@ -1,5 +1,5 @@
 {
-  "next": 9,
+  "next": 10,
   "map": {
     "578af86f": 2,
     "bc95fdb0": 3,
@@ -7,6 +7,7 @@
     "deffa396": 5,
     "468831e8": 6,
     "cd2b7577": 7,
-    "e342dc0a": 8
+    "e342dc0a": 8,
+    "09a337f9": 9
   }
 }
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 3a5c7be..af891a0 100644
--- a/public/index.html
+++ b/public/index.html
@@ -103,6 +103,7 @@
       <div class="chip" data-f="live">🟢 Live</div>
       <div class="chip" data-f="done">Done</div>
       <div class="chip" data-f="all">All</div>
+      <div class="chip" data-f="any">🔎 Any SKU</div>
     </div>
     <div class="dens">Size <input type="range" id="dens" min="1" max="4" value="1"></div>
   </div>
@@ -165,30 +166,47 @@ async function load(){
   ITEMS=d.items; render();
 }
 function priced(x){return x.price && parseFloat(x.price)>4.25;}
+let LOOKUP=[], LOOKUP_INDEXED=0, _lkT=0;
+async function doLookup(){
+  const q=($('#q').value||'').trim();
+  if(!q){ LOOKUP=[]; render(); return; }
+  try{ const r=await fetch('/api/lookup?q='+encodeURIComponent(q)); const d=await r.json();
+    LOOKUP=d.items||[]; LOOKUP_INDEXED=d.indexed||0; render();
+  }catch(e){ toast('Lookup failed'); }
+}
 function render(){
   const q=($('#q').value||'').toLowerCase(), sort=$('#sort').value;
-  let list=ITEMS.filter(x=>{
+  const src = filter==='any' ? LOOKUP : ITEMS;
+  let list=src.filter(x=>{
     if(filter==='need'&&(x.done||x.skipped))return false;
     if(filter==='live'&&!x.live)return false;
     if(filter==='done'&&!x.done)return false;
-    if(q&&!((x.title||'').toLowerCase().includes(q)||(x.dw_sku||'').toLowerCase().includes(q)||(x.mfr||'').toLowerCase().includes(q)))return false;
+    if(filter!=='any'&&q&&!((x.title||'').toLowerCase().includes(q)||(x.dw_sku||'').toLowerCase().includes(q)||(x.mfr||'').toLowerCase().includes(q)))return false;
     return true;
   });
   list.sort((a,b)=> sort==='title'?(a.title||'').localeCompare(b.title||'')
     : sort==='sku'?(a.dw_sku||'').localeCompare(b.dw_sku||'')
     : (priced(b)-priced(a))||(a.title||'').localeCompare(b.title||''));
-  const done=ITEMS.filter(x=>x.done).length, total=ITEMS.length;
-  $('#prog').style.width=(total?done/total*100:0)+'%';
-  $('#count').textContent=`${done} / ${total} photographed`;
-  $('#remain').textContent=`${total-done} remaining`;
-  grid.innerHTML = list.length? '' : '<div class="empty">Nothing here — switch filter or clear search.</div>';
+  if(filter==='any'){
+    $('#prog').style.width='0%';
+    $('#count').textContent = ($('#q').value||'').trim() ? `${list.length} match${list.length===1?'':'es'}` : 'Type a SKU, model #, or name to find any product';
+    $('#remain').textContent = LOOKUP_INDEXED?`${LOOKUP_INDEXED} indexed`:'';
+  } else {
+    const done=ITEMS.filter(x=>x.done).length, total=ITEMS.length;
+    $('#prog').style.width=(total?done/total*100:0)+'%';
+    $('#count').textContent=`${done} / ${total} photographed`;
+    $('#remain').textContent=`${total-done} remaining`;
+  }
+  grid.innerHTML = list.length? '' : `<div class="empty">${filter==='any'?(($('#q').value||'').trim()?'No Fentucci product matches that — try the model # or DW SKU.':'🔎 Search any TWIL/Fentucci SKU above to add or replace its photo.'):'Nothing here — switch filter or clear search.'}</div>`;
   for(const x of list){
-    const el=document.createElement('div'); el.className='card'+(x.done?' done':'');
-    const badge=x.push_err?'<span class="badge b-err">PUSH ERR</span>':x.live?'<span class="badge b-live">● LIVE</span>':x.done?'<span class="badge b-done">✓ PHOTO</span>':'<span class="badge b-need">NEEDS</span>';
-    const thumb=x.photo?`style="background-image:url('${x.photo}')"`:'';
-    const adjustable=x.done; // any done item can be re-opened in the editor (local photo or live Shopify image)
+    const photographed = x.done || !!x.image || x.live; // already has a photo (worklist-done OR a live Shopify image)
+    const el=document.createElement('div'); el.className='card'+(photographed?' done':'');
+    const badge=x.push_err?'<span class="badge b-err">PUSH ERR</span>':x.live?'<span class="badge b-live">● LIVE</span>':photographed?'<span class="badge b-done">✓ PHOTO</span>':'<span class="badge b-need">NEEDS</span>';
+    const thumbUrl=x.photo||x.image||'';
+    const thumb=thumbUrl?`style="background-image:url('${thumbUrl}')"`:'';
+    const adjustable=photographed; // anything with a photo can be re-opened in the editor (local or live Shopify image)
     el.innerHTML=`
-      <div class="thumb ${adjustable?'tap':''}" ${thumb} title="${adjustable?'click to adjust':''}">${x.photo?'':'▦'}${badge}</div>
+      <div class="thumb ${adjustable?'tap':''}" ${thumb} title="${adjustable?'click to adjust':''}">${thumbUrl?'':'▦'}${badge}</div>
       <div class="body">
         <div class="ttl">${(x.title||'').replace(/ \\| Fentucci$/,'')}</div>
         <div class="model" title="tap to copy model #" onclick="navigator.clipboard&&navigator.clipboard.writeText('${x.mfr||''}')"><small>Model&nbsp;#</small><b>${x.mfr||'—'}</b></div>
@@ -201,8 +219,8 @@ function render(){
         ${x.push_err?`<div class="when" style="color:var(--red)">${x.push_err}</div>`:''}
         <div class="actions">
           ${adjustable?'<button class="enh">✨ Enhance</button>':''}
-          <label class="shoot">${x.done?'📷 Retake':'📷 Open Camera'}<input type="file" accept="image/*" capture="environment"></label>
-          ${x.done?'':'<button class="skip">Skip</button>'}
+          <label class="shoot">${photographed?'📷 Update Photo':'📷 Add Photo'}<input type="file" accept="image/*" capture="environment"></label>
+          ${(x.done||x.live||x.image)?'':'<button class="skip">Skip</button>'}
         </div>
       </div>`;
     const editSrc=()=> (x.photo||('/api/current-image?pid='+x.product_id))+(x.photo?'?':'&')+'t='+Date.now();
@@ -453,12 +471,15 @@ $('#edUse').addEventListener('click',()=>{
 });
 async function skip(x){await fetch('/api/skip',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dw_sku:x.dw_sku,skipped:true})});x.skipped=true;render();}
 
-$('#q').addEventListener('input',render);
+$('#q').addEventListener('input',()=>{ if(filter==='any'){ clearTimeout(_lkT); _lkT=setTimeout(doLookup,250); } else render(); });
 $('#sort').addEventListener('change',e=>{setLS('sort',e.target.value);render();});
 ['input','change'].forEach(ev=>$('#dens').addEventListener(ev,e=>{applyDensity(e.target.value);setLS('dens',e.target.value);}));
 document.querySelectorAll('.chip').forEach(c=>c.addEventListener('click',()=>{
-  document.querySelectorAll('.chip').forEach(z=>z.classList.remove('on'));c.classList.add('on');filter=c.dataset.f;setLS('filter',filter);render();}));
-load(); setInterval(load, 60000);
+  document.querySelectorAll('.chip').forEach(z=>z.classList.remove('on'));c.classList.add('on');filter=c.dataset.f;setLS('filter',filter);
+  if(filter==='any'){ $('#q').placeholder='🔎 Any TWIL SKU, model #, or name…'; $('#q').focus(); doLookup(); }
+  else { $('#q').placeholder='🔎 Search name or model #…'; render(); }
+}));
+load(); setInterval(()=>{ if(filter!=='any') load(); }, 60000);
 </script>
 </body>
 </html>
diff --git a/server.js b/server.js
index fe78c37..4de8213 100644
--- a/server.js
+++ b/server.js
@@ -164,6 +164,23 @@ const server = http.createServer((req, res) => {
     return send(res, 200, { total: q.length, done: q.filter(x => x.done).length, items: q });
   }
 
+  // Look up ANY Fentucci (TWIL) product — by DW SKU, model #, or name — to add/replace its photo.
+  if (u.pathname === '/api/lookup' && req.method === 'GET') {
+    const q = (u.searchParams.get('q') || '').trim().toLowerCase();
+    if (!q) return send(res, 200, { items: [], indexed: CATALOG.length });
+    const terms = q.split(/\s+/);
+    const matches = CATALOG.filter(x => {
+      const hay = `${x.dw_sku} ${x.mfr} ${x.title}`.toLowerCase();
+      return terms.every(t => hay.includes(t));
+    }).slice(0, 80);
+    return send(res, 200, { total: matches.length, items: matches, indexed: CATALOG.length });
+  }
+
+  if (u.pathname === '/api/reindex' && req.method === 'POST') {
+    buildCatalog().then(() => {}).catch(() => {});
+    return send(res, 200, { ok: true, indexing: true, current: CATALOG.length });
+  }
+
   if (u.pathname.startsWith('/photos/')) {
     const f = path.join(PHOTOS, path.basename(u.pathname));
     if (fs.existsSync(f)) { res.writeHead(200, { 'Content-Type': 'image/jpeg' }); return res.end(fs.readFileSync(f)); }
@@ -232,4 +249,42 @@ const server = http.createServer((req, res) => {
   send(res, 404, { err: 'not found' });
 });
 
-server.listen(PORT, '0.0.0.0', () => console.log(`DW Photo Capture on http://0.0.0.0:${PORT}  (Shopify push: ${TOKEN ? 'ON' : 'OFF — no token'})`));
+// In-memory index of ALL non-archived Fentucci products (for "find any SKU" lookup).
+let CATALOG = [];
+async function buildCatalog() {
+  const Q = `query($c:String){products(first:100,query:"vendor:Fentucci",after:$c){pageInfo{hasNextPage endCursor}
+    edges{node{legacyResourceId title status
+      imgs:images(first:1){edges{node{src}}}
+      mf:metafield(namespace:"custom",key:"manufacturer_sku"){value}
+      mf2:metafield(namespace:"dwc",key:"manufacturer_sku"){value}
+      v:variants(first:3){edges{node{sku title price}}}}}}}`;
+  let out = [], cursor = null, pages = 0;
+  try {
+    while (pages < 60) {
+      const r = await gql(Q, { c: cursor });
+      const pr = r.data && r.data.products;
+      if (!pr) break;
+      for (const e of pr.edges) {
+        const n = e.node;
+        if (n.status === 'ARCHIVED') continue;
+        const roll = n.v.edges.map(x => x.node).find(x => !(x.sku || '').endsWith('-Sample') && (x.title || '').toLowerCase() !== 'sample') || (n.v.edges[0] && n.v.edges[0].node);
+        out.push({
+          product_id: n.legacyResourceId, title: n.title, status: n.status,
+          dw_sku: (roll || {}).sku || '', mfr: (n.mf || {}).value || (n.mf2 || {}).value || '',
+          price: (roll || {}).price || null,
+          image: (n.imgs.edges[0] && n.imgs.edges[0].node.src) || null,
+          done: false
+        });
+      }
+      pages++;
+      if (pr.pageInfo.hasNextPage) cursor = pr.pageInfo.endCursor; else break;
+    }
+    CATALOG = out;
+    console.log(`catalog indexed: ${CATALOG.length} Fentucci products`);
+  } catch (e) { console.log('catalog index error:', e.message); }
+}
+
+server.listen(PORT, '0.0.0.0', () => {
+  console.log(`DW Photo Capture on http://0.0.0.0:${PORT}  (Shopify push: ${TOKEN ? 'ON' : 'OFF — no token'})`);
+  if (TOKEN) buildCatalog(); // background index for "find any SKU"
+});

← 89c5686 Fix grid density slider + editor preview-equals-export bake  ·  back to Dw Photo Capture  ·  Add 'All Shopify' button: live store-wide SKU search to upda b4532b2 →