← back to Designer Wallcoverings
phase-3 image repair scripts: as_creation, bnwalls, missoni (verify-then-update, backup TSV first)
dcd602fb55f0b92a5350d672b0c692cc636bc72b · 2026-07-01 15:20:38 -0700 · Steve
Files touched
A shopify/scripts/repair-ascreation-images.jsA shopify/scripts/repair-bnwalls-images.jsA shopify/scripts/repair-missoni-images.js
Diff
commit dcd602fb55f0b92a5350d672b0c692cc636bc72b
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Jul 1 15:20:38 2026 -0700
phase-3 image repair scripts: as_creation, bnwalls, missoni (verify-then-update, backup TSV first)
---
shopify/scripts/repair-ascreation-images.js | 163 ++++++++++++++++++++++++++
shopify/scripts/repair-bnwalls-images.js | 171 ++++++++++++++++++++++++++++
shopify/scripts/repair-missoni-images.js | 154 +++++++++++++++++++++++++
3 files changed, 488 insertions(+)
diff --git a/shopify/scripts/repair-ascreation-images.js b/shopify/scripts/repair-ascreation-images.js
new file mode 100644
index 00000000..451fe153
--- /dev/null
+++ b/shopify/scripts/repair-ascreation-images.js
@@ -0,0 +1,163 @@
+#!/usr/bin/env node
+/**
+ * repair-ascreation-images.js — Phase-3 image repair for as_creation (2026-07-01)
+ *
+ * The stored cdn.as-creation.com ts-param image URLs expired (404). Product pages
+ * on products.as-creation.com are live and carry fresh og:image + gallery
+ * data-full-image URLs.
+ *
+ * Pattern (sanctioned vendor-scraper repair, DTD verdict A 2026-07-01):
+ * 1. SELECT as_creation_catalog rows joined to enrichment_tracking
+ * (phase3_ai_at IS NULL) with a product_url.
+ * 2. Backup old image_url/all_images to TSV BEFORE any write.
+ * 3. Fetch product_url, extract og:image (primary) + gallery data-full-image.
+ * 4. HEAD-verify every new URL is HTTP 200 image/* before UPDATE.
+ * 5. UPDATE ONLY image_url + all_images (+ last_scraped/updated_at).
+ * No Shopify writes. No product-state writes.
+ *
+ * Usage: node repair-ascreation-images.js [--dry-run] [--limit N]
+ */
+
+const https = require('https');
+const http = require('http');
+const fs = require('fs');
+const { Pool } = require('pg');
+
+const pool = new Pool({ connectionString: 'postgresql://dw_admin@127.0.0.1:5432/dw_unified' });
+
+const BACKUP = '/root/DW-Agents/logs/ascreation-imgrepair-backup-20260701.tsv';
+const DELAY_MS = 1200;
+const TIMEOUT_MS = 25000;
+const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36';
+
+const args = process.argv.slice(2);
+const DRY_RUN = args.includes('--dry-run');
+const LIMIT = parseInt(args[args.indexOf('--limit') + 1]) || 10000;
+
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+function request(url, method, redirectCount = 0) {
+ if (redirectCount > 5) return Promise.reject(new Error('Too many redirects'));
+ const mod = url.startsWith('https') ? https : http;
+ return new Promise((resolve, reject) => {
+ const req = mod.request(url, {
+ method,
+ headers: {
+ 'User-Agent': UA,
+ 'Accept': method === 'HEAD' ? '*/*' : 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ 'Accept-Language': 'en-US,en;q=0.9',
+ }
+ }, res => {
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
+ let loc = res.headers.location;
+ if (loc.startsWith('/')) { const u = new URL(url); loc = u.protocol + '//' + u.host + loc; }
+ res.resume();
+ return request(loc, method, redirectCount + 1).then(resolve, reject);
+ }
+ if (method === 'HEAD') {
+ res.resume();
+ return resolve({ status: res.statusCode, contentType: res.headers['content-type'] || '', body: '' });
+ }
+ let body = '';
+ res.on('data', d => body += d);
+ res.on('end', () => resolve({ status: res.statusCode, contentType: res.headers['content-type'] || '', body }));
+ });
+ req.on('error', reject);
+ req.setTimeout(TIMEOUT_MS, () => { req.destroy(); reject(new Error('timeout')); });
+ req.end();
+ });
+}
+
+async function headOk(url) {
+ try {
+ const r = await request(url, 'HEAD');
+ return r.status === 200 && /image\//i.test(r.contentType);
+ } catch (e) { return false; }
+}
+
+function extract(html) {
+ const flat = html.replace(/\s+/g, ' ');
+ const og = flat.match(/<meta\s+property="og:image"\s+content="(https:\/\/cdn\.as-creation\.com[^"]+)"/i);
+ const gallery = [];
+ const re = /data-full-image="(https:\/\/cdn\.as-creation\.com[^"]+)"/g;
+ let m;
+ while ((m = re.exec(flat)) !== null) if (!gallery.includes(m[1])) gallery.push(m[1]);
+ return { og: og ? og[1] : null, gallery };
+}
+
+async function run() {
+ console.log('=== AS Creation image repair', DRY_RUN ? '(DRY RUN)' : '', '— started', new Date().toISOString(), '===');
+
+ const { rows } = await pool.query(`
+ SELECT c.id, c.mfr_sku, c.product_url, c.image_url, c.all_images
+ FROM as_creation_catalog c
+ JOIN enrichment_tracking t
+ ON t.catalog_table = 'as_creation_catalog' AND t.mfr_sku = c.mfr_sku
+ WHERE t.phase3_ai_at IS NULL
+ AND c.product_url IS NOT NULL AND c.product_url <> ''
+ ORDER BY c.id
+ LIMIT $1`, [LIMIT]);
+
+ console.log('Selected ' + rows.length + ' backlog rows');
+ if (rows.length === 0) { await pool.end(); return; }
+
+ // Backup FIRST (all selected rows, before any write)
+ if (!DRY_RUN) {
+ const hdr = 'id\tmfr_sku\told_image_url\told_all_images\tbacked_up_at\n';
+ const lines = rows.map(r =>
+ [r.id, r.mfr_sku, r.image_url || '', JSON.stringify(r.all_images || null), new Date().toISOString()].join('\t')
+ ).join('\n') + '\n';
+ fs.writeFileSync(BACKUP, hdr + lines);
+ console.log('Backup written: ' + BACKUP + ' (' + rows.length + ' rows)');
+ }
+
+ let repaired = 0, skipped = 0, failed = 0;
+ for (let i = 0; i < rows.length; i++) {
+ const p = rows[i];
+ const tag = '[' + (i + 1) + '/' + rows.length + '] ' + p.mfr_sku;
+ try {
+ const page = await request(p.product_url, 'GET');
+ if (page.status !== 200 || page.body.length < 500) {
+ console.log(tag + ' SKIP — page HTTP ' + page.status);
+ skipped++; await sleep(DELAY_MS); continue;
+ }
+ const { og, gallery } = extract(page.body);
+ // Candidate order: og:image first, then gallery. Some pages carry a stale
+ // og:image (404) while gallery data-full-image assets are live — fall
+ // through to the first candidate that actually HEAD-verifies.
+ const candidates = [];
+ if (og) candidates.push(og);
+ for (const g of gallery) if (!candidates.includes(g)) candidates.push(g);
+ if (candidates.length === 0) { console.log(tag + ' SKIP — no og:image/gallery found'); skipped++; await sleep(DELAY_MS); continue; }
+
+ const verified = [];
+ for (const c of candidates) {
+ if (await headOk(c)) verified.push(c);
+ await sleep(200);
+ }
+ if (verified.length === 0) {
+ console.log(tag + ' SKIP — all ' + candidates.length + ' candidates failed HEAD-verify');
+ skipped++; await sleep(DELAY_MS); continue;
+ }
+
+ if (DRY_RUN) {
+ console.log(tag + ' DRY — would set image_url=' + verified[0] + ' (+' + (verified.length - 1) + ' gallery)');
+ repaired++;
+ } else {
+ const res = await pool.query(
+ 'UPDATE as_creation_catalog SET image_url=$1, all_images=$2, last_scraped=NOW(), updated_at=NOW() WHERE id=$3',
+ [verified[0], JSON.stringify(verified), p.id]);
+ if (res.rowCount === 1) { repaired++; console.log(tag + ' OK — ' + verified.length + ' verified imgs, primary=' + verified[0].slice(0, 90)); }
+ else { failed++; console.log(tag + ' FAIL — UPDATE matched 0 rows'); }
+ }
+ } catch (e) {
+ failed++; console.log(tag + ' ERROR — ' + e.message);
+ }
+ await sleep(DELAY_MS);
+ }
+
+ console.log('\n=== DONE: repaired=' + repaired + ' skipped=' + skipped + ' failed=' + failed + ' of ' + rows.length + ' — ' + new Date().toISOString() + ' ===');
+ await pool.end();
+}
+
+run().catch(e => { console.error('Fatal:', e); process.exit(1); });
diff --git a/shopify/scripts/repair-bnwalls-images.js b/shopify/scripts/repair-bnwalls-images.js
new file mode 100644
index 00000000..c62d95f2
--- /dev/null
+++ b/shopify/scripts/repair-bnwalls-images.js
@@ -0,0 +1,171 @@
+#!/usr/bin/env node
+/**
+ * repair-bnwalls-images.js — Phase-3 image repair for bnwalls (2026-07-01)
+ *
+ * Stored bn-walls.s3.eu-central-1.amazonaws.com image URLs return 403 (bucket
+ * locked down — verified 403 from Kamatera AND Mac2, with/without Referer/UA).
+ * Product pages on bnwalls.com return 200 with a Chrome UA (bot UA gets 403).
+ *
+ * Selection differs from enrich-bnwalls-images.js (which only selects EMPTY
+ * image_url): this selects phase-3 backlog rows whose image is the 403ing S3
+ * pattern. Extraction reuses its data-full / data-zoom / og:image logic.
+ *
+ * Pattern (sanctioned vendor-scraper repair, DTD verdict A 2026-07-01):
+ * backup TSV first → fetch page → extract candidates → HEAD-verify 200
+ * image/* → UPDATE image_url (+all_images) ONLY. No Shopify writes.
+ *
+ * NOTE: if the page's own image URLs still point at the locked S3 bucket,
+ * HEAD-verify fails and the row is SKIPPED (fail-closed, no write).
+ *
+ * Usage: node repair-bnwalls-images.js [--dry-run] [--limit N]
+ */
+
+const https = require('https');
+const http = require('http');
+const fs = require('fs');
+const { Pool } = require('pg');
+
+const pool = new Pool({ connectionString: 'postgresql://dw_admin@127.0.0.1:5432/dw_unified' });
+
+const BACKUP = '/root/DW-Agents/logs/bnwalls-imgrepair-backup-20260701.tsv';
+const DELAY_MS = 1200;
+const TIMEOUT_MS = 25000;
+const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36';
+
+const args = process.argv.slice(2);
+const DRY_RUN = args.includes('--dry-run');
+const LIMIT = parseInt(args[args.indexOf('--limit') + 1]) || 10000;
+
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+function request(url, method, redirectCount = 0) {
+ if (redirectCount > 5) return Promise.reject(new Error('Too many redirects'));
+ const mod = url.startsWith('https') ? https : http;
+ return new Promise((resolve, reject) => {
+ const req = mod.request(url, {
+ method,
+ headers: {
+ 'User-Agent': UA,
+ 'Accept': method === 'HEAD' ? '*/*' : 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ 'Accept-Language': 'en-US,en;q=0.9',
+ 'Referer': 'https://bnwalls.com/',
+ }
+ }, res => {
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
+ let loc = res.headers.location;
+ if (loc.startsWith('/')) { const u = new URL(url); loc = u.protocol + '//' + u.host + loc; }
+ res.resume();
+ return request(loc, method, redirectCount + 1).then(resolve, reject);
+ }
+ if (method === 'HEAD') {
+ res.resume();
+ return resolve({ status: res.statusCode, contentType: res.headers['content-type'] || '', body: '' });
+ }
+ let body = '';
+ res.on('data', d => body += d);
+ res.on('end', () => resolve({ status: res.statusCode, contentType: res.headers['content-type'] || '', body }));
+ });
+ req.on('error', reject);
+ req.setTimeout(TIMEOUT_MS, () => { req.destroy(); reject(new Error('timeout')); });
+ req.end();
+ });
+}
+
+async function headOk(url) {
+ try {
+ const r = await request(url, 'HEAD');
+ return r.status === 200 && /image\//i.test(r.contentType);
+ } catch (e) { return false; }
+}
+
+// Reuses enrich-bnwalls-images.js extraction: data-full > data-zoom > og:image
+function extract(html) {
+ const flat = html.replace(/\s+/g, ' ');
+ const set = new Set();
+ let m;
+ const fullRe = /data-full="(https:\/\/[^"]+)"/g;
+ while ((m = fullRe.exec(flat)) !== null) set.add(m[1]);
+ const zoomRe = /data-zoom="(https:\/\/[^"]+)"/g;
+ while ((m = zoomRe.exec(flat)) !== null) {
+ const id = m[1].match(/image-responsive\/(\d+)\//);
+ if (id && [...set].some(u => u.includes('image-responsive/' + id[1] + '/'))) continue;
+ set.add(m[1]);
+ }
+ const og = flat.match(/<meta[^>]*property="og:image"[^>]*content="(https:\/\/[^"]+)"/i)
+ || flat.match(/<meta[^>]*content="(https:\/\/[^"]+)"[^>]*property="og:image"/i);
+ if (og) set.add(og[1]);
+ return [...set].filter(u => !/logo|icon|favicon/i.test(u));
+}
+
+async function run() {
+ console.log('=== BN Walls image repair', DRY_RUN ? '(DRY RUN)' : '', '— started', new Date().toISOString(), '===');
+
+ const { rows } = await pool.query(`
+ SELECT c.id, c.mfr_sku, c.product_url, c.image_url, c.all_images
+ FROM bnwalls_catalog c
+ JOIN enrichment_tracking t
+ ON t.catalog_table = 'bnwalls_catalog' AND t.mfr_sku = c.mfr_sku
+ WHERE t.phase3_ai_at IS NULL
+ AND c.product_url IS NOT NULL AND c.product_url <> ''
+ AND c.image_url LIKE '%bn-walls.s3%'
+ ORDER BY c.id
+ LIMIT $1`, [LIMIT]);
+
+ console.log('Selected ' + rows.length + ' backlog rows (403ing S3 image pattern)');
+ if (rows.length === 0) { await pool.end(); return; }
+
+ if (!DRY_RUN) {
+ const hdr = 'id\tmfr_sku\told_image_url\told_all_images\tbacked_up_at\n';
+ const lines = rows.map(r =>
+ [r.id, r.mfr_sku, r.image_url || '', JSON.stringify(r.all_images || null), new Date().toISOString()].join('\t')
+ ).join('\n') + '\n';
+ fs.writeFileSync(BACKUP, hdr + lines);
+ console.log('Backup written: ' + BACKUP + ' (' + rows.length + ' rows)');
+ }
+
+ let repaired = 0, skipped = 0, failed = 0, deadImgPages = 0;
+ for (let i = 0; i < rows.length; i++) {
+ const p = rows[i];
+ const tag = '[' + (i + 1) + '/' + rows.length + '] ' + p.mfr_sku;
+ try {
+ // English page mirrors localized content
+ const url = p.product_url.replace(/\/(nl|de|fr)\/athome\//, '/athome/');
+ const page = await request(url, 'GET');
+ if (page.status !== 200 || page.body.length < 500) {
+ console.log(tag + ' SKIP — page HTTP ' + page.status);
+ skipped++; await sleep(DELAY_MS); continue;
+ }
+ const candidates = extract(page.body);
+ if (candidates.length === 0) { console.log(tag + ' SKIP — no image candidates on page'); skipped++; await sleep(DELAY_MS); continue; }
+
+ const verified = [];
+ for (const c of candidates) {
+ if (await headOk(c)) verified.push(c);
+ await sleep(200);
+ }
+ if (verified.length === 0) {
+ console.log(tag + ' SKIP — all ' + candidates.length + ' candidates failed HEAD-verify (dead S3)');
+ deadImgPages++; skipped++; await sleep(DELAY_MS); continue;
+ }
+
+ if (DRY_RUN) {
+ console.log(tag + ' DRY — would set image_url=' + verified[0] + ' (+' + (verified.length - 1) + ' more)');
+ repaired++;
+ } else {
+ const res = await pool.query(
+ 'UPDATE bnwalls_catalog SET image_url=$1, all_images=$2, last_scraped=NOW(), updated_at=NOW() WHERE id=$3',
+ [verified[0], JSON.stringify(verified), p.id]);
+ if (res.rowCount === 1) { repaired++; console.log(tag + ' OK — ' + verified.length + ' verified imgs'); }
+ else { failed++; console.log(tag + ' FAIL — UPDATE matched 0 rows'); }
+ }
+ } catch (e) {
+ failed++; console.log(tag + ' ERROR — ' + e.message);
+ }
+ await sleep(DELAY_MS);
+ }
+
+ console.log('\n=== DONE: repaired=' + repaired + ' skipped=' + skipped + ' (dead-img pages=' + deadImgPages + ') failed=' + failed + ' of ' + rows.length + ' — ' + new Date().toISOString() + ' ===');
+ await pool.end();
+}
+
+run().catch(e => { console.error('Fatal:', e); process.exit(1); });
diff --git a/shopify/scripts/repair-missoni-images.js b/shopify/scripts/repair-missoni-images.js
new file mode 100644
index 00000000..8316ea1f
--- /dev/null
+++ b/shopify/scripts/repair-missoni-images.js
@@ -0,0 +1,154 @@
+#!/usr/bin/env node
+/**
+ * repair-missoni-images.js — Phase-3 image repair for missoni (2026-07-01)
+ *
+ * scrape-missoni.js refreshed rows but wrote the OLD image scheme
+ * (/media/parati/wallcoverings06/SCAN_<num>.jpg — 404). Live scheme is
+ * /media/parati/<collection-slug>/<pattern-slug>/<num>.jpg (200 image/jpeg),
+ * discoverable from each product page's <img> tags (thumbnails carry a
+ * .300x300_q85_crop_upscale.jpg suffix; stripping it yields the full-size).
+ *
+ * Pattern (sanctioned vendor-scraper repair, DTD verdict A 2026-07-01):
+ * backup TSV first → fetch product_url → find the img matching this SKU's
+ * colorway number → HEAD-verify full-size 200 image/* → UPDATE image_url
+ * (+all_images incl. verified PHS_ room shot). Catalog only, no Shopify.
+ *
+ * Usage: node repair-missoni-images.js [--dry-run] [--limit N]
+ */
+
+const https = require('https');
+const http = require('http');
+const fs = require('fs');
+const { Pool } = require('pg');
+
+const pool = new Pool({ connectionString: 'postgresql://dw_admin@127.0.0.1:5432/dw_unified' });
+
+const BACKUP = '/root/DW-Agents/logs/missoni-imgrepair-backup-20260701.tsv';
+const HOST = 'https://www.missonihome-wallcoverings.com';
+const DELAY_MS = 1200;
+const TIMEOUT_MS = 25000;
+const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36';
+
+const args = process.argv.slice(2);
+const DRY_RUN = args.includes('--dry-run');
+const LIMIT = parseInt(args[args.indexOf('--limit') + 1]) || 10000;
+
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+function request(url, method, redirectCount = 0) {
+ if (redirectCount > 5) return Promise.reject(new Error('Too many redirects'));
+ const mod = url.startsWith('https') ? https : http;
+ return new Promise((resolve, reject) => {
+ const req = mod.request(url, {
+ method,
+ headers: { 'User-Agent': UA, 'Accept': '*/*', 'Accept-Language': 'en-US,en;q=0.9' }
+ }, res => {
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
+ let loc = res.headers.location;
+ if (loc.startsWith('/')) { const u = new URL(url); loc = u.protocol + '//' + u.host + loc; }
+ res.resume();
+ return request(loc, method, redirectCount + 1).then(resolve, reject);
+ }
+ if (method === 'HEAD') { res.resume(); return resolve({ status: res.statusCode, contentType: res.headers['content-type'] || '', body: '' }); }
+ let body = '';
+ res.on('data', d => body += d);
+ res.on('end', () => resolve({ status: res.statusCode, contentType: res.headers['content-type'] || '', body }));
+ });
+ req.on('error', reject);
+ req.setTimeout(TIMEOUT_MS, () => { req.destroy(); reject(new Error('timeout')); });
+ req.end();
+ });
+}
+
+async function headOk(url) {
+ try { const r = await request(url, 'HEAD'); return r.status === 200 && /image\//i.test(r.contentType); }
+ catch (e) { return false; }
+}
+
+async function run() {
+ console.log('=== Missoni image repair', DRY_RUN ? '(DRY RUN)' : '', '— started', new Date().toISOString(), '===');
+
+ const { rows } = await pool.query(`
+ SELECT c.id, c.mfr_sku, c.product_url, c.image_url, c.all_images
+ FROM missoni_catalog c
+ JOIN enrichment_tracking t
+ ON t.catalog_table = 'missoni_catalog' AND t.mfr_sku = c.mfr_sku
+ WHERE t.phase3_ai_at IS NULL
+ AND c.product_url IS NOT NULL AND c.product_url <> ''
+ ORDER BY c.id
+ LIMIT $1`, [LIMIT]);
+
+ console.log('Selected ' + rows.length + ' backlog rows');
+ if (rows.length === 0) { await pool.end(); return; }
+
+ if (!DRY_RUN) {
+ const hdr = 'id\tmfr_sku\told_image_url\told_all_images\tbacked_up_at\n';
+ const lines = rows.map(r =>
+ [r.id, r.mfr_sku, r.image_url || '', JSON.stringify(r.all_images || null), new Date().toISOString()].join('\t')
+ ).join('\n') + '\n';
+ fs.writeFileSync(BACKUP, hdr + lines);
+ console.log('Backup written: ' + BACKUP + ' (' + rows.length + ' rows)');
+ }
+
+ // Cache pages — many colorways share one product page
+ const pageCache = new Map();
+ let repaired = 0, skipped = 0, failed = 0;
+
+ for (let i = 0; i < rows.length; i++) {
+ const p = rows[i];
+ const tag = '[' + (i + 1) + '/' + rows.length + '] ' + p.mfr_sku;
+ const num = (p.mfr_sku.match(/(\d+)\s*$/) || [])[1];
+ if (!num) { console.log(tag + ' SKIP — no colorway number in SKU'); skipped++; continue; }
+
+ try {
+ let html = pageCache.get(p.product_url);
+ if (html === undefined) {
+ const page = await request(p.product_url, 'GET');
+ html = page.status === 200 ? page.body : null;
+ pageCache.set(p.product_url, html);
+ await sleep(DELAY_MS);
+ }
+ if (!html) { console.log(tag + ' SKIP — product page not 200'); skipped++; continue; }
+
+ // Find this colorway's parati image (thumb suffix stripped = full size).
+ // Newer collections: /media/parati/<coll>/<pattern>/<num>.jpg
+ // Older collections: /media/parati/<coll>/<pattern>/SCAN_<num>.jpg
+ const re = new RegExp('src="(/media/parati/[^"]*?(?:SCAN_)?' + num + '\\.jpg)(?:\\.[^"]*)?"', 'i');
+ const m = html.match(re);
+ if (!m) { console.log(tag + ' SKIP — no parati img for colorway ' + num + ' on page'); skipped++; continue; }
+ const primary = HOST + m[1];
+
+ if (!(await headOk(primary))) {
+ console.log(tag + ' SKIP — full-size failed HEAD-verify: ' + primary);
+ skipped++; await sleep(300); continue;
+ }
+
+ const all = [primary];
+ const phs = html.match(new RegExp('src="(/media/ambientazioni/[^"]*' + num + '[^"]*\\.jpg)"', 'i'));
+ if (phs) {
+ const phsUrl = HOST + phs[1];
+ if (await headOk(phsUrl)) all.push(phsUrl);
+ }
+
+ if (DRY_RUN) {
+ console.log(tag + ' DRY — would set image_url=' + primary + ' (' + all.length + ' imgs)');
+ repaired++;
+ } else {
+ const res = await pool.query(
+ 'UPDATE missoni_catalog SET image_url=$1, all_images=$2, last_scraped=NOW(), updated_at=NOW() WHERE id=$3',
+ [primary, JSON.stringify(all), p.id]);
+ if (res.rowCount === 1) { repaired++; console.log(tag + ' OK — ' + primary.slice(HOST.length)); }
+ else { failed++; console.log(tag + ' FAIL — UPDATE matched 0 rows'); }
+ }
+ await sleep(300);
+ } catch (e) {
+ failed++; console.log(tag + ' ERROR — ' + e.message);
+ await sleep(DELAY_MS);
+ }
+ }
+
+ console.log('\n=== DONE: repaired=' + repaired + ' skipped=' + skipped + ' failed=' + failed + ' of ' + rows.length + ' — ' + new Date().toISOString() + ' ===');
+ await pool.end();
+}
+
+run().catch(e => { console.error('Fatal:', e); process.exit(1); });
← 8da7b499 auto-save: 2026-07-01T15:07:02 (7 files) — DW-Programming/Im
·
back to Designer Wallcoverings
·
auto-save: 2026-07-01T15:37:13 (4 files) — DW-Programming/Im b57b199a →