← back to AbramsEgo
auto-save: 2026-07-01T14:06:35 (5 files) — build-queue/tasks/17-localfleet-lastgood-cache.md public/index.html server.js build-queue/done/17-localfleet-lastgood-cache.md data/local-fleet.json
27d1b769ac78557c729cbbe473779b3d4abac61b · 2026-07-01 14:06:39 -0700 · Steve Abrams
Files touched
R100 build-queue/tasks/17-localfleet-lastgood-cache.md build-queue/done/17-localfleet-lastgood-cache.mdA data/local-fleet.jsonM public/index.htmlM server.js
Diff
commit 27d1b769ac78557c729cbbe473779b3d4abac61b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jul 1 14:06:39 2026 -0700
auto-save: 2026-07-01T14:06:35 (5 files) — build-queue/tasks/17-localfleet-lastgood-cache.md public/index.html server.js build-queue/done/17-localfleet-lastgood-cache.md data/local-fleet.json
---
.../17-localfleet-lastgood-cache.md | 0
data/local-fleet.json | 1 +
public/index.html | 4 +--
server.js | 36 +++++++++++++++++++---
4 files changed, 34 insertions(+), 7 deletions(-)
diff --git a/build-queue/tasks/17-localfleet-lastgood-cache.md b/build-queue/done/17-localfleet-lastgood-cache.md
similarity index 100%
rename from build-queue/tasks/17-localfleet-lastgood-cache.md
rename to build-queue/done/17-localfleet-lastgood-cache.md
diff --git a/data/local-fleet.json b/data/local-fleet.json
new file mode 100644
index 0000000..937e567
--- /dev/null
+++ b/data/local-fleet.json
@@ -0,0 +1 @@
+{"fetchedAt":"2026-07-01T21:06:39.101Z","procs":[{"name":"pm2-logrotate","status":"online","cpu":17,"memMB":72,"restarts":0,"uptimeMs":1516,"startedAt":"2026-07-01T21:06:37.585Z"},{"name":"abramsego","status":"online","cpu":18,"memMB":72,"restarts":0,"uptimeMs":1468,"startedAt":"2026-07-01T21:06:37.633Z"}]}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index e8d2703..3da9989 100644
--- a/public/index.html
+++ b/public/index.html
@@ -339,8 +339,8 @@ async function load(){
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>`;
renderKamFleet(k);
- $('loc-up').innerHTML=`<span class="good">${l.up??'—'}</span><span class="dim" style="font-size:16px">/${l.total??'—'} up</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">no local pm2 procs</span>';
+ $('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>`;
// ---- canaries ---- (sort + density + infinite scroll like every DW grid)
const cans=(d.canaries&&d.canaries.canaries)||[];
diff --git a/server.js b/server.js
index 9d79ba8..321cd8f 100644
--- a/server.js
+++ b/server.js
@@ -105,12 +105,28 @@ async function collectSystem() {
};
}
+// Local pm2 gets the same last-good disk cache as Kamatera — the Mac2 pm2
+// daemon crawls under swap pressure, and a timed-out jlist must degrade to the
+// cached snapshot instead of a silently dead panel.
+const LOCAL_CACHE = path.join(DATA_DIR, 'local-fleet.json');
async function collectLocalFleet() {
const pm2Bin = path.join(HOME, '.npm-global', 'bin', 'pm2');
- const r = await sh(pm2Bin, ['jlist'], 8000);
- if (!r.ok) return { procs: [], error: r.err || 'pm2 unavailable' };
- let list = [];
- try { list = JSON.parse(r.out); } catch (e) { return { procs: [], error: 'pm2 parse' }; }
+ const r = await sh(pm2Bin, ['jlist'], 20000);
+ let list = null;
+ if (r.ok) { try { list = JSON.parse(r.out); } catch (e) { /* fall through to cache */ } }
+ if (!list) {
+ const err = r.ok ? 'pm2 parse' : (r.err || 'pm2 unavailable');
+ try {
+ const c = JSON.parse(fs.readFileSync(LOCAL_CACHE, 'utf8'));
+ return {
+ procs: c.procs, up: c.procs.filter((p) => p.status === 'online').length,
+ total: c.procs.length, fetchedAt: c.fetchedAt,
+ note: 'pm2 slow — cached snapshot', error: err,
+ };
+ } catch (e) {
+ return { procs: [], up: null, total: null, note: 'pm2 unreachable — no cached snapshot yet', error: err };
+ }
+ }
const procs = list.map((p) => ({
name: p.name,
status: p.pm2_env && p.pm2_env.status,
@@ -121,6 +137,7 @@ async function collectLocalFleet() {
// created/started time — every fleet tile obeys Steve's admin-card date+time rule
startedAt: p.pm2_env && p.pm2_env.pm_uptime ? new Date(p.pm2_env.pm_uptime).toISOString() : null,
}));
+ try { fs.writeFileSync(LOCAL_CACHE, JSON.stringify({ fetchedAt: new Date().toISOString(), procs })); } catch (e) { /* cache write is best-effort */ }
return { procs, up: procs.filter((p) => p.status === 'online').length, total: procs.length };
}
@@ -161,7 +178,16 @@ async function collectCanaries() {
try {
const j = JSON.parse(fs.readFileSync(f, 'utf8'));
verdict = j.verdict || j.status || (j.down > 0 ? 'DOWN' : j.degraded > 0 ? 'DEGRADED' : (j.up != null ? 'UP' : null));
- scanned = j.scanned_at || j.checked_at || null;
+ // nested shapes: per-unit verdicts (worst-of) or top-level fail/warn/leaking/dead counters
+ if (!verdict) {
+ const worst = (vs) => { const V = vs.filter(Boolean).map(v => String(v).toUpperCase());
+ return V.some(v => /FAIL|CRIT|DOWN/.test(v)) ? 'FAIL' : V.some(v => /WARN|DEGRAD/.test(v)) ? 'WARN' : V.length ? 'PASS' : null; };
+ if (Array.isArray(j.units)) verdict = worst(j.units.map(u => u.verdict));
+ else if ((j.fail > 0) || (j.leaking > 0) || (j.dead > 0)) verdict = 'FAIL';
+ else if (j.warn > 0) verdict = 'WARN';
+ else if (j.fail === 0 || j.leaking === 0 || j.dead === 0) verdict = 'PASS';
+ }
+ scanned = j.scanned_at || j.checked_at || j.ts || j.at || null;
} catch (e) {}
const ageMin = Math.round((Date.now() - st.mtimeMs) / 60000);
// heartbeat/last-run time so each canary card can show created date+time
← 7492a84 chore: replenish audit (fable) — 2 real gaps: Fleet·Local pa
·
back to AbramsEgo
·
fix: canary board — derive verdicts for nested shapes (6/12 5b8c855 →