← back to Designer Wallcoverings
auto-save: 2026-07-16T05:41:28 (4 files) — pending-approval/boost-filter-consolidation-2026-06-25 scripts/pr-contact-for-price vendor-scrapers/china-seas-refresh DW-Programming/refresh-broken-images.js
df470f6408f6e1e7dc5b53604ef6bf2e7f007230 · 2026-07-16 05:41:32 -0700 · Steve Abrams
Files touched
A DW-Programming/refresh-broken-images.js
Diff
commit df470f6408f6e1e7dc5b53604ef6bf2e7f007230
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 16 05:41:32 2026 -0700
auto-save: 2026-07-16T05:41:28 (4 files) — pending-approval/boost-filter-consolidation-2026-06-25 scripts/pr-contact-for-price vendor-scrapers/china-seas-refresh DW-Programming/refresh-broken-images.js
---
DW-Programming/refresh-broken-images.js | 67 +++++++++++++++++++++++++++++++++
1 file changed, 67 insertions(+)
diff --git a/DW-Programming/refresh-broken-images.js b/DW-Programming/refresh-broken-images.js
new file mode 100644
index 00000000..914089ae
--- /dev/null
+++ b/DW-Programming/refresh-broken-images.js
@@ -0,0 +1,67 @@
+#!/usr/bin/env node
+/**
+ * refresh-broken-images.js
+ *
+ * The color enricher quarantined ~1,913 products as 'no-palette' because their
+ * mirror image_url returned 404. Root cause: the live Shopify product HAS a
+ * valid featured image, but the dw_unified mirror kept a stale CDN URL (image
+ * re-uploaded/renamed on Shopify). This refreshes the current featured image
+ * from live Shopify (bulk GraphQL nodes()), writes it back to BOTH
+ * color_enrich_todo (clearing err + done_at so it re-enriches) and
+ * shopify_products (so future color-index builds get the good image), and
+ * reports any product that is genuinely imageless on Shopify (real gap).
+ *
+ * $0 (Shopify reads are free). Then re-run enrich-active-colors.js.
+ * node refresh-broken-images.js [--limit N]
+ */
+const https = require('https');
+const { Pool } = require('pg');
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+const STORE = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+const args = process.argv.slice(2);
+const LIMIT = parseInt(args.find((_, i, a) => a[i - 1] === '--limit') || '0', 10) || 0;
+const pool = new Pool({ connectionString: process.env.TODO_DB_URL || 'postgres://stevestudio2@/dw_unified?host=/tmp' });
+
+function gql(query) {
+ return new Promise((resolve, reject) => {
+ const body = JSON.stringify({ query });
+ const req = https.request({ hostname: STORE, path: `/admin/api/${API}/graphql.json`, method: 'POST',
+ headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': TOKEN, 'Content-Length': Buffer.byteLength(body) }, timeout: 30000 },
+ (res) => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(e); } }); });
+ req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
+ req.write(body); req.end();
+ });
+}
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+(async () => {
+ if (!TOKEN) { console.error('Missing SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+ const { rows } = await pool.query(
+ `SELECT DISTINCT numeric_id FROM color_enrich_todo WHERE err='no-palette' AND done_at IS NULL ${LIMIT ? 'LIMIT ' + LIMIT : ''}`);
+ console.log(`refresh-broken-images: ${rows.length} quarantined products`);
+ let refreshed = 0, stillBroken = 0, imageless = 0;
+ const BATCH = 40;
+ for (let i = 0; i < rows.length; i += BATCH) {
+ const batch = rows.slice(i, i + BATCH);
+ const ids = batch.map(r => `"gid://shopify/Product/${r.numeric_id}"`).join(',');
+ let data;
+ try { data = await gql(`{ nodes(ids:[${ids}]){ ... on Product { id featuredImage{ url } } } }`); }
+ catch (e) { console.log(` batch ${i} error: ${e.message}`); await sleep(2000); continue; }
+ if (data.errors && !data.data) { console.log(` batch ${i} throttled, backing off`); await sleep(3000); i -= BATCH; continue; }
+ for (const node of (data.data && data.data.nodes) || []) {
+ if (!node || !node.id) { continue; }
+ const numId = node.id.replace(/.*\//, '');
+ const url = node.featuredImage && node.featuredImage.url;
+ if (!url) { imageless++; await pool.query(`UPDATE color_enrich_todo SET err='imageless-on-shopify' WHERE numeric_id=$1`, [numId]); continue; }
+ // Fresh URL from live Shopify → write to both the queue (reset for re-enrich) and the mirror.
+ await pool.query(`UPDATE color_enrich_todo SET image_url=$2, err=NULL, done_at=NULL WHERE numeric_id=$1`, [numId, url]);
+ await pool.query(`UPDATE shopify_products SET image_url=$2 WHERE substring(shopify_id from '[0-9]+$')=$1`, [numId, url]);
+ refreshed++;
+ }
+ if ((i / BATCH) % 5 === 0) console.log(` ${Math.min(i + BATCH, rows.length)}/${rows.length} — refreshed ${refreshed}, imageless ${imageless}`);
+ await sleep(250);
+ }
+ console.log(`\nrefreshed ${refreshed} image URLs, ${imageless} genuinely imageless on Shopify, ${stillBroken} still broken.`);
+ await pool.end();
+})().catch(e => { console.error('FAILED:', e.message); pool.end(); process.exit(1); });
← a35017db auto-save: 2026-07-16T05:11:21 (4 files) — pending-approval/
·
back to Designer Wallcoverings
·
auto-save: 2026-07-16T07:11:48 (4 files) — DW-Programming/co 9ce2eb7e →