← back to Designer Wallcoverings
Newwall width re-crawl: public-HTML spec-table scraper (DTD-A), validated on 25/6-vendor sample
63dcdeeea8690f580749b2a4c06036a2030ef191 · 2026-06-20 12:34:21 -0700 · Steve
Files touched
A shopify/scripts/cadence/data/newwall-width-recrawl/found-2026-06-20T19-33-13-642Z.validate.jsonlA shopify/scripts/cadence/data/newwall-width-recrawl/found-2026-06-20T19-33-58-365Z.validate.jsonlA shopify/scripts/cadence/data/newwall-width-recrawl/skipped-2026-06-20T19-33-13-642Z.validate.jsonlA shopify/scripts/cadence/data/newwall-width-recrawl/skipped-2026-06-20T19-33-58-365Z.validate.jsonlA shopify/scripts/cadence/recrawl-newwall-width.js
Diff
commit 63dcdeeea8690f580749b2a4c06036a2030ef191
Author: Steve <steve@designerwallcoverings.com>
Date: Sat Jun 20 12:34:21 2026 -0700
Newwall width re-crawl: public-HTML spec-table scraper (DTD-A), validated on 25/6-vendor sample
---
.../found-2026-06-20T19-33-13-642Z.validate.jsonl | 9 +
.../found-2026-06-20T19-33-58-365Z.validate.jsonl | 10 ++
...skipped-2026-06-20T19-33-13-642Z.validate.jsonl | 16 ++
...skipped-2026-06-20T19-33-58-365Z.validate.jsonl | 15 ++
shopify/scripts/cadence/recrawl-newwall-width.js | 199 +++++++++++++++++++++
5 files changed, 249 insertions(+)
diff --git a/shopify/scripts/cadence/data/newwall-width-recrawl/found-2026-06-20T19-33-13-642Z.validate.jsonl b/shopify/scripts/cadence/data/newwall-width-recrawl/found-2026-06-20T19-33-13-642Z.validate.jsonl
new file mode 100644
index 00000000..fbe6ddb8
--- /dev/null
+++ b/shopify/scripts/cadence/data/newwall-width-recrawl/found-2026-06-20T19-33-13-642Z.validate.jsonl
@@ -0,0 +1,9 @@
+{"dw_sku":"DWXW-1005230","handle":"a06-cb-01w","vendor":"17 Patterns","dim":"27.5” w x 11 yards","cov":"","fmt":"Roll","width_in":27.5,"canonical":"27.5 Inches"}
+{"dw_sku":"DWXW-1005233","handle":"a06-cb-04w","vendor":"17 Patterns","dim":"27.5” w x 11 yards","cov":"","fmt":"Roll","width_in":27.5,"canonical":"27.5 Inches"}
+{"dw_sku":"DWXW-1005231","handle":"cloudbusting-a06-cb-02w","vendor":"17 Patterns","dim":"27.5” w x 11 yards","cov":"","fmt":"Roll","width_in":27.5,"canonical":"27.5 Inches"}
+{"dw_sku":"DWXW-1005232","handle":"cloudbusting-a06-cb-03w","vendor":"17 Patterns","dim":"27.5” w x 11 yards","cov":"","fmt":"Roll","width_in":27.5,"canonical":"27.5 Inches"}
+{"dw_sku":"DWXW-1013126","handle":"beltane-hemp-cosmos","vendor":"House of Hackney","dim":"54","cov":"","fmt":"Yard","width_in":54,"canonical":"54 Inches"}
+{"dw_sku":"DWXW-1013125","handle":"beltane-hemp-euphorbia","vendor":"House of Hackney","dim":"54","cov":"","fmt":"Yard","width_in":54,"canonical":"54 Inches"}
+{"dw_sku":"DWXW-1008144","handle":"camelot-stripe-3","vendor":"House of Hackney","dim":"53.5” wide","cov":"","fmt":"Yard","width_in":53.5,"canonical":"53.5 Inches"}
+{"dw_sku":"DWXW-1013153","handle":"heavenly-hemp-amber","vendor":"House of Hackney","dim":"54","cov":"","fmt":"Yard","width_in":54,"canonical":"54 Inches"}
+{"dw_sku":"DWXW-1013152","handle":"heavenly-hemp-anemone","vendor":"House of Hackney","dim":"54","cov":"","fmt":"Yard","width_in":54,"canonical":"54 Inches"}
diff --git a/shopify/scripts/cadence/data/newwall-width-recrawl/found-2026-06-20T19-33-58-365Z.validate.jsonl b/shopify/scripts/cadence/data/newwall-width-recrawl/found-2026-06-20T19-33-58-365Z.validate.jsonl
new file mode 100644
index 00000000..1ec59f00
--- /dev/null
+++ b/shopify/scripts/cadence/data/newwall-width-recrawl/found-2026-06-20T19-33-58-365Z.validate.jsonl
@@ -0,0 +1,10 @@
+{"dw_sku":"DWXW-1005230","handle":"a06-cb-01w","vendor":"17 Patterns","dim":"27.5” w x 11 yards","cov":"","fmt":"Roll","width_in":27.5,"canonical":"27.5 Inches"}
+{"dw_sku":"DWXW-1005233","handle":"a06-cb-04w","vendor":"17 Patterns","dim":"27.5” w x 11 yards","cov":"","fmt":"Roll","width_in":27.5,"canonical":"27.5 Inches"}
+{"dw_sku":"DWXW-1005231","handle":"cloudbusting-a06-cb-02w","vendor":"17 Patterns","dim":"27.5” w x 11 yards","cov":"","fmt":"Roll","width_in":27.5,"canonical":"27.5 Inches"}
+{"dw_sku":"DWXW-1005232","handle":"cloudbusting-a06-cb-03w","vendor":"17 Patterns","dim":"27.5” w x 11 yards","cov":"","fmt":"Roll","width_in":27.5,"canonical":"27.5 Inches"}
+{"dw_sku":"DWXW-1013126","handle":"beltane-hemp-cosmos","vendor":"House of Hackney","dim":"54","cov":"","fmt":"Yard","width_in":54,"canonical":"54 Inches"}
+{"dw_sku":"DWXW-1013125","handle":"beltane-hemp-euphorbia","vendor":"House of Hackney","dim":"54","cov":"","fmt":"Yard","width_in":54,"canonical":"54 Inches"}
+{"dw_sku":"DWXW-1008144","handle":"camelot-stripe-3","vendor":"House of Hackney","dim":"53.5” wide","cov":"","fmt":"Yard","width_in":53.5,"canonical":"53.5 Inches"}
+{"dw_sku":"DWXW-1013153","handle":"heavenly-hemp-amber","vendor":"House of Hackney","dim":"54","cov":"","fmt":"Yard","width_in":54,"canonical":"54 Inches"}
+{"dw_sku":"DWXW-1013152","handle":"heavenly-hemp-anemone","vendor":"House of Hackney","dim":"54","cov":"","fmt":"Yard","width_in":54,"canonical":"54 Inches"}
+{"dw_sku":"DWXW-1012918","handle":"casa-di-tartan-15568","vendor":"Masi x Newwall","dim":"20.5\" wide x 11 yards","cov":"50 sq. ft.","fmt":"Roll","width_in":20.5,"canonical":"20.5 Inches"}
diff --git a/shopify/scripts/cadence/data/newwall-width-recrawl/skipped-2026-06-20T19-33-13-642Z.validate.jsonl b/shopify/scripts/cadence/data/newwall-width-recrawl/skipped-2026-06-20T19-33-13-642Z.validate.jsonl
new file mode 100644
index 00000000..2114ea9b
--- /dev/null
+++ b/shopify/scripts/cadence/data/newwall-width-recrawl/skipped-2026-06-20T19-33-13-642Z.validate.jsonl
@@ -0,0 +1,16 @@
+{"dw_sku":"DWXW-1005696","handle":"6500013-product","vendor":"Coordonné","reason":"mural_custom"}
+{"dw_sku":"DWXW-1005697","handle":"6500014-product","vendor":"Coordonné","reason":"mural_custom"}
+{"dw_sku":"DWXW-1005698","handle":"6500015-product","vendor":"Coordonné","reason":"mural_custom"}
+{"dw_sku":"DWXW-1005699","handle":"6500016-product","vendor":"Coordonné","reason":"mural_custom"}
+{"dw_sku":"DWXW-1005700","handle":"6500017-product","vendor":"Coordonné","reason":"mural_custom"}
+{"dw_sku":"DWXW-1012918","handle":"casa-di-tartan-15568","vendor":"Masi x Newwall","reason":"coverage_mismatch(dim=20.5\" wide x 11 yards,cov=50 sq. ft.)"}
+{"dw_sku":"DWXW-1011674","handle":"abstract-mex-blue","vendor":"Tres Tintas","reason":"mural_custom"}
+{"dw_sku":"DWXW-1011673","handle":"abstract-mex-coral","vendor":"Tres Tintas","reason":"mural_custom"}
+{"dw_sku":"DWXW-1011675","handle":"abstract-mex-orange","vendor":"Tres Tintas","reason":"mural_custom"}
+{"dw_sku":"DWXW-1011676","handle":"abstract-mex-purple","vendor":"Tres Tintas","reason":"mural_custom"}
+{"dw_sku":"DWXW-1007035","handle":"acassia","vendor":"Tres Tintas","reason":"unparsed:Line drawing required"}
+{"dw_sku":"DWXW-1007683","handle":"70s-wd702001","vendor":"Wall&decò","reason":"mural_custom"}
+{"dw_sku":"DWXW-1007684","handle":"70s-wd702002","vendor":"Wall&decò","reason":"mural_custom"}
+{"dw_sku":"DWXW-1012412","handle":"a-new-day-wdnd2501","vendor":"Wall&decò","reason":"mural_custom"}
+{"dw_sku":"DWXW-1012413","handle":"a-new-day-wdnd2502","vendor":"Wall&decò","reason":"mural_custom"}
+{"dw_sku":"DWXW-1009818","handle":"a-sign-in-the-water","vendor":"Wall&decò","reason":"mural_custom"}
diff --git a/shopify/scripts/cadence/data/newwall-width-recrawl/skipped-2026-06-20T19-33-58-365Z.validate.jsonl b/shopify/scripts/cadence/data/newwall-width-recrawl/skipped-2026-06-20T19-33-58-365Z.validate.jsonl
new file mode 100644
index 00000000..28706c1b
--- /dev/null
+++ b/shopify/scripts/cadence/data/newwall-width-recrawl/skipped-2026-06-20T19-33-58-365Z.validate.jsonl
@@ -0,0 +1,15 @@
+{"dw_sku":"DWXW-1005696","handle":"6500013-product","vendor":"Coordonné","reason":"mural_custom"}
+{"dw_sku":"DWXW-1005697","handle":"6500014-product","vendor":"Coordonné","reason":"mural_custom"}
+{"dw_sku":"DWXW-1005698","handle":"6500015-product","vendor":"Coordonné","reason":"mural_custom"}
+{"dw_sku":"DWXW-1005699","handle":"6500016-product","vendor":"Coordonné","reason":"mural_custom"}
+{"dw_sku":"DWXW-1005700","handle":"6500017-product","vendor":"Coordonné","reason":"mural_custom"}
+{"dw_sku":"DWXW-1011674","handle":"abstract-mex-blue","vendor":"Tres Tintas","reason":"mural_custom"}
+{"dw_sku":"DWXW-1011673","handle":"abstract-mex-coral","vendor":"Tres Tintas","reason":"http_503"}
+{"dw_sku":"DWXW-1011675","handle":"abstract-mex-orange","vendor":"Tres Tintas","reason":"http_503"}
+{"dw_sku":"DWXW-1011676","handle":"abstract-mex-purple","vendor":"Tres Tintas","reason":"mural_custom"}
+{"dw_sku":"DWXW-1007035","handle":"acassia","vendor":"Tres Tintas","reason":"unparsed:Line drawing required"}
+{"dw_sku":"DWXW-1007683","handle":"70s-wd702001","vendor":"Wall&decò","reason":"mural_custom"}
+{"dw_sku":"DWXW-1007684","handle":"70s-wd702002","vendor":"Wall&decò","reason":"mural_custom"}
+{"dw_sku":"DWXW-1012412","handle":"a-new-day-wdnd2501","vendor":"Wall&decò","reason":"mural_custom"}
+{"dw_sku":"DWXW-1012413","handle":"a-new-day-wdnd2502","vendor":"Wall&decò","reason":"mural_custom"}
+{"dw_sku":"DWXW-1009818","handle":"a-sign-in-the-water","vendor":"Wall&decò","reason":"mural_custom"}
diff --git a/shopify/scripts/cadence/recrawl-newwall-width.js b/shopify/scripts/cadence/recrawl-newwall-width.js
new file mode 100644
index 00000000..ec727d44
--- /dev/null
+++ b/shopify/scripts/cadence/recrawl-newwall-width.js
@@ -0,0 +1,199 @@
+#!/usr/bin/env node
+'use strict';
+/**
+ * recrawl-newwall-width.js — LOCAL reversible Newwall WIDTH re-crawl + backfill
+ * for the genuine crawl-gap cohort (vp-dw-commerce, 2026-06-20; DTD verdict A, 3/3).
+ *
+ * THE GAP: 1,922 net-new costed Newwall rows in Mac2-LOCAL dw_unified.newwall_catalog
+ * (6 confirmed lines: Tres Tintas, Coordonné, Wall&decò, House of Hackney, 17 Patterns,
+ * Masi x Newwall) have NO width anywhere → they fail validate-before-activate.js
+ * (width HARD-required) and stay DRAFT.
+ *
+ * WHERE WIDTH ACTUALLY LIVES (verified read-only 2026-06-20): NOT in Newwall's
+ * /products/{handle}.js feed (it carries no width, auth or not). It IS in the
+ * PUBLIC product-page server HTML spec table under label "Dimensions"
+ * (e.g. "Dimensions: 54" = 54 inches; cross-validated by "Wall coverage: 13.5 sq. ft."
+ * = 54" × 36" = 1 yard). No login, no reCAPTCHA, no headless browser, $0.
+ *
+ * RULES:
+ * - NEVER invent width. Made-to-Measure Mural variants render "Dimensions: Custom"
+ * (no fixed roll width) → those rows are LEFT UNTOUCHED (stay DRAFT, correctly).
+ * - Only accept a plausible inch value (10–250"). The "Dimensions" field on these
+ * Newwall lines is the roll/panel WIDTH in inches (54", 27", 36", 21" …).
+ * - Optional sanity check: if "Wall coverage" (sq ft) is present, width ≈
+ * coverage*144 / 36 (1 yard length) — used only to reject implausible parses,
+ * never to derive a width.
+ * - Writes width + width_inches to Mac2-LOCAL dw_unified ONLY (reversible).
+ * --dry-run by default; every pre-change value captured in the audit snapshot.
+ * - Politeness: ~1 req/sec, 1 retry on transient failure, realistic UA.
+ *
+ * Usage:
+ * node recrawl-newwall-width.js --validate # crawl 30 sample products, NO DB write, print parse
+ * node recrawl-newwall-width.js # DRY-RUN full crawl (no DB write), writes change manifest
+ * node recrawl-newwall-width.js --commit # full crawl + apply width UPDATEs (reversible)
+ * node recrawl-newwall-width.js --limit 100 # cap rows (testing)
+ */
+const fs = require('fs');
+const path = require('path');
+const { execFileSync } = require('child_process');
+
+const args = process.argv.slice(2);
+const COMMIT = args.includes('--commit');
+const VALIDATE = args.includes('--validate');
+const LIMIT = (() => { const i = args.indexOf('--limit'); return i >= 0 ? parseInt(args[i + 1], 10) : null; })();
+
+const VENDORS = "('Coordonné','17 Patterns','House of Hackney','Masi x Newwall','Tres Tintas','Wall&decò')";
+const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36';
+const MIN_IN = 10, MAX_IN = 250;
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+function psql(sql) {
+ return execFileSync('psql', ['-At', '-F', '\t', '-d', 'dw_unified', '-c', sql], { encoding: 'utf8', maxBuffer: 1 << 28 }).trim();
+}
+
+function plausible(n) { return Number.isFinite(n) && n >= MIN_IN && n <= MAX_IN; }
+
+// Parse the label:value spec table out of a Newwall product-page HTML string.
+function parseSpecs(html) {
+ const pairs = {};
+ for (const m of html.matchAll(/>([A-Z][A-Za-z \/]{2,28}):\s*<\/[^>]+>\s*(?:<[^>]+>\s*)*([^<]{1,60})</g)) {
+ const k = m[1].trim(), v = m[2].trim();
+ if (!(k in pairs)) pairs[k] = v; // first occurrence wins
+ }
+ return pairs;
+}
+
+// Pull a plausible inch WIDTH from the "Dimensions" spec value. Returns number or NaN.
+// Accepts: "54", "54\"", "54 in", "27 Inches", "20.5". Rejects "Custom", "", non-numeric.
+function widthFromDimensions(dim) {
+ if (!dim) return NaN;
+ const s = String(dim).trim();
+ if (/custom/i.test(s)) return NaN; // made-to-measure mural → no fixed width
+ const m = s.match(/(\d+(?:\.\d+)?)/); // leading number
+ if (!m) return NaN;
+ return parseFloat(m[1]);
+}
+
+// Coverage cross-check: "13.5 sq. ft." with 1-yard (36") length implies width(in) = cov*144/36.
+function coverageWidth(cov) {
+ if (!cov) return NaN;
+ const m = String(cov).match(/(\d+(?:\.\d+)?)/);
+ if (!m) return NaN;
+ const sqft = parseFloat(m[1]);
+ return Number.isFinite(sqft) ? (sqft * 144) / 36 : NaN;
+}
+
+function canonical(inches) {
+ const r = Math.round(inches * 100) / 100;
+ return `${r % 1 === 0 ? r : r} Inches`;
+}
+
+async function fetchHtml(handle) {
+ const url = `https://newwall.com/products/${encodeURIComponent(handle)}`;
+ for (let a = 0; a < 2; a++) {
+ try {
+ const r = await fetch(url, { headers: { 'User-Agent': UA, 'Accept': 'text/html' } });
+ if (r.status === 429) { await sleep(3000 * (a + 1)); continue; }
+ if (!r.ok) return { status: r.status, html: null };
+ return { status: r.status, html: await r.text() };
+ } catch (e) { if (a === 1) return { status: 0, html: null, err: e.message }; await sleep(1500); }
+ }
+ return { status: 0, html: null };
+}
+
+(async () => {
+ // 1. load the gap cohort
+ const where = `coalesce(shopify_product_id::text,'')='' AND price_trade>0
+ AND coalesce(width,'')='' AND coalesce(width_inches::text,'')=''
+ AND vendor_name IN ${VENDORS} AND coalesce(handle,'')<>''`;
+ let rows = psql(`SELECT dw_sku, handle, vendor_name FROM newwall_catalog WHERE ${where} ORDER BY vendor_name, handle;`)
+ .split('\n').filter(Boolean).map(l => { const [dw_sku, handle, vendor] = l.split('\t'); return { dw_sku, handle, vendor }; });
+
+ if (VALIDATE) {
+ // 30 across all 6 vendors: 5 each (or as many as exist)
+ const byV = {}; for (const r of rows) (byV[r.vendor] ||= []).push(r);
+ rows = Object.values(byV).flatMap(list => list.slice(0, 5));
+ console.log(`VALIDATE mode: ${rows.length} sample products across ${Object.keys(byV).length} vendors (NO DB write)\n`);
+ } else if (LIMIT) rows = rows.slice(0, LIMIT);
+
+ console.log(`crawling ${rows.length} Newwall product pages (polite ~1/sec)…\n`);
+
+ const found = []; // {dw_sku, vendor, handle, dim, cov, width_in, canonical}
+ const noWidth = []; // {dw_sku, vendor, handle, reason}
+ let httpErr = 0, i = 0;
+
+ for (const r of rows) {
+ i++;
+ const { status, html } = await fetchHtml(r.handle);
+ if (!html) { httpErr++; noWidth.push({ ...r, reason: `http_${status}` }); if (!VALIDATE) await sleep(1000); continue; }
+ const specs = parseSpecs(html);
+ const dim = specs['Dimensions'] || '';
+ const cov = specs['Wall coverage'] || '';
+ const fmt = specs['Format available'] || '';
+ let w = widthFromDimensions(dim);
+ // coverage sanity: ONLY valid for Format="Yard" (length = 1 yard = 36").
+ // Roll-format products are multi-yard (e.g. "x 11 yards"), so the 36" assumption
+ // is wrong for them — never reject a Roll width on coverage (would drop valid widths
+ // like Masi "20.5" wide x 11 yards"). Reject only a gross (>2x) Yard-format mismatch.
+ const cw = coverageWidth(cov);
+ let rejected = false;
+ if (/^yard$/i.test(fmt) && plausible(w) && plausible(cw) && (w / cw > 2 || cw / w > 2)) { rejected = true; }
+ if (plausible(w) && !rejected) {
+ found.push({ ...r, dim, cov, fmt, width_in: w, canonical: canonical(w) });
+ } else {
+ noWidth.push({ ...r, reason: rejected ? `coverage_mismatch(dim=${dim},cov=${cov})` : (/custom/i.test(dim) ? 'mural_custom' : (dim ? `unparsed:${dim}` : 'no_dimensions_field')) });
+ }
+ if (VALIDATE) {
+ const tag = plausible(w) && !rejected ? `WIDTH=${canonical(w)}` : `SKIP(${noWidth[noWidth.length-1].reason})`;
+ console.log(` [${r.vendor}] ${r.handle.padEnd(34)} Dimensions="${dim}" Format="${fmt}" Coverage="${cov}" → ${tag}`);
+ } else if (i % 50 === 0) {
+ process.stdout.write(`\r ${i}/${rows.length} found=${found.length} no-width=${noWidth.length} httpErr=${httpErr}`);
+ }
+ await sleep(VALIDATE ? 250 : 1000); // politeness
+ }
+ console.log(`\n\n=== Newwall width re-crawl ${VALIDATE ? '(VALIDATE)' : COMMIT ? '(COMMIT)' : '(DRY-RUN)'} ===`);
+ console.log(` crawled : ${rows.length}`);
+ console.log(` width found : ${found.length}`);
+ console.log(` no width (skip) : ${noWidth.length} (mural Custom / no spec / http err — left DRAFT, never invented)`);
+ console.log(` http errors : ${httpErr}`);
+ // breakdown of skip reasons
+ const reasons = {}; for (const n of noWidth) { const k = n.reason.split('(')[0].split(':')[0]; reasons[k] = (reasons[k]||0)+1; }
+ console.log(` skip reasons : ${JSON.stringify(reasons)}`);
+
+ // write audit snapshot + change manifest
+ const auditDir = path.join(__dirname, 'data', 'newwall-width-recrawl');
+ fs.mkdirSync(auditDir, { recursive: true });
+ const TS = new Date().toISOString().replace(/[:.]/g, '-');
+ const suffix = VALIDATE ? 'validate' : COMMIT ? 'applied' : 'dryrun';
+ fs.writeFileSync(path.join(auditDir, `found-${TS}.${suffix}.jsonl`), found.map(r => JSON.stringify(r)).join('\n') + (found.length ? '\n' : ''));
+ fs.writeFileSync(path.join(auditDir, `skipped-${TS}.${suffix}.jsonl`), noWidth.map(r => JSON.stringify(r)).join('\n') + (noWidth.length ? '\n' : ''));
+ console.log(` manifests → ${auditDir}/{found,skipped}-${TS}.${suffix}.jsonl`);
+
+ if (VALIDATE || !COMMIT) {
+ if (!VALIDATE) console.log('\nDRY-RUN — re-run with --commit to write width + width_inches to Mac2-local newwall_catalog.');
+ process.exit(0);
+ }
+
+ // COMMIT: snapshot prior values for the touched dw_skus, then batched UPDATE (reversible)
+ console.log('\napplying width + width_inches to Mac2-local newwall_catalog…');
+ const ids = found.map(r => `'${r.dw_sku.replace(/'/g, "''")}'`).join(',');
+ const before = psql(`SELECT dw_sku, coalesce(width,''), coalesce(width_inches::text,'') FROM newwall_catalog WHERE dw_sku IN (${ids});`);
+ fs.writeFileSync(path.join(auditDir, `before-${TS}.tsv`), before + '\n');
+ console.log(` before-snapshot → ${auditDir}/before-${TS}.tsv (reversal source)`);
+
+ let applied = 0;
+ for (let j = 0; j < found.length; j += 400) {
+ const batch = found.slice(j, j + 400);
+ const wCases = batch.map(r => `WHEN '${r.dw_sku.replace(/'/g, "''")}' THEN '${r.canonical.replace(/'/g, "''")}'`).join(' ');
+ const wiCases = batch.map(r => `WHEN '${r.dw_sku.replace(/'/g, "''")}' THEN ${r.width_in}`).join(' ');
+ const bIds = batch.map(r => `'${r.dw_sku.replace(/'/g, "''")}'`).join(',');
+ psql(`UPDATE newwall_catalog
+ SET width = CASE dw_sku ${wCases} END,
+ width_inches = CASE dw_sku ${wiCases} END,
+ updated_at = now()
+ WHERE dw_sku IN (${bIds});`);
+ applied += batch.length;
+ process.stdout.write(`\r updated ${applied}/${found.length}…`);
+ }
+ console.log(`\napplied ${applied} width backfills. Reverse from before-${TS}.tsv. Next: node normalize-widths.js --only Newwall --commit`);
+})().catch(e => { console.error('ERR:', e.message); process.exit(1); });
← d3a6b2c6 Relabel 8 Brewster & York drafts -> Malibu Wallpaper + promo
·
back to Designer Wallcoverings
·
active-gate-add-samples: additive sample-variant pass for th 1c17d27c →