[object Object]

← 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

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 →