Heartbeat honesty: SYNC badge reads refreshed_at, not grade-lock updated_at

The "SIGNAL LIVE vs STALE 8h" contradiction was a field mismatch, not a dead
pipeline. updated_at is the grade-LOCK time (advances only on a full snapshot,
5×/day — grades never change in-game, so it is intentionally stable). The
SYNC badge measured the 20-min intraday cadence (expected_interval_s=1200)
against that 5×/day field → structurally guaranteed STALE between slots even
when intraday refreshes lines perfectly.

- snapshotService: full snapshot now seeds refreshed_at at lock time
- intradayRefresh already bumps refreshed_at every ~20 min (unchanged)
- /api/snapshot/summary + GET /:sport now expose refreshed_at (was written to
  Redis but never serialized → no public liveness signal existed)
- LiveLayer SYNC badge measures freshness from refreshed_at (fallback updated_at)

Exposing refreshed_at also gives a public heartbeat probe: it advances every
intraday slot, so pipeline liveness is verifiable without container logs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-07-15 16:09:18 -04:00
parent 4d7fe09360
commit 7712f0a442
4 changed files with 54 additions and 5 deletions
+7 -1
View File
@@ -399,7 +399,13 @@ async function runSnapshot(sport, opts = {}) {
// Lock: previous = old latest, latest = new, grades = enriched.
if (prev) await deps.cacheSet(`snapshot:${sp}:previous`, prev, SNAP_TTL);
const snapshot = { sport: sp, updated_at: ts, grades: enriched, deltas, gradeCount: enriched.length };
// `updated_at` = grade-LOCK time (advances only on a full snapshot, 5×/day —
// grades never change in-game, so this is intentionally stable). `refreshed_at`
// = the freshness heartbeat: seeded here at lock time, then bumped every
// intraday refresh (intradayRefreshService). The SYNC badge keys off
// refreshed_at — measuring the 20-min cadence against the grade-lock field is
// what produced the "SIGNAL LIVE vs STALE 8h" contradiction.
const snapshot = { sport: sp, updated_at: ts, refreshed_at: ts, grades: enriched, deltas, gradeCount: enriched.length };
await deps.cacheSet(`snapshot:${sp}:latest`, snapshot, SNAP_TTL);
// Session 59 — grades:{sport} must outlive the gap between cron runs (up to
// 5h) or team rosters / Explore / leaders go dark mid-day. SNAP_TTL (6h),