[object Object]

← back to Designer Wallcoverings

ship: color-dot 10% CIELAB tolerance grid — STEP 1 (dwphoto /apps/color-index live) + STEP 2 (theme snippet live #144076931123); memo → DONE

0da7a73d260aaede5c5449cb83a0eadc0e51eafa · 2026-07-09 09:23:46 -0700 · Steve

Files touched

Diff

commit 0da7a73d260aaede5c5449cb83a0eadc0e51eafa
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 9 09:23:46 2026 -0700

    ship: color-dot 10% CIELAB tolerance grid — STEP 1 (dwphoto /apps/color-index live) + STEP 2 (theme snippet live #144076931123); memo → DONE
---
 .../MEMO.md                                        | 159 +++++++++++++++++++++
 .../push-to-live.sh                                |  55 +++++++
 2 files changed, 214 insertions(+)

diff --git a/pending-approval/DONE-color-dot-index-10pct-tolerance-2026-07-09/MEMO.md b/pending-approval/DONE-color-dot-index-10pct-tolerance-2026-07-09/MEMO.md
new file mode 100644
index 00000000..4be7efb4
--- /dev/null
+++ b/pending-approval/DONE-color-dot-index-10pct-tolerance-2026-07-09/MEMO.md
@@ -0,0 +1,159 @@
+# ✅ SHIPPED 2026-07-09 (Steve authorized "ship it")
+
+**STEP 1 (dwphoto endpoint) — LIVE.** Deployed 602bb7f (server.js + scripts/build-color-index.cjs)
+to Kamatera /root/public-projects/dwphoto (rsync; not a git repo). Regenerated data/color-index.json
+(60,960 products) locally on mac3 via NODE_PATH→pg, rsynced up (16,890,088 bytes, byte-identical).
+`pm2 restart dwphoto` + `pm2 save`. Remote server.js backed up →
+/root/public-projects/dwphoto/server.js.PRE-colorindex.20260709-161836.bak.
+SMOKE: POST /apps/color-index #3C5B49 k=8 → ok:true, total_in_tolerance=97, max ΔE among returned=5 (≤10),
+all roll handles/no Sample. Raw :9912/:9914 NOT publicly reachable (000). Guards: badhex→400, GET→405, OPTIONS→204+CORS, homepage still 401.
+
+**STEP 2 (theme snippet) — LIVE.** push-to-live.sh backed up live snippet →
+shopify/theme-backups/color-palette.liquid.LIVE-144076931123.20260709-091959.bak (27,198 bytes),
+PUT 200 (29,416 bytes) to live theme #144076931123. Live PDP headless click (Umbrella Leaves / Boråstapeter):
+clicked base swatch #424a40 → live /apps/color-index 200, total_in_tolerance=537, 36-card grid opened in-page,
+all roll /products/<handle>, multi-vendor. NON-REGRESSION: /apps/similar CLIP strip 200 + "More Wallcoverings
+Like This" + family-collection deep-link href all intact, coexist with the new grid. $0 (local infra).
+
+**Revert:** theme → PUT theme-backups/color-palette.liquid.LIVE-144076931123.20260709-091959.bak back to
+snippets/color-palette.liquid on 144076931123. endpoint → restore server.js.PRE-colorindex.20260709-161836.bak
+on Kamatera + pm2 restart dwphoto.
+
+---
+
+# PENDING APPROVAL — Color-dot click → in-page INDEX of many products within 10% tolerance
+
+**Date:** 2026-07-09
+**Owner:** vp-dw-commerce
+**Status:** committed locally, verified, STAGED — NOT pushed. Two Steve-gated steps.
+**Supersedes behavior of:** `DONE-color-dot-specific-product-deeplink-2026-07-09/`
+(the single-product deeplink, commit 6ba5016). This replaces "click → ONE product"
+with "click → GRID of MANY products in that color."
+
+## Steve's feedback (verbatim)
+> "bring up an index of many images, not just that exact hex. tolerance 10% for color to pull more"
+
+## What it does (before → after)
+- **BEFORE (6ba5016, live):** clicking a color dot deep-linked to the ONE nearest
+  specific product+variant of that hue and navigated away.
+- **AFTER (staged):** clicking a color dot opens an **in-page expanding grid**
+  right below the palette — an index of MANY products (up to 36) whose dominant
+  color is within a perceptual **10% tolerance (CIELAB ΔE76)** of the clicked hex,
+  drawn **CATALOG-WIDE** (not one collection). Header shows the clicked color +
+  a close (×); cards are image + pattern + vendor, each linking to
+  `/products/<handle>` (the roll variant — **never** the $4.25 Sample). Graceful
+  empty/error states. No navigation away (matches the CLIP-strip pattern).
+- **Unchanged / intact:** the swatch hex fill, the interpolated spectrum, the
+  legend + proportion bar, and the separate CLIP **"More Wallcoverings Like This"**
+  strip. Family-collection href kept as the plain `href` so middle-click / new-tab
+  / crawlers still get a valid destination.
+
+## Data source (proof) + why
+**Catalog-wide color-index endpoint** (the source the task explicitly prefers),
+NOT the narrow one-collection products.json client sample.
+
+- New endpoint `POST /apps/color-index` on the dwphoto app (:9912), alongside the
+  existing `/apps/similar`. Same public-but-tightly-bounded posture (CORS
+  allowlist to the storefront origins, input allowlist `{hex,k}` only, output
+  allowlist of public catalog fields).
+- Backed by a static `data/color-index.json` — **one dominant color per ACTIVE
+  product with a PRECOMPUTED CIELAB LAB** — built from `dw_unified.product_colors`
+  (which already stores `lab_l/lab_a/lab_b`) by
+  `dw-photo-capture/scripts/build-color-index.cjs`. **60,960 products** in the
+  index. No DB at request time; the endpoint loads the index once and hot-reloads
+  on mtime change. `$0 (local)`.
+- Why static-JSON not live-PG: the dwphoto prod app carries no `pg` driver and is
+  intentionally dependency-light; the static index keeps it that way while still
+  being genuinely catalog-wide. The generator runs where PG lives (or with
+  `NODE_PATH` pointed at a node_modules that has `pg`).
+
+## The 10% perceptual metric (named tunable constant)
+- ONE knob: `COLOR_INDEX_TOLERANCE_PCT = 0.10` (in `server.js`).
+- Mapped to a **ΔE76 ceiling** = `TOLERANCE_PCT × 100 = 10`, where 100 is the
+  practical perceptual ΔE range for two clearly-different colors. So "10%" = a
+  ΔE76 ceiling of 10 — tight enough to stay same-family, loose enough to pull
+  MANY products. Retune by changing the one constant; the ceiling derives from it.
+- Perceptual (CIELAB), not raw hex/RGB math, as required. Product LAB is
+  precomputed in `product_colors`; the clicked hex is converted sRGB→LAB in JS
+  (`hexToLab`, matching the upstream computation).
+
+## Local verification (all $0 local)
+- `node --check` on `server.js`, `build-color-index.cjs`, and BOTH extracted
+  `<script>` blocks of the snippet → all OK.
+- `shopify theme check` on `color-palette.liquid` → **5 warnings, all pre-existing**
+  (OrphanedSnippet, 3× VariableName on the CLIP block's liquid vars, DeprecatedFilter
+  `img_url` on the untouched featured_image line). **0 new offenses, 0 errors** —
+  same baseline as the prior deeplink task.
+- Endpoint smoke: forest `#3C5B49`→97 in tol, blush `#DE5D83`→80, gold `#D4AF37`→41,
+  navy `#1B2A4A`→235; bad-input/405/CORS-preflight all correct.
+- **Headless click proof (puppeteer, real snippet markup+JS over http):** clicked
+  the forest-green base dot on a rendered palette → the in-page grid opened with
+  **36 product cards**, ALL linking to `/products/<handle>` (roll, no Sample),
+  spanning 7+ vendors (Phillipe Romano, Boråstapeter, Sandberg, Hollywood, Kravet,
+  Coordonné, Rebel Walls, Lee Jofa Modern). Zero console errors in the flow.
+
+### Tolerance proof (clicked hex → resolved product hexes + ΔE + count)
+Clicked **#3C5B49** → `total_in_tolerance=97` (ΔE≤10), grid shows nearest-first:
+
+| product hex | ΔE from clicked | vendor | title |
+|---|---|---|---|
+| #3c5b49 | 0.0 | Phillipe Romano | Crushed Coral - Forest |
+| #3f5f4d | 1.6 | Boråstapeter | Umbrella Leaves |
+| #375f50 | 3.6 | Sandberg | Igor jade |
+| #445d4f | 3.8 | Hollywood | Ashbourne Type II Vinyl |
+| #436354 | 3.9 | Sandberg | Raphael green |
+| #3f594d | 4.3 | Kravet Design | W3863-30 Sage |
+| #3a5447 | 4.4 | Coordonné | Cherry Blossom Emerald |
+| #35543c | 5.0 | Rebel Walls | Opulence, Jade |
+| … | … | … | (max ΔE among 36 returned = 6.3, all ≤ ceiling 10) |
+
+## Commit hashes
+- dwphoto (endpoint + generator): `602bb7f` in `~/Projects/dw-photo-capture`
+  (`server.js`, `scripts/build-color-index.cjs`, `.gitignore`). The 16MB generated
+  `data/color-index.json` is gitignored — rebuild it, don't commit it.
+- shopify theme (snippet): `7b0ae3d` in `~/Projects/Designer-Wallcoverings/shopify`
+  (`theme-LIVE-591-freshpull-20260709/snippets/color-palette.liquid` +
+  `theme-LIVE-591/snippets/color-palette.liquid`).
+
+## TWO Steve-gated ship steps
+
+### STEP 1 (required first) — deploy the color-index endpoint publicly
+The theme grid fetches `https://photo.designerwallcoverings.com/apps/color-index`.
+That endpoint is built + verified LOCAL only. To go live it must (a) exist on the
+public dwphoto host and (b) have a freshly generated index there:
+
+```bash
+# on the box that serves photo.designerwallcoverings.com (dwphoto :9912):
+#   1. pull the new server.js + scripts/build-color-index.cjs (commit 602bb7f)
+#   2. generate the index where PG (dw_unified) is reachable:
+cd <dwphoto-dir> && node scripts/build-color-index.cjs      # writes data/color-index.json
+#   3. restart dwphoto so the new /apps/color-index route + index load:
+pm2 restart dwphoto   # (or the process manager in use)
+#   4. smoke test:
+curl -s -X POST https://photo.designerwallcoverings.com/apps/color-index \
+  -H 'Content-Type: application/json' -d '{"hex":"#3C5B49","k":6}' | head
+```
+Also schedule `build-color-index.cjs` (e.g. nightly) so the index tracks catalog
+changes. Keep loopback-only services loopback; this endpoint is the only public
+surface added.
+
+### STEP 2 — push the theme snippet to the live theme
+Only ship AFTER Step 1 is live (otherwise the grid shows the graceful
+"could not load matches" empty state). Equivalent to the prior deeplink push:
+
+```bash
+cd ~/Projects/Designer-Wallcoverings/shopify/theme-LIVE-591-freshpull-20260709
+SHOPIFY_CLI_THEME_TOKEN=$SHOPIFY_THEME_TOKEN \
+  shopify theme push --allow-live --theme 144076931123 \
+  --store designer-laboratory-sandbox.myshopify.com \
+  --only snippets/color-palette.liquid
+```
+Or run the ready script (backs up the current live snippet to
+`shopify/theme-backups/` first, then PUTs via Asset API):
+```bash
+bash ~/Projects/Designer-Wallcoverings/pending-approval/color-dot-index-10pct-tolerance-2026-07-09/push-to-live.sh
+```
+
+**Revert:** PUT the `theme-backups/color-palette.liquid.LIVE-144076931123.<ts>.bak`
+back to `snippets/color-palette.liquid` on theme 144076931123 (restores the
+current live deeplink behavior).
diff --git a/pending-approval/DONE-color-dot-index-10pct-tolerance-2026-07-09/push-to-live.sh b/pending-approval/DONE-color-dot-index-10pct-tolerance-2026-07-09/push-to-live.sh
new file mode 100755
index 00000000..2fddc4c8
--- /dev/null
+++ b/pending-approval/DONE-color-dot-index-10pct-tolerance-2026-07-09/push-to-live.sh
@@ -0,0 +1,55 @@
+#!/usr/bin/env bash
+# STEVE-GATED — STEP 2 of 2. Push the color-palette "index of many" fix to LIVE.
+# Ships snippets/color-palette.liquid ONLY. Backs up the current live version first.
+#
+# ⚠️ RUN STEP 1 FIRST: the /apps/color-index endpoint must be LIVE on
+# photo.designerwallcoverings.com (deploy dwphoto commit 602bb7f + generate the
+# index + restart) or the in-page grid shows the graceful empty state. See MEMO.md.
+#
+# Change: a color-dot click now opens an IN-PAGE grid of MANY products whose
+# dominant color is within 10% (CIELAB deltaE76) of the clicked hex, catalog-wide,
+# instead of deep-linking to ONE product. (Steve 2026-07-09b)
+set -euo pipefail
+
+STORE="designer-laboratory-sandbox.myshopify.com"
+API="2024-10"
+TID="144076931123"   # live theme 5.9.1 [Dev] hero-appt-CTA
+SRC="$HOME/Projects/Designer-Wallcoverings/shopify/theme-LIVE-591-freshpull-20260709/snippets/color-palette.liquid"
+SECRETS="$HOME/Projects/secrets-manager/.env"
+
+# quick pre-flight: warn (don't block) if the public endpoint isn't answering yet
+if ! curl -s -m 8 -X POST "https://photo.designerwallcoverings.com/apps/color-index" \
+     -H 'Content-Type: application/json' -d '{"hex":"#3C5B49","k":1}' | grep -q '"ok"'; then
+  echo "⚠️  WARNING: /apps/color-index is not returning ok yet — run STEP 1 (deploy dwphoto) first,"
+  echo "    or the grid will show the graceful empty state. Continuing in 5s (Ctrl-C to abort)…"
+  sleep 5
+fi
+
+TOK="$(grep -hE '^SHOPIFY_THEME_TOKEN=' "$SECRETS" | head -1 | cut -d= -f2- | tr -d '"'"'"'')"
+[ -z "$TOK" ] && { echo "no theme token"; exit 1; }
+[ -f "$SRC" ] || { echo "source snippet missing: $SRC"; exit 1; }
+echo "token …${TOK: -4} | live theme $TID"
+
+BK="$HOME/Projects/Designer-Wallcoverings/shopify/theme-backups/color-palette.liquid.LIVE-$TID.$(date +%Y%m%d-%H%M%S).bak"
+mkdir -p "$(dirname "$BK")"
+curl -s "https://$STORE/admin/api/$API/themes/$TID/assets.json?asset%5Bkey%5D=snippets/color-palette.liquid" \
+  -H "X-Shopify-Access-Token: $TOK" \
+  | python3 -c "import sys,json;open('$BK','w').write(json.load(sys.stdin)['asset']['value'])"
+echo "backup: $BK ($(wc -c < "$BK") bytes)"
+
+# PUT the new snippet
+python3 - "$STORE" "$API" "$TID" "$TOK" "$SRC" <<'PY'
+import sys, json, urllib.request
+store, api, tid, tok, src = sys.argv[1:6]
+val = open(src).read()
+body = json.dumps({"asset": {"key": "snippets/color-palette.liquid", "value": val}}).encode()
+r = urllib.request.Request(f"https://{store}/admin/api/{api}/themes/{tid}/assets.json",
+                           data=body, method="PUT",
+                           headers={"X-Shopify-Access-Token": tok, "Content-Type": "application/json"})
+resp = urllib.request.urlopen(r)
+print("PUT", resp.status, "OK", f"({len(val)} bytes)")
+PY
+
+echo "verify on any PDP with a color palette:"
+echo "  click a color dot → an in-page grid of many products in that color opens below the palette"
+echo "revert: PUT $BK back to snippets/color-palette.liquid on theme $TID"

← 3baf80b0 pending-approval: color-dot index-of-many 10% tolerance memo  ·  back to Designer Wallcoverings  ·  auto-save: 2026-07-09T09:39:56 (5 files) — pending-approval/ f4e6d315 →