[object Object]

← back to Corvette Dashboard Viewer

auto-save: 2026-07-28T08:58:52 (2 files) — server.js public/new.html

c665d54989709ffb3d8c31bb2a29b09eeb1c99b0 · 2026-07-28 08:58:56 -0700 · Steve Abrams

Files touched

Diff

commit c665d54989709ffb3d8c31bb2a29b09eeb1c99b0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jul 28 08:58:56 2026 -0700

    auto-save: 2026-07-28T08:58:52 (2 files) — server.js public/new.html
---
 public/new.html | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 server.js       |  47 ++++++++++++++++++++++++++
 2 files changed, 149 insertions(+)

diff --git a/public/new.html b/public/new.html
new file mode 100644
index 0000000..b54420e
--- /dev/null
+++ b/public/new.html
@@ -0,0 +1,102 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>New Arrivals — Live · TK-135</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link href="https://fonts.googleapis.com/css2?family=Racing+Sans+One&family=Oswald:wght@300;500;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
+<style>
+:root{--red:#C8102E;--red-lit:#ff2d4d;--amber:#FFB300;--cream:#e9e4d6;--green:#39d353}
+*{box-sizing:border-box;margin:0;padding:0}
+body{font-family:'Oswald',sans-serif;color:var(--cream);
+  background:radial-gradient(120% 80% at 50% -10%,#2a2b2f,#141416 45%,#050506);min-height:100vh;padding:20px 14px 60px;-webkit-font-smoothing:antialiased}
+.wrap{max-width:1080px;margin:0 auto}
+header{display:flex;align-items:center;justify-content:space-between;gap:14px;
+  background:linear-gradient(180deg,#1c1c1e,#0c0c0d);border:1px solid #2a2a2d;border-radius:16px;
+  padding:16px 22px;box-shadow:inset 0 2px 0 rgba(255,255,255,.05),0 20px 50px rgba(0,0,0,.7);position:sticky;top:10px;z-index:5}
+.brand{display:flex;align-items:center;gap:12px}
+.brand .h1{font-family:'Racing Sans One',cursive;font-size:22px;letter-spacing:1px;
+  background:linear-gradient(180deg,#f7f7f9,#b8bcc4 50%,#6e7178);-webkit-background-clip:text;background-clip:text;color:transparent}
+.live{display:flex;align-items:center;gap:7px;font-family:'Share Tech Mono',monospace;font-size:12px;letter-spacing:2px;color:var(--green)}
+.dot{width:9px;height:9px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);animation:pulse 1.4s infinite}
+@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.7)}}
+.stats{text-align:right;font-family:'Share Tech Mono',monospace}
+.stats .big{font-size:26px;color:var(--amber);text-shadow:0 0 12px rgba(255,179,0,.5);line-height:1}
+.stats .lbl{font-size:9px;letter-spacing:3px;color:#8b8f98}
+.sub{text-align:center;font-size:10px;letter-spacing:4px;color:#7d818b;margin:12px 0 16px}
+.list{display:flex;flex-direction:column;gap:9px}
+.row{display:grid;grid-template-columns:38px 1fr 200px;gap:14px;align-items:center;
+  background:linear-gradient(180deg,#1a1a1c,#101011);border:1px solid #2c2c31;border-radius:12px;
+  padding:11px 14px;box-shadow:0 5px 14px rgba(0,0,0,.45);transition:box-shadow .15s}
+.row:hover{box-shadow:0 8px 20px rgba(0,0,0,.7),0 0 0 1px rgba(255,179,0,.3)}
+.row.fresh{animation:land .8s ease}
+@keyframes land{0%{opacity:0;transform:translateY(-12px);box-shadow:0 0 0 2px var(--amber)}100%{opacity:1;transform:none}}
+.idx{font-family:'Share Tech Mono',monospace;font-size:15px;color:#5f636c;text-align:center}
+.idx.top{color:var(--red-lit);text-shadow:0 0 8px rgba(255,45,77,.6)}
+.main .t{font-size:15px;font-weight:500;line-height:1.15}
+.main .t a{color:var(--cream);text-decoration:none;border-bottom:1px dotted #4a4a50}
+.main .t a:hover{color:var(--amber);border-color:var(--amber)}
+.main .meta{font-size:10px;letter-spacing:1px;color:#8b8f98;margin:3px 0 7px;text-transform:uppercase}
+.chips{display:flex;flex-wrap:wrap;gap:4px}
+.chip{font-size:9px;letter-spacing:.5px;padding:2px 8px;border-radius:20px;background:#26262b;border:1px solid #37373d;color:#c3c7cf}
+.right{display:flex;flex-direction:column;gap:5px;align-items:flex-end}
+.pal{display:flex;width:100%;height:30px;border-radius:6px;overflow:hidden;border:1px solid #000;box-shadow:inset 0 0 0 1px rgba(255,255,255,.05)}
+.pal i{position:relative;display:flex;align-items:flex-end;justify-content:center}
+.pal i span{font-family:'Share Tech Mono',monospace;font-size:7px;color:rgba(255,255,255,.9);text-shadow:0 1px 2px #000;opacity:0;padding-bottom:1px}
+.pal:hover i span{opacity:1}
+.gidl{font-family:'Share Tech Mono',monospace;font-size:9px;color:#5f636c;letter-spacing:1px}
+.hint{text-align:center;color:#5f636c;font-size:11px;margin-top:20px;letter-spacing:2px}
+a.open{font-family:'Share Tech Mono',monospace;font-size:9px;color:var(--amber);text-decoration:none;letter-spacing:1px}
+a.open:hover{text-shadow:0 0 8px rgba(255,179,0,.6)}
+</style>
+</head>
+<body>
+<div class="wrap">
+  <header>
+    <div class="brand">
+      <span style="font-size:22px">🏁</span>
+      <div><div class="h1">NEW ARRIVALS · LIVE</div>
+        <div class="live"><span class="dot"></span><span id="clock">— streaming TK-135 —</span></div></div>
+    </div>
+    <div class="stats"><div class="big" id="processed">0</div><div class="lbl">SKUS UPDATED</div></div>
+  </header>
+  <div class="sub">N E W E S T &nbsp; C O L O R - N O R M A L I Z E D &nbsp; I T E M S &nbsp; · &nbsp; T O P &nbsp; 5 0 &nbsp; · &nbsp; A U T O - R E F R E S H</div>
+  <div class="list" id="list"></div>
+  <div class="hint" id="hint">connecting…</div>
+</div>
+<script>
+let seen=new Set(), first=true;
+function esc(s){return (s||'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));}
+async function tick(){
+  try{
+    const r=await fetch('/api/new-items?limit=50',{cache:'no-store'});
+    const d=await r.json();
+    document.getElementById('processed').textContent=(d.processed||0).toLocaleString();
+    document.getElementById('clock').textContent='updated '+new Date().toLocaleTimeString();
+    document.getElementById('hint').textContent=d.records.length?'▲ newest at top · live feed':'waiting for items to land…';
+    const list=document.getElementById('list'); list.innerHTML='';
+    d.records.forEach((p,i)=>{
+      const isNew=!seen.has(p.gid)&&!first;
+      const total=p.palette.reduce((a,c)=>a+(+c.pct||0),0)||1;
+      const pal=p.palette.length?p.palette.map(c=>`<i style="flex:${c.pct};background:${c.hex}"><span>${c.hex}</span></i>`).join(''):'<i style="flex:1;background:#222"></i>';
+      const title=p.href?`<a href="${p.href}" target="_blank">${esc(p.title)}</a>`:esc(p.title);
+      const chips=(p.tags||[]).map(t=>`<span class="chip">${esc(t)}</span>`).join('');
+      const row=document.createElement('div');
+      row.className='row'+(isNew?' fresh':'');
+      row.innerHTML=`<div class="idx ${i<3?'top':''}">${i+1}</div>
+        <div class="main"><div class="t">${title}</div>
+          <div class="meta">${esc(p.vendor)||'—'} · ${p.palette.length} colors</div>
+          <div class="chips">${chips}</div></div>
+        <div class="right"><div class="pal">${pal}</div>
+          <div class="gidl">#${p.gid}</div>
+          ${p.href?`<a class="open" href="${p.href}" target="_blank">OPEN PAGE ▸</a>`:''}</div>`;
+      list.appendChild(row);
+    });
+    seen=new Set(d.records.map(p=>p.gid)); first=false;
+  }catch(e){document.getElementById('hint').textContent='feed error: '+e.message;}
+}
+tick(); setInterval(tick,4000);
+</script>
+</body>
+</html>
diff --git a/server.js b/server.js
index 12a6bfb..4d951da 100644
--- a/server.js
+++ b/server.js
@@ -66,6 +66,53 @@ http.createServer((req, res) => {
     return;
   }
 
+  // ---- LIVE "new items as they land" ----
+  // Source of truth for freshness = the TK-135 apply-job progress logs (GIDs appended
+  // in write-order; tail = most recently updated). We take the tail of the color-normalize
+  // log (the comprehensive active writer), reverse to newest-first, then join live details
+  // (title/vendor/handle/tags) from the mirror + the deep hex palette from palette-proposed.tsv.
+  if (p === '/api/new-items') {
+    const limit = Math.max(1, Math.min(parseInt(u.searchParams.get('limit'), 10) || 50, 200));
+    const TK = process.env.TK135DIR || (process.env.HOME + '/Projects/Designer-Wallcoverings/shopify/scripts/data/tk135');
+    // newest GIDs from the progress log tail
+    let gids = [], processed = 0;
+    try {
+      const lines = fs.readFileSync(path.join(TK, 'normalize-progress.log'), 'utf8').trim().split('\n').filter(Boolean);
+      processed = lines.length;
+      gids = lines.slice(-limit).reverse();
+    } catch (e) {}
+    if (!gids.length) { res.writeHead(200, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ records: [], processed: 0 })); }
+    // palette map (gid-number -> [{hex,pct}]), loaded once + cached
+    if (!global.__pal) {
+      global.__pal = {};
+      try { for (const l of fs.readFileSync(path.join(TK, 'palette-proposed.tsv'), 'utf8').split('\n')) {
+        const f = l.split('\t'); if (f.length < 3 || f[2].startsWith('ERR')) continue;
+        try { global.__pal[f[0].replace(/.*\//, '')] = JSON.parse(f[2]); } catch (e) {} } } catch (e) {}
+    }
+    const nums = gids.map(g => g.replace(/.*\//, ''));
+    const inList = gids.map(g => `'${g.replace(/'/g, "''")}'`).join(',');
+    const sql = `select coalesce(json_agg(row_to_json(t)),'[]') from (
+      select regexp_replace(shopify_id,'.*/','') as gid, title, vendor, handle, image_url,
+             coalesce(nullif(dw_sku,''),nullif(mfr_sku,''),nullif(variant_sku,'')) as sku,
+             (select json_agg(v) from (select distinct trim(x) v from unnest(string_to_array(tags,',')) x
+                where trim(x) !~ '[:/.]' and trim(x) !~ '^[0-9]' and length(trim(x)) between 2 and 22
+                limit 8) s) as tags
+      from shopify_products where shopify_id in (${inList})) t`;
+    execFile('psql', ['-h', PGHOST, '-d', PGDB, '-tAc', sql], { maxBuffer: 16 * 1024 * 1024 }, (err, stdout) => {
+      let byGid = {};
+      if (!err && stdout.trim()) { try { JSON.parse(stdout.trim()).forEach(r => byGid[r.gid] = r); } catch (e) {} }
+      const records = nums.map(n => {
+        const d = byGid[n] || {};
+        return { gid: n, sku: d.sku || '', image: d.image_url || '', title: d.title || '(loading…)', vendor: d.vendor || '', handle: d.handle || '',
+          tags: d.tags || [], palette: global.__pal[n] || [],
+          href: d.handle ? `https://designer-laboratory-sandbox.myshopify.com/products/${d.handle}` : null };
+      });
+      res.writeHead(200, { 'Content-Type': 'application/json', 'X-Data-Source': 'live-progress' });
+      res.end(JSON.stringify({ records, processed }));
+    });
+    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'); }

← 8bbdd4d Harden fallback (guard snapshot JSON.parse against process c  ·  back to Corvette Dashboard Viewer  ·  Live SKU ticker below engine (image+sku+palette, slide-in mo c4c8d1a →