← back to Naturaltextilewallpaper
scripts/resync-images.mjs
101 lines
// 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(); }
})();