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>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-28 20:12:13 +00:00
parent d92affabec
commit 9eab576bf1
5 changed files with 23 additions and 3 deletions
+1
View File
@@ -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.
+1
View File
@@ -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).
+2 -1
View File
@@ -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',
});
+3 -2
View File
@@ -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',
});
+16
View File
@@ -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');
}