← 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
R100 build-queue/tasks/21-kamatera-staleness-note.md build-queue/done/21-kamatera-staleness-note.mdM data/local-fleet.jsonM public/index.htmlM server.js
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 →