[object Object]

← back to Wallco Ai

feat(/designs surprise-me): defensive null-id handling + 'Try this prompt again' + 'Open full page' link

a18829223134162afadbfcb5641c2a8123aa65f3 · 2026-05-14 09:39:28 -0700 · SteveStudio2

When prod schema drift causes the spoon_all_designs INSERT to silently
fail, the chat handler returns design.id=null. The client used to:
- Render a 'Design #null' label
- Set element ID 'home-review-null'
- Fire interior-designer review with design_id:null → 404 → ugly error

Now:
- Number.isFinite() guards every id-dependent action
- '(unsaved)' label + amber notice explains what's wrong
- Interior-designer review skipped on null id (button reset still runs)
- 'Open full page →' gold pill added when id IS valid (was missing)
- 'Try this prompt again' button (re-renders SAME prompt; previously only random)
- Element IDs now use a per-result random nonce instead of did, so multiple
  successive re-renders don't collide on 'home-review-null'

The retry handler shows a stripped-down result card (just image + links) so
the page doesn't grow unbounded if the user mashes 'Try again'.

Files touched

Diff

commit a18829223134162afadbfcb5641c2a8123aa65f3
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Thu May 14 09:39:28 2026 -0700

    feat(/designs surprise-me): defensive null-id handling + 'Try this prompt again' + 'Open full page' link
    
    When prod schema drift causes the spoon_all_designs INSERT to silently
    fail, the chat handler returns design.id=null. The client used to:
    - Render a 'Design #null' label
    - Set element ID 'home-review-null'
    - Fire interior-designer review with design_id:null → 404 → ugly error
    
    Now:
    - Number.isFinite() guards every id-dependent action
    - '(unsaved)' label + amber notice explains what's wrong
    - Interior-designer review skipped on null id (button reset still runs)
    - 'Open full page →' gold pill added when id IS valid (was missing)
    - 'Try this prompt again' button (re-renders SAME prompt; previously only random)
    - Element IDs now use a per-result random nonce instead of did, so multiple
      successive re-renders don't collide on 'home-review-null'
    
    The retry handler shows a stripped-down result card (just image + links) so
    the page doesn't grow unbounded if the user mashes 'Try again'.
---
 server.js | 46 +++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 39 insertions(+), 7 deletions(-)

