From 9eab576bf1b59350e4b5d0cd108d2fed17535583 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 20:12:13 +0000 Subject: [PATCH] Fix timezone display: parse UTC timestamps correctly in logs pages Agent-Logs-Url: https://github.com/christianlouis/InboxConverge/sessions/70a69601-e0b9-4a00-bcf4-0a85d2bdf6cb Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- CHANGELOG.md | 1 + docs/TODO.md | 1 + frontend/src/app/admin/logs/page.tsx | 3 ++- frontend/src/app/logs/page.tsx | 5 +++-- frontend/src/lib/date-utils.ts | 16 ++++++++++++++++ 5 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 frontend/src/lib/date-utils.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index cdcb5a6..591f508 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Fix timezone display in Mailbox Activity / Admin Logs pages: timestamps from the server were parsed as local time when no timezone indicator was present, causing relative times ("1h ago") and absolute dates to be shifted by the client's UTC offset. - Worker tasks: use a fresh DB session for `send_user_notification` calls and move notifications after `db.commit()` to prevent the post-rollback `greenlet_spawn` SQLAlchemy error. - Worker tasks: ensure `last_check_at` and error status are always committed before notifications, fixing accounts being endlessly re-queued after IMAP auth failures. diff --git a/docs/TODO.md b/docs/TODO.md index b5ada15..243de06 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -4,6 +4,7 @@ Comprehensive task breakdown for repository improvements and production readines ## ✅ Recently Completed +- [x] Fixed timezone display bug in Mailbox Activity and Admin Logs pages: ISO timestamps without a `Z` suffix were parsed as local time by JavaScript, shifting "Xm ago" / "Xh ago" displays and absolute dates by the client's UTC offset. - [x] Fixed worker `send_user_notification` using rolled-back DB session causing `greenlet_spawn has not been called` errors; status/`last_check_at` now always committed before sending notifications via a fresh session. - [x] **Pull Now**: Added "Pull Now" button on Accounts page that immediately queues a `process_mail_account` Celery task via `POST /mail-accounts/{id}/pull-now`. Button shows spinner while in flight and is disabled for inactive accounts. - [x] Fixed 21 mypy type errors: `Column[T]` vs native type mismatches in `notification_service.py`, `mail_processor.py`, `auth.py`, `tasks.py`, `providers.py`, `mail_accounts.py`, and `main.py` (`lifespan` parameter rename). diff --git a/frontend/src/app/admin/logs/page.tsx b/frontend/src/app/admin/logs/page.tsx index c2c8c23..a614da9 100644 --- a/frontend/src/app/admin/logs/page.tsx +++ b/frontend/src/app/admin/logs/page.tsx @@ -8,6 +8,7 @@ import { useAuthStore } from '@/store/authStore'; import { useRouter } from 'next/navigation'; import { useEffect } from 'react'; import { adminApi, AdminProcessingRun } from '@/lib/api'; +import { parseUTC } from '@/lib/date-utils'; import { Activity, ChevronLeft, @@ -31,7 +32,7 @@ function formatDuration(seconds?: number | null): string { } function formatDate(iso: string): string { - return new Date(iso).toLocaleString(undefined, { + return parseUTC(iso).toLocaleString(undefined, { dateStyle: 'short', timeStyle: 'medium', }); diff --git a/frontend/src/app/logs/page.tsx b/frontend/src/app/logs/page.tsx index 7212f61..9a47161 100644 --- a/frontend/src/app/logs/page.tsx +++ b/frontend/src/app/logs/page.tsx @@ -5,6 +5,7 @@ import { AuthGuard } from '@/components/AuthGuard'; import { DashboardLayout } from '@/components/DashboardLayout'; import { useQuery } from '@tanstack/react-query'; import { processingRunsApi, mailAccountsApi, MailAccount, ProcessingRun, ProcessingLog } from '@/lib/api'; +import { parseUTC } from '@/lib/date-utils'; import { Inbox, ChevronLeft, @@ -20,7 +21,7 @@ import { function formatRelative(iso?: string | null): string { if (!iso) return 'Never'; - const diff = Date.now() - new Date(iso).getTime(); + const diff = Date.now() - parseUTC(iso).getTime(); const minutes = Math.floor(diff / 60000); if (minutes < 1) return 'Just now'; if (minutes < 60) return `${minutes}m ago`; @@ -30,7 +31,7 @@ function formatRelative(iso?: string | null): string { } function formatDate(iso: string): string { - return new Date(iso).toLocaleString(undefined, { + return parseUTC(iso).toLocaleString(undefined, { dateStyle: 'short', timeStyle: 'medium', }); diff --git a/frontend/src/lib/date-utils.ts b/frontend/src/lib/date-utils.ts new file mode 100644 index 0000000..f41868e --- /dev/null +++ b/frontend/src/lib/date-utils.ts @@ -0,0 +1,16 @@ +/** + * Parse an ISO-8601 datetime string as UTC. + * + * The backend stores all timestamps in UTC. When the serialised string has no + * explicit timezone indicator (no trailing `Z` or `±HH:MM` offset), JavaScript's + * `Date` constructor treats it as *local* time. For clients in UTC+N this + * shifts every timestamp N hours into the past, causing "Xh ago" relative + * labels and `toLocaleString()` absolute dates to be wrong. + * + * This helper appends `Z` to any timezone-naive ISO string so it is always + * interpreted as UTC. Strings that already carry timezone info are passed + * through unchanged. + */ +export function parseUTC(iso: string): Date { + return new Date(/Z|[+-]\d{2}:?\d{2}$/.test(iso) ? iso : iso + 'Z'); +}