← back to AbramsEgo
server: add per-source data-freshness block + real 7-day wins time-series
fb901d1b913c67730014f9e35e386afff880f627 · 2026-07-08 10:50:13 -0700 · Steve
- computeFreshness() reports each dated source's updatedAt/age/state (fresh|aging|stale);
honestly flags revenue.json as stale (7d) instead of trusting the frozen self-funding number
- summarizeWins() now emits series7d (real daily buckets from the wins array — no invented history)
- /api/freshness slice route added
Files touched
Diff
commit fb901d1b913c67730014f9e35e386afff880f627
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Jul 8 10:50:13 2026 -0700
server: add per-source data-freshness block + real 7-day wins time-series
- computeFreshness() reports each dated source's updatedAt/age/state (fresh|aging|stale);
honestly flags revenue.json as stale (7d) instead of trusting the frozen self-funding number
- summarizeWins() now emits series7d (real daily buckets from the wins array — no invented history)
- /api/freshness slice route added
---
server.js | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 60 insertions(+), 2 deletions(-)
diff --git a/server.js b/server.js
index 61852f0..2092b90 100644
--- a/server.js
+++ b/server.js
@@ -811,6 +811,48 @@ function buildPnL(cost, revenue) {
// ---------------------------------------------------------------------------
// snapshot builder + stale-while-revalidate cache
// ---------------------------------------------------------------------------
+// --- DATA FRESHNESS ---------------------------------------------------------
+// Every tile that sources from a cached/dated file can be silently stale (the
+// self-funding tile sourced revenue.json which sat 6 days old on Jul 2). This
+// computes each real source's updatedAt + its age so the UI can mark a tile
+// visibly stale ("as of Nh ago") instead of trusting a frozen number. Thresholds
+// are per-source: fast loops (fleet/build) go stale in minutes, dated ledgers in
+// hours/days. Grounded ONLY in timestamps the sources actually carry.
+function computeFreshness(snap) {
+ const now = Date.now();
+ const age = (iso) => { const t = Date.parse(iso); return isNaN(t) ? null : Math.max(0, now - t); };
+ // src: [label, updatedAt-iso, warnMs, staleMs]
+ const HR = 3.6e6, MIN = 60e3, DAY = 24 * HR;
+ const rev = snap.revenue || {}, kam = snap.kamatera || {}, act = snap.activity || {};
+ const lf = snap.localFleet || {};
+ const srcs = [
+ { key: 'snapshot', label: 'Snapshot', at: snap.builtAt, warn: 2 * MIN, stale: 5 * MIN },
+ { key: 'revenue', label: 'Revenue ledger', at: rev.updatedAt, warn: 12 * HR, stale: 2 * DAY },
+ { key: 'kamatera', label: 'Kamatera fleet', at: kam.fetchedAt, warn: 8 * MIN, stale: 15 * MIN },
+ { key: 'localFleet', label: 'Local fleet', at: lf.fetchedAt || snap.builtAt, warn: 2 * MIN, stale: 6 * MIN },
+ { key: 'activity', label: 'Git activity', at: act.fetchedAt, warn: 8 * MIN, stale: 20 * MIN },
+ { key: 'officers', label: 'Officers', at: snap.officers && snap.officers.fetchedAt, warn: 2 * MIN, stale: 10 * MIN },
+ ];
+ const out = {};
+ let worst = 'fresh';
+ for (const s of srcs) {
+ const a = age(s.at);
+ let state = 'unknown';
+ if (a != null) state = a >= s.stale ? 'stale' : a >= s.warn ? 'aging' : 'fresh';
+ out[s.key] = { label: s.label, updatedAt: s.at || null, ageMs: a, ageLabel: a == null ? null : humanAge(a), state };
+ if (state === 'stale') worst = 'stale';
+ else if (state === 'aging' && worst !== 'stale') worst = 'aging';
+ }
+ return { sources: out, worst, staleCount: Object.values(out).filter((s) => s.state === 'stale').length };
+}
+function humanAge(ms) {
+ const s = Math.round(ms / 1000);
+ if (s < 90) return s + 's ago';
+ const m = Math.round(s / 60); if (m < 90) return m + 'm ago';
+ const h = Math.round(m / 60); if (h < 36) return h + 'h ago';
+ return Math.round(h / 24) + 'd ago';
+}
+
let SNAP = { builtAt: null };
let BUILDING = null; // in-flight build promise — callers coalesce onto it so an
// awaited /api/refresh (or a post-write rebuild) always
@@ -845,6 +887,8 @@ async function doBuild() {
topModel: usage.models && usage.models.allTime && usage.models.allTime[0] && usage.models.allTime[0].model,
},
};
+ // per-source data-freshness (computed last so it sees every collected key)
+ SNAP.freshness = computeFreshness(SNAP);
try { fs.writeFileSync(SNAPSHOT, JSON.stringify(SNAP)); } catch (e) {}
return SNAP;
}
@@ -863,7 +907,21 @@ function summarizeSessions(s) {
function summarizeWins(w) {
const arr = Array.isArray(w) ? w : (w && w.wins) || [];
const today = new Date().toISOString().slice(0, 10);
- return { count: arr.length, today: arr.filter((x) => (x.date || '').slice(0, 10) === today).length, recent: arr.slice(0, 10) };
+ // real wins-over-time: 7 daily buckets (local calendar day) from the actual
+ // wins array — a genuine time-series, not invented history. Empty days = 0.
+ const now = Date.now();
+ const dayKey = (ms) => {
+ const dt = new Date(ms);
+ return `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, '0')}-${String(dt.getDate()).padStart(2, '0')}`;
+ };
+ const byKey = {}; const series7d = [];
+ for (let i = 6; i >= 0; i--) { const key = dayKey(now - i * 24 * 3.6e6); const b = { day: key, count: 0 }; series7d.push(b); byKey[key] = b; }
+ for (const x of arr) {
+ // wins carry either a `date` (YYYY-MM-DD, already local) or createdAt (epoch ms)
+ const key = (x.date && String(x.date).slice(0, 10)) || (x.createdAt ? dayKey(Number(x.createdAt)) : null);
+ if (key && byKey[key]) byKey[key].count++;
+ }
+ return { count: arr.length, today: arr.filter((x) => (x.date || '').slice(0, 10) === today).length, series7d, recent: arr.slice(0, 10) };
}
function summarizeOfficers(o) {
const arr = Array.isArray(o) ? o : (o && (o.cabinet || o.officers)) || [];
@@ -950,7 +1008,7 @@ app.get('/api/healthz', (req, res) => res.json({ ok: true, service: 'abramsego',
app.get('/api/data', (req, res) => { res.json(SNAP); });
app.get('/api/refresh', async (req, res) => { const s = await buildSnapshot(); res.json(s); });
// per-panel slices (served from cache)
-for (const key of ['system', 'localFleet', 'kamatera', 'canaries', 'crons', 'activity', 'sessions', 'wins', 'officers', 'cost', 'revenue', 'pnl', 'usage']) {
+for (const key of ['system', 'localFleet', 'kamatera', 'canaries', 'crons', 'activity', 'sessions', 'wins', 'officers', 'cost', 'revenue', 'pnl', 'usage', 'freshness']) {
app.get(`/api/${key}`, (req, res) => res.json(SNAP[key] || {}));
}
← 4407f13 5x sweep 1: add favicon (fixes every-load /favicon.ico 404 t
·
back to AbramsEgo
·
index: liquid/fluid bg + motion graphics + vitals graphs + f 054fe68 →