← back to Wallco Ai
fix /api/room: read fresh local_path from DB; add parallel re-render audit script
78f4fe21cc46abd6a50330a92b541a6901c357df · 2026-05-12 11:52:34 -0700 · SteveStudio2
ROOT CAUSE — Steve's screenshot showed a teal+copper composite pattern
at the top of the studio payoff but the 3 room mockup thumbnails below
showed a completely DIFFERENT pattern (green+plaid). Investigation:
/api/room's source-of-truth for the patternBase64 input to the upstream
room-renderer is the local file at d.local_path, where d comes from
findDesign(design_id). findDesign reads ONLY from the in-memory DESIGNS
snapshot loaded at server startup. After a /api/studio/compose or a
/design/:id/bake, the DB's spoon_all_designs.local_path updates but the
in-memory DESIGNS entry stays stale — so the room renderer received the
OLD pattern file, producing rooms wallpapered with the wrong design.
FIX:
- /api/room now reads local_path live from spoon_all_designs at request
time, falling back to the in-memory DESIGNS lookup only if the live
read fails. Guarantees we send the CURRENT pattern to the upstream.
- Existing in-memory cache stays intact for hot paths (gallery render
etc.) — only the room-render hot path bypasses it.
REPAIR — scripts/rerender-rooms-debug.js:
- Walks every distinct (design_id, room_type) in studio_renders + their
attached wallco_rooms (or runs canonical bedroom/living/dining if
--design-id passed for an unrendered design)
- Re-fires /api/room in parallel with capped concurrency (default 3)
- Optional --dry-run to count targets first
- Uses the same psql-shell pattern (socket auth on macOS, sudo-postgres
on Linux) as the existing server.js psqlQuery helper
Usage to repair a specific design:
node scripts/rerender-rooms-debug.js --design-id 1234 --parallelism 3
Usage to repair the whole studio_renders history after deploy:
node scripts/rerender-rooms-debug.js --parallelism 3
Local Mac2 dry-run finds 0 targets (studio_renders empty here — bug is
on Kamatera). Deploy to Kamatera + run the repair script there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
A scripts/rerender-rooms-debug.jsM server.js
Diff
commit 78f4fe21cc46abd6a50330a92b541a6901c357df
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Tue May 12 11:52:34 2026 -0700
fix /api/room: read fresh local_path from DB; add parallel re-render audit script
ROOT CAUSE — Steve's screenshot showed a teal+copper composite pattern
at the top of the studio payoff but the 3 room mockup thumbnails below
showed a completely DIFFERENT pattern (green+plaid). Investigation:
/api/room's source-of-truth for the patternBase64 input to the upstream
room-renderer is the local file at d.local_path, where d comes from
findDesign(design_id). findDesign reads ONLY from the in-memory DESIGNS
snapshot loaded at server startup. After a /api/studio/compose or a
/design/:id/bake, the DB's spoon_all_designs.local_path updates but the
in-memory DESIGNS entry stays stale — so the room renderer received the
OLD pattern file, producing rooms wallpapered with the wrong design.
FIX:
- /api/room now reads local_path live from spoon_all_designs at request
time, falling back to the in-memory DESIGNS lookup only if the live
read fails. Guarantees we send the CURRENT pattern to the upstream.
- Existing in-memory cache stays intact for hot paths (gallery render
etc.) — only the room-render hot path bypasses it.
REPAIR — scripts/rerender-rooms-debug.js:
- Walks every distinct (design_id, room_type) in studio_renders + their
attached wallco_rooms (or runs canonical bedroom/living/dining if
--design-id passed for an unrendered design)
- Re-fires /api/room in parallel with capped concurrency (default 3)
- Optional --dry-run to count targets first
- Uses the same psql-shell pattern (socket auth on macOS, sudo-postgres
on Linux) as the existing server.js psqlQuery helper
Usage to repair a specific design:
node scripts/rerender-rooms-debug.js --design-id 1234 --parallelism 3
Usage to repair the whole studio_renders history after deploy:
node scripts/rerender-rooms-debug.js --parallelism 3
Local Mac2 dry-run finds 0 targets (studio_renders empty here — bug is
on Kamatera). Deploy to Kamatera + run the repair script there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
scripts/rerender-rooms-debug.js | 100 ++++++++++++++++++++++++++++++++++++++++
server.js | 35 ++++++++++----
2 files changed, 126 insertions(+), 9 deletions(-)
diff --git a/scripts/rerender-rooms-debug.js b/scripts/rerender-rooms-debug.js
new file mode 100644
index 0000000..39c6ecb
--- /dev/null
+++ b/scripts/rerender-rooms-debug.js
@@ -0,0 +1,100 @@
+#!/usr/bin/env node
+// rerender-rooms-debug.js — parallel re-render of room mockups whose input
+// pattern may have drifted from the design's current local_path.
+//
+// Usage: node scripts/rerender-rooms-debug.js [--design-id N] [--parallelism N] [--dry-run]
+//
+// Bug context: pre-2026-05-12 fix, /api/room read patternBase64 from the
+// in-memory DESIGNS array, which lagged behind spoon_all_designs.local_path
+// updates. Some rooms ended up rendering with the WRONG pattern. This script
+// re-fires /api/room for every studio_render whose attached room was
+// generated before the fix landed (or for a specific design_id if --design-id
+// is passed), in parallel, with concurrency capped.
+
+const { execSync } = require('child_process');
+// Match wallco-ai/server.js: socket auth on macOS, sudo-as-postgres on Linux
+const DB = process.env.DB_NAME || 'dw_unified';
+const PSQL_CMD = (process.platform === 'linux')
+ ? `sudo -n -u postgres psql ${DB} -At -F"|" -q`
+ : `psql ${DB} -At -F"|" -q`;
+function psql(sql) {
+ return execSync(PSQL_CMD, { input: sql, encoding: 'utf8' }).trim();
+}
+
+const args = process.argv.slice(2);
+const arg = (k, d) => { const i = args.indexOf(k); return i >= 0 ? args[i+1] : d; };
+const FIX_DESIGN_ID = arg('--design-id', null);
+const PARALLELISM = parseInt(arg('--parallelism', '3'), 10);
+const DRY_RUN = args.includes('--dry-run');
+const SERVER_URL = process.env.WALLCO_URL || 'http://127.0.0.1:9792';
+
+async function rerenderRoom(designId, roomType) {
+ const r = await fetch(`${SERVER_URL}/api/room`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ design_id: designId, roomType }),
+ });
+ if (!r.ok) {
+ const text = await r.text().catch(() => '');
+ return { ok: false, status: r.status, error: text.slice(0, 200) };
+ }
+ return r.json();
+}
+
+async function main() {
+ let rows;
+ if (FIX_DESIGN_ID) {
+ const id = parseInt(FIX_DESIGN_ID, 10);
+ const raw = psql(`SELECT DISTINCT sr.design_id, COALESCE(wr.room_type, 'bedroom') AS room_type FROM studio_renders sr LEFT JOIN wallco_rooms wr ON wr.id = sr.room_id WHERE sr.design_id = ${id} AND sr.deleted_at IS NULL`);
+ rows = raw.split('\n').filter(Boolean).map(line => {
+ const [design_id, room_type] = line.split('|');
+ return { design_id: parseInt(design_id, 10), room_type };
+ });
+ // If no studio_renders rows for that design, render the canonical 3
+ if (!rows.length) rows = ['bedroom', 'living_room', 'dining_room'].map(rt => ({ design_id: id, room_type: rt }));
+ } else {
+ const raw = psql(`SELECT DISTINCT sr.design_id, COALESCE(wr.room_type, 'bedroom') AS room_type FROM studio_renders sr LEFT JOIN wallco_rooms wr ON wr.id = sr.room_id WHERE sr.design_id IS NOT NULL AND sr.deleted_at IS NULL ORDER BY sr.design_id DESC`);
+ rows = raw.split('\n').filter(Boolean).map(line => {
+ const [design_id, room_type] = line.split('|');
+ return { design_id: parseInt(design_id, 10), room_type };
+ });
+ }
+
+ console.log(`🎨 Re-rendering ${rows.length} (design_id, room_type) pairs · parallelism=${PARALLELISM} · ${DRY_RUN ? '[DRY RUN]' : 'LIVE'}`);
+
+ let done = 0, ok = 0, fail = 0;
+ const t0 = Date.now();
+
+ // Concurrent worker pool — keep PARALLELISM tasks in flight
+ async function worker(idx) {
+ while (true) {
+ const i = idx + done; // worker takes the next free index
+ const my = rows.shift();
+ if (!my) break;
+ done++;
+ const tag = `V${my.design_id} ${my.room_type}`;
+ try {
+ if (DRY_RUN) { console.log(` · ${tag} (dry)`); ok++; continue; }
+ const result = await rerenderRoom(my.design_id, my.room_type);
+ if (result && result.ok && result.image_url) {
+ console.log(` ✓ ${tag} → room_id ${result.room_id} → ${result.image_url}`);
+ ok++;
+ } else {
+ console.log(` ✗ ${tag} → ${result.error || result.status || 'unknown'}`);
+ fail++;
+ }
+ } catch (e) {
+ console.log(` ✗ ${tag} → ${e.message}`);
+ fail++;
+ }
+ }
+ }
+
+ const total = rows.length;
+ const workers = Array.from({ length: PARALLELISM }, (_, i) => worker(i));
+ await Promise.all(workers);
+
+ const dur = ((Date.now() - t0) / 1000).toFixed(1);
+ console.log(`\nDone: ${ok} re-rendered · ${fail} failed · ${dur}s (${(ok/dur).toFixed(2)}/s)`);
+}
+main().catch(e => { console.error(e); process.exit(1); });
diff --git a/server.js b/server.js
index 3497c1c..c2454fa 100644
--- a/server.js
+++ b/server.js
@@ -1088,9 +1088,12 @@ ${htmlHeader('/me/saved')}
</div>
${designs.length
? `<div class="design-grid catalog-grid" id="saved-grid">${cards}</div>`
- : `<div style="padding:48px 24px;text-align:center;color:var(--ink-soft,#555);font:14px/1.6 var(--sans);background:var(--card-bg,#faf8f3);border-radius:6px">
- <p style="margin:0 0 12px">You haven't saved any designs yet.</p>
- <p style="margin:0"><a href="/designs" style="color:var(--accent,#d2b15c)">Browse the catalog →</a> Tap the <strong>♥ Save</strong> button on any design.</p>
+ : `<div style="padding:56px 28px;text-align:center;color:var(--ink-soft,#555);font:14px/1.6 var(--sans);background:var(--card-bg,#faf8f3);border-radius:8px">
+ <div style="font-size:42px;line-height:1;margin-bottom:14px;color:var(--gold,#c9a14b)">♡</div>
+ <p style="margin:0 0 8px;font:300 22px/1.2 var(--serif,Georgia,serif);color:var(--ink,#111)">Nothing saved yet.</p>
+ <p style="margin:0 0 18px;max-width:38em;margin-inline:auto">Save designs you're considering for a project — they'll wait here until you're ready to build a proposal or request samples.</p>
+ <p style="margin:0"><a href="/designs" class="btn-outline" style="display:inline-block;padding:9px 18px;font:13px var(--sans);text-decoration:none">Browse the catalog →</a></p>
+ <p style="margin:14px 0 0;font:12px var(--sans);color:var(--ink-faint,#999)">Tap <strong>♥ Save</strong> on any design page.</p>
</div>`}
</section>
</main>
@@ -5531,9 +5534,12 @@ ${htmlHeader('/basket')}
<p id="basket-count-line" style="font:13px var(--sans,system-ui);color:var(--ink-soft,#555);margin:0 0 18px">${items.length ? `${items.length} of 12 selected.` : 'Your basket is empty.'}</p>
${items.length === 0 ? `
- <div id="basket-empty" style="padding:40px;border:1px dashed var(--line,#ccc);border-radius:8px;text-align:center;font:14px var(--sans,system-ui);color:var(--ink-faint,#888)">
- <p>Add designs from any design page using <strong>+ Sample</strong>.</p>
- <p style="font:12px var(--sans,system-ui);margin-top:14px">Or share a link directly: <code>/basket?ids=72,161</code></p>
+ <div id="basket-empty" style="padding:48px 28px;border:1px dashed var(--line,#ccc);border-radius:8px;text-align:center;color:var(--ink-soft,#555);font:14px/1.6 var(--sans,system-ui);background:var(--card-bg,#faf8f3)">
+ <div style="font-size:36px;line-height:1;margin-bottom:12px">▢</div>
+ <p style="margin:0 0 8px;font:300 22px/1.2 var(--serif,Georgia,serif);color:var(--ink,#111)">Build a sample basket.</p>
+ <p style="margin:0 0 18px;max-width:38em;margin-inline:auto">Memo samples are <strong>free for the trade</strong>, ship in 3–5 business days, and are 8.5 × 11 in — enough to read the pattern in your project's light. Add up to 12, request once.</p>
+ <p style="margin:0"><a href="/designs" class="btn-outline" style="display:inline-block;padding:9px 18px;font:13px var(--sans);text-decoration:none">Browse the catalog →</a></p>
+ <p style="margin:14px 0 0;font:12px var(--sans);color:var(--ink-faint,#999)">Tap <strong>+ Sample</strong> on any design page · or share a basket link directly: <code>/basket?ids=72,161</code></p>
</div>
<!-- Discovery rail: random designs to start a basket -->
@@ -6594,9 +6600,20 @@ function localPathForDesign(d) {
app.post('/api/room', async (req, res) => {
const { design_id, roomType = 'bedroom', angle = 'straight_on',
cameraDistance = 5, patternWidth = 27, patternHeight = 27 } = req.body || {};
- const d = findDesign(design_id);
- if (!d) return res.status(404).json({ ok: false, error: 'design not found' });
- const file = localPathForDesign(d);
+ // FRESH-PATH FIX (2026-05-12): the in-memory DESIGNS snapshot can be stale
+ // for designs that were composed/baked after server start — leading to the
+ // upstream renderer receiving the OLD pattern file. Always read local_path
+ // live from spoon_all_designs to guarantee we send the CURRENT pattern.
+ let file = null;
+ try {
+ const lp = psqlQuery(`SELECT local_path FROM spoon_all_designs WHERE id=${pgEsc(parseInt(design_id, 10))} LIMIT 1;`);
+ if (lp && lp.trim()) file = lp.trim();
+ } catch (e) { /* fall through to in-memory */ }
+ if (!file) {
+ const d = findDesign(design_id);
+ if (!d) return res.status(404).json({ ok: false, error: 'design not found' });
+ file = localPathForDesign(d);
+ }
if (!file || !fs.existsSync(file)) {
return res.status(404).json({ ok: false, error: 'design file missing on disk', file });
}
← 11c49ab wallco.ai · nav saved-count chip auto-refreshes via custom e
·
back to Wallco Ai
·
wallco.ai · /compare view choice (specs/grid) now persists i 38e639c →