← back to Designer Wallcoverings
auto-save: 2026-07-31T09:26:15 (3 files) — DW-Agents/vendor-command-center/server.js scripts/color-index-feature/color-index-service.js shopify/theme-LIVE-pull-20260728-colorbar/snippets/color-palette.liquid
11adafb929d0dd9d7f100bdd10f8d4924a91d5a3 · 2026-07-31 09:26:30 -0700 · Steve Abrams
Files touched
M DW-Agents/vendor-command-center/server.jsM scripts/color-index-feature/color-index-service.jsM shopify/theme-LIVE-pull-20260728-colorbar/snippets/color-palette.liquid
Diff
commit 11adafb929d0dd9d7f100bdd10f8d4924a91d5a3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jul 31 09:26:30 2026 -0700
auto-save: 2026-07-31T09:26:15 (3 files) — DW-Agents/vendor-command-center/server.js scripts/color-index-feature/color-index-service.js shopify/theme-LIVE-pull-20260728-colorbar/snippets/color-palette.liquid
---
DW-Agents/vendor-command-center/server.js | 20 ++++-
scripts/color-index-feature/color-index-service.js | 28 ++++++-
.../snippets/color-palette.liquid | 85 ++++++++++++++++++++--
3 files changed, 118 insertions(+), 15 deletions(-)
diff --git a/DW-Agents/vendor-command-center/server.js b/DW-Agents/vendor-command-center/server.js
index 0936de03..09f16455 100644
--- a/DW-Agents/vendor-command-center/server.js
+++ b/DW-Agents/vendor-command-center/server.js
@@ -2203,7 +2203,9 @@ async function syncVendorCounts() {
for (const v of allVendors) {
const code = v.vendor_code;
- const catTable = v.catalog_table;
+ // catalog_table is freeform admin-editable text that gets interpolated into SQL —
+ // strip to identifier chars (same guard as the SKU-gen path at ~848)
+ const catTable = String(v.catalog_table || '').replace(/[^a-z0-9_]/gi, '') || null;
let catalogCount = 0, catWithCost = 0, catWithImages = 0, catOnShopify = 0;
let catWithWidth = 0, catWithRepeat = 0;
let catWithAboutVendor = 0, catWithAllImages = 0, catWithSpecSheet = 0, catWithBodyHtml = 0;
@@ -2225,8 +2227,10 @@ async function syncVendorCounts() {
const discCol = await resolveSharedDiscriminator(catTable);
if (discCol) {
const discVal = String((discCol === 'vendor_code' ? code : v.vendor_name) || '').replace(/'/g, "''");
- vendorFilter = ` WHERE ${discCol} = '${discVal}'`;
- vendorAnd = ` AND ${discCol} = '${discVal}'`;
+ // Case-fold both sides: catalog rows carry mixed-case codes (dwdp vs DWDP,
+ // grd vs GRD) and exact = silently dropped those rows to 0
+ vendorFilter = ` WHERE LOWER(${discCol}) = LOWER('${discVal}')`;
+ vendorAnd = ` AND LOWER(${discCol}) = LOWER('${discVal}')`;
}
}
@@ -2310,7 +2314,10 @@ async function syncVendorCounts() {
const specTotal = specResult.total;
// --- SECONDARY: Shopify products (what's published) ---
- const patterns = VENDOR_NAME_MAP[code];
+ // Unmapped vendors fall back to their registry name as the Shopify vendor pattern;
+ // without this, any vendor absent from VENDOR_NAME_MAP reads 0 on the dashboard
+ // (carnegie 558 / newwall 433 live products showed as dead vendors)
+ const patterns = VENDOR_NAME_MAP[code] || (v.vendor_name ? [v.vendor_name + '%'] : null);
if (patterns && patterns.length > 0) {
try {
const conditions = patterns.map((_, i) => `vendor ILIKE $${i + 1}`).join(' OR ');
@@ -2894,6 +2901,11 @@ app.get('/catalog-browse', (req, res) => {
res.sendFile(__dirname + '/public/catalog-browse.html');
});
+// Serve width gaps page (was orphaned — file existed with no route, TK-10084 tour)
+app.get('/width-gaps', (req, res) => {
+ res.sendFile(__dirname + '/public/width-gaps.html');
+});
+
// Run sync on startup (after 5s delay to let DB connect) and every 5 minutes
setTimeout(() => syncVendorCounts(), 5000);
setInterval(() => syncVendorCounts(), 300000);
diff --git a/scripts/color-index-feature/color-index-service.js b/scripts/color-index-feature/color-index-service.js
index 55b13025..2df34599 100644
--- a/scripts/color-index-feature/color-index-service.js
+++ b/scripts/color-index-feature/color-index-service.js
@@ -62,12 +62,27 @@ function hexToLab(hex){
// ── the catalog-wide color-tolerance query ───────────────────────────────────
// Roll variant is resolved storefront-side (product handle → PDP), so we return handle
// (never the $4.25 Sample). image/title/vendor come straight from the color index.
-async function colorIndex(hex, tol, limit, style){
+// Coordinate filters (Steve TK-10085): TYPE + USE buckets built from product_type.
+// The fragments are FIXED strings selected by a whitelist (never user-interpolated),
+// so there is no injection surface. USE 'residential' = the non-commercial complement.
+function ciTypeUseClause(type, use){
+ let c = '';
+ if(type==='wallcovering') c += ` AND (coalesce(product_type,'') ~* 'wallcover|wallpaper|mural')`;
+ else if(type==='fabric') c += ` AND (coalesce(product_type,'') ~* 'fabric|upholst|drapery|multipurpose|pillow')`;
+ else if(type==='other') c += ` AND (coalesce(product_type,'') !~* 'wallcover|wallpaper|mural|fabric|upholst|drapery|multipurpose|pillow')`;
+ if(use==='commercial') c += ` AND (coalesce(product_type,'') ~* 'commercial|contract')`;
+ else if(use==='residential') c += ` AND (coalesce(product_type,'') !~* 'commercial|contract')`;
+ return c;
+}
+async function colorIndex(hex, tol, limit, style, type, use){
const lab = hexToLab(hex);
if(!lab) return { ok:false, err:'bad hex', results:[] };
const params = [lab.L, lab.a, lab.b, tol, limit];
let styleClause = '';
if(style){ params.push(style.toLowerCase()); styleClause = ` AND (lower(coalesce(product_type,'')) LIKE '%'||$6||'%')`; }
+ const TYPE = ['wallcovering','fabric','other'].indexOf(type)>=0 ? type : '';
+ const USE = ['commercial','residential'].indexOf(use)>=0 ? use : '';
+ const filterClause = ciTypeUseClause(TYPE, USE);
const sql = `
SELECT handle, title, vendor, hex, image_url,
sqrt(power(lab_l-$1,2)+power(lab_a-$2,2)+power(lab_b-$3,2)) AS de
@@ -77,6 +92,7 @@ async function colorIndex(hex, tol, limit, style){
AND image_url IS NOT NULL AND image_url<>''
AND sqrt(power(lab_l-$1,2)+power(lab_a-$2,2)+power(lab_b-$3,2)) <= $4
${styleClause}
+ ${filterClause}
ORDER BY de ASC
LIMIT $5`;
const { rows } = await pool.query(sql, params);
@@ -95,15 +111,17 @@ const server = http.createServer((req,res)=>{
const u = new URL(req.url, 'http://127.0.0.1');
if(u.pathname==='/healthz') return send(res,200,{ok:true, tol:DELTA_E_10PCT, max:MAX_RESULTS});
if(u.pathname==='/color-index'){
- const run = (hex, tol, limit, style)=> colorIndex(hex, tol, limit, style)
+ const run = (hex, tol, limit, style, type, use)=> colorIndex(hex, tol, limit, style, type, use)
.then(r=>send(res,200,r)).catch(e=>send(res,500,{ok:false,err:String(e&&e.message||e),results:[]}));
if(req.method==='GET'){
const hex=(u.searchParams.get('hex')||'').slice(0,16);
const tol=Math.max(1,Math.min(parseFloat(u.searchParams.get('tol'))||DELTA_E_10PCT,40));
const limit=Math.max(1,Math.min(parseInt(u.searchParams.get('k'),10)||MAX_RESULTS,MAX_RESULTS));
const style=(u.searchParams.get('style')||'').slice(0,64);
+ const type=(u.searchParams.get('type')||'').slice(0,16).toLowerCase();
+ const use=(u.searchParams.get('use')||'').slice(0,16).toLowerCase();
if(!hex) return send(res,400,{ok:false,err:'hex required',results:[]});
- return run(hex,tol,limit,style);
+ return run(hex,tol,limit,style,type,use);
}
if(req.method==='POST'){
let body=''; req.on('data',c=>{ body+=c; if(body.length>4096) req.destroy(); });
@@ -113,8 +131,10 @@ const server = http.createServer((req,res)=>{
const tol=Math.max(1,Math.min(parseFloat(p.tol)||DELTA_E_10PCT,40));
const limit=Math.max(1,Math.min(parseInt(p.k,10)||MAX_RESULTS,MAX_RESULTS));
const style=(typeof p.style==='string'?p.style:'').slice(0,64);
+ const type=(typeof p.type==='string'?p.type:'').slice(0,16).toLowerCase();
+ const use=(typeof p.use==='string'?p.use:'').slice(0,16).toLowerCase();
if(!hex) return send(res,400,{ok:false,err:'hex required',results:[]});
- run(hex,tol,limit,style);
+ run(hex,tol,limit,style,type,use);
});
return;
}
diff --git a/shopify/theme-LIVE-pull-20260728-colorbar/snippets/color-palette.liquid b/shopify/theme-LIVE-pull-20260728-colorbar/snippets/color-palette.liquid
index d6f2f1e0..8660eccf 100644
--- a/shopify/theme-LIVE-pull-20260728-colorbar/snippets/color-palette.liquid
+++ b/shopify/theme-LIVE-pull-20260728-colorbar/snippets/color-palette.liquid
@@ -91,6 +91,24 @@
<span class="dw-color-index__label" data-dw-ci-label aria-live="polite"></span>
<button type="button" class="dw-color-index__close" data-dw-ci-close aria-label="Close color index">×</button>
</div>
+ {%- comment -%} Coordinate filters (Steve TK-10085): narrow the color-matched
+ coordinates by product TYPE and USE. Single-select chips per group; the choice
+ re-queries the color-index endpoint and persists to localStorage. {%- endcomment -%}
+ <div class="dw-color-index__filters" data-dw-ci-filters>
+ <div class="dw-ci-fgroup" role="radiogroup" aria-label="Filter coordinates by product type">
+ <span class="dw-ci-flabel">Type</span>
+ <button type="button" class="dw-ci-chip is-active" data-dw-ci-type="" role="radio" aria-checked="true">All</button>
+ <button type="button" class="dw-ci-chip" data-dw-ci-type="wallcovering" role="radio" aria-checked="false">Wallcovering</button>
+ <button type="button" class="dw-ci-chip" data-dw-ci-type="fabric" role="radio" aria-checked="false">Fabric</button>
+ <button type="button" class="dw-ci-chip" data-dw-ci-type="other" role="radio" aria-checked="false">Other</button>
+ </div>
+ <div class="dw-ci-fgroup" role="radiogroup" aria-label="Filter coordinates by use">
+ <span class="dw-ci-flabel">Use</span>
+ <button type="button" class="dw-ci-chip is-active" data-dw-ci-use="" role="radio" aria-checked="true">All</button>
+ <button type="button" class="dw-ci-chip" data-dw-ci-use="commercial" role="radio" aria-checked="false">Commercial</button>
+ <button type="button" class="dw-ci-chip" data-dw-ci-use="residential" role="radio" aria-checked="false" title="All non-contract lines">Residential</button>
+ </div>
+ </div>
<div class="dw-color-index__grid" data-dw-ci-grid aria-live="polite"></div>
{%- comment -%} Steve 2026-07-28: reveal +2 more rows of coordinates per click (refresh). {%- endcomment -%}
<button type="button" class="dw-color-index__more" data-dw-ci-more hidden aria-label="Show two more rows of coordinating wallcoverings">Show 2 more rows</button>
@@ -145,6 +163,16 @@
.dw-color-index__close{flex:0 0 auto;background:none;border:1px solid #e0ded9;border-radius:50%;width:26px;height:26px;line-height:1;font-size:15px;color:#8a8577;cursor:pointer;padding:0;transition:color .12s ease,border-color .12s ease;}
.dw-color-index__close:hover{color:#3D4246;border-color:#b8b3a8;}
.dw-color-index__close:focus-visible{outline:2px solid #b8b3a8;outline-offset:2px;}
+ /* Coordinate filter chips (Steve TK-10085) — two single-select groups (Type / Use). */
+ .dw-color-index__filters{display:flex;flex-wrap:wrap;align-items:center;gap:8px 20px;margin:0 0 16px;}
+ .dw-ci-fgroup{display:flex;flex-wrap:wrap;align-items:center;gap:6px;}
+ .dw-ci-flabel{font-family:Lora,serif;font-size:11px;letter-spacing:.09em;text-transform:uppercase;color:#9a958a;margin-right:2px;}
+ .dw-ci-chip{background:#fff;border:1px solid #d8d4cc;border-radius:999px;padding:5px 13px;font-family:Lora,serif;font-size:12px;letter-spacing:.02em;color:#5c5f63;cursor:pointer;transition:border-color .12s ease,color .12s ease,background .12s ease;}
+ .dw-ci-chip:hover{border-color:#b8b3a8;color:#3D4246;}
+ .dw-ci-chip:focus-visible{outline:2px solid #b8b3a8;outline-offset:2px;}
+ .dw-ci-chip.is-active{background:#3D4246;border-color:#3D4246;color:#fff;}
+ .dw-ci-usetag{font-size:12px;color:#9a958a;font-style:italic;margin-left:4px;}
+ @media(max-width:600px){.dw-color-index__filters{gap:8px 12px;}.dw-ci-flabel{width:100%;margin-bottom:-2px;}}
.dw-color-index__grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));grid-template-rows:repeat(2,auto);grid-auto-rows:0;gap:16px;overflow:hidden;}
@media(min-width:1000px){.dw-color-index__grid{grid-template-columns:repeat(8,1fr);}}
/* "Show 2 more rows" — reveals the next 2 rows of coordinates each click (Steve 2026-07-28) */
@@ -272,15 +300,35 @@
var CI = root.querySelector('[data-dw-color-index]');
var CI_ENDPOINT = (CI && CI.dataset.endpoint) || 'https://photo.designerwallcoverings.com/apps/color-index';
var CI_K = 48; // fetch up to ~6 rows; show 2, reveal +2 per "Show 2 more rows" click (Steve 2026-07-28)
- var _ciCache = {}; // hex -> Promise<{results,total,tolerance_pct}>
+ var _ciCache = {}; // key(hex|type|use) -> Promise<{results,total,tolerance_pct}>
+ /* Coordinate filters (Steve TK-10085): TYPE (''|wallcovering|fabric|other) and
+ USE (''|commercial|residential); '' = All (the default). Both are sent to the
+ color-index endpoint (SERVER-side filter, so the 48-row fetch always comes back
+ already matching — client-side filtering would empty the visible rows) and
+ persist to localStorage so a shopper's choice survives reloads. */
+ var _ciType = '', _ciUse = '', _ciHex = '', _ciName = '';
+ try{ _ciType = localStorage.getItem('dw_ci_type') || ''; _ciUse = localStorage.getItem('dw_ci_use') || ''; }catch(e){}
+ if(['','wallcovering','fabric','other'].indexOf(_ciType)<0) _ciType='';
+ if(['','commercial','residential'].indexOf(_ciUse)<0) _ciUse='';
function fetchColorIndex(hex){
- if(_ciCache[hex]) return _ciCache[hex];
- _ciCache[hex]=fetch(CI_ENDPOINT,{
+ var key = hex+'|'+_ciType+'|'+_ciUse;
+ if(_ciCache[key]) return _ciCache[key];
+ var payload = { hex:hex, k:CI_K };
+ if(_ciType) payload.type=_ciType;
+ if(_ciUse) payload.use=_ciUse;
+ _ciCache[key]=fetch(CI_ENDPOINT,{
method:'POST', mode:'cors', headers:{'Content-Type':'application/json'},
- body:JSON.stringify({hex:hex, k:CI_K})
+ body:JSON.stringify(payload)
}).then(function(r){ return r.ok?r.json():{results:[]}; })
.catch(function(){ return {results:[], _err:true}; });
- return _ciCache[hex];
+ return _ciCache[key];
+ }
+ /* Drawer heading noun follows the TYPE filter; USE appends a small italic tag. */
+ function ciNoun(){ return _ciType==='wallcovering' ? 'Wallcoverings' : _ciType==='fabric' ? 'Fabrics' : 'Coordinates'; }
+ function ciLabelHTML(name){
+ var fam = familyFromHex(_ciHex)||'';
+ var use = _ciUse==='commercial' ? ' · Commercial' : _ciUse==='residential' ? ' · Residential' : '';
+ return ciNoun()+' in this color — <b>'+(name||fam||_ciHex)+'</b>'+(use?'<span class="dw-ci-usetag">'+use+'</span>':'');
}
function ciEl(sel){ return CI ? CI.querySelector(sel) : null; }
function closeIndex(){ if(CI){ CI.classList.remove('is-open'); CI.hidden=true; } }
@@ -322,14 +370,37 @@
if(_cx) _cx.addEventListener('click', closeIndex);
// Esc closes the drawer when it (or something inside it) is focused (a11y).
CI.addEventListener('keydown', function(ev){ if(ev.key==='Escape'){ closeIndex(); } });
+ /* Coordinate filter chips (Steve TK-10085): single-select per group. Changing a
+ filter persists the choice and, if the drawer is open, re-queries for the color
+ currently shown (fetchColorIndex is cache-keyed on hex|type|use, so switching
+ back is instant). Chips reflect the persisted choice on load. */
+ function ciSyncChips(){
+ var t=CI.querySelectorAll('[data-dw-ci-type]'), u=CI.querySelectorAll('[data-dw-ci-use]'), i, on;
+ for(i=0;i<t.length;i++){ on=(t[i].getAttribute('data-dw-ci-type')||'')===_ciType; t[i].classList.toggle('is-active',on); t[i].setAttribute('aria-checked',on?'true':'false'); }
+ for(i=0;i<u.length;i++){ on=(u[i].getAttribute('data-dw-ci-use')||'')===_ciUse; u[i].classList.toggle('is-active',on); u[i].setAttribute('aria-checked',on?'true':'false'); }
+ }
+ function ciOnFilter(kind,val){
+ if(kind==='type'){ if(val===_ciType) return; _ciType=val; try{localStorage.setItem('dw_ci_type',val);}catch(e){} }
+ else { if(val===_ciUse) return; _ciUse=val; try{localStorage.setItem('dw_ci_use',val);}catch(e){} }
+ ciSyncChips();
+ if(_ciHex) openIndex(_ciHex,_ciName); // re-query the open color under the new filters
+ }
+ var _chips=CI.querySelectorAll('.dw-ci-chip');
+ for(var _c=0;_c<_chips.length;_c++){ (function(chip){
+ chip.addEventListener('click', function(){
+ if(chip.hasAttribute('data-dw-ci-type')) ciOnFilter('type', chip.getAttribute('data-dw-ci-type')||'');
+ else ciOnFilter('use', chip.getAttribute('data-dw-ci-use')||'');
+ });
+ })(_chips[_c]); }
+ ciSyncChips(); // reflect any persisted choice on load
}
/* Open the in-page index for a clicked hex. */
function openIndex(hex, name){
if(!CI) return;
+ _ciHex=hex; _ciName=(name||'');
var sw=ciEl('[data-dw-ci-swatch]'), lbl=ciEl('[data-dw-ci-label]'), grid=ciEl('[data-dw-ci-grid]');
- var fam=familyFromHex(hex)||'';
if(sw) sw.style.background=hex;
- if(lbl) lbl.innerHTML='Wallcoverings in this color — <b>'+(name||fam||hex)+'</b>';
+ if(lbl) lbl.innerHTML=ciLabelHTML(_ciName);
if(grid) grid.innerHTML='<div class="dw-color-index__loading">Finding matches…</div>';
CI.hidden=false;
// retrigger the soft reveal even on a re-click (refine 2026-07-09)
← 03599d65 auto-save: 2026-07-31T08:55:43 (3 files) — DW-Agents/.gitign
·
back to Designer Wallcoverings
·
TK-10046: build-residual-plan --inputs-dir fallback to durab 1fa0b3ac →