[object Object]

← 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

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 →