Fix repeater clock drift-drift on nav-away-come-back

This commit is contained in:
Jack Kingsman
2026-03-16 11:32:40 -07:00
parent c809dad05d
commit 8d7d926762
35 changed files with 90 additions and 53 deletions
@@ -16,8 +16,8 @@ export function NodeInfoPane({
}) {
const clockDrift = useMemo(() => {
if (!data?.clock_utc) return null;
return formatClockDrift(data.clock_utc);
}, [data?.clock_utc]);
return formatClockDrift(data.clock_utc, state.fetched_at ?? undefined);
}, [data?.clock_utc, state.fetched_at]);
return (
<RepeaterPane title="Node Info" state={state} onRefresh={onRefresh} disabled={disabled}>
@@ -39,7 +39,10 @@ export function formatDuration(seconds: number): string {
return `${mins}m`;
}
export function formatClockDrift(clockUtc: string): { text: string; isLarge: boolean } {
export function formatClockDrift(
clockUtc: string,
referenceTimeMs: number = Date.now()
): { text: string; isLarge: boolean } {
// Firmware format: "HH:MM - D/M/YYYY UTC" or "HH:MM:SS - D/M/YYYY UTC"
// Also handle ISO-like: "YYYY-MM-DD HH:MM:SS"
let parsed: Date;
@@ -56,7 +59,7 @@ export function formatClockDrift(clockUtc: string): { text: string; isLarge: boo
}
if (isNaN(parsed.getTime())) return { text: '(invalid)', isLarge: false };
const driftMs = Math.abs(Date.now() - parsed.getTime());
const driftMs = Math.abs(referenceTimeMs - parsed.getTime());
const driftSec = Math.floor(driftMs / 1000);
if (driftSec >= 86400) return { text: '>24 hours!', isLarge: true };
@@ -401,6 +401,40 @@ describe('RepeaterDashboard', () => {
expect(screen.getByText(/Fetched .*Just now/)).toBeInTheDocument();
});
it('keeps repeater clock drift anchored to fetch time across remounts', () => {
vi.useFakeTimers();
try {
const fetchedAt = Date.UTC(2024, 0, 1, 12, 0, 0);
vi.setSystemTime(fetchedAt);
mockHook.loggedIn = true;
mockHook.paneData.nodeInfo = {
name: 'TestRepeater',
lat: null,
lon: null,
clock_utc: '11:59:30 - 1/1/2024 UTC',
};
mockHook.paneStates.nodeInfo = {
loading: false,
attempt: 1,
error: null,
fetched_at: fetchedAt,
};
const firstRender = render(<RepeaterDashboard {...defaultProps} />);
expect(screen.getByText(/\(drift: 30s\)/)).toBeInTheDocument();
vi.setSystemTime(fetchedAt + 10 * 60 * 1000);
firstRender.unmount();
render(<RepeaterDashboard {...defaultProps} />);
expect(screen.getByText(/\(drift: 30s\)/)).toBeInTheDocument();
expect(screen.queryByText(/\(drift: 10m30s\)/)).not.toBeInTheDocument();
} finally {
vi.useRealTimers();
}
});
it('renders action buttons', () => {
mockHook.loggedIn = true;