[object Object]

← back to AbramsEgo

fix: Kamatera fleet panel surfaces cache age + ssh-failure note (no more silent stale)

9e9911e78d21d6df87ed1fa7249df8ae596c9123 · 2026-07-01 14:23:29 -0700 · Steve

Files touched

Diff

commit 9e9911e78d21d6df87ed1fa7249df8ae596c9123
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jul 1 14:23:29 2026 -0700

    fix: Kamatera fleet panel surfaces cache age + ssh-failure note (no more silent stale)
---
 build-queue/{tasks => done}/21-kamatera-staleness-note.md |  0
 data/local-fleet.json                                     |  2 +-
 public/index.html                                         |  2 +-
 server.js                                                 | 15 +++++++++++++--
 4 files changed, 15 insertions(+), 4 deletions(-)

diff --git a/build-queue/tasks/21-kamatera-staleness-note.md b/build-queue/done/21-kamatera-staleness-note.md
similarity index 100%
rename from build-queue/tasks/21-kamatera-staleness-note.md
rename to build-queue/done/21-kamatera-staleness-note.md
diff --git a/data/local-fleet.json b/data/local-fleet.json
index 8632d13..8067613 100644
--- a/data/local-fleet.json
+++ b/data/local-fleet.json
@@ -1 +1 @@
-{"fetchedAt":"2026-07-01T21:19:48.842Z","procs":[{"name":"pm2-logrotate","status":"online","cpu":0,"memMB":45,"restarts":0,"uptimeMs":791257,"startedAt":"2026-07-01T21:06:37.585Z"},{"name":"abramsego","status":"online","cpu":0,"memMB":76,"restarts":2,"uptimeMs":616,"startedAt":"2026-07-01T21:19:48.226Z"},{"name":"api-token-dashboard","status":"online","cpu":0,"memMB":56,"restarts":1,"uptimeMs":356176,"startedAt":"2026-07-01T21:13:52.666Z"}]}
\ No newline at end of file
+{"fetchedAt":"2026-07-01T21:23:08.090Z","procs":[{"name":"pm2-logrotate","status":"online","cpu":0,"memMB":45,"restarts":0,"uptimeMs":990505,"startedAt":"2026-07-01T21:06:37.585Z"},{"name":"abramsego","status":"online","cpu":24,"memMB":75,"restarts":3,"uptimeMs":715,"startedAt":"2026-07-01T21:23:07.375Z"},{"name":"api-token-dashboard","status":"online","cpu":0,"memMB":56,"restarts":2,"uptimeMs":59418,"startedAt":"2026-07-01T21:22:08.672Z"}]}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 518dfc5..0d38f77 100644
--- a/public/index.html
+++ b/public/index.html
@@ -337,7 +337,7 @@ async function load(){
 
   // ---- fleet ----
   const k=d.kamatera||{}, l=d.localFleet||{};
-  $('kam-up').innerHTML=`<span class="${cls(100-(k.up/(k.total||1)*100),20,50)}">${k.up??'—'}</span><span class="dim" style="font-size:16px">/${k.total??'—'} up</span>`;
+  $('kam-up').innerHTML=`<span class="${cls(100-(k.up/(k.total||1)*100),20,50)}">${k.up??'—'}</span><span class="dim" style="font-size:16px">/${k.total??'—'} up</span> `+(whenChip(k.fetchedAt)||'')+(k.note?` <span class="warn" style="font-size:11px" title="${esc(k.error||'')}">⚠ ${esc(k.note)}</span>`:'');
   renderKamFleet(k);
   $('loc-up').innerHTML=`<span class="good">${l.up??'—'}</span><span class="dim" style="font-size:16px">/${l.total??'—'} up</span>`+(l.note?` <span class="warn" style="font-size:11px" title="${esc(l.error||'')}">⚠ ${esc(l.note)}</span>`:'');
   $('loc-list').innerHTML=(l.procs||[]).map(pr=>lrow(`${statusDot(pr.status)}${esc(pr.name)}`,`${pr.memMB!=null?pr.memMB+'M':''}`,whenChip(pr.startedAt))).join('')||`<span class="muted">${esc(l.note||'no local pm2 procs')}</span>`;
diff --git a/server.js b/server.js
index 6d97abc..645c07e 100644
--- a/server.js
+++ b/server.js
@@ -145,6 +145,7 @@ async function collectLocalFleet() {
 // round-trips are too slow for the 30s snapshot loop.
 const KAM_CACHE = path.join(DATA_DIR, 'kamatera-fleet.json');
 let kamLastFetch = 0;
+let kamLastErr = null;
 async function collectKamateraFleet() {
   const now = Date.now();
   // refresh at most every 5 min
@@ -156,12 +157,22 @@ async function collectKamateraFleet() {
         const list = JSON.parse(r.out);
         const procs = list.map((p) => ({ name: p.name, status: p.pm2_env && p.pm2_env.status, restarts: p.pm2_env && p.pm2_env.restart_time, startedAt: p.pm2_env && p.pm2_env.pm_uptime ? new Date(p.pm2_env.pm_uptime).toISOString() : null }));
         fs.writeFileSync(KAM_CACHE, JSON.stringify({ fetchedAt: new Date().toISOString(), procs }));
-      } catch (e) { /* keep last-good */ }
+        kamLastErr = null;
+      } catch (e) { kamLastErr = 'bad pm2 jlist output: ' + e.message; /* keep last-good */ }
+    } else {
+      kamLastErr = (r.err || r.out || 'ssh failed').toString().trim().slice(0, 200);
     }
   }
   try {
     const c = JSON.parse(fs.readFileSync(KAM_CACHE, 'utf8'));
-    return { fetchedAt: c.fetchedAt, procs: c.procs, up: c.procs.filter((p) => p.status === 'online').length, total: c.procs.length };
+    const ageMin = c.fetchedAt ? Math.round((Date.now() - Date.parse(c.fetchedAt)) / 60000) : null;
+    const stale = ageMin != null && ageMin > 12;
+    const out = { fetchedAt: c.fetchedAt, procs: c.procs, up: c.procs.filter((p) => p.status === 'online').length, total: c.procs.length, stale };
+    if (stale || kamLastErr) {
+      out.note = 'ssh refresh failing — cached ' + (ageMin != null ? ageMin + 'm' : '?') + ' ago';
+      if (kamLastErr) out.error = kamLastErr;
+    }
+    return out;
   } catch (e) { return { procs: [], up: 0, total: 0, note: 'no cached kamatera snapshot yet' }; }
 }
 

← e5711b6 chore: mark task 20 done (cron cards last/next run shipped)  ·  back to AbramsEgo  ·  chore: replenish audit (fable) — all 8 SPEC panels live + ha ec4ed7b →