← back to Wallco
Fix dead Color/Style sort options, harden HTML escaping, add API + global error handling
67bf8cb95940606eeb0ef75d8e70fefcff449615 · 2026-05-18 20:30:08 -0700 · Steve Abrams
Files touched
Diff
commit 67bf8cb95940606eeb0ef75d8e70fefcff449615
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon May 18 20:30:08 2026 -0700
Fix dead Color/Style sort options, harden HTML escaping, add API + global error handling
---
server.js | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++-------------
1 file changed, 69 insertions(+), 17 deletions(-)
diff --git a/server.js b/server.js
index bf53010..059ea4e 100644
--- a/server.js
+++ b/server.js
@@ -532,16 +532,22 @@ app.get('/', (_req, res) => {
.catch(function(err){ status.innerHTML = '<div class="error">Upload failed: '+err.message+'</div>'; });
});
+ function he(s) {
+ return String(s == null ? '' : s).replace(/[&<>"']/g, function(c){
+ return { '&':'&','<':'<','>':'>','"':'"',"'":''' }[c];
+ });
+ }
+ function safeHex(h) { return /^#?[0-9a-fA-F]{3,8}$/.test(String(h||'')) ? String(h) : '#ccc'; }
function render(j) {
if (!j || !j.ok) {
- status.innerHTML = '<div class="error">'+(j && j.message ? j.message : 'Something went wrong')+'</div>';
+ status.innerHTML = '<div class="error">'+he(j && j.message ? j.message : 'Something went wrong')+'</div>';
return;
}
status.innerHTML = '';
- var swatches = (j.analysis.colors||[]).map(function(c){ return '<div class="swatch"><div class="chip" style="background:'+(c.hex||'#ccc')+'"></div>'+(c.name||c.hex||'')+'</div>'; }).join('');
- var styles = (j.analysis.styles||[]).map(function(s){ return '<div class="tag style">'+s+'</div>'; }).join('');
- var keywords = (j.analysis.keywords||[]).map(function(k){ return '<div class="tag">'+k+'</div>'; }).join('');
- var notice = j.analysis_ok ? '' : '<div class="error" style="margin-bottom:14px">'+(j.notice||'AI read unavailable; showing curated picks.')+'</div>';
+ var swatches = (j.analysis.colors||[]).map(function(c){ return '<div class="swatch"><div class="chip" style="background:'+safeHex(c.hex)+'"></div>'+he(c.name||c.hex||'')+'</div>'; }).join('');
+ var styles = (j.analysis.styles||[]).map(function(s){ return '<div class="tag style">'+he(s)+'</div>'; }).join('');
+ var keywords = (j.analysis.keywords||[]).map(function(k){ return '<div class="tag">'+he(k)+'</div>'; }).join('');
+ var notice = j.analysis_ok ? '' : '<div class="error" style="margin-bottom:14px">'+he(j.notice||'AI read unavailable; showing curated picks.')+'</div>';
result.innerHTML =
notice +
'<div class="preview">' +
@@ -580,24 +586,46 @@ app.get('/', (_req, res) => {
var savedCm = localStorage.getItem('wallco-cm');
if (savedCm) { cm.value = savedCm; grid.style.setProperty('--card-min', savedCm+'px'); }
} catch(e) {}
+ var COLOR_RX = [
+ ['1black', /\\b(black|noir|onyx|jet|midnight)\\b/i],
+ ['2white', /\\b(white|ivory|cream|alabaster|chalk)\\b/i],
+ ['3gray', /\\b(gray|grey|charcoal|graphite|ash|smoke|silver)\\b/i],
+ ['4brown', /\\b(brown|tan|taupe|sand|khaki|camel|chocolate|caramel|sepia|beige)\\b/i],
+ ['5red', /\\b(red|crimson|scarlet|ruby|brick|oxblood|cinnabar)\\b/i],
+ ['6orange', /\\b(orange|terracotta|copper|rust|amber|tangerine)\\b/i],
+ ['7yellow', /\\b(yellow|gold|ochre|saffron|honey|mustard|citrine)\\b/i],
+ ['8green', /\\b(green|sage|olive|moss|emerald|jade|fern|forest|seafoam|seaglass)\\b/i],
+ ['9blue', /\\b(blue|navy|cobalt|indigo|sapphire|aqua|teal|turquoise|aegean|sky)\\b/i],
+ ['Apurple', /\\b(purple|violet|lavender|plum|aubergine|mauve|lilac)\\b/i],
+ ['Bpink', /\\b(pink|rose|blush|coral|fuchsia|magenta)\\b/i]
+ ];
+ function bucketKey(p, rx) {
+ var hay = (p.title||'') + ' ' + (p.product_type||'');
+ for (var i=0;i<rx.length;i++) if (rx[i][1].test(hay)) return rx[i][0];
+ return 'Zother';
+ }
sort.addEventListener('change', function(){
try{localStorage.setItem('wallco-sort', sort.value);}catch(e){}
var arr = orig.slice();
if (sort.value === 'sku') arr.sort(function(a,b){ return (a.dw_sku||'').localeCompare(b.dw_sku||''); });
else if (sort.value === 'title') arr.sort(function(a,b){ return (a.title||'').localeCompare(b.title||''); });
- else if (sort.value === 'newest') arr = orig.slice();
+ else if (sort.value === 'color') arr.sort(function(a,b){ return bucketKey(a,COLOR_RX).localeCompare(bucketKey(b,COLOR_RX)) || (a.title||'').localeCompare(b.title||''); });
+ else if (sort.value === 'style') arr.sort(function(a,b){ return (a.product_type||'~').localeCompare(b.product_type||'~') || (a.title||'').localeCompare(b.title||''); });
+ else arr = orig.slice(); // match score / newest = server order
grid.innerHTML = arr.map(cardHtml).join('');
});
cm.addEventListener('input', function(){ grid.style.setProperty('--card-min', cm.value+'px'); try{localStorage.setItem('wallco-cm', cm.value);}catch(e){} });
try { gtag('event','room_searched',{ matches:(j.matches||[]).length, mood:j.analysis.mood||'' }); } catch(e){}
}
function cardHtml(p) {
- var memo = '${DW_SHOPIFY}/products/' + (p.handle||'') + '#sample';
- var view = '${DW_SHOPIFY}/products/' + (p.handle||'');
+ var handle = he(p.handle||'');
+ var memo = '${DW_SHOPIFY}/products/' + handle + '#sample';
+ var view = '${DW_SHOPIFY}/products/' + handle;
+ var img = p.image_url ? '<img loading="lazy" src="'+he(p.image_url)+'" alt="'+he(p.title)+'">' : '';
return '<article class="card">' +
- '<a class="thumb" href="'+view+'" target="_blank" rel="noopener">' + (p.image_url ? '<img loading="lazy" src="'+p.image_url+'" alt="'+(p.title||'').replace(/"/g,'"')+'">' : '') + '</a>' +
- '<div class="info"><div class="t">'+(p.title||'').replace(/</g,'<')+'</div>' +
- '<div class="sub">'+(p.dw_sku||'')+(p.product_type?(' · '+p.product_type):'')+'</div></div>' +
+ '<a class="thumb" href="'+view+'" target="_blank" rel="noopener">' + img + '</a>' +
+ '<div class="info"><div class="t">'+he(p.title)+'</div>' +
+ '<div class="sub">'+he(p.dw_sku)+(p.product_type?(' · '+he(p.product_type)):'')+'</div></div>' +
'<div class="actions">' +
'<a href="'+view+'" target="_blank" rel="noopener">View</a>' +
'<a class="cta" href="'+memo+'" target="_blank" rel="noopener">Order Memo</a>' +
@@ -652,9 +680,15 @@ app.get('/about', (_req, res) => {
app.post('/api/search', async (req, res) => {
const prose = String(req.body?.q || '').trim();
if (!prose) return res.status(400).json({ ok: false, message: 'No description provided.' });
- const analysis = await geminiAnalyzeText(prose);
- const signal = buildSignal(analysis, prose);
- const matches = topMatches(signal, 24);
+ let analysis, signal, matches;
+ try {
+ analysis = await geminiAnalyzeText(prose);
+ signal = buildSignal(analysis, prose);
+ matches = topMatches(signal, 24);
+ } catch (err) {
+ console.error('search failed:', err);
+ return res.status(500).json({ ok: false, message: 'Search failed. Please try again.' });
+ }
res.set('Cache-Control', 'no-store').json({
ok: true,
analysis_ok: analysis.ok,
@@ -675,9 +709,15 @@ app.post('/api/search', async (req, res) => {
app.post('/api/match-photo', upload.single('photo'), async (req, res) => {
if (!req.file) return res.status(400).json({ ok: false, message: 'No photo uploaded.' });
- const analysis = await geminiAnalyzeImage(req.file.buffer, req.file.mimetype);
- const signal = buildSignal(analysis, '');
- const matches = topMatches(signal, 24);
+ let analysis, signal, matches;
+ try {
+ analysis = await geminiAnalyzeImage(req.file.buffer, req.file.mimetype);
+ signal = buildSignal(analysis, '');
+ matches = topMatches(signal, 24);
+ } catch (err) {
+ console.error('match-photo failed:', err);
+ return res.status(500).json({ ok: false, message: 'Photo match failed. Please try again.' });
+ }
res.set('Cache-Control', 'no-store').json({
ok: true,
analysis_ok: analysis.ok,
@@ -701,6 +741,18 @@ app.get('/health', (_req, res) => res.json({
ga: GA_ID,
}));
+// Global error handler — catches multer file-size rejections and any
+// uncaught route error so requests fail cleanly instead of hanging.
+app.use((err, req, res, _next) => {
+ if (res.headersSent) return;
+ const tooLarge = err && err.code === 'LIMIT_FILE_SIZE';
+ console.error('request error:', err && err.message ? err.message : err);
+ res.status(tooLarge ? 413 : 500).json({
+ ok: false,
+ message: tooLarge ? 'Image is too large (15 MB max).' : 'Server error. Please try again.',
+ });
+});
+
app.listen(PORT, BIND, () => {
console.log(`wallco.ai on http://${BIND}:${PORT} · ${PRODUCTS.length} products · ga=${GA_ID} · gemini_key=${!!GEMINI_API_KEY}`);
});
← 1946431 initial scaffold + GA gtag (G-H47K8H8G5J) — wallco.ai AI sea
·
back to Wallco
·
scrub vendor name leaks from products.json source (176 produ 0cb038c →