← 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); });