[object Object]

← back to Dw Photo Capture

add /apps/color-widen route: tunable ΔE band (default 15%) for the Shop-by-Color wheel at /pages/colors

e2c9e0167391d8439014c601a8ca7636f914b517 · 2026-07-16 11:03:10 -0700 · Steve

Files touched

Diff

commit e2c9e0167391d8439014c601a8ca7636f914b517
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 16 11:03:10 2026 -0700

    add /apps/color-widen route: tunable ΔE band (default 15%) for the Shop-by-Color wheel at /pages/colors
---
 server.js | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 58 insertions(+), 1 deletion(-)

diff --git a/server.js b/server.js
index 41e2236..def8fff 100644
--- a/server.js
+++ b/server.js
@@ -901,7 +901,7 @@ function pipeUpstreamImage(src, res, headers) {
 
 const appHandler = (req, res) => {
   // public (no-auth) paths: health + the home-screen-install assets iOS fetches without creds
-  const PUBLIC = ['/healthz', '/icon-180.png', '/icon-512.png', '/apple-touch-icon.png', '/manifest.webmanifest', '/apps/similar', '/apps/color-index'];
+  const PUBLIC = ['/healthz', '/icon-180.png', '/icon-512.png', '/apple-touch-icon.png', '/manifest.webmanifest', '/apps/similar', '/apps/color-index', '/apps/color-widen'];
   const _p = req.url.split('?')[0];
   if (!PUBLIC.includes(_p) && !checkAuth(req)) {
     res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="DW Photo Capture"' });
@@ -1028,6 +1028,63 @@ const appHandler = (req, res) => {
     return;
   }
 
+  // ── Shop-by-Color WHEEL "wider band" index (designerwallcoverings.com/pages/colors) ──
+  // Same in-memory color index + ΔE76 math as /apps/color-index, but with a wider,
+  // caller-tunable tolerance (default 15% ≈ ΔE 15) and a higher result cap, so the
+  // public color-wheel page can surface a BIG set of matches per hue (the PDP dots
+  // stay on the tighter 10%/k60 path — that route is untouched). Read-only, public,
+  // same CORS posture. Input allowlist: ONLY {hex, k, ceiling}.
+  if (u.pathname === '/apps/color-widen') {
+    const ALLOWED_ORIGINS = new Set([
+      'https://designerwallcoverings.com',
+      'https://www.designerwallcoverings.com',
+      'https://designer-laboratory-sandbox.myshopify.com'
+    ]);
+    const origin = req.headers.origin || '';
+    const corsOrigin = ALLOWED_ORIGINS.has(origin) ? origin : 'https://designerwallcoverings.com';
+    const CORS = {
+      'Access-Control-Allow-Origin': corsOrigin,
+      'Vary': 'Origin',
+      'Access-Control-Allow-Methods': 'POST, OPTIONS',
+      'Access-Control-Allow-Headers': 'Content-Type'
+    };
+    if (req.method === 'OPTIONS') { res.writeHead(204, CORS); return res.end(); }
+    if (req.method !== 'POST') return send(res, 405, { err: 'POST required' }, CORS);
+    let body = '';
+    req.on('data', c => { body += c; if (body.length > 4096) req.destroy(); });
+    req.on('end', () => {
+      let p; try { p = JSON.parse(body || '{}'); } catch (e) { return send(res, 400, { err: 'bad json' }, CORS); }
+      const rawHex = (typeof p.hex === 'string') ? p.hex.trim() : '';
+      const hex = /^#?[0-9a-fA-F]{6}$/.test(rawHex) ? ('#' + rawHex.replace('#', '').toLowerCase()) : '';
+      if (!hex) return send(res, 400, { err: 'hex required (#rrggbb)' }, CORS);
+      const k = Math.max(1, Math.min(parseInt(p.k, 10) || 240, 400));
+      const ceil = Math.max(5, Math.min(Number(p.ceiling) || 15, 25)); // % ≈ ΔE76 ceiling
+      try {
+        const idx = loadColorIndex();               // cached, loaded once
+        const target = hexToLab(hex);
+        const within = [];
+        for (let i = 0; i < idx.length; i++) {
+          const it = idx[i];
+          const dl = it.l - target.l, da = it.a - target.a, db = it.b - target.b;
+          const de = Math.sqrt(dl * dl + da * da + db * db);
+          if (de <= ceil) within.push({ it, de });
+        }
+        within.sort((x, y) => x.de - y.de);          // nearest first
+        const results = within.slice(0, k).map(w => ({
+          handle: w.it.h, title: w.it.t, vendor: w.it.v,
+          hex: w.it.x, image: w.it.i, delta_e: Math.round(w.de * 10) / 10
+        }));
+        return send(res, 200, {
+          ok: true, hex, tolerance_pct: ceil / 100,
+          delta_e_ceiling: ceil, total_in_tolerance: within.length, results
+        }, CORS);
+      } catch (e) {
+        return send(res, 503, { ok: false, err: 'color index unavailable', results: [] }, CORS);
+      }
+    });
+    return;
+  }
+
   // ── Remote-shutter pairing routes ──
   // SSE stream for the PHONE cam page. Registers the cam role; listens for `shoot`.
   if (u.pathname === '/cam/events') {

← 5d305fe color-index: exclude sample-only ($4.25) products — require  ·  back to Dw Photo Capture  ·  color-widen: add min param — guarantee >=N results via neare 0d6c355 →