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:
@@ -41,7 +41,12 @@ const EKG = 'M0 12 H10 L13 4 L16 20 L19 12 H30 L33 9 L36 15 L39 12 H50';
|
||||
|
||||
interface SnapshotSummary {
|
||||
graded: number;
|
||||
/** Grade-LOCK time — advances only on a full snapshot (5×/day). Intentionally
|
||||
* stable (grades never change in-game); NOT the freshness signal. */
|
||||
updated_at: string | null;
|
||||
/** Freshness heartbeat — grade lock OR intraday line refresh (~every 20 min).
|
||||
* The SYNC badge keys off THIS, not updated_at. */
|
||||
refreshed_at: string | null;
|
||||
/** The pipeline's expected cadence (s) — SYNC thresholds key off this. */
|
||||
expected_interval_s?: number;
|
||||
}
|
||||
@@ -84,6 +89,7 @@ export function HeartbeatBar() {
|
||||
setSummary({
|
||||
graded: data.graded,
|
||||
updated_at: data.updated_at ?? null,
|
||||
refreshed_at: data.refreshed_at ?? data.updated_at ?? null,
|
||||
expected_interval_s: Number(data.expected_interval_s) > 0 ? Number(data.expected_interval_s) : undefined,
|
||||
});
|
||||
}
|
||||
@@ -97,7 +103,11 @@ export function HeartbeatBar() {
|
||||
}, []);
|
||||
|
||||
void live.tick; // subscription drives the 1s clock re-render
|
||||
const syncedAt = summary?.updated_at ? Date.parse(summary.updated_at) : NaN;
|
||||
// Freshness is measured from the intraday heartbeat (refreshed_at), NOT the
|
||||
// grade-lock time (updated_at). Measuring the 20-min cadence against the
|
||||
// 5×/day lock field is what made a live pipeline read "STALE 8h".
|
||||
const freshAt = summary?.refreshed_at ?? summary?.updated_at ?? null;
|
||||
const syncedAt = freshAt ? Date.parse(freshAt) : NaN;
|
||||
const elapsed = Number.isFinite(syncedAt) ? Date.now() - syncedAt : null;
|
||||
const expectedMs = (summary?.expected_interval_s ?? DEFAULT_EXPECTED_S) * 1000;
|
||||
const tier: 'normal' | 'amber' | 'stale' =
|
||||
|
||||
Reference in New Issue
Block a user