2026.09.13 — 2026-09-07
Fixed
Take the clock from Studio, and let the operator pick the timezone (6a970c39)
Every duration, expiry check and metric window in the UI was computed against the browser's clock. An operator whose workstation ran four minutes fast saw every request-reply flow age inflated by four minutes, a live API key rendered as
expired, and an empty metrics chart with nothing saying why — the window it asked the server for had not happened yet.Studio already refuses to trust a broker's clock (ADR-0053). The browser is one more foreign clock, and it was the only one still taken on faith.
What changes for you:
- Ages, staleness labels and token expiry now follow Studio's clock, not the machine the browser runs on. On a correctly-synchronised workstation nothing moves; on a skewed one, the numbers become right.
- The metrics window and the queue drawer's chart are quantised on the server's clock, so a skewed workstation no longer requests a range the server has no samples for.
- Timestamps now render in your own timezone by default, detected from the browser, where they were previously always UTC. Settings → Display picks a different one, including UTC to match container and broker logs. The choice is per-browser, needs no permission, and is never reset once made.
- Every absolute timestamp now names the offset it is written in (
Z,+03:30) and is rendered to whole seconds, where before some carried milliseconds and some did not. - The metric charts previously drew their axis in the browser's local zone while every table beside them was UTC, with nothing disclosing the difference. Both now follow the chosen zone.
How it works: a new
GET /api/v1/timereturns Studio's clock, and the client measures its own offset NTP-style with round-trip bracketing — the same estimator and the same constants asClockOffsetRegistry, so the two halves of the system agree about method. A reading is only allowed to teach the estimate when its round trip is at or near the best seen; an offset inside its own error bar is held at zero rather than correcting by noise. The SSEpingalready carried the server's clock and the client discarded it; it is now a drift detector that asks for a real probe, never a measurement of its own, since a one-way frame cannot measure its own latency. A wall-clock step — a laptop waking, an NTP correction — is caught by comparingDate.now()againstperformance.now(), the client-sideMonotonicClockWatch, and discards the estimate rather than slowly unlearning it.web/src/app/time.tsstates the rule for the whole frontend: durations and expiry against a server timestamp useserverNow(); a browser-stamped instant entering such a comparison (TanStack'sdataUpdatedAtis the only one) is normalised withtoServerMs()at the boundary; monotonic work keeps usingperformance.now(). Three duplicate "ago" formatters and seven copies of the absolute-timestamp formatter are folded into it.Known and deliberately out of scope: the queue detail drawer's chart window is memoised on the range spec alone, so it is fixed at first render and does not advance while the drawer stays open. That predates this change.