[object Object]

← 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

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 →