← back to Wallco Ai
studio/search: fix dead pattern thumbnails (generator + studio)
271f2316029b4444fac07356d4e7351aacf65ef6 · 2026-06-01 17:14:29 -0700 · Steve Abrams
The pattern-search results emitted /img/generated/<file> from spoon_all_designs.local_path without checking the file exists — generated PNGs that were deleted/quarantined 404'd as broken images. Now: (1) server only emits a local thumb URL when the file actually exists on the box, else falls back to the canonical /designs/img/by-id/<id> resolver (resolves prod files even when local_path is stale), else external image_url, else '' so the client paints a dominant-hex swatch — and never manufactures a by-id URL for a genuinely image-less row; (2) client adds a capture-phase 'error' listener that swaps any still-failing <img> for its dominant-hex swatch, so no broken-image icon ever shows. Verified headless: 0 broken images across peacock/damask searches (failed thumbs become swatches).
Files touched
Diff
commit 271f2316029b4444fac07356d4e7351aacf65ef6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jun 1 17:14:29 2026 -0700
studio/search: fix dead pattern thumbnails (generator + studio)
The pattern-search results emitted /img/generated/<file> from spoon_all_designs.local_path without checking the file exists — generated PNGs that were deleted/quarantined 404'd as broken images. Now: (1) server only emits a local thumb URL when the file actually exists on the box, else falls back to the canonical /designs/img/by-id/<id> resolver (resolves prod files even when local_path is stale), else external image_url, else '' so the client paints a dominant-hex swatch — and never manufactures a by-id URL for a genuinely image-less row; (2) client adds a capture-phase 'error' listener that swaps any still-failing <img> for its dominant-hex swatch, so no broken-image icon ever shows. Verified headless: 0 broken images across peacock/damask searches (failed thumbs become swatches).
---
server.js | 44 +++++++++++++++++++++++++++++++++++++-------
1 file changed, 37 insertions(+), 7 deletions(-)
diff --git a/server.js b/server.js
index cec8603..abd487a 100644
--- a/server.js
+++ b/server.js
@@ -22263,7 +22263,7 @@ document.getElementById('batch-patterns').addEventListener('input', async functi
BATCH_SEARCH = Array.isArray(r) ? r : [];
document.getElementById('batch-patterns-results').innerHTML = BATCH_SEARCH.map(function(x, i){
return '<div class="bp-result" data-idx="'+i+'" style="cursor:pointer;border:1px solid #444;border-radius:4px;padding:2px;background:#0f0e0c" title="'+((x.title||'').slice(0,60).replace(/"/g,'"'))+'">'+
- (x.thumb_url?'<img src="'+x.thumb_url+'" loading="lazy" style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:3px">':'<div style="aspect-ratio:1;background:'+(x.dominant_hex||'#222')+'"></div>')+
+ (x.thumb_url?'<img src="'+x.thumb_url+'" loading="lazy" data-hex="'+(x.dominant_hex||'#222')+'" style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:3px">':'<div style="aspect-ratio:1;background:'+(x.dominant_hex||'#222')+'"></div>')+
'</div>';
}).join('');
});
@@ -22272,6 +22272,16 @@ document.getElementById('batch-patterns-results').addEventListener('click', func
var x = BATCH_SEARCH[parseInt(card.dataset.idx, 10)];
if (x) pickBatchPattern(x);
});
+// Dead-thumbnail safety net: a result whose image still 404s (stale by-id,
+// dead external url) swaps to its dominant-hex swatch instead of a broken icon.
+// Capture phase because 'error' events don't bubble.
+document.getElementById('batch-patterns-results').addEventListener('error', function(e){
+ var img = e.target;
+ if (!img || img.tagName !== 'IMG') return;
+ var div = document.createElement('div');
+ div.style.cssText = 'width:100%;aspect-ratio:1;border-radius:3px;background:'+(img.getAttribute('data-hex')||'#222');
+ img.replaceWith(div);
+}, true);
window.pickBatchPattern = function(p){
if (BATCH.patterns.find(function(x){return x.id===p.id && x.source===p.source;})) return;
BATCH.patterns.push(p);
@@ -22767,13 +22777,33 @@ app.get('/api/studio/search', (req, res) => {
SELECT COALESCE(json_agg(t), '[]'::json) FROM
(SELECT * FROM pd UNION ALL SELECT * FROM sp UNION ALL SELECT * FROM gen) t;`;
const raw = psqlQuery(sql);
+ const dataPrefix = path.join(__dirname, 'data') + path.sep;
const items = JSON.parse(raw || '[]').map(it => {
- // Map local_path → /img URL when applicable
- let url = it.image_url || '';
- if (it.local_path && it.local_path.startsWith(path.join(__dirname, 'data') + path.sep)) {
- url = '/img/' + it.local_path.replace(path.join(__dirname, 'data') + path.sep, '');
- } else if (it.local_path && it.local_path.includes('/generated/')) {
- url = '/designs/img/' + path.basename(it.local_path);
+ // Resolve a thumb THIS box can actually serve. local_path in PG often
+ // points at generated PNGs that were since deleted/quarantined — emitting
+ // that URL blindly 404s (the dead-thumbnail bug). So: prefer a local file
+ // that exists on disk; else the canonical /designs/img/by-id resolver for
+ // wallco designs (works even when local_path is stale); else an external
+ // image_url; else '' so the client paints a dominant_hex swatch instead of
+ // a broken image.
+ let url = '';
+ const lp = it.local_path || '';
+ let fsPath = '';
+ if (lp.startsWith(dataPrefix)) {
+ url = '/img/' + lp.slice(dataPrefix.length);
+ fsPath = lp;
+ } else if (lp.includes('/generated/')) {
+ url = '/designs/img/' + path.basename(lp);
+ fsPath = path.join(IMG_DIR, path.basename(lp));
+ }
+ if (url) { try { if (!fs.existsSync(fsPath)) url = ''; } catch { url = ''; } }
+ if (!url) {
+ // Only attempt the by-id resolver when the row actually references an
+ // image we just couldn't serve locally — don't manufacture a 404-ing
+ // URL for a genuinely image-less row (let it paint a swatch instead).
+ const hadImageRef = !!(lp || (it.image_url || ''));
+ if (it.source === 'wallco' && hadImageRef && /^\d+$/.test(String(it.id))) url = '/designs/img/by-id/' + it.id;
+ else if (/^https?:\/\//i.test(it.image_url || '')) url = it.image_url;
}
return { ...it, thumb_url: url };
});
← 60d7b6f extend hex-consistent naming to grid (/api/designs) + search
·
back to Wallco Ai
·
murals: add sort dropdown + density slider (Steve standing r 8a16967 →