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:
@@ -0,0 +1,82 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { Box, Button, config, Line, Text } from 'folds';
|
||||
import { clockSkewAtom } from '../../state/clockSkew';
|
||||
import { ContainerColor } from '../../styles/ContainerColor.css';
|
||||
import { clockFixHint, describeSkewVsServer, detectClockFixPlatform } from '../../utils/clockSkew';
|
||||
|
||||
const DISMISS_KEY = 'lotus-clock-skew-dismissed-until';
|
||||
const DISMISS_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
const readDismissedUntil = (): number => {
|
||||
try {
|
||||
const raw = localStorage.getItem(DISMISS_KEY);
|
||||
const n = raw ? Number(raw) : 0;
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* [Gitea #158] "Your computer's clock is 14 minutes ahead of the server."
|
||||
* Same slot and style as the sync banners. Shown while the skew monitor is
|
||||
* over its threshold; the direction matters, so it is said. Dismissable for
|
||||
* 24 h; never auto-corrects anything.
|
||||
*/
|
||||
export function ClockSkewBanner() {
|
||||
const { skewMs, warning } = useAtomValue(clockSkewAtom);
|
||||
const [dismissedUntil, setDismissedUntil] = useState(readDismissedUntil);
|
||||
const [showHint, setShowHint] = useState(false);
|
||||
|
||||
const dismiss = useCallback(() => {
|
||||
const until = Date.now() + DISMISS_MS;
|
||||
try {
|
||||
localStorage.setItem(DISMISS_KEY, String(until));
|
||||
} catch {
|
||||
// storage unavailable — dismiss for this session only
|
||||
}
|
||||
setDismissedUntil(until);
|
||||
}, []);
|
||||
|
||||
if (!warning || skewMs === null || Date.now() < dismissedUntil) return null;
|
||||
|
||||
return (
|
||||
<Box direction="Column" shrink="No">
|
||||
<Box
|
||||
className={ContainerColor({ variant: 'Warning' })}
|
||||
style={{ padding: `${config.space.S100} ${config.space.S300}` }}
|
||||
direction="Column"
|
||||
alignItems="Center"
|
||||
gap="100"
|
||||
role="alert"
|
||||
>
|
||||
<Box alignItems="Center" gap="300" wrap="Wrap" justifyContent="Center">
|
||||
<Text size="L400" align="Center">
|
||||
Your computer's clock is <b>{describeSkewVsServer(skewMs)}</b>. Encrypted messages
|
||||
and voice calls will fail until it is fixed.
|
||||
</Text>
|
||||
<Button
|
||||
size="300"
|
||||
variant="Warning"
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
onClick={() => setShowHint((v) => !v)}
|
||||
aria-expanded={showHint}
|
||||
>
|
||||
<Text size="B300">How to fix</Text>
|
||||
</Button>
|
||||
<Button size="300" variant="Warning" fill="None" radii="300" onClick={dismiss}>
|
||||
<Text size="B300">Dismiss for 24 h</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
{showHint && (
|
||||
<Text size="T200" align="Center">
|
||||
{clockFixHint(detectClockFixPlatform(navigator.userAgent))}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Line variant="Warning" size="300" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user