diff --git a/app/(main)/admin/__tests__/save-bar-switches.test.tsx b/app/(main)/admin/__tests__/save-bar-switches.test.tsx index 23a5d4ed2..b0d8d63ee 100644 --- a/app/(main)/admin/__tests__/save-bar-switches.test.tsx +++ b/app/(main)/admin/__tests__/save-bar-switches.test.tsx @@ -81,4 +81,30 @@ describe('admin telemetry page', () => { expect.objectContaining({ url: '/api/admin/telemetry', body: { action: 'set-consent', consent: 'on' } }), ])); }); + + it('shows no next send and no consent date while heartbeats are off', async () => { + routes['/api/admin/telemetry'] = { + ...(routes['/api/admin/telemetry'] as object), + consent: 'off', consentedAt: '2026-10-01T10:00:00Z', lastSentAt: '2026-10-02T10:00:00Z', + nextScheduledAt: new Date(Date.now() - 7 * 3600_000).toISOString(), + }; + render(); + await screen.findByRole('switch', { name: 'Send anonymous heartbeats' }); + expect(screen.queryByText('Next scheduled')).toBeNull(); + expect(screen.queryByText('Consented at')).toBeNull(); + expect(screen.queryByText(/hours ago/)).toBeNull(); + expect(screen.getByText('Last sent')).toBeTruthy(); + }); + + it('shows a past next send as due, not as "hours ago", while heartbeats are on', async () => { + routes['/api/admin/telemetry'] = { + ...(routes['/api/admin/telemetry'] as object), + consent: 'on', consentedAt: '2026-10-01T10:00:00Z', + nextScheduledAt: new Date(Date.now() - 7 * 3600_000).toISOString(), + }; + render(); + await screen.findByText('Next scheduled'); + expect(screen.getByText('Due now')).toBeTruthy(); + expect(screen.getByText('Consented at')).toBeTruthy(); + }); }); diff --git a/app/(main)/admin/_pages/telemetry.tsx b/app/(main)/admin/_pages/telemetry.tsx index 73cdae984..7d03ca985 100644 --- a/app/(main)/admin/_pages/telemetry.tsx +++ b/app/(main)/admin/_pages/telemetry.tsx @@ -40,6 +40,13 @@ function timeAgo(iso: string | null): string { return `${days} days ago`; } +/** When the next heartbeat goes out; a time already past means it is due. */ +function nextRun(iso: string | null): string { + if (!iso) return '-'; + const at = new Date(iso); + return at.getTime() <= Date.now() ? 'Due now' : at.toLocaleString(); +} + export function TelemetryPage() { const [status, setStatus] = useState(null); const [loading, setLoading] = useState(true); @@ -177,12 +184,18 @@ export function TelemetryPage() { {timeAgo(status.lastSentAt)} - - {timeAgo(status.nextScheduledAt)} - - - {status.consentedAt ? new Date(status.consentedAt).toLocaleString() : '-'} - + {/* Only a running schedule has a next send and a consent date; with + heartbeats off they were stale ("Next scheduled: 7 hours ago"). */} + {isOn && ( + <> + + {nextRun(status.nextScheduledAt)} + + + {status.consentedAt ? new Date(status.consentedAt).toLocaleString() : '-'} + + + )}