From 792a3369376f7e04ea21a75468f202d19f4d3941 Mon Sep 17 00:00:00 2001
From: Brad Groux <3053586+BradGroux@users.noreply.github.com>
Date: Fri, 4 Sep 2026 11:42:26 -0500
Subject: [PATCH] fix: use UTC calendar bounds for metrics export
---
docs/design/TASK-OVERLAY-ACCEPTANCE.md | 2 ++
.../dashboard-drilldowns-mantine.test.tsx | 21 +++++++++++++++++++
web/src/components/dashboard/ExportDialog.tsx | 5 ++++-
3 files changed, 27 insertions(+), 1 deletion(-)
diff --git a/docs/design/TASK-OVERLAY-ACCEPTANCE.md b/docs/design/TASK-OVERLAY-ACCEPTANCE.md
index 376d56d1..562c6180 100644
--- a/docs/design/TASK-OVERLAY-ACCEPTANCE.md
+++ b/docs/design/TASK-OVERLAY-ACCEPTANCE.md
@@ -72,6 +72,8 @@ Independent specification and standards reviews found no actionable source issue
### Metrics export
+Export dates use inclusive UTC calendar-day bounds: From starts at 00:00:00.000Z and To ends at 23:59:59.999Z on the selected dates. The dialog states this convention. Exact-query component checks cover ordinary dates and daylight-saving transition dates; run the date slice under UTC, America/Chicago, and Asia/Tokyo to verify independence from the host timezone.
+
`task-export-popout.spec.ts` opens export from task History/Metrics and tests both themes and motion settings at the three viewport/text combinations. It checks fixed footer geometry and hit targets, viewport containment, nested inert state, guarded Escape/header/backdrop dismissal, disabled filters while exporting, retained filters after failure, and a successful synthetic download on retry with an identical query and exact opener restoration. The retry uses a response with no filename header and verifies the generic fallback name. No real telemetry export is performed.
The original browser case reproduced dismissal while the request was pending. Export now uses a synchronous submission/dismissal lock, exposes an inline error, and preserves scope/date filters on failure. The error receives focus without native scrolling and is then centered in the primary scroller; focus alone left its bottom edge clipped at minimum size. Browser and component checks verify the focus/scroll behavior. The component regression also tests immediate duplicate submission and restored Cancel availability.
diff --git a/web/src/__tests__/dashboard-drilldowns-mantine.test.tsx b/web/src/__tests__/dashboard-drilldowns-mantine.test.tsx
index 9bf49b7b..4e5c3043 100644
--- a/web/src/__tests__/dashboard-drilldowns-mantine.test.tsx
+++ b/web/src/__tests__/dashboard-drilldowns-mantine.test.tsx
@@ -246,6 +246,27 @@ describe('dashboard Mantine drilldown surfaces', () => {
expect(onOpenChange).toHaveBeenCalledWith(false);
});
+ it.each(['2026-09-02', '2026-03-08', '2026-11-01'])(
+ 'exports the full UTC calendar day for %s regardless of host timezone',
+ async (date) => {
+ const request = vi
+ .spyOn(apiHelpers, 'apiResponse')
+ .mockRejectedValueOnce(new Error('Fixture export failed'));
+ renderWithProviders();
+ const dialog = screen.getByRole('dialog', { name: 'Export Metrics' });
+ fireEvent.change(within(dialog).getByLabelText('From'), { target: { value: date } });
+ fireEvent.change(within(dialog).getByLabelText('To'), { target: { value: date } });
+ fireEvent.click(within(dialog).getByRole('button', { name: 'Export' }));
+ await waitFor(() => expect(request).toHaveBeenCalledTimes(1));
+ const url = new URL(String(request.mock.calls[0][0]), 'http://fixture.local');
+ expect(url.searchParams.get('from')).toBe(`${date}T00:00:00.000Z`);
+ expect(url.searchParams.get('to')).toBe(`${date}T23:59:59.999Z`);
+ await waitFor(() =>
+ expect(within(dialog).getByRole('alert').textContent).toContain('Fixture export failed')
+ );
+ }
+ );
+
it('renders dashboard drilldown content through direct Mantine primitives and preserves selection', async () => {
const user = userEvent.setup();
const onTaskClick = vi.fn();
diff --git a/web/src/components/dashboard/ExportDialog.tsx b/web/src/components/dashboard/ExportDialog.tsx
index 3f38eeef..c6cd7a28 100644
--- a/web/src/components/dashboard/ExportDialog.tsx
+++ b/web/src/components/dashboard/ExportDialog.tsx
@@ -89,7 +89,7 @@ export function ExportDialog({
}
if (toDate) {
const toDateTime = new Date(toDate);
- toDateTime.setHours(23, 59, 59, 999);
+ toDateTime.setUTCHours(23, 59, 59, 999);
params.set('to', toDateTime.toISOString());
}
@@ -200,6 +200,9 @@ export function ExportDialog({
/>
)}
+
+ Dates use UTC. Both selected days are included.
+