← back to Corvette Dashboard Viewer
5x sweep 1: proxy live-ticker images via /img (placeholder on dead CDN asset) — kills intermittent console 404
5539a5ca622a8764e0dc113df0e5bfc7209e3992 · 2026-07-28 09:27:18 -0700 · Steve Abrams
Files touched
A 5x/sweep-1.mdA assets/dash-live.pngM public/index.htmlM server.js
Diff
commit 5539a5ca622a8764e0dc113df0e5bfc7209e3992
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Jul 28 09:27:18 2026 -0700
5x sweep 1: proxy live-ticker images via /img (placeholder on dead CDN asset) — kills intermittent console 404
---
5x/sweep-1.md | 6 ++++++
assets/dash-live.png | Bin 0 -> 941905 bytes
public/index.html | 4 +++-
server.js | 21 ++++++++++++++++++++-
4 files changed, 29 insertions(+), 2 deletions(-)
diff --git a/5x/sweep-1.md b/5x/sweep-1.md
new file mode 100644
index 0000000..8b41eb4
--- /dev/null
+++ b/5x/sweep-1.md
@@ -0,0 +1,6 @@
+# 5x sweep 1
+Target: http://127.0.0.1:9797/
+Result: 6/7 (M3 automation FAIL — 1 JS error)
+CAUGHT: intermittent console 404 — live-ticker rendered product <img> directly from cdn.shopify.com; a dead CDN asset (rotating through the newest-12 window) logged a network 404 console error.
+FIX: added same-origin /img proxy (SSRF-guarded to shopify.com CDN, https only) that returns a transparent PNG placeholder (200) on any upstream non-200/error/timeout; front-end ticker now loads /img?u=<url> instead of the raw CDN URL. Dead images degrade silently → browser never sees a 404.
+VERIFIED: real→200 image/jpeg, dead→200 placeholder, SSRF(169.254.169.254)→200 placeholder (not fetched).
diff --git a/assets/dash-live.png b/assets/dash-live.png
new file mode 100644
index 0000000..a713b57
Binary files /dev/null and b/assets/dash-live.png differ
diff --git a/public/index.html b/public/index.html
index 04ff2e3..b2b2543 100644
--- a/public/index.html
+++ b/public/index.html
@@ -331,7 +331,9 @@ async function pollLive(){
if(seenLive.has(rec.gid))continue;
seenLive.add(rec.gid);
const pal=(rec.palette||[]).map(c=>`<span style="background:${c.hex}"></span>`).join('')||'<span style="background:#333"></span>';
- const img=rec.image?`<img src="${rec.image}" loading="lazy" onerror="this.style.visibility='hidden'">`:'<img alt="">';
+ // Route through the same-origin /img proxy: dead Shopify assets return a
+ // transparent placeholder (200) so the browser never logs a 404 console error.
+ const img=`<img src="/img?u=${encodeURIComponent(rec.image||'')}" loading="lazy">`;
const row=document.createElement('div'); row.className='live-row';
row.innerHTML=`${img}<span class="lr-sku">${rec.sku||rec.gid}</span>`+
`<span class="lr-t">${rec.title||''}<small>${rec.vendor||''}</small></span>`+
diff --git a/server.js b/server.js
index d0b6285..1750916 100644
--- a/server.js
+++ b/server.js
@@ -4,7 +4,7 @@
// dw_unified mirror. Zero-dependency: the DB shapes the JSON via json_agg and we
// shell out to psql, so no `pg` module is needed. If the DB is unreachable the
// endpoint falls back to the frozen public/data.json snapshot so the console never breaks.
-const http = require('http'), fs = require('fs'), path = require('path'), { execFile } = require('child_process');
+const http = require('http'), https = require('https'), fs = require('fs'), path = require('path'), { execFile } = require('child_process');
const PORT = process.env.PORT || 9797;
const ROOT = path.join(__dirname, 'public');
const PGDB = process.env.PGDATABASE || 'dw_unified';
@@ -141,6 +141,25 @@ http.createServer((req, res) => {
return;
}
+ // ---- image proxy for the live ticker ----
+ // Fetch the product image server-side so a dead Shopify CDN asset degrades to a
+ // transparent placeholder (same-origin 200) instead of a browser-console 404.
+ // SSRF-guarded: only https shopify.com CDN hosts are fetched; anything else → placeholder.
+ if (p === '/img') {
+ const PLACEHOLDER = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64');
+ const placeholder = () => { if (res.writableEnded) return; res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'no-store' }); res.end(PLACEHOLDER); };
+ let tu; try { tu = new URL(u.searchParams.get('u') || ''); } catch { return placeholder(); }
+ if (tu.protocol !== 'https:' || !/(^|\.)shopify\.com$/.test(tu.hostname)) return placeholder();
+ const ir = https.get(tu.href, (pres) => {
+ if (pres.statusCode !== 200) { pres.resume(); return placeholder(); }
+ res.writeHead(200, { 'Content-Type': pres.headers['content-type'] || 'image/jpeg', 'Cache-Control': 'public,max-age=3600' });
+ pres.pipe(res);
+ });
+ ir.on('error', placeholder);
+ ir.setTimeout(6000, () => { ir.destroy(); placeholder(); });
+ return;
+ }
+
if (p === '/') p = '/index.html';
const fp = path.join(ROOT, path.normalize(p));
if (!fp.startsWith(ROOT)) { res.writeHead(403); return res.end('forbidden'); }
← bad80a3 live twin gauges: speedo=SKUs normalized, tach=descriptor ta
·
back to Corvette Dashboard Viewer
·
5x sweep 3: add inline data-URI favicon — kills /favicon.ico dcde435 →