← 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
M data/build.jsonM public/index.htmlM server.js
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 #</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 →