Freshness, of the data Most freshness indicators report on the page, not the data. A refresh that succeeds against a stalled pipeline produces a confidently current-looking screen full of old numbers. Tokens this needs: --foreground, --muted, --muted-foreground, --border, --status-warn, --state-live, --state-stale, --status-critical, --status-unknown The status, chart, scale, state and direction names are an extension, not a rename. shadcn has --destructive and five --chart-* and nothing else in this territory. ──────────────────────────────────────────────────────────────────────── // FreshnessIndicator.tsx ──────────────────────────────────────────────────────────────────────── import { freshness, FRESHNESS_COLOR } from "./freshness"; /** Prometheus's default lookback: an instant query returns the newest sample * younger than this, so anything inside it draws as if it were current. */ const LOOKBACK_MS = 5 * 60_000; const ago = (ms: number) => { const s = Math.round(ms / 1000); if (s < 60) return `${s}s ago`; const m = Math.round(s / 60); return m < 60 ? `${m}m ago` : `${Math.floor(m / 60)}h ${m % 60}m ago`; }; /** * Three ages on one axis. The page's own refresh is reported, but it is the * last of the three, because a query that succeeds against a stalled pipeline * is not news about the data. The state comes from when the newest sample * arrived against the cadence it was expected at. */ export function FreshnessIndicator({ lastArrival, expectedEveryMs, lastQuery, covers, lookbackMs = LOOKBACK_MS, now, width = 640 }: { /** When the newest sample arrived. Null when nothing has ever arrived. */ lastArrival: Date | null; expectedEveryMs: number; /** When the page last ran its queries. Not the data's age. */ lastQuery: Date; /** The span the value summarises, which is neither of the other two. */ covers: { from: Date; to: Date }; lookbackMs?: number; /** The clock to age against. Pass one to render on a server. */ now?: Date; width?: number; }) { const clock = now ?? new Date(); const { state, ageMs } = freshness(lastArrival, expectedEveryMs, clock); const color = FRESHNESS_COLOR[state]; // The axis runs from the start of the covered window to now. const span = clock.getTime() - covers.from.getTime(); const x = (d: Date) => 80 + ((d.getTime() - covers.from.getTime()) / span) * (width - 100); const minutesBefore = (d: Date) => Math.round((clock.getTime() - d.getTime()) / 60_000); const mid = new Date(covers.from.getTime() + span / 2); const lookbackStart = new Date(clock.getTime() - lookbackMs); return (

{lastArrival ? `last sample arrived, ${ago(ageMs!)}` : "no sample has arrived"} · {state}

lookback window now the window the value covers {lastArrival && ( <> last sample arrived, {ago(ageMs!)} )} page last ran its queries {[covers.from, mid, clock].map((d) => ( {minutesBefore(d) ? `-${minutesBefore(d)}m` : "0"} ))}
); } ──────────────────────────────────────────────────────────────────────── // freshness.ts ──────────────────────────────────────────────────────────────────────── /** * Freshness is a property of the data, not of the request that fetched it. * * The common implementation reports when the page last re-queried, which stays * cheerful while a pipeline is stalled. This takes the arrival time of the * newest sample and the cadence it is expected at, so "late" is something the * component can work out rather than something a human notices eventually. */ export type Freshness = "live" | "late" | "stale" | "unknown"; export function freshness( lastArrival: Date | null, expectedEveryMs: number, now = new Date(), ): { state: Freshness; ageMs: number | null } { if (!lastArrival) return { state: "unknown", ageMs: null }; const ageMs = now.getTime() - lastArrival.getTime(); // One missed interval is late. Three is stale—the difference matters, // because late is worth a glance and stale means stop trusting the number. if (ageMs > expectedEveryMs * 3) return { state: "stale", ageMs }; if (ageMs > expectedEveryMs) return { state: "late", ageMs }; return { state: "live", ageMs }; } export const FRESHNESS_COLOR: Record = { live: "hsl(var(--state-live))", late: "hsl(var(--state-stale))", stale: "hsl(var(--status-critical))", unknown: "hsl(var(--status-unknown))", }; ──────────────────────────────────────────────────────────────────────── // demo.tsx ──────────────────────────────────────────────────────────────────────── import { FreshnessIndicator } from "./FreshnessIndicator"; /** * The page ran its queries just now and got a value that covers the last * quarter hour. The newest sample is four minutes old against a two-minute * cadence, so the state is late: inside the lookback window, still drawing, * and the page alone would never say so. The clock is fixed so the ages render * the same on the server and in the browser. */ const NOW = new Date("2026-09-15T09:00:00Z"); const minutesAgo = (m: number) => new Date(NOW.getTime() - m * 60_000); export default function Demo() { return ( ); }