← back to Designer Wallcoverings
LN colorway decollapse tool (image-hex → distinct names)
63611c9a40def3daa48592888f1948678ab5da3f · 2026-07-06 15:54:29 -0700 · Steve
Files touched
A scripts/decollapse_ln.js
Diff
commit 63611c9a40def3daa48592888f1948678ab5da3f
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Jul 6 15:54:29 2026 -0700
LN colorway decollapse tool (image-hex → distinct names)
---
scripts/decollapse_ln.js | 90 ++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 90 insertions(+)
diff --git a/scripts/decollapse_ln.js b/scripts/decollapse_ln.js
new file mode 100644
index 00000000..447b60dd
--- /dev/null
+++ b/scripts/decollapse_ln.js
@@ -0,0 +1,90 @@
+#!/usr/bin/env node
+/**
+ * Decollapse live LN colorway collisions (Malibu Laurel - Pacific Mist ×20, Sage Strand - Dune Mist ×2).
+ * Fetch each product's Shopify image → dominant hex (magick) → distinct colorway name.
+ * DRY-RUN by default: prints dw_sku, current title, hex, proposed color. --apply updates PG + Shopify title.
+ */
+require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
+const { Client } = require('pg');
+const { execSync } = require('child_process');
+const fs = require('fs');
+const SHOP = process.env.SHOPIFY_STORE_DOMAIN;
+const TOKEN = process.env.SHOPIFY_ADMIN_ACCESS_TOKEN || process.env.SHOPIFY_ADMIN_TOKEN;
+const VER = process.env.SHOPIFY_ADMIN_API_VERSION || '2024-01';
+const REST = `https://${SHOP}/admin/api/${VER}`;
+const DBURL = process.env.DATABASE_URL || 'postgresql://stevestudio2@/dw_unified?host=/tmp';
+const APPLY = process.argv.includes('--apply');
+const sleep = (ms) => new Promise(r => setTimeout(r, ms));
+const GROUPS = ['Malibu Laurel - Pacific Mist', 'Sage Strand - Dune Mist'];
+
+// small luxury lexicon: [name, r,g,b]
+const LEX = [
+ ['Onyx',20,20,22],['Charcoal',60,63,66],['Slate',80,90,100],['Pewter',128,128,124],['Dove Grey',160,162,160],
+ ['Silver',196,198,196],['Alabaster',232,230,222],['Ivory',238,232,208],['Oatmeal',214,208,180],['Linen',210,200,178],
+ ['Greige',198,192,180],['Sand',201,183,140],['Wheat',188,164,110],['Camel',183,150,110],['Taupe',140,128,110],
+ ['Mushroom',150,138,124],['Fog',200,206,206],['Mist',186,200,198],['Seafoam',176,206,196],['Celadon',150,178,160],
+ ['Sage',132,155,140],['Eucalyptus',120,150,140],['Juniper',70,95,80],['Pine',40,80,60],['Emerald',12,90,66],
+ ['Teal',25,110,120],['Lagoon',10,110,130],['Steel Blue',60,100,140],['Denim',55,95,130],['Indigo',30,45,80],
+ ['Navy',25,40,70],['Midnight',20,30,55],['Aegean',40,110,150],['Aqua',150,205,205],['Blush',235,205,192],
+ ['Terracotta',180,95,75],['Garnet',150,55,60],['Oxblood',110,45,45],['Cocoa',90,70,55],['Espresso',55,45,35],
+];
+const nameFor = (r, g, b) => { let best = LEX[0], bd = 1e9; for (const [n, R, G, B] of LEX) { const d = (r - R) ** 2 + (g - G) ** 2 + (b - B) ** 2; if (d < bd) { bd = d; best = [n]; } } return best[0]; };
+
+async function rest(method, path, body) {
+ for (let a = 0; a < 6; a++) {
+ const res = await fetch(`${REST}${path}`, { method, headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined });
+ if (res.status === 429) { await sleep(2000); continue; }
+ if (!res.ok) throw new Error(`${res.status}: ${(await res.text()).slice(0, 120)}`);
+ return res.json();
+ }
+ throw new Error('429');
+}
+
+(async () => {
+ const db = new Client({ connectionString: DBURL }); await db.connect();
+ const { rows } = await db.query(
+ `SELECT dw_sku, shopify_product_id, pattern_name, color_name
+ FROM phillipe_romano_catalog
+ WHERE created_at::date='2026-07-06' AND mfr_sku LIKE 'LN%' AND shopify_product_id IS NOT NULL
+ AND (pattern_name||' - '||color_name) = ANY($1) ORDER BY dw_sku`, [GROUPS]);
+ fs.mkdirSync('/tmp/ln-dc', { recursive: true });
+ const used = {}; // per-pattern name uniqueness
+ for (const r of rows) {
+ const prod = (await rest('GET', `/products/${r.shopify_product_id}.json?fields=images,title`)).product;
+ const src = prod.images?.[0]?.src;
+ let hex = '?', nm = 'Mist';
+ if (src) {
+ const f = `/tmp/ln-dc/${r.dw_sku}.jpg`;
+ try {
+ execSync(`curl -s -A Mozilla/5.0 "${src}" -o "${f}"`, { timeout: 30000 });
+ hex = execSync(`magick "${f}" -resize 1x1! -format '%[hex:p{0,0}]' info:`, { timeout: 15000 }).toString().trim();
+ const r2 = parseInt(hex.slice(0, 2), 16), g2 = parseInt(hex.slice(2, 4), 16), b2 = parseInt(hex.slice(4, 6), 16);
+ nm = nameFor(r2, g2, b2);
+ } catch {}
+ }
+ // uniqueness within pattern: append qualifier if taken
+ const key = r.pattern_name; used[key] ||= new Set();
+ let final = nm; let i = 2;
+ while (used[key].has(final)) { final = `${nm} ${i++}`; }
+ used[key].add(final);
+ r.hex = hex; r.newColor = final; r.newTitle = `${r.pattern_name} - ${final}`;
+ await sleep(300);
+ }
+
+ console.log(`${APPLY ? '' : '[DRY-RUN] '}Decollapse ${rows.length} live LN products`);
+ console.log('DW_SKU HEX OLD → NEW COLOR');
+ for (const r of rows) console.log(` ${r.dw_sku.padEnd(13)} #${r.hex.padEnd(7)} ${r.color_name} → ${r.newColor}`);
+
+ if (!APPLY) { console.log('\nNO WRITES. --apply to update dw_unified color_name + Shopify title.'); await db.end(); return; }
+ console.log('\nApplying…'); let ok = 0;
+ for (const r of rows) {
+ try {
+ await rest('PUT', `/products/${r.shopify_product_id}.json`, { product: { id: Number(r.shopify_product_id), title: `${r.newTitle} | Phillipe Romano` } });
+ await db.query(`UPDATE phillipe_romano_catalog SET color_name=$1, pl_updated_at=now() WHERE dw_sku=$2`, [r.newColor, r.dw_sku]);
+ ok++;
+ } catch (e) { console.log(` ✗ ${r.dw_sku}: ${e.message.slice(0, 100)}`); }
+ await sleep(350);
+ }
+ console.log(`Decollapsed ${ok}/${rows.length}`);
+ await db.end();
+})().catch(e => { console.error('FATAL', e); process.exit(1); });
← d1369ad4 WallQuest->PR: Lillian August 140 live (material titles) + P
·
back to Designer Wallcoverings
·
Create Phillipe Romano 'Abruzzo Natural Collection' (368 Wal cd7214e5 →