← back to Corvette Dashboard Viewer
pm2-manage the console (ecosystem.config.js, reboot-safe)
26818b39344c14f2242782ad16b98d06d9d4483a · 2026-07-28 09:12:18 -0700 · Steve Abrams
Files touched
A assets/new-live.pngA ecosystem.config.jsM public/index.htmlM server.js
Diff
commit 26818b39344c14f2242782ad16b98d06d9d4483a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Jul 28 09:12:18 2026 -0700
pm2-manage the console (ecosystem.config.js, reboot-safe)
---
assets/new-live.png | Bin 0 -> 964175 bytes
ecosystem.config.js | 14 ++++++++++++++
public/index.html | 37 ++++++++++++++++++++-----------------
server.js | 16 ++++++++++++++++
4 files changed, 50 insertions(+), 17 deletions(-)
diff --git a/assets/new-live.png b/assets/new-live.png
new file mode 100644
index 0000000..c0d52d4
Binary files /dev/null and b/assets/new-live.png differ
diff --git a/ecosystem.config.js b/ecosystem.config.js
new file mode 100644
index 0000000..9e23a5d
--- /dev/null
+++ b/ecosystem.config.js
@@ -0,0 +1,14 @@
+// pm2 process definition for the '67 Corvette Catalog Console.
+// Reboot-safe + version-controlled: `pm2 start ecosystem.config.js && pm2 save`.
+// server.js already defaults PORT→9797 and PGHOST→/tmp; pinned here so the
+// process definition is explicit and lives in git, not just pm2's dump.
+module.exports = {
+ apps: [{
+ name: 'corvette-console',
+ script: 'server.js',
+ cwd: __dirname,
+ env: { PORT: 9797, PGHOST: '/tmp', PGDATABASE: 'dw_unified' },
+ autorestart: true,
+ max_restarts: 20,
+ }],
+};
diff --git a/public/index.html b/public/index.html
index 86e13af..273dd36 100644
--- a/public/index.html
+++ b/public/index.html
@@ -225,16 +225,21 @@ function gauge(el, {min=0,max=100,value=0,title='',unit='',redFrom=0.82,big=fals
</svg>
<div class="g-label"><div class="g-title">${title}</div>
<div class="g-value" id="${el.id}-val">0</div><div class="g-unit">${unit}</div></div>`;
- // animate needle: rotate from a0 baseline to value angle. rotation delta = (val2ang(value)-a0) negated for SVG y-down
- requestAnimationFrame(()=>{
- const ndl=document.getElementById(el.id+'-ndl');
- const deg=-(val2ang(value)-a0);
- ndl.style.transform=`rotate(${deg}deg)`;
- // count up
- const vEl=document.getElementById(el.id+'-val'); let s=null;
- function step(ts){if(!s)s=ts;const p=Math.min(1,(ts-s)/1600);vEl.textContent=Math.round(value*p).toLocaleString();if(p<1)requestAnimationFrame(step);}
- requestAnimationFrame(step);
- });
+ // Store a RE-DRIVABLE updater on the element so the gauge is a LIVE instrument:
+ // each poll calls el.__drive(newValue) and the needle sweeps + the readout counts
+ // from the previous value to the new one. Needle pins at max if value exceeds it.
+ const ndlId=el.id+'-ndl', valId=el.id+'-val';
+ el.__cur=0;
+ el.__drive=function(v){
+ const ndl=document.getElementById(ndlId), vEl=document.getElementById(valId);
+ if(!ndl||!vEl) return;
+ const target=Math.max(min,Math.min(max,v));
+ ndl.style.transform=`rotate(${-(val2ang(target)-a0)}deg)`;
+ const from=el.__cur||0; let s=null;
+ function step(ts){if(!s)s=ts;const pr=Math.min(1,(ts-s)/1400);vEl.textContent=Math.round(from+(v-from)*pr).toLocaleString();if(pr<1)requestAnimationFrame(step);}
+ requestAnimationFrame(step); el.__cur=v;
+ };
+ requestAnimationFrame(()=>el.__drive(value));
}
let DATA=[], TOTALS=null, activeFilter=null;
@@ -252,13 +257,11 @@ function init(){
const colors=TOTALS?TOTALS.colors:DATA.reduce((a,p)=>a+p.palette.length,0);
const avg=skus?(colors/skus).toFixed(1):'0';
const vendors=TOTALS?TOTALS.vendors:new Set(DATA.map(p=>p.vendor)).size;
- gauge(document.getElementById('gaugeSpeedo'),{min:0,max:Math.ceil(skus/50)*50,value:skus,title:'SKUS TAGGED',unit:'PRODUCTS',big:true,redFrom:0.85});
- gauge(document.getElementById('gaugeTach'),{min:0,max:Math.ceil(colors/100)*100,value:colors,title:'HEX COLORS',unit:'x100 RPM',redFrom:0.8});
- document.getElementById('mVen').textContent=vendors;
- document.getElementById('mAvg').textContent=avg;
- // style/material counts from tags-in-color field? we only have color; approximate distinct color names
- document.getElementById('mStyle').textContent=new Set(DATA.map(p=>p.color)).size;
- document.getElementById('mMat').textContent=DATA.filter(p=>p.palette.length>=5).length;
+ // LIVE gauges: speedo = SKUs color-normalized (climbs to 81,102 as the job runs),
+ // tach = descriptor tags written (climbs to ~66k). Both re-driven by pollStats().
+ gauge(document.getElementById('gaugeSpeedo'),{min:0,max:81102,value:0,title:'SKUS NORMALIZED',unit:'/ 81,102',big:true,redFrom:0.9});
+ gauge(document.getElementById('gaugeTach'),{min:0,max:66193,value:0,title:'DESCRIPTOR TAGS',unit:'WRITTEN',redFrom:0.9});
+ pollStats(); setInterval(pollStats,3000);
// switches = top color buckets as filters
const buckets={}; DATA.forEach(p=>buckets[p.color]=(buckets[p.color]||0)+1);
const top=Object.entries(buckets).sort((a,b)=>b[1]-a[1]).slice(0,5).map(x=>x[0]);
diff --git a/server.js b/server.js
index 837aeaa..d0b6285 100644
--- a/server.js
+++ b/server.js
@@ -66,6 +66,22 @@ http.createServer((req, res) => {
return;
}
+ // ---- LIVE gauge stats: real-time counts from the TK-135 apply-job progress logs.
+ // These numbers change every few seconds as the jobs write, so the dashboard gauges
+ // are live instruments (needles climb) rather than a static snapshot.
+ if (p === '/api/stats') {
+ const TK = process.env.TK135DIR || (process.env.HOME + '/Projects/Designer-Wallcoverings/shopify/scripts/data/tk135');
+ const cnt = f => { try { return fs.readFileSync(path.join(TK, f), 'utf8').split('\n').filter(Boolean).length; } catch (e) { return 0; } };
+ const color = cnt('normalize-progress.log'), style = cnt('style-proposed.progress.log');
+ const material = cnt('material-primary-proposed.progress.log'), collection = cnt('collection-proposed.progress.log');
+ const label = cnt('label-normalize-progress.log');
+ const descriptors = style + material + collection + label;
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
+ res.end(JSON.stringify({ color, style, material, collection, label, descriptors,
+ writes: color + descriptors, targets: { color: 81102, descriptors: 66193 } }));
+ 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
← 21a046b live new-arrivals viewer (new.html + /api/new-items): newest
·
back to Corvette Dashboard Viewer
·
live twin gauges: speedo=SKUs normalized, tach=descriptor ta bad80a3 →