[object Object]

← back to Dw Pairs Well

chore: lint (W3/W4 script fixes), refactor (shared paint helpers, test dedup), v0.1.1 (session close)

da590e8726b1ba91f3510960a6920ddf1b49a8d8 · 2026-07-13 09:40:58 -0700 · Steve Abrams

Files touched

Diff

commit da590e8726b1ba91f3510960a6920ddf1b49a8d8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jul 13 09:40:58 2026 -0700

    chore: lint (W3/W4 script fixes), refactor (shared paint helpers, test dedup), v0.1.1 (session close)
---
 5x/clickthrough.mjs        | 26 +++++++++---------
 deploy/block2-clip-flip.sh |  7 +++--
 deploy/fix-osp-kamatera.sh |  4 ++-
 package-lock.json          |  4 +--
 package.json               |  2 +-
 server.js                  | 68 ++++++++++++++++++++--------------------------
 6 files changed, 53 insertions(+), 58 deletions(-)

diff --git a/5x/clickthrough.mjs b/5x/clickthrough.mjs
index 66d80c3..b0166e4 100644
--- a/5x/clickthrough.mjs
+++ b/5x/clickthrough.mjs
@@ -45,6 +45,17 @@ async function step(name, fn) {
   }
 }
 
+// Expand the Design-Coordinate CTA panel (collapsed by default on source change)
+// then wait for the grid to fill with real product links/images.
+async function expandAndWaitForGrid() {
+  const cta = page.locator('#pairs-well-with .pairs-well__cta');
+  if ((await cta.getAttribute('aria-expanded')) !== 'true') await cta.click();
+  await page.waitForFunction(() => {
+    const g = document.getElementById('catalog-grid');
+    return g && !g.hidden && g.querySelectorAll('a[href*="/products/"], img').length > 0;
+  }, null, { timeout: 25000 });
+}
+
 console.log(`\npairs-well clickthrough → ${URL_}\n`);
 await page.goto(URL_, { waitUntil: 'domcontentloaded', timeout: 20000 });
 
