← back to Wallco Ai

scripts/backfill-wallco-rooms.js

74 lines

#!/usr/bin/env node
/**
 * backfill-wallco-rooms — the /admin/rooms gallery reads the wallco_rooms table,
 * but it's only populated by the interactive "save room render" endpoint
 * (server.js ~19435). The BULK room mockups generated by
 * scripts/generate_room_mockups.py land in data/rooms/design_<id>_<room>.png +
 * the spoon_all_designs.room_mockups JSONB — and were NEVER inserted into
 * wallco_rooms. So the gallery showed ~200 of the ~1,400+ renders that exist.
 *
 * This scans data/rooms/design_*.png and inserts a wallco_rooms row for any not
 * already present (matched by image_path = /designs/room/<basename>, which is
 * the public static route that serves data/rooms/). Idempotent — re-runnable.
 * Tagged notes='backfill:data/rooms' so it's trivially reversible:
 *   DELETE FROM wallco_rooms WHERE notes='backfill:data/rooms';
 *
 * Usage:  node scripts/backfill-wallco-rooms.js [--dry]
 */
'use strict';
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const ROOT = path.resolve(__dirname, '..');
const ROOMS = path.join(ROOT, 'data', 'rooms');
const DRY = process.argv.includes('--dry');

// On prod (Linux) psql must run as the postgres role — root has no PG role
// (FATAL: role "root" does not exist). On Mac2 the local user owns dw_unified.
const PSQL = process.platform === 'linux' ? 'sudo -u postgres psql' : 'psql';
function psql(sql) {
  return execSync(`${PSQL} dw_unified -At -F'\x1f' -c ${JSON.stringify(sql)}`, {
    encoding: 'utf8', maxBuffer: 64 * 1024 * 1024,
  });
}
function esc(s) { return String(s).replace(/'/g, "''"); }

// 1) existing image_paths already in wallco_rooms (idempotency guard)
const existing = new Set(
  psql("SELECT image_path FROM wallco_rooms WHERE image_path IS NOT NULL;")
    .split('\n').map(s => s.trim()).filter(Boolean)
);

// 2) scan data/rooms/design_<id>_<room>.png — skip templates/masks
const files = fs.readdirSync(ROOMS).filter(f => /^design_\d+_.+\.png$/.test(f));
const rows = [];
let skipExisting = 0, skipParse = 0;
for (const f of files) {
  const m = f.match(/^design_(\d+)_(.+)\.png$/);
  if (!m) { skipParse++; continue; }
  const designId = parseInt(m[1], 10);
  const roomType = m[2];                       // e.g. living_room, bedroom, living_room_hero
  const imagePath = `/designs/room/${f}`;
  if (existing.has(imagePath)) { skipExisting++; continue; }
  let bytes = 0;
  try { bytes = fs.statSync(path.join(ROOMS, f)).size; } catch {}
  rows.push({ designId, roomType, filename: f, imagePath, bytes });
}

console.log(`data/rooms PNGs: ${files.length} · already in wallco_rooms: ${skipExisting} · unparseable: ${skipParse} · to insert: ${rows.length}`);
if (DRY) { console.log('--- DRY: no inserts. Sample:', rows.slice(0, 3)); process.exit(0); }
if (!rows.length) { console.log('nothing to backfill.'); process.exit(0); }

// 3) batched multi-row INSERT (chunks of 500)
let inserted = 0;
for (let i = 0; i < rows.length; i += 500) {
  const chunk = rows.slice(i, i + 500);
  const values = chunk.map(r =>
    `(${r.designId}, '${esc(r.roomType)}', '${esc(r.filename)}', '${esc(r.imagePath)}', ${r.bytes}, FALSE, 'backfill:data/rooms')`
  ).join(',');
  psql(`INSERT INTO wallco_rooms (design_id, room_type, filename, image_path, file_size_bytes, selected_for_marketing, notes) VALUES ${values};`);
  inserted += chunk.length;
  process.stdout.write(`  inserted ${inserted}/${rows.length}\r`);
}
console.log(`\n✓ backfilled ${inserted} room rows into wallco_rooms (tagged notes='backfill:data/rooms').`);