From 57db094d44b084b268a1c45ffa4f03df570d33c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 19:57:55 +0000 Subject: [PATCH 1/3] Initial plan From d92affabec3c953c682ff02e7877e1d481bfcfec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 20:04:52 +0000 Subject: [PATCH 2/3] Fix worker status tracking: use fresh DB session for notifications, commit before notifying 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 | 7 ++++ backend/app/workers/tasks.py | 73 +++++++++++++++++++++--------------- docs/TODO.md | 1 + 3 files changed, 50 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a222f96..cdcb5a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [Unreleased] + +### Fixed + +- 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. + ## v0.3.2 (2026-03-28) ### Bug Fixes diff --git a/backend/app/workers/tasks.py b/backend/app/workers/tasks.py index d07eb31..1520caa 100644 --- a/backend/app/workers/tasks.py +++ b/backend/app/workers/tasks.py @@ -285,13 +285,14 @@ async def process_mail_account(account_id: int): "User must re-authorise." ) try: - await send_user_notification( - db=db, - user_id=int(account.user_id), - title="InboxRescue: Gmail Authorization Expired", - body=f"Your Gmail credentials for account '{account.name}' have been revoked. Please re-authorize Gmail access in Settings.", - notify_on_error=True, - ) + async with async_session_maker() as notif_db: + await send_user_notification( + db=notif_db, + user_id=int(account.user_id), + title="InboxRescue: Gmail Authorization Expired", + body=f"Your Gmail credentials for account '{account.name}' have been revoked. Please re-authorize Gmail access in Settings.", + notify_on_error=True, + ) except Exception as notify_exc: logger.warning( f"Failed to send revocation notification: {notify_exc}" @@ -364,19 +365,25 @@ async def process_mail_account(account_id: int): account.status = AccountStatus.ERROR # type: ignore[assignment] account.last_error_at = datetime.now(timezone.utc) # type: ignore[assignment] account.last_error_message = f"{emails_failed} emails failed to forward" # type: ignore[assignment] - try: - await send_user_notification( - db=db, - user_id=int(account.user_id), - title="InboxRescue: Mail Forwarding Failures", - body=f"Mail account '{account.name}': {emails_failed} email(s) failed to forward.", - notify_on_error=True, - ) - except Exception as notify_exc: - logger.warning(f"Failed to send notification: {notify_exc}") await db.commit() + # Send failure notification after the commit so the status is + # persisted even if the notification fails. Use a fresh session + # to avoid interfering with the (now-committed) main transaction. + if emails_failed > 0: + try: + async with async_session_maker() as notif_db: + await send_user_notification( + db=notif_db, + user_id=int(account.user_id), + title="InboxRescue: Mail Forwarding Failures", + body=f"Mail account '{account.name}': {emails_failed} email(s) failed to forward.", + notify_on_error=True, + ) + except Exception as notify_exc: + logger.warning(f"Failed to send notification: {notify_exc}") + # Record Prometheus metrics for this completed run _run_status = "completed" if emails_failed == 0 else "partial_failure" MAIL_PROCESSING_RUNS_TOTAL.labels(status=_run_status).inc() @@ -437,20 +444,6 @@ async def process_mail_account(account_id: int): # throttles re-dispatch instead of queuing a new task every cycle. account.last_check_at = datetime.now(timezone.utc) # type: ignore[assignment] - # Notify user about the error - try: - await send_user_notification( - db=db, - user_id=int(account.user_id), - title="InboxRescue: Mail Processing Error", - body=f"Error processing mail account '{account.name}': {e}", - notify_on_error=True, - ) - except Exception as notify_exc: - logger.warning( - f"Failed to send error notification: {notify_exc}" - ) - try: await db.commit() except Exception as commit_exc: @@ -459,6 +452,24 @@ async def process_mail_account(account_id: int): f"{account_id}: {commit_exc}" ) + # Send error notification after the commit (and outside the run/account + # guards) so the status is always persisted first. Use a fresh session + # to avoid the post-rollback session's broken greenlet context causing + # the notification query itself to fail with "greenlet_spawn has not + # been called". + if "account" in locals() and account is not None: + try: + async with async_session_maker() as notif_db: + await send_user_notification( + db=notif_db, + user_id=int(account.user_id), + title="InboxRescue: Mail Processing Error", + body=f"Error processing mail account '{account.name}': {e}", + notify_on_error=True, + ) + except Exception as notify_exc: + logger.warning(f"Failed to send error notification: {notify_exc}") + @celery_app.task(base=AsyncTask, name="app.workers.tasks.process_all_enabled_accounts") async def process_all_enabled_accounts(): diff --git a/docs/TODO.md b/docs/TODO.md index 82bca22..b5ada15 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 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). - [x] **Provider logos rework**: Logos now displayed as full-width banner strips at the top of each account card using `next/image fill + object-contain`. Handles all aspect ratios (1:1 square to 6:1 wordmark) without distortion. Proton Mail added. 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 3/3] 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'); +}