diff --git a/server.js b/server.js
index cba3050..67c1246 100644
--- a/server.js
+++ b/server.js
@@ -3738,6 +3738,9 @@ ${cat === 'mural-scenic' ? `
           var d = (j.actions || []).find(function(a){ return a.type === 'new_design'; });
           if (d && d.design) {
             var did = d.design.id;
+            var idValid = Number.isFinite(did);
+            var idLabel = idValid ? '#' + did : '(unsaved)';
+            var nonce = Math.random().toString(36).slice(2, 9);
             var imgUrl = d.design.image_url;
             var hex = d.design.dominant_hex || '#888';
             var promptText = String(d.design.prompt || '').replace(/[<>]/g, '');
@@ -3747,22 +3750,47 @@ ${cat === 'mural-scenic' ? `
                   '<div style="aspect-ratio:1;background:url(\\''+imgUrl+'\\') center/cover #f2efe8;border-radius:10px;border:1px solid var(--line);max-width:520px"></div>' +
                   '<div style="margin-top:10px;font:13px/1.5 var(--sans);color:var(--ink-soft)">' +
                     '<span style="display:inline-block;width:11px;height:11px;border-radius:50%;background:'+hex+';vertical-align:middle;margin-right:6px;border:1px solid rgba(0,0,0,.12)"></span>' +
-                    '<b style="color:var(--ink)">Design #'+did+'</b> · '+pick[0]+' · '+motif+' · '+hex +
+                    '<b style="color:var(--ink)">Design '+idLabel+'</b> · '+pick[0]+' · '+motif+' · '+hex +
                   '</div>' +
                 '</div>' +
                 '<div style="display:flex;flex-direction:column;gap:14px;min-width:0">' +
                   (promptText ? '<details><summary style="cursor:pointer;font:11px/1 var(--sans);color:var(--ink-faint);text-transform:uppercase;letter-spacing:.08em">Prompt ▾</summary><div style="margin-top:8px;font:11px/1.5 ui-monospace,Menlo,monospace;color:var(--ink-soft);max-height:180px;overflow-y:auto;padding:8px;background:var(--card-bg);border:1px solid var(--line);border-radius:6px">'+promptText+'</div></details>' : '') +
-                  '<div id="home-review-'+did+'" style="padding:14px;border:1px solid var(--line);border-radius:8px;background:var(--card-bg)"><div style="font:11px/1 var(--sans);color:var(--ink-faint);text-transform:uppercase;letter-spacing:.08em">Interior-designer review · #'+did+'</div><div style="margin-top:8px;font:13px/1.5 var(--sans);color:var(--ink-soft)"><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--gold);animation:rev-pulse 1.2s ease-in-out infinite;margin-right:6px;vertical-align:middle"></span>Running…</div></div>' +
+                  (idValid
+                    ? '<div id="home-review-'+did+'" style="padding:14px;border:1px solid var(--line);border-radius:8px;background:var(--card-bg)"><div style="font:11px/1 var(--sans);color:var(--ink-faint);text-transform:uppercase;letter-spacing:.08em">Interior-designer review · #'+did+'</div><div style="margin-top:8px;font:13px/1.5 var(--sans);color:var(--ink-soft)"><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--gold);animation:rev-pulse 1.2s ease-in-out infinite;margin-right:6px;vertical-align:middle"></span>Running…</div></div>'
+                    : '<div style="padding:12px;border:1px dashed var(--line);border-radius:8px;background:#fff8e8;font:12px/1.5 var(--sans);color:#7a5f10">Image generated, but the row didn\\'t persist (DB schema drift on prod). Image is available to download — review unavailable until persisted.</div>') +
                   '<div style="display:flex;flex-direction:column;gap:6px;margin-top:auto">' +
+                    (idValid ? '<a href="/design/'+did+'" style="display:block;text-align:center;padding:8px 14px;background:var(--gold,#c9a14b);color:var(--accent,#0d0d0d);text-decoration:none;font-size:13px;font-weight:500;border-radius:6px">Open full page →</a>' : '') +
                     '<a href="'+imgUrl+'" download style="display:block;text-align:center;padding:8px 14px;border:1px solid var(--ink);color:var(--ink);text-decoration:none;font-size:13px;border-radius:6px">Download pattern</a>' +
-                    '<button id="home-again-'+did+'" type="button" style="padding:8px 14px;background:var(--ink);color:var(--bg);border:0;border-radius:6px;cursor:pointer;font:13px var(--sans)">Surprise me again</button>' +
+                    '<button id="home-retry-'+nonce+'" type="button" style="padding:8px 14px;background:transparent;color:var(--ink);border:1px solid var(--line);border-radius:6px;cursor:pointer;font:13px var(--sans)" data-prompt="'+promptText.replace(/"/g,'&quot;')+'">Try this prompt again</button>' +
+                    '<button id="home-again-'+nonce+'" type="button" style="padding:8px 14px;background:var(--ink);color:var(--bg);border:0;border-radius:6px;cursor:pointer;font:13px var(--sans)">Surprise me again</button>' +
                   '</div>' +
                 '</div>' +
               '</div>' +
               '<style>@keyframes rev-pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.4;transform:scale(.6)} }</style>';
-            var againBtn = document.getElementById('home-again-'+did);
+            var againBtn = document.getElementById('home-again-'+nonce);
             if (againBtn) againBtn.addEventListener('click', function(){ btn.click(); });
-            fetch('/api/studio/interior-design-review', {
+            var retryBtn = document.getElementById('home-retry-'+nonce);
+            if (retryBtn) retryBtn.addEventListener('click', async function(){
+              var p = retryBtn.getAttribute('data-prompt') || promptText;
+              if (!p) return;
+              box.innerHTML = '<div style="font:13px/1.5 -apple-system,sans-serif;color:var(--ink-soft)">Re-rendering same prompt… (~30s)</div>';
+              btn.disabled = true; btn.textContent = '… generating';
+              try {
+                var rr = await fetch('/api/chat/catalog', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({message: p}) });
+                var jj = await rr.json();
+                var dd = (jj.actions||[]).find(function(a){return a.type==='new_design';});
+                if (dd && dd.design) {
+                  var ddid = dd.design.id;
+                  var ddidValid = Number.isFinite(ddid);
+                  var u = dd.design.image_url, h = dd.design.dominant_hex || '#888';
+                  box.innerHTML = '<div style="display:flex;gap:18px;align-items:start;flex-wrap:wrap"><div style="aspect-ratio:1;width:520px;max-width:100%;background:url(\\''+u+'\\') center/cover #f2efe8;border-radius:10px;border:1px solid var(--line)"></div><div style="font:13px var(--sans);color:var(--ink-soft)"><span style="display:inline-block;width:11px;height:11px;border-radius:50%;background:'+h+';margin-right:6px;border:1px solid rgba(0,0,0,.12);vertical-align:middle"></span>Same prompt, fresh render '+(ddidValid?'· #'+ddid:'(unsaved)')+'<br><br>'+(ddidValid?'<a href="/design/'+ddid+'" style="color:var(--gold)">Open full page →</a><br>':'')+'<a href="'+u+'" download style="color:var(--ink)">Download pattern</a></div></div>';
+                } else {
+                  box.innerHTML = '<div style="color:#b03a2e">Re-render failed: ' + (jj.error || 'unknown').slice(0,300) + '</div>';
+                }
+              } catch (e) { box.innerHTML = '<div style="color:#b03a2e">Error: ' + e.message + '</div>'; }
+              btn.disabled = false; btn.textContent = orig;
+            });
+            if (idValid) fetch('/api/studio/interior-design-review', {
               method: 'POST', headers: {'Content-Type':'application/json'},
               body: JSON.stringify({ design_id: did })
             }).then(function(r){ return r.json(); }).then(function(rj){
@@ -3963,10 +3991,14 @@ ${FOOTER}
       var f = buildFilter();
       var imgs = document.querySelectorAll('.design-card .card-img');
       for (var i=0;i<imgs.length;i++) imgs[i].style.filter = f;
-      // Fix precedence trap — prior version was `state.hue||0!==0` which parses
-      // as `state.hue || (0!==0)` so `changed` could be a number when hue!=0
+      // Fix precedence trap — prior version had state.hue||0!==0 which parses
+      // as state.hue || (0!==0) so 'changed' could be a number when hue!=0
       // and was false when hue was 0 regardless of other sliders. Use explicit
       // per-key comparison against defaults so summary always reflects truth.
+      // NEVER put backticks in inline comments in this file — the /designs
+      // response is one big template literal; a comment backtick closes it
+      // and parses every downstream line as top-level JS (broke startup
+      // for ~3 minutes on 2026-05-14 commit 109dda6).
       var changed = Object.keys(defaults).some(function(k){ return state[k] !== defaults[k]; });
       if (summary) summary.textContent = changed ? 'edited' : 'defaults';
     }

← 7bbdf26 feat(/designs): keyboard shortcut '/' focuses search (GH/X/S  ·  back to Wallco Ai  ·  db: add 003_schema_parity migration to bring prod in line wi 628c81f →