← back to Reidwitlin Landing
scripts/backfill-source-images.js
64 lines
#!/usr/bin/env node
/**
* DB-free standalone patcher: apply the source-image backfill to the CURRENT
* data/products.json (the full rebuild reads the gated Kamatera DB; this runs the
* SAME lib against the on-disk snapshot). Handles the own-photo-or-draft model:
* applies to products[] AND drafts[], clears image_note on any that get a real
* photo, then re-partitions so newly-photo'd drafts auto-restore to published, and
* recomputes facets. build-rwltd-data.js runs the same lib, so a rebuild preserves
* this. Writes a .bak first. $0 — public feed. Idempotent (safe to re-run).
*/
const fs = require('fs');
const path = require('path');
const { backfillFromSourceFeed } = require('../lib/backfill-source-images');
const OUT = path.join(__dirname, '..', 'data', 'products.json');
const BUCKET_ORDER = ['white','grey','black','pink','red','orange','brown','gold','green','teal','blue','purple'];
(async () => {
const snap = JSON.parse(fs.readFileSync(OUT, 'utf8'));
snap.drafts = snap.drafts || [];
// Apply to both arrays (a drafted colorway getting a photo must be able to restore).
const r1 = await backfillFromSourceFeed(snap.products);
// Feed-sanity guard: a bot-block/empty 200 returns ~0 patterns. Bail BEFORE the
// re-partition rather than silently re-draft (Contrarian catch, 2026-07-07).
if (r1.sourcePatterns < 50) { console.error(`ABORT: source feed suspicious (${r1.sourcePatterns} patterns) — not repartitioning`); process.exit(1); }
const r2 = await backfillFromSourceFeed(snap.drafts);
const backfilled = r1.backfilled + r2.backfilled;
const conflicts = [...r1.conflicts, ...r2.conflicts];
// Re-partition: anything with a real photo (no image_note) is published; rest drafted.
const all = [...snap.products, ...snap.drafts];
snap.products = all.filter(p => !p.image_note);
snap.drafts = all.filter(p => p.image_note);
snap.count = snap.products.length;
snap.drafted_no_own_photo = snap.drafts.length;
// Recompute facets from the NEW published set (the partition changed which are live).
const bookCounts = {}, seriesCounts = {}, colorCounts = {};
for (const p of snap.products) {
if (p.book) bookCounts[p.book] = (bookCounts[p.book] || 0) + 1;
if (p.series) seriesCounts[p.series] = (seriesCounts[p.series] || 0) + 1;
if (p.color_bucket) colorCounts[p.color_bucket] = (colorCounts[p.color_bucket] || 0) + 1;
}
snap.facets = {
total: snap.products.length,
books: Object.entries(bookCounts).sort((a, b) => b[1] - a[1]),
series: Object.entries(seriesCounts).sort((a, b) => b[1] - a[1]).slice(0, 300),
colors: BUCKET_ORDER.filter(b => colorCounts[b]).map(b => [b, colorCounts[b]]),
};
console.log(`backfilled ${backfilled} colorways | published ${snap.products.length} | drafted ${snap.drafts.length} | facets.total ${snap.facets.total}`);
if (conflicts.length) {
console.log(`${conflicts.length} already-imaged differ from source (NOT auto-changed — review):`);
for (const c of conflicts) console.log(` ${c.handle} (${c.color}): ours=${c.ours} source=${c.source}`);
}
const bak = `${OUT}.${new Date().toISOString().replace(/[:.]/g, '-')}.bak`;
fs.copyFileSync(OUT, bak);
snap.image_backfilled_at = new Date().toISOString();
fs.writeFileSync(OUT, JSON.stringify(snap));
console.log(`backup -> ${path.basename(bak)} | products.json rewritten`);
})().catch(e => { console.error(e); process.exit(1); });