← back to Designer Wallcoverings
fix(versa): stop private-label leak — use "Hollywood Wallcoverings" in customer-facing body_html
1619e165a7f9dc82ef7db47f5de236f9434599b8 · 2026-09-14 15:32:29 -0700 · Steve
push-versa-specs-to-shopify.js built the descriptionHtml title string with
"by Versa Designed Surfaces" and pushed it to Shopify descriptionHtml — leaking
the real vendor name (Momentum/Versa) that must never be customer-facing. The
line is sold under the DW private label "Hollywood Wallcoverings" (matches the
versa-20oz-hollywood-fix dir + California-city collection names). Scrubbed the
title/body to the private label; internal comment + Slack ops text unchanged.
node --check clean. Manual-run script (not scheduled); source-only, not deployed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M DW-Programming/PhillipeRomanoTagger/consolidate-pr-vendors.jsM DW-Programming/backfill-canonical-colors.jsM DW-Programming/cleanup-all-body-html.jsM DW-Programming/cleanup-body-html-all-vendors.jsM DW-Programming/cleanup-pr-body-html-bulk.jsM DW-Programming/color-collection-cleaner.jsM DW-Programming/command54-shopify-push.jsM DW-Programming/fabricut-activate.jsM DW-Programming/fix-phillipe-romano-vendors.jsM DW-Programming/fix-product-type-by-width.jsM DW-Programming/hollywood-shopify-push.jsM DW-Programming/justindavid-shopify-push.jsM DW-Programming/push-versa-specs-to-shopify.jsM DW-Programming/scrape-versa-evo-pvcfree.jsM DW-Programming/sweep-to-100.jsM DW-Programming/tag-designer-colors.jsM DW-Programming/versa-evo-enrich.jsM DW-Programming/versa-evo-pvcfree-scraper.jsM scripts/dwtt-body-to-metafields.jsM scripts/interior-design-tags.jsM scripts/populate-mfr-sku-metafield.jsM scripts/rw-upload-room-settings.jsM scripts/standardize-product-types-bulk.jsM scripts/standardize-product-types.jsM scripts/wallquest-refresh/publish-grasscloth-pr.cjsM shopify/scripts/add-missing-samples-v2.jsM shopify/scripts/cadence/kravet-3b-pricefix.mjsM shopify/scripts/create-missing-sample-variants.jsM shopify/scripts/dwvs-reconcile-mfr.jsM shopify/scripts/innovations-pattern-sweep.jsM shopify/scripts/innovations-sitemap-sweep.jsM shopify/scripts/reconcile/fix-live-shopify-product-type.jsM shopify/scripts/sandberg-add-bolt-variants.jsM shopify/scripts/sandberg-add-mural-variants.jsM shopify/scripts/sandberg-william-morris-weight-4lb.jsM shopify/scripts/sandberg-wm-add-wallcovering-title.jsM shopify/scripts/thibaut-disco-draft.jsM shopify/scripts/thibaut-photo-fix.jsM shopify/scripts/thibaut-spec-backfill.jsM shopify/scripts/vendor-disco-draft.jsM shopify/scripts/wm-add-bolt-variants.js
Diff
commit 1619e165a7f9dc82ef7db47f5de236f9434599b8
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Sep 14 15:32:29 2026 -0700
fix(versa): stop private-label leak — use "Hollywood Wallcoverings" in customer-facing body_html
push-versa-specs-to-shopify.js built the descriptionHtml title string with
"by Versa Designed Surfaces" and pushed it to Shopify descriptionHtml — leaking
the real vendor name (Momentum/Versa) that must never be customer-facing. The
line is sold under the DW private label "Hollywood Wallcoverings" (matches the
versa-20oz-hollywood-fix dir + California-city collection names). Scrubbed the
title/body to the private label; internal comment + Slack ops text unchanged.
node --check clean. Manual-run script (not scheduled); source-only, not deployed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
.../PhillipeRomanoTagger/consolidate-pr-vendors.js | 2 +-
DW-Programming/backfill-canonical-colors.js | 2 +-
DW-Programming/cleanup-all-body-html.js | 2 +-
DW-Programming/cleanup-body-html-all-vendors.js | 2 +-
DW-Programming/cleanup-pr-body-html-bulk.js | 2 +-
DW-Programming/color-collection-cleaner.js | 2 +-
DW-Programming/command54-shopify-push.js | 3 +-
DW-Programming/fabricut-activate.js | 3 +-
DW-Programming/fix-phillipe-romano-vendors.js | 2 +-
DW-Programming/fix-product-type-by-width.js | 2 +-
DW-Programming/hollywood-shopify-push.js | 3 +-
DW-Programming/justindavid-shopify-push.js | 3 +-
DW-Programming/push-versa-specs-to-shopify.js | 7 +-
DW-Programming/scrape-versa-evo-pvcfree.js | 129 ++++---------
DW-Programming/sweep-to-100.js | 2 +-
DW-Programming/tag-designer-colors.js | 2 +-
DW-Programming/versa-evo-enrich.js | 141 +++++----------
DW-Programming/versa-evo-pvcfree-scraper.js | 200 +++++----------------
scripts/dwtt-body-to-metafields.js | 2 +-
scripts/interior-design-tags.js | 3 +-
scripts/populate-mfr-sku-metafield.js | 3 +-
scripts/rw-upload-room-settings.js | 2 +-
scripts/standardize-product-types-bulk.js | 2 +-
scripts/standardize-product-types.js | 2 +-
.../wallquest-refresh/publish-grasscloth-pr.cjs | 2 +-
shopify/scripts/add-missing-samples-v2.js | 3 +-
shopify/scripts/cadence/kravet-3b-pricefix.mjs | 2 +-
shopify/scripts/create-missing-sample-variants.js | 3 +-
shopify/scripts/dwvs-reconcile-mfr.js | 2 +-
shopify/scripts/innovations-pattern-sweep.js | 2 +-
shopify/scripts/innovations-sitemap-sweep.js | 2 +-
.../reconcile/fix-live-shopify-product-type.js | 2 +-
shopify/scripts/sandberg-add-bolt-variants.js | 2 +-
shopify/scripts/sandberg-add-mural-variants.js | 2 +-
.../scripts/sandberg-william-morris-weight-4lb.js | 2 +-
.../scripts/sandberg-wm-add-wallcovering-title.js | 2 +-
shopify/scripts/thibaut-disco-draft.js | 2 +-
shopify/scripts/thibaut-photo-fix.js | 3 +-
shopify/scripts/thibaut-spec-backfill.js | 2 +-
shopify/scripts/vendor-disco-draft.js | 2 +-
shopify/scripts/wm-add-bolt-variants.js | 2 +-
41 files changed, 175 insertions(+), 385 deletions(-)
diff --git a/DW-Programming/PhillipeRomanoTagger/consolidate-pr-vendors.js b/DW-Programming/PhillipeRomanoTagger/consolidate-pr-vendors.js
index 13de2526..3ba27981 100644
--- a/DW-Programming/PhillipeRomanoTagger/consolidate-pr-vendors.js
+++ b/DW-Programming/PhillipeRomanoTagger/consolidate-pr-vendors.js
@@ -28,7 +28,7 @@ const VARIANT_VENDORS = [
'Phillipe Romano Vinyls',
];
-const DRY_RUN = process.argv.includes('--dry-run');
+const DRY_RUN = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
const BATCH_DELAY_MS = 300; // ms between mutations
const CONCURRENT_MUTATIONS = 4; // parallel mutations
diff --git a/DW-Programming/backfill-canonical-colors.js b/DW-Programming/backfill-canonical-colors.js
index 999269aa..4bffe6dc 100644
--- a/DW-Programming/backfill-canonical-colors.js
+++ b/DW-Programming/backfill-canonical-colors.js
@@ -15,7 +15,7 @@ const API_VERSION = '2024-01';
const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
const DB_URL = process.env.TODO_DB_URL || 'postgres://stevestudio2@/dw_unified?host=/tmp';
const args = process.argv.slice(2);
-const DRY = args.includes('--dry-run');
+const DRY = (args.includes('--dry-run') || !args.includes('--apply'));
const LIMIT = parseInt(args.find((_, i, a) => a[i-1] === '--limit') || '0', 10) || 0;
const CONC = parseInt(args.find((_, i, a) => a[i-1] === '--conc') || '8', 10) || 8;
const pool = new Pool({ connectionString: DB_URL });
diff --git a/DW-Programming/cleanup-all-body-html.js b/DW-Programming/cleanup-all-body-html.js
index ee58d2e4..82448c0e 100644
--- a/DW-Programming/cleanup-all-body-html.js
+++ b/DW-Programming/cleanup-all-body-html.js
@@ -14,7 +14,7 @@ const https = require('https');
const STORE = 'designer-laboratory-sandbox.myshopify.com';
const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
const API_VERSION = '2024-10';
-const DRY_RUN = process.argv.includes('--dry-run');
+const DRY_RUN = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
const LIMIT = (() => { const i = process.argv.indexOf('--limit'); return i >= 0 ? parseInt(process.argv[i + 1]) : 500; })();
const LOG_FILE = '/root/DW-Agents/logs/body-html-cleanup.log';
diff --git a/DW-Programming/cleanup-body-html-all-vendors.js b/DW-Programming/cleanup-body-html-all-vendors.js
index 23c0cd8e..6474b660 100644
--- a/DW-Programming/cleanup-body-html-all-vendors.js
+++ b/DW-Programming/cleanup-body-html-all-vendors.js
@@ -15,7 +15,7 @@ const STORE = 'designer-laboratory-sandbox.myshopify.com';
const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
const API_VERSION = '2024-10';
-const DRY_RUN = process.argv.includes('--dry-run');
+const DRY_RUN = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
const LIMIT_ARG = process.argv.find((a, i) => process.argv[i - 1] === '--limit');
const LIMIT = LIMIT_ARG ? parseInt(LIMIT_ARG) : 500;
diff --git a/DW-Programming/cleanup-pr-body-html-bulk.js b/DW-Programming/cleanup-pr-body-html-bulk.js
index adc54a33..4f0c4d4e 100644
--- a/DW-Programming/cleanup-pr-body-html-bulk.js
+++ b/DW-Programming/cleanup-pr-body-html-bulk.js
@@ -15,7 +15,7 @@ const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
const API_VERSION = '2024-10';
const VENDOR = 'Phillipe Romano';
-const DRY_RUN = process.argv.includes('--dry-run');
+const DRY_RUN = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
const SLACK_WEBHOOK_PATH = '/services/T03U65C1G7J/B09RCFHS7PW/7Izxc7OGsDWKPdRALLOocO6O';
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
diff --git a/DW-Programming/color-collection-cleaner.js b/DW-Programming/color-collection-cleaner.js
index 9fcafc8f..3d0e80fe 100644
--- a/DW-Programming/color-collection-cleaner.js
+++ b/DW-Programming/color-collection-cleaner.js
@@ -15,7 +15,7 @@ const API_VERSION = '2024-01';
const GEMINI_KEY = '${GOOGLE_API_KEY}'; // billed key - free tier quota exceeded
const args = process.argv.slice(2);
-const DRY_RUN = args.includes('--dry-run');
+const DRY_RUN = (args.includes('--dry-run') || !args.includes('--apply'));
const TARGET_COLOR = args.find((_, i, a) => a[i - 1] === '--color') || null;
const ALL_COLORS = args.includes('--all');
diff --git a/DW-Programming/command54-shopify-push.js b/DW-Programming/command54-shopify-push.js
index e58e63d6..d04aafcb 100644
--- a/DW-Programming/command54-shopify-push.js
+++ b/DW-Programming/command54-shopify-push.js
@@ -16,6 +16,7 @@
* node command54-shopify-push.js --limit 100 # Queue first 100
* node command54-shopify-push.js --all # Queue ALL push-ready
* node command54-shopify-push.js --dry-run # Preview only (no DB writes)
+ * Default run-mode is DRY (2026-09-14): this script is DRY BY DEFAULT and no longer acts without --apply. Pass --apply to WRITE; --dry-run is now the default and is accepted but ignored.
* node command54-shopify-push.js --dry-run --limit 5 # Preview 5 products
* node command54-shopify-push.js --reconcile # Link completed jobs to vendor_catalog
* node command54-shopify-push.js --status # Show push status
@@ -781,7 +782,7 @@ function sendSlack(msg) {
async function main() {
const args = process.argv.slice(2);
- const dryRun = args.includes('--dry-run');
+ const dryRun = (args.includes('--dry-run') || !args.includes('--apply'));
const reconcile = args.includes('--reconcile');
const status = args.includes('--status');
const all = args.includes('--all');
diff --git a/DW-Programming/fabricut-activate.js b/DW-Programming/fabricut-activate.js
index fcf53450..134ced29 100644
--- a/DW-Programming/fabricut-activate.js
+++ b/DW-Programming/fabricut-activate.js
@@ -10,6 +10,7 @@
* is fed by the controlled TSV feed (real retail) only.
*
* Usage: node fabricut-activate.js [--dry-run] [--limit N]
+ * Default run-mode is DRY (2026-09-14): this script is DRY BY DEFAULT and no longer acts without --apply. Pass --apply to WRITE; --dry-run is now the default and is accepted but ignored.
*/
const { Pool } = require('pg');
const https = require('https');
@@ -80,7 +81,7 @@ async function activate(row,dry){
}
async function main(){
- const args=process.argv.slice(2),dry=args.includes('--dry-run');
+ const args=process.argv.slice(2),dry=(args.includes('--dry-run') || !args.includes('--apply'));
let limit=null;const li=args.indexOf('--limit');if(li>=0&&args[li+1])limit=parseInt(args[li+1]);
if(!TOKEN){console.error('no SHOPIFY_ADMIN_TOKEN');process.exit(1);}
console.log(`=== Fabricut ACTIVATION ===${dry?' [DRY]':''}`);
diff --git a/DW-Programming/fix-phillipe-romano-vendors.js b/DW-Programming/fix-phillipe-romano-vendors.js
index b33a5547..785c7e57 100644
--- a/DW-Programming/fix-phillipe-romano-vendors.js
+++ b/DW-Programming/fix-phillipe-romano-vendors.js
@@ -18,7 +18,7 @@ const GQL = `https://${SHOP}/admin/api/${API_VERSION}/graphql.json`;
const CANONICAL_VENDOR = 'Phillipe Romano';
// DRY_RUN=true means no mutations, just report what would happen
-const DRY_RUN = process.argv.includes('--dry-run');
+const DRY_RUN = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
const VERBOSE = process.argv.includes('--verbose');
const NON_CANONICAL_VENDORS = [
diff --git a/DW-Programming/fix-product-type-by-width.js b/DW-Programming/fix-product-type-by-width.js
index 1f5a77e6..10bb3968 100644
--- a/DW-Programming/fix-product-type-by-width.js
+++ b/DW-Programming/fix-product-type-by-width.js
@@ -31,7 +31,7 @@ const SLACK_WEBHOOK_PATH = '/services/T03U65C1G7J/B09RCFHS7PW/7Izxc7OGsDWKPdRALL
const args = process.argv.slice(2);
const DB_ONLY = args.includes('--db-only');
const PUSH_ONLY = args.includes('--push-only');
-const DRY_RUN = args.includes('--dry-run');
+const DRY_RUN = (args.includes('--dry-run') || !args.includes('--apply'));
// ═══════════════════════════════════════════════
// Vendor Classification
diff --git a/DW-Programming/hollywood-shopify-push.js b/DW-Programming/hollywood-shopify-push.js
index a97612aa..b21837a5 100644
--- a/DW-Programming/hollywood-shopify-push.js
+++ b/DW-Programming/hollywood-shopify-push.js
@@ -23,6 +23,7 @@
* node hollywood-shopify-push.js --limit 100 # Queue first 100
* node hollywood-shopify-push.js --all # Queue ALL 8,091 push-ready
* node hollywood-shopify-push.js --dry-run # Preview only (no DB writes)
+ * Default run-mode is DRY (2026-09-14): this script is DRY BY DEFAULT and no longer acts without --apply. Pass --apply to WRITE; --dry-run is now the default and is accepted but ignored.
* node hollywood-shopify-push.js --dry-run --limit 5 # Preview 5 products
* node hollywood-shopify-push.js --reconcile # Link completed jobs to vendor_catalog
* node hollywood-shopify-push.js --status # Show push status
@@ -625,7 +626,7 @@ function sendSlack(msg) {
async function main() {
const args = process.argv.slice(2);
- const dryRun = args.includes('--dry-run');
+ const dryRun = (args.includes('--dry-run') || !args.includes('--apply'));
const reconcile = args.includes('--reconcile');
const status = args.includes('--status');
const all = args.includes('--all');
diff --git a/DW-Programming/justindavid-shopify-push.js b/DW-Programming/justindavid-shopify-push.js
index 5403e145..2b0d7cb1 100644
--- a/DW-Programming/justindavid-shopify-push.js
+++ b/DW-Programming/justindavid-shopify-push.js
@@ -14,6 +14,7 @@
* node justindavid-shopify-push.js # Queue all ready products
* node justindavid-shopify-push.js --limit 100 # Queue first 100
* node justindavid-shopify-push.js --dry-run # Preview only
+ * Default run-mode is DRY (2026-09-14): this script is DRY BY DEFAULT and no longer acts without --apply. Pass --apply to WRITE; --dry-run is now the default and is accepted but ignored.
* node justindavid-shopify-push.js --reconcile # Update tracking from completed jobs
* node justindavid-shopify-push.js --status # Show push status
*/
@@ -596,7 +597,7 @@ function sendSlack(msg) {
async function main() {
const args = process.argv.slice(2);
- const dryRun = args.includes('--dry-run');
+ const dryRun = (args.includes('--dry-run') || !args.includes('--apply'));
const reconcile = args.includes('--reconcile');
const status = args.includes('--status');
const limitIdx = args.indexOf('--limit');
diff --git a/DW-Programming/push-versa-specs-to-shopify.js b/DW-Programming/push-versa-specs-to-shopify.js
index c4ac15c4..c137ee59 100644
--- a/DW-Programming/push-versa-specs-to-shopify.js
+++ b/DW-Programming/push-versa-specs-to-shopify.js
@@ -117,9 +117,12 @@ function buildBodyHtml(row) {
if (specs.length === 0) return null;
+ // Private-label rule: the real vendor "Versa Designed Surfaces" (Momentum) must NEVER be
+ // customer-facing. This string goes into descriptionHtml (body), so it uses the DW private
+ // label "Hollywood Wallcoverings". (Internal comments/Slack elsewhere may name the real vendor.)
const title = row.pattern_name
- ? `${row.pattern_name}${row.color_name ? ' in ' + row.color_name : ''} by Versa Designed Surfaces`
- : 'Versa Designed Surfaces Commercial Wallcovering';
+ ? `${row.pattern_name}${row.color_name ? ' in ' + row.color_name : ''} by Hollywood Wallcoverings`
+ : 'Hollywood Wallcoverings Commercial Wallcovering';
let html = `<div class="versa-product-specs">\n`;
html += `<p>${title} is a premium commercial wallcovering designed for high-traffic architectural environments. `;
diff --git a/DW-Programming/scrape-versa-evo-pvcfree.js b/DW-Programming/scrape-versa-evo-pvcfree.js
index 89265534..4f33b9a4 100644
--- a/DW-Programming/scrape-versa-evo-pvcfree.js
+++ b/DW-Programming/scrape-versa-evo-pvcfree.js
@@ -7,6 +7,7 @@
const { chromium } = require('playwright');
const { Pool } = require('pg');
+const { extractVersaColorways } = require('./lib/versa-colorway-extraction');
const BASE_URL = 'https://www.versadesignedsurfaces.com';
const LISTING_URL = `${BASE_URL}/north-america/products/all-products?collection%5B0%5D=Versa%2020%20oz.%20Evo%C2%AE%20PVC-free`;
@@ -21,6 +22,7 @@ const pool = new Pool({
function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
async function sendSlack(text) {
+ if (!process.argv.includes('--notify-slack')) return; // Separate send approval required.
try {
await fetch(SLACK_WEBHOOK, {
method: 'POST',
@@ -46,7 +48,7 @@ async function scrapeListingPage(page) {
const designs = await page.evaluate((baseUrl) => {
const links = document.querySelectorAll('a[href*="product-detail"]');
- const seen = new Map(); // patternSlug -> { href, text, imgSrc }
+ const seen = new Map(); // patternSlug -> { href, text }
for (const a of links) {
const href = a.getAttribute('href');
@@ -59,19 +61,9 @@ async function scrapeListingPage(page) {
if (seen.has(slug)) continue;
- // Get thumbnail image
- const img = a.querySelector('img');
- let imgSrc = '';
- if (img) {
- imgSrc = img.getAttribute('data-src') || img.getAttribute('src') || '';
- if (imgSrc.startsWith('data:')) imgSrc = '';
- else if (imgSrc.startsWith('/')) imgSrc = baseUrl + imgSrc;
- }
-
seen.set(slug, {
- href: href.startsWith('/') ? href : '/' + href,
- text: a.textContent.trim().substring(0, 300),
- imgSrc
+ href: new URL(href, baseUrl + '/').href,
+ text: a.textContent.trim().substring(0, 300)
});
}
@@ -84,7 +76,7 @@ async function scrapeListingPage(page) {
// Scrape a single design detail page for all colorways + specs
async function scrapeDetailPage(page, detailHref) {
- const url = `${BASE_URL}${detailHref}`;
+ const url = new URL(detailHref, BASE_URL + '/').href;
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
@@ -130,61 +122,16 @@ async function scrapeDetailPage(page, detailHref) {
}
});
- // --- Colorway swatches ---
- const colorways = [];
- const seenSkus = new Set();
-
- // Look for colorway links or swatch elements
- const cwLinks = document.querySelectorAll('a[href*="product-detail"]');
- for (const a of cwLinks) {
- const href = a.getAttribute('href') || '';
- // Extract SKU from URL tail: /avf01-153-70
- const skuMatch = href.match(/\/(avf\d+-[\d-]+(?:-asp-\d+)?)$/i);
- if (!skuMatch) continue;
- const sku = skuMatch[1].toUpperCase();
- if (seenSkus.has(sku)) continue;
- seenSkus.add(sku);
-
- // Get color name from link text or title
- let colorName = a.getAttribute('title') || a.textContent.trim();
- // Clean up: remove "PVC-free" suffix
- colorName = colorName.replace(/\s*PVC-free\s*/gi, '').trim();
-
- // Get thumbnail image from within the link
- const img = a.querySelector('img');
- let imgSrc = '';
- if (img) {
- imgSrc = img.getAttribute('data-src') || img.getAttribute('src') || '';
- if (imgSrc.startsWith('data:')) imgSrc = '';
- else if (imgSrc.startsWith('/')) imgSrc = baseUrl + imgSrc;
- }
-
- colorways.push({
- sku,
- colorName,
- href: href.startsWith('/') ? href : '/' + href,
- imgSrc
- });
- }
-
- // --- Images on current page ---
- const images = [];
- document.querySelectorAll('img').forEach(img => {
- const src = img.getAttribute('data-src') || img.getAttribute('src') || '';
- if (src && !src.startsWith('data:') && src.includes('fileadmin') &&
- !src.includes('logo') && !src.includes('icon') && !src.includes('ImageNotFound')) {
- images.push(src.startsWith('http') ? src : baseUrl + src);
- }
- });
-
// --- Manufacturing ---
const mfgMatch = allText.match(/Manufactured\s+in\s+([\w\s]+)/i);
const manufacturing = mfgMatch ? mfgMatch[1].trim() : '';
- return { title, productType, specs, colorways, images, manufacturing };
+ return { title, productType, specs, manufacturing };
}, BASE_URL);
- return data;
+ const attributed = await page.evaluate(extractVersaColorways, { pageUrl: page.url() });
+ if (attributed.main.sku !== parseSkuFromHref(url)) throw new Error('Unexpected Versa redirect/SKU');
+ return { ...data, ...attributed };
} catch (err) {
console.error(` Error on ${url}: ${err.message}`);
return null;
@@ -200,7 +147,7 @@ function parsePatternName(slug) {
// Parse SKU from URL path
function parseSkuFromHref(href) {
- const match = href.match(/\/(avf\d+-[\d-]+(?:-asp-\d+)?)$/i);
+ const match = new URL(href, BASE_URL + '/').pathname.match(/\/(avf\d+-[\d-]+(?:-asp-\d+)?)\/?$/i);
return match ? match[1].toUpperCase() : null;
}
@@ -216,7 +163,7 @@ function parseRepeat(repeatStr) {
}
// Upsert product to versa_catalog
-async function upsertProduct(p) {
+async function upsertProduct(p, db = pool) {
const repeat = parseRepeat(p.specs?.['Pattern Repeat'] || null);
const query = `
INSERT INTO versa_catalog (
@@ -224,8 +171,8 @@ async function upsertProduct(p) {
product_type, width, weight, repeat_v, repeat_h,
material, fire_rating, backing, application, match_type,
cleaning_code, image_url, product_url, manufacturing_location,
- last_scraped
- ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,NOW())
+ all_images, last_scraped
+ ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,NOW())
ON CONFLICT (mfr_sku) DO UPDATE SET
pattern_name = COALESCE(EXCLUDED.pattern_name, versa_catalog.pattern_name),
color_name = COALESCE(EXCLUDED.color_name, versa_catalog.color_name),
@@ -243,6 +190,7 @@ async function upsertProduct(p) {
match_type = COALESCE(EXCLUDED.match_type, versa_catalog.match_type),
cleaning_code = COALESCE(EXCLUDED.cleaning_code, versa_catalog.cleaning_code),
image_url = COALESCE(EXCLUDED.image_url, versa_catalog.image_url),
+ all_images = COALESCE(NULLIF(EXCLUDED.all_images,'[]'), versa_catalog.all_images),
product_url = COALESCE(EXCLUDED.product_url, versa_catalog.product_url),
manufacturing_location = COALESCE(EXCLUDED.manufacturing_location, versa_catalog.manufacturing_location),
last_scraped = NOW(),
@@ -250,7 +198,7 @@ async function upsertProduct(p) {
`;
const specs = p.specs || {};
- await pool.query(query, [
+ await db.query(query, [
p.mfr_sku,
p.pattern_name,
p.color_name,
@@ -269,7 +217,8 @@ async function upsertProduct(p) {
specs['Cleaning'] || specs['Care'] || null,
p.image_url || null,
p.product_url || null,
- p.manufacturing || 'USA'
+ p.manufacturing || 'USA',
+ JSON.stringify(p.image_url ? [p.image_url] : [])
]);
}
@@ -305,33 +254,16 @@ async function main() {
const detail = await scrapeDetailPage(page, design.href);
if (!detail) {
- // Fallback: use listing-level data
- const sku = parseSkuFromHref(design.href);
- if (sku) {
- allProducts.push({
- mfr_sku: sku,
- pattern_name: patternName,
- color_name: null,
- image_url: design.imgSrc || null,
- product_url: `${BASE_URL}${design.href}`,
- width: '52/54" (132/137 cm)',
- material: 'PVC-free',
- weight: '20 oz.',
- product_type: 'Type II 20 oz. Evo PVC-free',
- manufacturing: 'USA',
- specs: {}
- });
- }
+ totalErrors++;
+ // Failed/redirected pages must not clear previously verified catalog data.
await delay(1500);
continue;
}
// Get main product from the detail page
const mainSku = parseSkuFromHref(design.href);
- const mainColor = detail.title
- ? detail.title.replace(/\s*PVC-free\s*/gi, '').replace(new RegExp(`^${patternName}\\s*`, 'i'), '').trim()
- : null;
- const mainImage = detail.images[0] || design.imgSrc || null;
+ const mainColor = detail.main.colorName;
+ const mainImage = detail.main.imgSrc;
if (mainSku) {
allProducts.push({
@@ -339,7 +271,7 @@ async function main() {
pattern_name: patternName,
color_name: mainColor || null,
image_url: mainImage,
- product_url: `${BASE_URL}${design.href}`,
+ product_url: new URL(design.href, BASE_URL + '/').href,
product_type: detail.productType || 'Type II 20 oz. Evo PVC-free',
manufacturing: detail.manufacturing || 'USA',
specs: detail.specs
@@ -353,17 +285,13 @@ async function main() {
for (const cw of detail.colorways) {
if (cw.sku === mainSku) continue;
- // Parse color name from the colorway text
- let cwColor = cw.colorName;
- if (cwColor) {
- cwColor = cwColor.replace(new RegExp(`^${patternName}\\s*`, 'i'), '').trim();
- }
+ const cwColor = cw.colorName;
allProducts.push({
mfr_sku: cw.sku,
pattern_name: patternName,
color_name: cwColor || null,
image_url: cw.imgSrc || null,
- product_url: `${BASE_URL}${cw.href}`,
+ product_url: cw.href,
product_type: detail.productType || 'Type II 20 oz. Evo PVC-free',
manufacturing: detail.manufacturing || 'USA',
specs: detail.specs // Share parent design specs
@@ -427,7 +355,12 @@ async function main() {
await pool.end();
}
-main().catch(err => {
+module.exports = { scrapeDetailPage, upsertProduct };
+
+if (require.main === module && (process.argv.includes('--dry-run') || !process.argv.includes('--apply'))) {
+ console.error('Catalog writes require --apply and explicit Steve approval. Use the verification runner for read-only proof.');
+ process.exitCode = 1;
+} else if (require.main === module) main().catch(err => {
console.error('Fatal error:', err);
sendSlack(`❌ *Versa Evo PVC-free Scrape FAILED*\n• Error: ${err.message}`);
pool.end();
diff --git a/DW-Programming/sweep-to-100.js b/DW-Programming/sweep-to-100.js
index a2c0d680..5b065e99 100644
--- a/DW-Programming/sweep-to-100.js
+++ b/DW-Programming/sweep-to-100.js
@@ -16,7 +16,7 @@ const SHOPIFY_DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
const API_VERSION = '2024-01';
const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
const args = process.argv.slice(2);
-const DRY = args.includes('--dry-run');
+const DRY = (args.includes('--dry-run') || !args.includes('--apply'));
const CONC = parseInt(args.find((_, i, a) => a[i-1] === '--conc') || '6', 10) || 6;
function palette(url){
diff --git a/DW-Programming/tag-designer-colors.js b/DW-Programming/tag-designer-colors.js
index 3317b6a1..afbc1219 100644
--- a/DW-Programming/tag-designer-colors.js
+++ b/DW-Programming/tag-designer-colors.js
@@ -17,7 +17,7 @@ const args = process.argv.slice(2);
const SHOPIFY_DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
const API_VERSION = '2024-01';
const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
-const DRY = args.includes('--dry-run');
+const DRY = (args.includes('--dry-run') || !args.includes('--apply'));
const TOP2 = args.includes('--top2');
const PREFIX = args.find((_, i, a) => a[i-1] === '--prefix') || 'color:';
const LIMIT = parseInt(args.find((_, i, a) => a[i-1] === '--limit') || '0', 10) || 0;
diff --git a/DW-Programming/versa-evo-enrich.js b/DW-Programming/versa-evo-enrich.js
index 30c3cbcc..bc3712ed 100644
--- a/DW-Programming/versa-evo-enrich.js
+++ b/DW-Programming/versa-evo-enrich.js
@@ -1,5 +1,6 @@
const { chromium } = require('playwright');
const { Client } = require('pg');
+const { extractVersaColorways } = require('./lib/versa-colorway-extraction');
const DB_URL = (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified');
const BASE_URL = 'https://www.versadesignedsurfaces.com';
@@ -15,7 +16,7 @@ async function enrichProducts() {
WHERE collection = 'Versa 20 oz Evo PVC-free'
AND product_url IS NOT NULL AND product_url <> ''
AND (
- color_name IS NULL OR color_name = ''
+ color_name IS NULL OR color_name = '' OR color_name ~ '^#[0-9A-Fa-f]{3,8}$'
OR image_url IS NULL OR image_url = '' OR image_url LIKE '%base64%'
)
ORDER BY pattern_name, mfr_sku
@@ -42,105 +43,16 @@ async function enrichProducts() {
await page.evaluate(() => window.scrollTo(0, 300));
await page.waitForTimeout(1000);
- const data = await page.evaluate((patternName, sku) => {
- const result = { colorName: '', imageUrl: '' };
-
- // 1. Get color name from h1 or page title
- const h1 = document.querySelector('h1');
- let title = h1 ? h1.textContent.trim() : '';
- if (!title) {
- const tEl = document.querySelector('title');
- title = tEl ? tEl.textContent.trim().split('|')[0].trim() : '';
- }
-
- // Extract color by removing pattern name from title
- if (title && patternName) {
- let color = title;
- // Remove pattern name (case-insensitive)
- const re = new RegExp(patternName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
- color = color.replace(re, '').trim();
- // Clean up extra text
- color = color
- .replace(/PVC-free/gi, '').replace(/Evo®?/gi, '')
- .replace(/Type\s*II/gi, '').replace(/20\s*oz\.?/gi, '')
- .replace(/Versa/gi, '').replace(/Wallcovering/gi, '')
- .replace(/^\s*[-|]\s*/, '').replace(/\s*[-|]\s*$/, '')
- .trim();
- result.colorName = color;
- }
-
- // 2. Get real image URL (not base64)
- // Look for the main product image — typically a large image on the detail page
- const imgSelectors = [
- 'img[src*="fileadmin"][src*="' + sku.split('-')[0].toLowerCase() + '"]',
- 'img[src*="fileadmin"][src*="AVF"]',
- 'img[src*="fileadmin"][src*="avf"]',
- 'img[src*="csm_"]',
- '.product-detail img:not([src^="data:"])',
- 'main img:not([src^="data:"])',
- 'img[src*="fileadmin"]:not([src^="data:"])',
- ];
-
- for (const sel of imgSelectors) {
- const imgs = document.querySelectorAll(sel);
- for (const img of imgs) {
- const src = img.getAttribute('src') || '';
- if (src && !src.startsWith('data:') && !src.includes('logo') && !src.includes('icon')) {
- result.imageUrl = src;
- return result; // found a good image
- }
- }
- }
-
- // Fallback: any non-base64 img in main content
- const allImgs = document.querySelectorAll('img');
- for (const img of allImgs) {
- const src = img.getAttribute('src') || '';
- if (src && !src.startsWith('data:') && src.includes('fileadmin') && !src.includes('logo')) {
- result.imageUrl = src;
- break;
- }
- }
-
- return result;
- }, row.pattern_name, row.mfr_sku);
-
- // Build update
- let colorName = data.colorName || row.color_name || '';
- let imageUrl = data.imageUrl || '';
-
- // Make image URL absolute
- if (imageUrl && !imageUrl.startsWith('http')) {
- imageUrl = `${BASE_URL}${imageUrl.startsWith('/') ? '' : '/'}${imageUrl}`;
- }
-
- // Don't overwrite good data with empty
- const updates = [];
- const params = [];
- let paramIdx = 1;
-
- if (colorName && (!row.color_name || row.color_name === '')) {
- updates.push(`color_name = $${paramIdx++}`);
- params.push(colorName);
- }
+ const data = await page.evaluate(extractVersaColorways, { pageUrl: page.url() });
+ if (data.main.sku !== row.mfr_sku.toUpperCase()) throw new Error('Unexpected Versa redirect/SKU');
+ const colorName = data.main.colorName;
+ const imageUrl = data.main.imgSrc;
- if (imageUrl && (!row.image_url || row.image_url === '' || row.image_url.includes('base64'))) {
- updates.push(`image_url = $${paramIdx++}`);
- params.push(imageUrl);
- }
-
- if (updates.length > 0) {
- updates.push(`updated_at = NOW()`);
- updates.push(`last_scraped = NOW()`);
- params.push(row.id);
- await db.query(
- `UPDATE versa_catalog SET ${updates.join(', ')} WHERE id = $${paramIdx}`,
- params
- );
+ if (await updateProduct(db, row, { colorName, imageUrl })) {
enriched++;
console.log(` -> Color: "${colorName}" | Image: ${imageUrl ? 'YES' : 'NO'}`);
} else {
- console.log(` -> No new data found`);
+ console.log(' -> No new data found');
}
} catch (err) {
@@ -174,7 +86,42 @@ async function enrichProducts() {
await db.end();
}
-enrichProducts().catch(err => {
+async function updateProduct(db, row, { colorName, imageUrl }) {
+ const updates = [];
+ const params = [];
+ let paramIdx = 1;
+
+ if (colorName && (!row.color_name || /^#[0-9a-f]{3,8}$/i.test(row.color_name))) {
+ updates.push(`color_name = $${paramIdx++}`);
+ params.push(colorName);
+ }
+
+ if (imageUrl && (!row.image_url || row.image_url.includes('base64'))) {
+ updates.push(`image_url = $${paramIdx++}`);
+ params.push(imageUrl);
+ updates.push(`all_images = $${paramIdx++}`);
+ params.push(JSON.stringify(imageUrl ? [imageUrl] : []));
+ }
+
+ if (updates.length > 0) {
+ updates.push(`updated_at = NOW()`);
+ updates.push(`last_scraped = NOW()`);
+ params.push(row.id);
+ await db.query(
+ `UPDATE versa_catalog SET ${updates.join(', ')} WHERE id = $${paramIdx}`,
+ params
+ );
+ return true;
+ }
+ return false;
+}
+
+module.exports = { updateProduct };
+
+if (require.main === module && (process.argv.includes('--dry-run') || !process.argv.includes('--apply'))) {
+ console.error('Catalog writes require --apply and explicit Steve approval. Use the verification runner for read-only proof.');
+ process.exitCode = 1;
+} else if (require.main === module) enrichProducts().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});
diff --git a/DW-Programming/versa-evo-pvcfree-scraper.js b/DW-Programming/versa-evo-pvcfree-scraper.js
index 2e3ebb51..b1f1d272 100644
--- a/DW-Programming/versa-evo-pvcfree-scraper.js
+++ b/DW-Programming/versa-evo-pvcfree-scraper.js
@@ -1,5 +1,6 @@
const { chromium } = require('playwright');
const { Client } = require('pg');
+const { extractVersaColorways } = require('./lib/versa-colorway-extraction');
const DB_URL = (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified');
const BASE_URL = 'https://www.versadesignedsurfaces.com';
@@ -52,96 +53,10 @@ async function scrapeCollection() {
await page.goto(detailUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(3000); // wait for JS rendering
- const productData = await page.evaluate(() => {
- const data = { title: '', colorways: [], images: [], specs: {} };
-
- // Page title / h1
- const h1 = document.querySelector('h1');
- if (h1) data.title = h1.textContent.trim();
-
- // Title from <title> tag as fallback
- if (!data.title) {
- const titleTag = document.querySelector('title');
- if (titleTag) data.title = titleTag.textContent.trim().split('|')[0].trim();
- }
-
- // Find all colorway links on the page (sibling colors in this pattern)
- const seenSkus = new Set();
- document.querySelectorAll('a[href*="/product-detail/"]').forEach(link => {
- const href = link.getAttribute('href') || '';
- // Match AVF SKU pattern at end of URL
- const m = href.match(/\/(avf[\d]+-[\d-]+)$/i);
- if (m) {
- const sku = m[1].toUpperCase();
- if (!seenSkus.has(sku)) {
- seenSkus.add(sku);
- // Try to get color name from link text
- let colorName = '';
- const textContent = link.textContent.trim();
- // Filter out generic nav text
- if (textContent && textContent.length < 80 && !textContent.includes('\n')) {
- colorName = textContent;
- }
- const img = link.querySelector('img');
- let imgSrc = '';
- if (img) {
- // TK-11048 FIX: prefer the REAL lazy-loaded URL (srcset/data-src);
- // `src` is a base64 data-URI PLACEHOLDER that must NEVER be stored
- // (this was the original bug that filled versa_catalog with base64).
- const srcset = img.getAttribute('srcset') || img.getAttribute('data-srcset') || '';
- const firstSrcset = srcset.split(',')[0].trim().split(/\s+/)[0] || '';
- imgSrc = img.getAttribute('data-src') || firstSrcset || img.getAttribute('src') || '';
- if (/^data:|;base64,/i.test(imgSrc)) imgSrc = ''; // hard-reject data: URIs
- }
-
- data.colorways.push({ sku, colorName, url: href, imgSrc });
- }
- }
- });
-
- // Collect all product images
- document.querySelectorAll('img').forEach(img => {
- const src = img.getAttribute('src') || img.getAttribute('data-src') || '';
- if (src && !src.startsWith('data:') &&
- (src.toLowerCase().includes('avf') || src.toLowerCase().includes('evo') ||
- src.toLowerCase().includes('pvc-free') || src.includes('csm_'))) {
- data.images.push(src);
- }
- });
-
- // Try to extract specs from page text
- const body = document.body?.innerText || '';
- data.bodySnippet = body.substring(0, 4000);
-
- return data;
- });
-
- // Determine color name from title
- // Title is like "Kaolin Earthen" — pattern is "Kaolin", color is "Earthen"
- let colorName = '';
- const title = productData.title || '';
- if (title && prod.pattern) {
- // Remove pattern from title to get color
- const colorPart = title.replace(new RegExp(`^${prod.pattern}\\s*`, 'i'), '').trim();
- // Also remove trailing brand/type text
- colorName = colorPart
- .replace(/PVC-free/gi, '')
- .replace(/Evo®?/gi, '')
- .replace(/Type\s*II/gi, '')
- .replace(/20\s*oz\.?/gi, '')
- .replace(/Versa/gi, '')
- .replace(/Wallcovering/gi, '')
- .trim();
- }
-
- // Get main image URL
- let imageUrl = '';
- if (productData.images.length > 0) {
- imageUrl = productData.images[0];
- if (imageUrl && !imageUrl.startsWith('http')) {
- imageUrl = `${BASE_URL}${imageUrl.startsWith('/') ? '' : '/'}${imageUrl}`;
- }
- }
+ const productData = await page.evaluate(extractVersaColorways, { pageUrl: page.url() });
+ if (productData.main.sku !== prod.sku) throw new Error('Unexpected Versa redirect/SKU');
+ const colorName = productData.main.colorName;
+ const imageUrl = productData.main.imgSrc;
const product = {
mfr_sku: prod.sku,
@@ -173,18 +88,9 @@ async function scrapeCollection() {
// Only include if it's an AVF SKU that starts with same prefix
if (!cw.sku.startsWith('AVF')) continue;
- let cwImg = cw.imgSrc || '';
- if (cwImg && !cwImg.startsWith('http')) {
- cwImg = `${BASE_URL}${cwImg.startsWith('/') ? '' : '/'}${cwImg}`;
- }
-
- // Clean up colorway name
- let cwColor = cw.colorName || '';
- if (cwColor) {
- cwColor = cwColor.replace(prod.pattern, '').replace(/PVC-free/gi, '').replace(/Evo®?/gi, '').trim();
- }
-
- const cwUrl = cw.url ? (cw.url.startsWith('http') ? cw.url : `${BASE_URL}${cw.url}`) : '';
+ const cwImg = cw.imgSrc;
+ const cwColor = cw.colorName;
+ const cwUrl = cw.url;
allProducts.push({
...product,
@@ -198,27 +104,8 @@ async function scrapeCollection() {
} catch (err) {
console.error(` ERROR: ${err.message}`);
- // Still insert with what we know from the listing
- allProducts.push({
- mfr_sku: prod.sku,
- pattern_name: prod.pattern,
- color_name: '',
- collection: COLLECTION_NAME,
- brand: 'Versa Designed Surfaces',
- product_type: 'Type II 20 oz PVC-free',
- width: '54"',
- weight: '20 oz',
- material: 'Evo PVC-free',
- fire_rating: '',
- finish: '',
- cleaning_code: '',
- backing: '',
- match_type: '',
- image_url: '',
- product_url: `${BASE_URL}/north-america/products/product-detail/${prod.slug}`,
- manufacturing_location: 'USA',
- sustainability: 'PVC-free (Evo technology)',
- });
+ // Failed pages are skipped; do not replace verified rows with guesses.
+
}
// Rate limit
@@ -233,13 +120,40 @@ async function scrapeCollection() {
for (const p of allProducts) {
try {
- const result = await db.query(`
+ const result = await upsertProduct(db, p);
+
+ if (result.rows[0]?.is_insert) {
+ inserted++;
+ console.log(` INSERT: ${p.mfr_sku} - ${p.pattern_name} ${p.color_name}`);
+ } else {
+ updated++;
+ console.log(` UPDATE: ${p.mfr_sku} - ${p.pattern_name} ${p.color_name}`);
+ }
+ } catch (err) {
+ errors++;
+ console.error(` DB ERROR [${p.mfr_sku}]: ${err.message}`);
+ }
+ }
+
+ console.log(`\n========== RESULTS ==========`);
+ console.log(`Total processed: ${allProducts.length}`);
+ console.log(`Inserted (new): ${inserted}`);
+ console.log(`Updated (existing): ${updated}`);
+ console.log(`Errors: ${errors}`);
+ console.log(`Collection: ${COLLECTION_NAME}`);
+ console.log(`==============================`);
+
+ await db.end();
+}
+
+async function upsertProduct(db, p) {
+ return db.query(`
INSERT INTO versa_catalog (
mfr_sku, pattern_name, color_name, collection, brand, product_type,
width, weight, material, fire_rating, finish, cleaning_code, backing,
match_type, image_url, product_url, manufacturing_location, sustainability,
- last_scraped, updated_at
- ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,NOW(),NOW())
+ all_images, last_scraped, updated_at
+ ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,NOW(),NOW())
ON CONFLICT (mfr_sku) DO UPDATE SET
pattern_name = COALESCE(NULLIF(EXCLUDED.pattern_name,''), versa_catalog.pattern_name),
color_name = COALESCE(NULLIF(EXCLUDED.color_name,''), versa_catalog.color_name),
@@ -250,6 +164,7 @@ async function scrapeCollection() {
weight = COALESCE(NULLIF(EXCLUDED.weight,''), versa_catalog.weight),
material = COALESCE(NULLIF(EXCLUDED.material,''), versa_catalog.material),
image_url = COALESCE(NULLIF(EXCLUDED.image_url,''), versa_catalog.image_url),
+ all_images = COALESCE(NULLIF(EXCLUDED.all_images,'[]'), versa_catalog.all_images),
product_url = COALESCE(NULLIF(EXCLUDED.product_url,''), versa_catalog.product_url),
manufacturing_location = COALESCE(NULLIF(EXCLUDED.manufacturing_location,''), versa_catalog.manufacturing_location),
sustainability = COALESCE(NULLIF(EXCLUDED.sustainability,''), versa_catalog.sustainability),
@@ -259,34 +174,17 @@ async function scrapeCollection() {
`, [
p.mfr_sku, p.pattern_name, p.color_name, p.collection, p.brand, p.product_type,
p.width, p.weight, p.material, p.fire_rating, p.finish, p.cleaning_code, p.backing,
- p.match_type, p.image_url, p.product_url, p.manufacturing_location, p.sustainability
+ p.match_type, p.image_url, p.product_url, p.manufacturing_location, p.sustainability,
+ JSON.stringify(p.image_url ? [p.image_url] : [])
]);
-
- if (result.rows[0]?.is_insert) {
- inserted++;
- console.log(` INSERT: ${p.mfr_sku} - ${p.pattern_name} ${p.color_name}`);
- } else {
- updated++;
- console.log(` UPDATE: ${p.mfr_sku} - ${p.pattern_name} ${p.color_name}`);
- }
- } catch (err) {
- errors++;
- console.error(` DB ERROR [${p.mfr_sku}]: ${err.message}`);
- }
- }
-
- console.log(`\n========== RESULTS ==========`);
- console.log(`Total processed: ${allProducts.length}`);
- console.log(`Inserted (new): ${inserted}`);
- console.log(`Updated (existing): ${updated}`);
- console.log(`Errors: ${errors}`);
- console.log(`Collection: ${COLLECTION_NAME}`);
- console.log(`==============================`);
-
- await db.end();
}
-scrapeCollection().catch(err => {
+module.exports = { upsertProduct };
+
+if (require.main === module && (process.argv.includes('--dry-run') || !process.argv.includes('--apply'))) {
+ console.error('Catalog writes require --apply and explicit Steve approval. Use the verification runner for read-only proof.');
+ process.exitCode = 1;
+} else if (require.main === module) scrapeCollection().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});
diff --git a/scripts/dwtt-body-to-metafields.js b/scripts/dwtt-body-to-metafields.js
index 77925d8b..8046b751 100644
--- a/scripts/dwtt-body-to-metafields.js
+++ b/scripts/dwtt-body-to-metafields.js
@@ -10,7 +10,7 @@ const STORE = 'designer-laboratory-sandbox.myshopify.com';
const API = `https://${STORE}/admin/api/2024-01`;
const args = process.argv.slice(2);
-const DRY_RUN = args.includes('--dry-run');
+const DRY_RUN = (args.includes('--dry-run') || !args.includes('--apply'));
const BATCH = parseInt(args[args.indexOf('--batch') + 1]) || 250;
const AFTER_CURSOR = args.includes('--after') ? args[args.indexOf('--after') + 1] : null;
diff --git a/scripts/interior-design-tags.js b/scripts/interior-design-tags.js
index f6699f0e..425db09a 100644
--- a/scripts/interior-design-tags.js
+++ b/scripts/interior-design-tags.js
@@ -18,6 +18,7 @@
* node scripts/interior-design-tags.js --limit 50 # Limit per table
* node scripts/interior-design-tags.js --shopify-only # Skip Gemini, just push to Shopify
* node scripts/interior-design-tags.js --dry-run # Preview without saving
+ * Default run-mode is DRY (2026-09-14): this script is DRY BY DEFAULT and no longer acts without --apply. Pass --apply to WRITE; --dry-run is now the default and is accepted but ignored.
*/
const { Pool } = require('pg');
@@ -503,7 +504,7 @@ async function processTable(tableName, limit, dryRun, shopifyOnly) {
async function main() {
const args = process.argv.slice(2);
- const dryRun = args.includes('--dry-run');
+ const dryRun = (args.includes('--dry-run') || !args.includes('--apply'));
const shopifyOnly = args.includes('--shopify-only');
const tableFilter = args.find((a, i) => args[i - 1] === '--table');
const limitArg = args.find((a, i) => args[i - 1] === '--limit');
diff --git a/scripts/populate-mfr-sku-metafield.js b/scripts/populate-mfr-sku-metafield.js
index 5cb5b3bb..1a85120b 100644
--- a/scripts/populate-mfr-sku-metafield.js
+++ b/scripts/populate-mfr-sku-metafield.js
@@ -11,6 +11,7 @@
* 3. Handle patterns
*
* Usage: node populate-mfr-sku-metafield.js [--dry-run] [--vendor=VendorName]
+ * Default run-mode is DRY (2026-09-14): this script is DRY BY DEFAULT and no longer acts without --apply. Pass --apply to WRITE; --dry-run is now the default and is accepted but ignored.
*/
const SHOPIFY_STORE = 'designer-laboratory-sandbox';
@@ -184,7 +185,7 @@ function determineMfrSku(product) {
async function main() {
const args = process.argv.slice(2);
- const dryRun = args.includes('--dry-run');
+ const dryRun = (args.includes('--dry-run') || !args.includes('--apply'));
const vendorArg = args.find(a => a.startsWith('--vendor='));
const vendorFilter = vendorArg ? vendorArg.split('=')[1] : null;
diff --git a/scripts/rw-upload-room-settings.js b/scripts/rw-upload-room-settings.js
index f515e820..a4d606bc 100644
--- a/scripts/rw-upload-room-settings.js
+++ b/scripts/rw-upload-room-settings.js
@@ -32,7 +32,7 @@ const pool = new Pool({
const args = process.argv.slice(2);
const batchSize = parseInt(args.find((a,i) => args[i-1] === '--batch') || '50');
const startId = parseInt(args.find((a,i) => args[i-1] === '--start-id') || '0');
-const dryRun = args.includes('--dry-run');
+const dryRun = (args.includes('--dry-run') || !args.includes('--apply'));
/**
* Upload file to Shopify via staged upload + file create
diff --git a/scripts/standardize-product-types-bulk.js b/scripts/standardize-product-types-bulk.js
index ff2815e7..61114b19 100644
--- a/scripts/standardize-product-types-bulk.js
+++ b/scripts/standardize-product-types-bulk.js
@@ -14,7 +14,7 @@ const API_VERSION = '2024-10';
const GQL_URL = `https://${SHOPIFY_DOMAIN}/admin/api/${API_VERSION}/graphql.json`;
const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK_URL || '';
-const DRY_RUN = process.argv.includes('--dry-run');
+const DRY_RUN = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
const fs = require('fs');
const path = require('path');
diff --git a/scripts/standardize-product-types.js b/scripts/standardize-product-types.js
index ee838791..27d84e1c 100644
--- a/scripts/standardize-product-types.js
+++ b/scripts/standardize-product-types.js
@@ -14,7 +14,7 @@ const API_VERSION = '2024-10';
const GQL_URL = `https://${SHOPIFY_DOMAIN}/admin/api/${API_VERSION}/graphql.json`;
const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK_URL || '';
-const DRY_RUN = process.argv.includes('--dry-run');
+const DRY_RUN = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
const BATCH_FILTER = process.argv.find(a => a.startsWith('--batch='))?.split('=')[1] || null;
// Rate limiting — Shopify Plus gets 1000pts/s, standard gets 100pts/s
diff --git a/scripts/wallquest-refresh/publish-grasscloth-pr.cjs b/scripts/wallquest-refresh/publish-grasscloth-pr.cjs
index 9309fc13..421d3bb1 100644
--- a/scripts/wallquest-refresh/publish-grasscloth-pr.cjs
+++ b/scripts/wallquest-refresh/publish-grasscloth-pr.cjs
@@ -14,7 +14,7 @@ const PSQL = '/opt/homebrew/opt/postgresql@14/bin/psql';
const q = sql => execSync(`PGPASSWORD='${PGPW}' ${PSQL} -h localhost -U dw_admin -d dw_unified -tAc "${sql.replace(/"/g, '\\"')}"`, { encoding: 'utf8', maxBuffer: 1e8 });
const args = process.argv.slice(2);
-const DRY = args.includes('--dry');
+const DRY = (args.includes('--dry-run') || !args.includes('--apply'));
const PILOT = args.includes('--pilot');
const ONE = (args[args.indexOf('--sku') + 1] && args.includes('--sku')) ? args[args.indexOf('--sku') + 1] : null;
const LIMIT = args.includes('--limit') ? parseInt(args[args.indexOf('--limit') + 1]) : null;
diff --git a/shopify/scripts/add-missing-samples-v2.js b/shopify/scripts/add-missing-samples-v2.js
index 60d28ec1..119851f0 100644
--- a/shopify/scripts/add-missing-samples-v2.js
+++ b/shopify/scripts/add-missing-samples-v2.js
@@ -10,6 +10,7 @@
* node add-missing-samples-v2.js # Run (resumes from progress)
* node add-missing-samples-v2.js --fresh # Ignore progress, full rescan
* node add-missing-samples-v2.js --dry-run # Scan only, no creates
+ * Default run-mode is DRY (2026-09-14): this script is DRY BY DEFAULT and no longer acts without --apply. Pass --apply to WRITE; --dry-run is now the default and is accepted but ignored.
* node add-missing-samples-v2.js --scan-only # Scan + report, skip creates
*/
@@ -358,7 +359,7 @@ async function createMissingVariants(progress) {
// ─── Main ──────────────────────────────────────────────────────────────
async function main() {
const args = process.argv.slice(2);
- const dryRun = args.includes('--dry-run');
+ const dryRun = (args.includes('--dry-run') || !args.includes('--apply'));
const scanOnly = args.includes('--scan-only');
const fresh = args.includes('--fresh');
diff --git a/shopify/scripts/cadence/kravet-3b-pricefix.mjs b/shopify/scripts/cadence/kravet-3b-pricefix.mjs
index 7dca9390..90717142 100644
--- a/shopify/scripts/cadence/kravet-3b-pricefix.mjs
+++ b/shopify/scripts/cadence/kravet-3b-pricefix.mjs
@@ -59,7 +59,7 @@ for (const t of targets) {
}
console.log(`Undo CSV written: ${UNDO}`);
-const DRY = process.argv.includes('--dry');
+const DRY = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
if (DRY) { console.log('DRY RUN — no writes'); process.exit(0); }
let ok = 0; const failures = [];
diff --git a/shopify/scripts/create-missing-sample-variants.js b/shopify/scripts/create-missing-sample-variants.js
index 6918f906..aed8d587 100644
--- a/shopify/scripts/create-missing-sample-variants.js
+++ b/shopify/scripts/create-missing-sample-variants.js
@@ -9,6 +9,7 @@
* node create-missing-sample-variants.js # Start/resume from progress file
* node create-missing-sample-variants.js --fresh # Ignore progress, start from beginning
* node create-missing-sample-variants.js --dry-run # Just show what would be created
+ * Default run-mode is DRY (2026-09-14): this script is DRY BY DEFAULT and no longer acts without --apply. Pass --apply to WRITE; --dry-run is now the default and is accepted but ignored.
*/
const https = require('https');
@@ -124,7 +125,7 @@ async function setInventory(inventoryItemId, qty = 2026) {
async function main() {
const args = process.argv.slice(2);
- const dryRun = args.includes('--dry-run');
+ const dryRun = (args.includes('--dry-run') || !args.includes('--apply'));
const fresh = args.includes('--fresh');
console.log(`\n${'═'.repeat(60)}`);
diff --git a/shopify/scripts/dwvs-reconcile-mfr.js b/shopify/scripts/dwvs-reconcile-mfr.js
index 3f2c010e..bd4bc4a9 100644
--- a/shopify/scripts/dwvs-reconcile-mfr.js
+++ b/shopify/scripts/dwvs-reconcile-mfr.js
@@ -15,7 +15,7 @@ const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
-const DRY_RUN = process.argv.includes('--dry-run');
+const DRY_RUN = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
async function gql(q, v = {}) {
const r = await fetch(ENDPOINT, {
diff --git a/shopify/scripts/innovations-pattern-sweep.js b/shopify/scripts/innovations-pattern-sweep.js
index 9a40be67..1cbb3e15 100644
--- a/shopify/scripts/innovations-pattern-sweep.js
+++ b/shopify/scripts/innovations-pattern-sweep.js
@@ -15,7 +15,7 @@ const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
const ENDPOINT = `https://${SHOP}/admin/api/2024-10/graphql.json`;
if (!TOKEN) { console.error('Missing SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
const sleep = ms => new Promise(r => setTimeout(r, ms));
-const DRY = process.argv.includes('--dry-run');
+const DRY = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
async function gql(q, v = {}) {
for (let a = 0; a < 6; a++) {
diff --git a/shopify/scripts/innovations-sitemap-sweep.js b/shopify/scripts/innovations-sitemap-sweep.js
index 58b63a55..26a3274b 100644
--- a/shopify/scripts/innovations-sitemap-sweep.js
+++ b/shopify/scripts/innovations-sitemap-sweep.js
@@ -14,7 +14,7 @@ const ENDPOINT = `https://${SHOP}/admin/api/2024-10/graphql.json`;
const SITEMAP = 'https://www.innovationsusa.com/sitemap.xml';
if (!TOKEN) { console.error('Missing SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
const sleep = ms => new Promise(r => setTimeout(r, ms));
-const DRY = process.argv.includes('--dry-run');
+const DRY = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
async function gql(q, v = {}) {
for (let a = 0; a < 6; a++) {
diff --git a/shopify/scripts/reconcile/fix-live-shopify-product-type.js b/shopify/scripts/reconcile/fix-live-shopify-product-type.js
index 46ab6b78..0cb9c932 100644
--- a/shopify/scripts/reconcile/fix-live-shopify-product-type.js
+++ b/shopify/scripts/reconcile/fix-live-shopify-product-type.js
@@ -27,7 +27,7 @@
const fs = require('fs');
const path = require('path');
-const DRY = process.argv.includes('--dry');
+const DRY = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
const API = '2024-10';
diff --git a/shopify/scripts/sandberg-add-bolt-variants.js b/shopify/scripts/sandberg-add-bolt-variants.js
index ed1ceca1..7aaad952 100644
--- a/shopify/scripts/sandberg-add-bolt-variants.js
+++ b/shopify/scripts/sandberg-add-bolt-variants.js
@@ -99,7 +99,7 @@ async function processOne(row, dry) {
}
async function run() {
- const dry = process.argv.includes('--dry-run');
+ const dry = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
console.log(`${dry ? '[DRY RUN] ' : ''}Adding bolt variants to ${ROLLS.length} Sandberg products.`);
console.log(` option: "${OPTION_NAME}" = "${BOLT_OPTION_VALUE}"`);
console.log(` weight: ${TARGET_WEIGHT_LB} lb`);
diff --git a/shopify/scripts/sandberg-add-mural-variants.js b/shopify/scripts/sandberg-add-mural-variants.js
index 9fffef3b..2f02d8ec 100644
--- a/shopify/scripts/sandberg-add-mural-variants.js
+++ b/shopify/scripts/sandberg-add-mural-variants.js
@@ -70,7 +70,7 @@ async function processOne(row, dry) {
}
async function run() {
- const dry = process.argv.includes('--dry-run');
+ const dry = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
console.log(`${dry ? '[DRY RUN] ' : ''}Adding m² variants to ${ITEMS.length} Sandberg murals.`);
const results = [];
diff --git a/shopify/scripts/sandberg-william-morris-weight-4lb.js b/shopify/scripts/sandberg-william-morris-weight-4lb.js
index b2e09201..8d15329c 100644
--- a/shopify/scripts/sandberg-william-morris-weight-4lb.js
+++ b/shopify/scripts/sandberg-william-morris-weight-4lb.js
@@ -116,7 +116,7 @@ async function run() {
fs.writeFileSync(path.join(outDir, `sandberg-wm-plan-${stamp}.json`), JSON.stringify({ bolts, sampleOnly }, null, 2));
- if (process.argv.includes('--dry-run')) {
+ if ((process.argv.includes('--dry-run') || !process.argv.includes('--apply'))) {
console.log('Dry run — no updates made. Plan written.');
return;
}
diff --git a/shopify/scripts/sandberg-wm-add-wallcovering-title.js b/shopify/scripts/sandberg-wm-add-wallcovering-title.js
index 3591fba2..9efd531f 100644
--- a/shopify/scripts/sandberg-wm-add-wallcovering-title.js
+++ b/shopify/scripts/sandberg-wm-add-wallcovering-title.js
@@ -89,7 +89,7 @@ async function run() {
fs.writeFileSync(path.join(outDir, `sandberg-wm-title-plan-${stamp}.json`), JSON.stringify({ plan, skipped }, null, 2));
- if (process.argv.includes('--dry-run')) {
+ if ((process.argv.includes('--dry-run') || !process.argv.includes('--apply'))) {
console.log('[title] Dry run — no updates made.');
if (plan.length) console.log('[title] Sample:', plan.slice(0, 5));
return;
diff --git a/shopify/scripts/thibaut-disco-draft.js b/shopify/scripts/thibaut-disco-draft.js
index 5729a6e9..2b8c8a53 100644
--- a/shopify/scripts/thibaut-disco-draft.js
+++ b/shopify/scripts/thibaut-disco-draft.js
@@ -16,7 +16,7 @@ const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
const ENDPOINT = `https://${SHOP}/admin/api/2024-10/graphql.json`;
if (!TOKEN) { console.error('Missing SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
const sleep = ms => new Promise(r => setTimeout(r, ms));
-const DRY = process.argv.includes('--dry-run');
+const DRY = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
const LIM = parseInt((process.argv.find(a=>a.startsWith('--limit='))||'--limit=0').split('=')[1]) || 0;
async function gql(q, v = {}) {
diff --git a/shopify/scripts/thibaut-photo-fix.js b/shopify/scripts/thibaut-photo-fix.js
index 5fc71dff..78f43049 100644
--- a/shopify/scripts/thibaut-photo-fix.js
+++ b/shopify/scripts/thibaut-photo-fix.js
@@ -10,6 +10,7 @@
* present does it delete the recorded low-res media ids (so the
* 2520 becomes the sole / featured image). Safe + idempotent.
* --dry-run with either pass: report, no writes.
+ * Default run-mode is DRY (2026-09-14): this script is DRY BY DEFAULT and no longer acts without --apply. Pass --apply to WRITE; --dry-run is now the default and is accepted but ignored.
*/
const fs = require('fs');
const path = require('path');
@@ -129,7 +130,7 @@ async function doPrune(dry) {
}
(async () => {
- const dry = process.argv.includes('--dry-run');
+ const dry = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
if (process.argv.includes('--prune')) await doPrune(dry);
else await doAdd(dry); // default = add
})().catch(e => { console.error('\nFATAL', e); process.exit(1); });
diff --git a/shopify/scripts/thibaut-spec-backfill.js b/shopify/scripts/thibaut-spec-backfill.js
index dc69e0f8..d1ea730d 100644
--- a/shopify/scripts/thibaut-spec-backfill.js
+++ b/shopify/scripts/thibaut-spec-backfill.js
@@ -12,7 +12,7 @@ const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
const ENDPOINT = `https://${SHOP}/admin/api/2024-10/graphql.json`;
if (!TOKEN) { console.error('Missing SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
const sleep = ms => new Promise(r => setTimeout(r, ms));
-const DRY = process.argv.includes('--dry-run');
+const DRY = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
async function gql(q, v = {}) {
for (let a = 0; a < 6; a++) {
diff --git a/shopify/scripts/vendor-disco-draft.js b/shopify/scripts/vendor-disco-draft.js
index d567a6fa..9bac3045 100644
--- a/shopify/scripts/vendor-disco-draft.js
+++ b/shopify/scripts/vendor-disco-draft.js
@@ -15,7 +15,7 @@ const ENDPOINT = `https://${SHOP}/admin/api/2024-10/graphql.json`;
if (!TOKEN) { console.error('Missing SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
const sleep = ms => new Promise(r => setTimeout(r, ms));
const arg = k => (process.argv.find(a => a.startsWith(`--${k}=`)) || '').split('=').slice(1).join('=');
-const VENDOR = arg('vendor'), TABLE = arg('table'), DRY = process.argv.includes('--dry-run');
+const VENDOR = arg('vendor'), TABLE = arg('table'), DRY = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
if (!VENDOR || !TABLE) { console.error('need --vendor and --table'); process.exit(1); }
async function gql(q, v = {}) {
diff --git a/shopify/scripts/wm-add-bolt-variants.js b/shopify/scripts/wm-add-bolt-variants.js
index 7d9c954d..f1221106 100644
--- a/shopify/scripts/wm-add-bolt-variants.js
+++ b/shopify/scripts/wm-add-bolt-variants.js
@@ -60,7 +60,7 @@ async function processOne(row, dry) {
}
async function run() {
- const dry = process.argv.includes('--dry-run');
+ const dry = (process.argv.includes('--dry-run') || !process.argv.includes('--apply'));
console.log(`${dry ? '[DRY] ' : ''}Adding Bolt variants to ${TARGETS.length} William Morris products at $${PRICE_USD}`);
const results = [];
for (const row of TARGETS) {
← 2fafce86 fix(review): 6 code-review findings — Versa color, price-gat
·
back to Designer Wallcoverings
·
auto-data-snapshot: 2026-09-14T15:41:36 (3 data files) — sho 57ba59ec →