← back to Naturaltextilewallpaper
snapshot before restart: preserve in-flight work (auto-saved by /restart pre-reboot)
3b83be04c2535290a73b80997469ed6a50eecaab · 2026-08-17 06:52:34 -0700 · Steve
Files touched
A scripts/resync-images.mjs
Diff
commit 3b83be04c2535290a73b80997469ed6a50eecaab
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Aug 17 06:52:34 2026 -0700
snapshot before restart: preserve in-flight work (auto-saved by /restart pre-reboot)
---
scripts/resync-images.mjs | 100 ++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 100 insertions(+)
diff --git a/scripts/resync-images.mjs b/scripts/resync-images.mjs
new file mode 100644
index 0000000..7d04ab2
--- /dev/null
+++ b/scripts/resync-images.mjs
@@ -0,0 +1,100 @@
+// resync-images.mjs — safe, verify-before-write re-sync of stale microsite_products
+// image_url for a microsite slug (default naturaltextilewallpaper).
+//
+// Steve-approved (TK-10371, yoloforever). Canonical dw_unified write on Kamatera.
+//
+// SAFE by construction:
+// - Only UPDATEs a row when the CURRENT microsite image is genuinely broken (non-200
+// at the raw CDN) AND the shopify_products replacement (same handle) resolves 200
+// AND differs. Never touches a working image (no regressions).
+// - DRY_RUN by default (reads + probes only). Pass --apply to write.
+// - Every applied change (handle, sku, before, after) is written to
+// data/resync-images-<ts>.json for reversibility BEFORE the transaction commits.
+//
+// Run ON Kamatera from the app dir (peer-auth socket, app's own pg):
+// node scripts/resync-images.mjs # dry-run report
+// node scripts/resync-images.mjs --apply # apply the safe subset
+import pg from 'pg';
+import fs from 'fs';
+import path from 'path';
+
+const SLUG = process.env.SLUG || 'naturaltextilewallpaper';
+const APPLY = process.argv.includes('--apply');
+const CONC = 24;
+const TS = new Date().toISOString().replace(/[:.]/g, '-');
+
+const pool = new pg.Pool(process.env.MICROSITE_DB_URL
+ ? { connectionString: process.env.MICROSITE_DB_URL }
+ : { host: process.env.PGHOST || '/var/run/postgresql', database: 'dw_unified' });
+
+async function resolves(url) {
+ if (!url || !/^https?:\/\//.test(url)) return false;
+ try {
+ const r = await fetch(url, { method: 'GET', redirect: 'follow', signal: AbortSignal.timeout(15000) });
+ return r.ok; // 2xx
+ } catch { return false; }
+}
+
+async function mapLimit(items, limit, fn) {
+ const out = new Array(items.length); let i = 0;
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
+ while (i < items.length) { const idx = i++; out[idx] = await fn(items[idx], idx); }
+ }));
+ return out;
+}
+
+(async () => {
+ const { rows } = await pool.query(
+ `SELECT m.sku, m.handle, m.image_url AS micro, s.image_url AS shop
+ FROM microsite_products m
+ LEFT JOIN shopify_products s ON s.handle = m.handle
+ WHERE m.site_slug = $1`, [SLUG]);
+ console.log(`[resync] ${SLUG}: ${rows.length} rows loaded`);
+
+ // 1) find rows whose CURRENT microsite image is broken
+ const probed = await mapLimit(rows, CONC, async (r) => ({ ...r, microOk: await resolves(r.micro) }));
+ const broken = probed.filter(r => !r.microOk);
+ console.log(`[resync] broken microsite images: ${broken.length} / ${rows.length}`);
+
+ // 2) of broken, keep those where the shopify replacement RESOLVES and differs
+ const withCand = await mapLimit(broken, CONC, async (r) => ({ ...r, shopOk: r.shop && r.shop !== r.micro ? await resolves(r.shop) : false }));
+ const fixable = withCand.filter(r => r.shopOk);
+ const unfixable = withCand.filter(r => !r.shopOk);
+ console.log(`[resync] FIXABLE (broken→working replacement): ${fixable.length}`);
+ console.log(`[resync] UNFIXABLE (no working shopify image, needs upstream refresh): ${unfixable.length}`);
+
+ // report a few examples
+ for (const r of fixable.slice(0, 5)) console.log(` FIX ${r.handle}`);
+ for (const r of unfixable.slice(0, 5)) console.log(` SKIP ${r.handle} (shop also broken/missing)`);
+
+ const outDir = path.join(process.cwd(), 'data');
+ const report = {
+ ts: TS, slug: SLUG, apply: APPLY, total: rows.length,
+ broken: broken.length, fixable: fixable.length, unfixable: unfixable.length,
+ changes: fixable.map(r => ({ handle: r.handle, sku: r.sku, before: r.micro, after: r.shop })),
+ unfixable_handles: unfixable.map(r => r.handle),
+ };
+ fs.writeFileSync(path.join(outDir, `resync-images-${TS}.json`), JSON.stringify(report, null, 2));
+ console.log(`[resync] report → data/resync-images-${TS}.json`);
+
+ if (!APPLY) { console.log('[resync] DRY-RUN — no writes. Re-run with --apply to write the FIXABLE subset.'); await pool.end(); return; }
+ if (!fixable.length) { console.log('[resync] nothing to apply.'); await pool.end(); return; }
+
+ const client = await pool.connect();
+ try {
+ await client.query('BEGIN');
+ let n = 0;
+ for (const r of fixable) {
+ const res = await client.query(
+ `UPDATE microsite_products SET image_url=$1 WHERE site_slug=$2 AND sku=$3 AND image_url=$4`,
+ [r.shop, SLUG, r.sku, r.micro]);
+ n += res.rowCount;
+ }
+ await client.query('COMMIT');
+ console.log(`[resync] APPLIED ${n} image_url updates (transaction committed).`);
+ } catch (e) {
+ await client.query('ROLLBACK');
+ console.error(`[resync] ROLLBACK — ${e.message}`);
+ process.exitCode = 1;
+ } finally { client.release(); await pool.end(); }
+})();
← fc783a8 auto-data-snapshot: 2026-08-09T17:28:38 (1 data files) — dat
·
back to Naturaltextilewallpaper
·
TK-11341: add AdSense Auto-Ads loader + ads.txt (revert to r d032b78 →