[object Object]

← 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 →