← back to Wallco Ai
fix(colorways/promote): designs.json fallback + nullable FK
8ae50314e35acd91f3ba6e195528769a3097a126 · 2026-05-29 14:37:15 -0700 · Steve Abrams
Two issues surfaced by the loopback end-to-end test:
1. prod PG is sparse (~1.5k rows) while the shipped catalog
(data/designs.json) has ~50k. PG lookup for the source design
came back empty → promote silently bailed. Added an mtime-
invalidated designs.json cache as a fallback and a defaults
fallback when both miss.
2. spoon_all_designs has an FK on parent_design_id. When the source
id isn't in PG (only in designs.json), inserting with that id
violated the constraint. Now: parent_design_id only written
when source actually exists in PG; otherwise NULL. Lineage
stays in tags ('colorway-of-<src>') + prompt text.
Verified end-to-end via loopback admin auth on prod: save returned
new_design_id, /design/<new_id> served 200, image served 200, PNG
on disk (~1.2K).
Files touched
Diff
commit 8ae50314e35acd91f3ba6e195528769a3097a126
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri May 29 14:37:15 2026 -0700
fix(colorways/promote): designs.json fallback + nullable FK
Two issues surfaced by the loopback end-to-end test:
1. prod PG is sparse (~1.5k rows) while the shipped catalog
(data/designs.json) has ~50k. PG lookup for the source design
came back empty → promote silently bailed. Added an mtime-
invalidated designs.json cache as a fallback and a defaults
fallback when both miss.
2. spoon_all_designs has an FK on parent_design_id. When the source
id isn't in PG (only in designs.json), inserting with that id
violated the constraint. Now: parent_design_id only written
when source actually exists in PG; otherwise NULL. Lineage
stays in tags ('colorway-of-<src>') + prompt text.
Verified end-to-end via loopback admin auth on prod: save returned
new_design_id, /design/<new_id> served 200, image served 200, PNG
on disk (~1.2K).
---
src/colorways.js | 52 +++++++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 45 insertions(+), 7 deletions(-)
diff --git a/src/colorways.js b/src/colorways.js
index 733885e..3e2029d 100644
--- a/src/colorways.js
+++ b/src/colorways.js
@@ -27,6 +27,23 @@ const { psqlQuery: psql, pgEsc } = require('../lib/db');
const { isAdmin } = require('./admin-gate');
const expressJson = require('express').json({ limit: '32mb' }); // raised — data_url PNGs
+// data/designs.json fallback — prod PG has ~1.5k rows while the shipped
+// catalog (data/designs.json) carries ~50k. mtime-invalidated cache so we
+// don't reparse on every save. Used when PG misses the source design id.
+let _rawDesignsCache = null, _rawDesignsMtime = 0;
+function _rawDesignsById() {
+ try {
+ const p = path.join(__dirname, '..', 'data', 'designs.json');
+ const mt = fs.statSync(p).mtimeMs;
+ if (!_rawDesignsCache || mt !== _rawDesignsMtime) {
+ const arr = JSON.parse(fs.readFileSync(p, 'utf8'));
+ _rawDesignsCache = new Map(arr.map(d => [d.id, d]));
+ _rawDesignsMtime = mt;
+ }
+ } catch (e) { if (!_rawDesignsCache) _rawDesignsCache = new Map(); }
+ return _rawDesignsCache;
+}
+
// Synthetic admin trade-user — lets logged-in admins (or loopback) save
// colorways without having a real trade-user account. Created lazily on first
// admin use. Shared across all admin sessions (Steve is typically the only
@@ -115,7 +132,7 @@ function mount(app, deps) {
if (buf.length >= 1024 && buf.slice(0, 8).equals(Buffer.from([0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a]))) {
promoteBuf = buf;
}
- } catch (e) { /* ignore — fall through to ink_map-only save */ }
+ } catch (e) { /* ignore — ink_map-only save still succeeds */ }
}
try {
@@ -151,11 +168,32 @@ function mount(app, deps) {
// hit the catalog grid; viewable directly via /design/<new_id> (per the
// existing PDP fallback for unpublished). Returns new id, or null on failure.
function promoteToSpoonRow({ srcId, userId, colorwayId, version, buf, palette }) {
- const srcRaw = psql(`SELECT row_to_json(t) FROM (SELECT id, kind, brand, width_in, height_in,
- panels, category, dominant_hex, motifs, tags, product_line, generator
- FROM spoon_all_designs WHERE id=${parseInt(srcId, 10)}) t;`);
- if (!srcRaw) return null;
- const src = JSON.parse(srcRaw);
+ // Prefer PG (full metadata) but fall back to the data/designs.json snapshot
+ // when prod PG is sparse — see memory feedback_wallco_prod_pg_sparse_*.
+ // If both miss, use sensible defaults so the colorway still promotes.
+ let src = null, srcInPg = false;
+ try {
+ const srcRaw = psql(`SELECT row_to_json(t) FROM (SELECT id, kind, brand, width_in, height_in,
+ panels, category, dominant_hex, motifs, tags, product_line, generator
+ FROM spoon_all_designs WHERE id=${parseInt(srcId, 10)}) t;`);
+ if (srcRaw && srcRaw.trim()) { src = JSON.parse(srcRaw); srcInPg = true; }
+ } catch (e) { /* fall through to disk */ }
+ if (!src) {
+ try {
+ const map = _rawDesignsById();
+ const hit = map.get(parseInt(srcId, 10));
+ if (hit) src = {
+ kind: hit.kind, brand: hit.brand || 'wallco.ai',
+ width_in: hit.width_in, height_in: hit.height_in, panels: hit.panels,
+ category: hit.category, dominant_hex: hit.dominant_hex,
+ motifs: hit.motifs, tags: hit.tags, product_line: hit.product_line
+ };
+ } catch (e) { /* fall through to defaults */ }
+ }
+ if (!src) {
+ src = { kind: 'seamless_tile', brand: 'wallco.ai', category: 'mixed',
+ dominant_hex: '#888888', motifs: null, tags: null, product_line: null };
+ }
const ts = Date.now();
const seed = require('crypto').randomInt(1, 2 ** 31 - 1);
const filename = `colorway_${srcId}_cw${colorwayId}_${ts}.png`;
@@ -187,7 +225,7 @@ VALUES
${arrLit(newTags)},
${esc(src.category || 'mixed')},
FALSE,
- ${parseInt(srcId, 10)},
+ ${srcInPg ? parseInt(srcId, 10) : 'NULL'},
${esc(src.product_line || null)})
RETURNING id;`).trim();
const idNum = parseInt(newId, 10);
← 669fc05 add backfill-wallco-rooms.js: populate /admin/rooms gallery
·
back to Wallco Ai
·
color-dots: double-click on colorway chip launches its produ 463100b →