From e3fd576543e250461d40f7082c398b65d67ea3cb Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Wed, 28 Oct 2026 10:24:34 +0100
Subject: [PATCH] fix: hide the next heartbeat and consent date in admin usage
stats while heartbeats are off
---
.../__tests__/save-bar-switches.test.tsx | 26 +++++++++++++++++++
app/(main)/admin/_pages/telemetry.tsx | 25 +++++++++++++-----
2 files changed, 45 insertions(+), 6 deletions(-)
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() : '-'}
+
+ >
+ )}