← 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
M 5x/clickthrough.mjsM deploy/block2-clip-flip.shM deploy/fix-osp-kamatera.shM package-lock.jsonM package.jsonM server.js
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 →