@@ -54,13 +65,7 @@ await step('page load, no console errors', async () => {
 
 await step('sample SKU chip + Design-Coordinate CTA expand → grid populates', async () => {
   await page.locator('button[data-sku]').first().click();
-  // grid renders lazily behind the collapsed CTA — expand like a real shopper would
-  const cta = page.locator('#pairs-well-with .pairs-well__cta');
-  if ((await cta.getAttribute('aria-expanded')) !== 'true') await cta.click();
-  await page.waitForFunction(() => {
-    const g = document.getElementById('catalog-grid');
-    return g && !g.hidden && g.querySelectorAll('a[href*="/products/"], img').length > 0;
-  }, null, { timeout: 25000 });
+  await expandAndWaitForGrid();
 });
 
 await step('all rendered product links are /products/<handle>', async () => {
@@ -91,12 +96,7 @@ await step('second sample chip → grid swaps with real cards', async () => {
     return t && t.textContent.trim().length > 3 && t.textContent !== prev;
   }, prevTitle, { timeout: 25000 });
   // loadSource collapses the Design-Coordinate panel for the new source — re-expand
-  const cta = page.locator('#pairs-well-with .pairs-well__cta');
-  if ((await cta.getAttribute('aria-expanded')) !== 'true') await cta.click();
-  await page.waitForFunction(() => {
-    const g = document.getElementById('catalog-grid');
-    return g && !g.hidden && g.querySelectorAll('a[href*="/products/"], img').length > 0;
-  }, null, { timeout: 25000 });
+  await expandAndWaitForGrid();
 });
 
 await step('unknown SKU → graceful error state, no page exceptions', async () => {
diff --git a/deploy/block2-clip-flip.sh b/deploy/block2-clip-flip.sh
index 725b85c..c047088 100755
--- a/deploy/block2-clip-flip.sh
+++ b/deploy/block2-clip-flip.sh
@@ -43,9 +43,12 @@ for p in json.load(sys.stdin):
   export DW_UNIFIED_DB="$(grep ^DW_UNIFIED_DB= ../.env | cut -d= -f2-)"
   nohup nice -n 15 python3 embed_shopify_gap.py --workers 16 > gap-embed.log 2>&1 &
 
-  # flip CLIP defaults
+  # flip CLIP defaults — update-or-append so an existing =0 line cannot silently win
   cd /root/Projects/dw-pairs-well
-  grep -q PAIRS_CLIP_DEFAULT .env 2>/dev/null || printf "PAIRS_CLIP_DEFAULT=1\nSIMILAR_CLIP_DEFAULT=1\n" >> .env
+  for K in PAIRS_CLIP_DEFAULT SIMILAR_CLIP_DEFAULT; do
+    if grep -q "^${K}=" .env 2>/dev/null; then sed -i "s/^${K}=.*/${K}=1/" .env
+    else echo "${K}=1" >> .env; fi
+  done
   pm2 restart dw-pairs-well
   sleep 6
   curl -sf https://pairs.designerwallcoverings.com/healthz >/dev/null && echo "pairs-well healthy, CLIP defaults ON"
diff --git a/deploy/fix-osp-kamatera.sh b/deploy/fix-osp-kamatera.sh
index 6a7a701..8a3d718 100755
--- a/deploy/fix-osp-kamatera.sh
+++ b/deploy/fix-osp-kamatera.sh
@@ -20,8 +20,10 @@ AGE_H=$(( ( $(date +%s) - $(date -j -f '%Y-%m-%dT%H:%M:%SZ' "$EXPORT_TS" +%s 2>/
 ROWS=$(wc -l < osp-flags.tsv | tr -d ' ')
 [ "$ROWS" -gt 70000 ] || { echo "osp-flags.tsv has only $ROWS rows (<70k) — refusing"; exit 1; }
 # 3. Spot provenance: a published + an unpublished sample must match the LIVE store.
-SPOT_T=$(grep -m1 $'\tt$' osp-flags.tsv >/dev/null && psql -d dw_unified -h /tmp -tA -c "SELECT handle FROM shopify_products WHERE status='ACTIVE' AND online_store_published IS TRUE AND handle IS NOT NULL LIMIT 1")
+grep -qm1 $'\tt$' osp-flags.tsv || { echo "no published (t) rows in TSV — wrong export format?"; exit 1; }
+SPOT_T=$(psql -d dw_unified -h /tmp -tA -c "SELECT handle FROM shopify_products WHERE status='ACTIVE' AND online_store_published IS TRUE AND handle IS NOT NULL LIMIT 1")
 SPOT_F=$(psql -d dw_unified -h /tmp -tA -c "SELECT handle FROM shopify_products WHERE status='ACTIVE' AND online_store_published IS FALSE AND handle IS NOT NULL LIMIT 1")
+[ -n "$SPOT_T" ] && [ -n "$SPOT_F" ] || { echo "could not resolve spot-check handles from mirror"; exit 1; }
 CT=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 -L "https://designerwallcoverings.com/products/$SPOT_T")
 CF=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 -L "https://designerwallcoverings.com/products/$SPOT_F")
 [ "$CT" = 200 ] && [ "$CF" = 404 ] || { echo "provenance spot check FAILED (published→$CT expected 200, unpublished→$CF expected 404) — mirror flags don't match live store, DO NOT SYNC"; exit 1; }
diff --git a/package-lock.json b/package-lock.json
index cd10037..f12d88f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "dw-pairs-well",
-  "version": "0.1.0",
+  "version": "0.1.1",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "dw-pairs-well",
-      "version": "0.1.0",
+      "version": "0.1.1",
       "dependencies": {
         "dotenv": "^16.4.5",
         "express": "^4.19.2",
diff --git a/package.json b/package.json
index caf9354..b0af6cb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "dw-pairs-well",
-  "version": "0.1.0",
+  "version": "0.1.1",
   "description": "Returns 6 design-coordinating wallcoverings for any DW SKU — powers the Pairs Well With This Design button on Shopify product pages.",
   "main": "server.js",
   "scripts": {
diff --git a/server.js b/server.js
index 2a1128a..87193a7 100644
--- a/server.js
+++ b/server.js
@@ -757,6 +757,27 @@ function safeTitle(row) {
   return degenerate ? bestPatternLabel(row, '') : row.title;
 }
 
+// Paint-chip lookup helpers — shared by /api/pairs and /api/similar.
+// paintMap is the per-request result of nearestPaintsBatch; passed explicitly so both
+// routes can use a single definition without sharing mutable state.
+function paintsFor(hex, paintMap) {
+  const m = paintMap[String(hex || '').toLowerCase()] || null;
+  if (!m) return null;
+  // Return all 7 brands; UI picks which to render. Short keys for transport size.
+  return {
+    sw:   m['sherwin-williams'] || null,
+    de:   m['dunn-edwards']     || null,
+    bm:   m['benjamin-moore']   || null,
+    behr: m['behr']             || null,
+    ppg:  m['ppg']              || null,
+    fb:   m['farrow-ball']      || null,
+    ral:  m['ral']              || null
+  };
+}
+function paletteWithPaints(hexList, paintMap) {
+  return hexList.map(h => ({ hex: h, paints: paintsFor(h, paintMap) }));
+}
+
 // Resolve a single product to its group key (used when caller passes handle/dw_sku).
 async function resolveGroupKey({ dw_sku, handle, pattern_id }) {
   if (pattern_id) return pattern_id;
@@ -1078,16 +1099,6 @@ app.get('/api/similar', async (req, res) => {
     const allHexes = sourcePalette.slice();
     for (const p of top) for (const h of p._palette) allHexes.push(h);
     const paintMap = await nearestPaintsBatch(pool, allHexes);
-    function paintsFor(hex) {
-      const m = paintMap[String(hex || '').toLowerCase()] || null;
-      if (!m) return null;
-      return {
-        sw: m['sherwin-williams'] || null, de: m['dunn-edwards'] || null,
-        bm: m['benjamin-moore'] || null, behr: m['behr'] || null,
-        ppg: m['ppg'] || null, fb: m['farrow-ball'] || null, ral: m['ral'] || null
-      };
-    }
-    const paletteWithPaints = hexList => hexList.map(h => ({ hex: h, paints: paintsFor(h) }));
 
     // no-store: carries /products/ links — see link-guarantee cache note on /api/colorways.
     res.set('Cache-Control', 'no-store');
@@ -1096,12 +1107,12 @@ app.get('/api/similar', async (req, res) => {
       engine: usedEngine,
       source: {
         dw_sku: source.dw_sku, handle: source.handle, title: source.title,
-        vendor: source.vendor, image_url: source.image_url, palette: paletteWithPaints(sourcePalette)
+        vendor: source.vendor, image_url: source.image_url, palette: paletteWithPaints(sourcePalette, paintMap)
       },
       similar: top.map(p => ({
         dw_sku: p.dw_sku, handle: p.handle, title: safeTitle(p), vendor: p.vendor,
         image_url: p.image_url, url: `/products/${p.handle}`,
-        score: p._score, why: p._why, delta_e: p._dE, palette: paletteWithPaints(p._palette)
+        score: p._score, why: p._why, delta_e: p._dE, palette: paletteWithPaints(p._palette, paintMap)
       }))
     });
   } catch (e) {
@@ -1158,15 +1169,12 @@ app.get('/api/pairs', async (req, res) => {
     const requestedK = Math.max(1, Math.min(24, parseInt(req.query.limit, 10) || 24));
     const candidates = await loadCandidates(source, 1200);
 
-    // ?engine=clip blends a CLIP style-world sub-score into the tag+palette scorer
-    // (palette hard gate unchanged). clipPairwiseMap returns {} on any miss, which
-    // makes the blend a no-op — the response never depends on the service being up.
+    // Blend CLIP style-world sub-score when explicitly requested (?engine=clip) OR when
+    // PAIRS_CLIP_DEFAULT is on for all callers — but ?engine=tag always force-disables.
+    // clipPairwiseMap returns {} on any miss (service down / circuit open), so the blend
+    // degrades to tag+palette and the response never depends on the service being up.
     let usedEngine = 'tag';
     let clipMap = null;
-    // Blend CLIP when explicitly requested (?engine=clip) OR when PAIRS_CLIP_DEFAULT is on
-    // for all callers — but ?engine=tag always force-disables. clipPairwiseMap returns {}
-    // on any miss (service down / circuit open), so the blend degrades to tag+palette and
-    // the response never depends on the service being up.
     const wantClip = req.query.engine === 'clip'
       || (PAIRS_CLIP_DEFAULT && req.query.engine !== 'tag');
     if (wantClip) {
@@ -1182,24 +1190,6 @@ app.get('/api/pairs', async (req, res) => {
     for (const p of top) for (const h of p._palette) allHexes.push(h);
     const paintMap = await nearestPaintsBatch(pool, allHexes);
 
-    function paintsFor(hex) {
-      const m = paintMap[String(hex || '').toLowerCase()] || null;
-      if (!m) return null;
-      // Return all 7 brands; UI picks which to render. Short keys for transport size.
-      return {
-        sw:   m['sherwin-williams'] || null,
-        de:   m['dunn-edwards']     || null,
-        bm:   m['benjamin-moore']   || null,
-        behr: m['behr']             || null,
-        ppg:  m['ppg']              || null,
-        fb:   m['farrow-ball']      || null,
-        ral:  m['ral']              || null
-      };
-    }
-    function paletteWithPaints(hexList) {
-      return hexList.map(h => ({ hex: h, paints: paintsFor(h) }));
-    }
-
     // no-store: carries /products/ links — see link-guarantee cache note on /api/colorways.
     res.set('Cache-Control', 'no-store');
     res.json({
@@ -1211,7 +1201,7 @@ app.get('/api/pairs', async (req, res) => {
         title: source.title,
         vendor: source.vendor,
         image_url: source.image_url,
-        palette: paletteWithPaints(sourcePalette)
+        palette: paletteWithPaints(sourcePalette, paintMap)
       },
       pairs: top.map(p => ({
         dw_sku: p.dw_sku,
@@ -1223,7 +1213,7 @@ app.get('/api/pairs', async (req, res) => {
         score: p._score,
         why: p._why,
         delta_e: p._dE,
-        palette: paletteWithPaints(p._palette)
+        palette: paletteWithPaints(p._palette, paintMap)
       }))
     });
   } catch (e) {

← 6103fe5 auto-save: 2026-07-13T08:53:46 (2 files) — 5x/live-sweep-2.m  ·  back to Dw Pairs Well  ·  pipe-title-backfill: apply script for the 23 CLEAN rows (PG- e438fe4 →