[object Object]

← back to Loops Dashboard

fix: cache + background refresh — 21s → <100ms page load

36c10b11b8045c4e5a71494b89f68ca64bb499fa · 2026-05-08 07:34:54 -0700 · Steve Abrams

Files touched

Diff

commit 36c10b11b8045c4e5a71494b89f68ca64bb499fa
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri May 8 07:34:54 2026 -0700

    fix: cache + background refresh — 21s → <100ms page load
---
 server.js | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 52 insertions(+), 6 deletions(-)

diff --git a/server.js b/server.js
index 120653d..aa9cd89 100644
--- a/server.js
+++ b/server.js
@@ -344,20 +344,66 @@ function renderPage(data) {
 </body></html>`;
 }
 
+// --------------------------------- Cache + background refresh ---------------------------------
+// Each source has a TTL. Page renders read cache instantly (snappy). A background
+// loop refreshes stale entries every REFRESH_MS so the page never blocks on slow ops.
+const REFRESH_MS = 30_000;
+const cache = {
+  idea:    { value: null, ts: 0, ttl: 5_000 },   // cheap (file reads), short TTL
+  codex3:  { value: null, ts: 0, ttl: 30_000 },  // git show calls, 30s
+  morning: { value: null, ts: 0, ttl: 60_000 },  // 6 scanners on the other end, 1 min
+  launchd: { value: null, ts: 0, ttl: 15_000 },  // launchctl list, fast
+  pm2:     { value: null, ts: 0, ttl: 30_000 }   // pm2 jlist, slow, 30s
+};
+
+async function refresh(key, fn) {
+  try {
+    const v = await fn();
+    cache[key].value = v;
+    cache[key].ts = Date.now();
+  } catch (e) {
+    console.error(`[cache:${key}] refresh error:`, e.message);
+  }
+}
+
+async function refreshAll() {
+  await Promise.all([
+    refresh('idea',    async () => getIdeaLoopState()),
+    refresh('codex3',  async () => getCodex3WayState()),
+    refresh('morning', async () => await getMorningReviewState()),
+    refresh('launchd', async () => getLaunchdState()),
+    refresh('pm2',     async () => getPm2State())
+  ]);
+}
+
+// Read cache (refresh in background if stale; never block the read).
+function readCache(key, fallback) {
+  const c = cache[key];
+  if (!c.value) return fallback;
+  return c.value;
+}
+
+// Kick off initial refresh + interval
+refreshAll().catch(() => {});
+setInterval(() => { refreshAll().catch(() => {}); }, REFRESH_MS);
+
 // --------------------------------- Server ---------------------------------
 
 const server = http.createServer(async (req, res) => {
   if (req.url === '/healthz') {
     res.writeHead(200, { 'Content-Type': 'application/json' });
-    return res.end(JSON.stringify({ ok: true, ts: new Date().toISOString() }));
+    return res.end(JSON.stringify({
+      ok: true, ts: new Date().toISOString(),
+      cache_ages_s: Object.fromEntries(Object.entries(cache).map(([k, v]) => [k, v.ts ? Math.round((Date.now() - v.ts) / 1000) : -1]))
+    }));
   }
 
   try {
-    const idea = getIdeaLoopState();
-    const codex3 = getCodex3WayState();
-    const morning = await getMorningReviewState();
-    const launchd = getLaunchdState();
-    const pm2 = getPm2State();
+    const idea    = readCache('idea',    { accepted: 0, rejected: 0, decisions: 0, top: [], last_tick_age: '—' });
+    const codex3  = readCache('codex3',  []);
+    const morning = readCache('morning', { total: 0, by_source: {} });
+    const launchd = readCache('launchd', { total: 0, problems: 0, problem_jobs: [], ai_loop_jobs: [] });
+    const pm2     = readCache('pm2',     { total: 0, online: 0, errored: 0, ai_loop: [] });
 
     if (req.url === '/api/state') {
       res.writeHead(200, { 'Content-Type': 'application/json' });

← 5ba48a6 initial scaffold: loops-dashboard at :9930 aggregating idea-  ·  back to Loops Dashboard  ·  gitignore: protect .env* from accidental commits per MEMORY f4cb25a →