feat: warn when the local clock is far off the homeserver's (#158)
Incident 2026-09-17: a wrong Windows clock broke calls and media keys while the server answered 200 to everything, with no hint in the UI. Measurement needs no extra requests and no CORS-exposed headers: every live event carries origin_server_ts and unsigned.age (our server's now − ts at response time), so localTimestamp − origin_server_ts is the skew. Only RoomEvent.Timeline live events count (cache replays have stale age and are already flagged liveEvent=false by the SDK); the initial network sync qualifies, so a wrong clock is flagged within seconds of startup. Median of the last 5 samples, ≥3 needed; warn at |skew| > 30 s, clear below 15 s. UI: a banner in the sync-status slot — "Your computer's clock is 14 minutes ahead of the server. Encrypted messages and voice calls will fail until it is fixed." with a per-OS How-to-fix hint and Dismiss for 24 h — plus the same line in the call status bar while in a call. Never auto-corrects anything. Unit-tested (median, hysteresis, stale-age rejection, wording); verified headless with Playwright's clock skewed +14 min and −3 h (banner, hint, in-call line, dismiss) and in sync (nothing shown). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Box, Spinner } from 'folds';
|
||||
import { Box, Spinner, Text, color } from 'folds';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import classNames from 'classnames';
|
||||
import { LiveChip } from './LiveChip';
|
||||
import * as css from './styles.css';
|
||||
@@ -14,6 +15,8 @@ import { CallEmbed } from '../../plugins/call/CallEmbed';
|
||||
import { useCallJoined } from '../../hooks/useCallEmbed';
|
||||
import { useCallSpeakers } from '../../hooks/useCallSpeakers';
|
||||
import { MemberSpeaking } from './MemberSpeaking';
|
||||
import { clockSkewAtom } from '../../state/clockSkew';
|
||||
import { describeSkewVsServer } from '../../utils/clockSkew';
|
||||
|
||||
type CallStatusProps = {
|
||||
callEmbed: CallEmbed;
|
||||
@@ -26,6 +29,8 @@ export function CallStatus({ callEmbed }: CallStatusProps) {
|
||||
const screenSize = useScreenSize();
|
||||
const callJoined = useCallJoined(callEmbed);
|
||||
const speakers = useCallSpeakers(callEmbed);
|
||||
// [Gitea #158] Same warning as the top banner, where the user is looking during a call.
|
||||
const clockSkew = useAtomValue(clockSkewAtom);
|
||||
|
||||
const compact = screenSize === ScreenSize.Mobile;
|
||||
|
||||
@@ -51,6 +56,19 @@ export function CallStatus({ callEmbed }: CallStatusProps) {
|
||||
{!compact && (
|
||||
<>
|
||||
<CallRoomName room={room} />
|
||||
{clockSkew.warning && clockSkew.skewMs !== null && (
|
||||
<>
|
||||
<StatusDivider />
|
||||
<Text
|
||||
size="T200"
|
||||
truncate
|
||||
style={{ color: color.Warning.Main }}
|
||||
title="Fix your computer's clock — calls and encryption depend on it"
|
||||
>
|
||||
Clock {describeSkewVsServer(clockSkew.skewMs)} — calls will fail
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{speakers.size > 0 && (
|
||||
<>
|
||||
<StatusDivider />
|
||||
|
||||
Reference in New Issue
Block a user