From aab60d686681fbbae724f72b6121a8df377412c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 May 2026 18:53:52 +0000 Subject: [PATCH] feat: frontend clear-error button, debug trace viewer, debug_logging toggle, docs updates Agent-Logs-Url: https://github.com/christianlouis/InboxConverge/sessions/5d2918de-630e-4a66-8355-1b036c620b1c Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- CHANGELOG.md | 50 +++++++++++++ backend/app/api/v1/endpoints/mail_accounts.py | 1 + backend/tests/unit/test_mail_accounts.py | 1 + docs/TODO.md | 19 +++++ frontend/src/app/accounts/page.tsx | 26 ++++++- frontend/src/app/dashboard/page.tsx | 22 +++++- frontend/src/app/logs/page.tsx | 74 +++++++++++++++++-- .../src/components/AddMailAccountModal.tsx | 16 ++++ frontend/src/lib/api.ts | 8 ++ 9 files changed, 204 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 302b72c..ccbf9cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [Unreleased] + +### Added + +- **Friendly error messages**: Introduced `_format_connection_error()` helper in + `mail_processor.py` that translates raw OS/socket/SSL/POP3/IMAP exceptions into + human-readable sentences including the host:port and actionable guidance (DNS + failure, TLS error, connection timeout, authentication rejection, etc.). The + helper is applied at every `raise MailFetchError` / `raise MailConnectionError` + site and in both `_test_pop3_connection` and `_test_imap_connection`. + +- **Per-account debug logging** (`debug_logging` column on `MailAccount`): when + enabled, the next processing run records a structured connection trace + (connect timing, TLS details, auth, INBOX selection, message UIDs/sizes, + elapsed milliseconds per phase) via the new `MailDebugRecorder` class. The + trace is persisted as a `ProcessingLog` row with `level="DEBUG"` and surfaced + in the "Mailbox Activity" logs page as a collapsible "Connection trace" panel. + Debug logging auto-disables after 5 completed runs in a 24-hour window. + +- **"Clear error" button**: new `POST /api/v1/mail-accounts/{id}/clear-error` + endpoint that nulls `last_error_message`/`last_error_at` and resets `status` + to `ACTIVE` when currently `ERROR`. Wired into the error banners on both the + Accounts page and the Mailbox Activity (Logs) page. + +- **Debug-logging toggle** in the account edit form (Add/Edit Account modal): + checkbox labelled "Debug logging (auto-disables after 5 runs)". + +- Alembic migration `0002_add_debug_logging.py` adding the `debug_logging` + boolean column to `mail_accounts` (idempotent via `ADD COLUMN IF NOT EXISTS`). + +### Fixed + +- **Empty IMAP error messages** — `IMAP fetch error:` with a blank suffix was + caused by `asyncio.TimeoutError` and `aioimaplib.Abort` having an empty + `str()`. The new `_format_connection_error()` helper always produces a + non-empty, human-readable message. + +- **Cryptic DNS error** — `POP3 fetch error: [Errno -5] No address associated + with hostname` is now surfaced as `Could not resolve hostname 'pop.web.de' — + check that the server address is correct (DNS lookup failed: …)`. + +- **Sticky ERROR status after transient fetch failures**: the `tasks.py` + processing loop previously set `account.status = ERROR` and + `last_error_message = "{N} emails failed to forward"` even when the + connection and fetch succeeded but some individual email-forward operations + failed. Now, a successful fetch (no exception from `fetch_emails`) always + clears `last_error_message`/`last_error_at` and sets `status = ACTIVE`, + regardless of per-email forwarding failures. Per-email failures continue to + be tracked in `ProcessingLog` and the run's `emails_failed` counter. + ## v0.8.0 (2026-05-03) ### Bug Fixes diff --git a/backend/app/api/v1/endpoints/mail_accounts.py b/backend/app/api/v1/endpoints/mail_accounts.py index c8a5dee..a738c27 100644 --- a/backend/app/api/v1/endpoints/mail_accounts.py +++ b/backend/app/api/v1/endpoints/mail_accounts.py @@ -104,6 +104,7 @@ async def create_mail_account( check_interval_minutes=account_in.check_interval_minutes, max_emails_per_check=account_in.max_emails_per_check, delete_after_forward=account_in.delete_after_forward, + debug_logging=account_in.debug_logging, provider_name=account_in.provider_name, ) diff --git a/backend/tests/unit/test_mail_accounts.py b/backend/tests/unit/test_mail_accounts.py index d3e5ef5..be5f145 100644 --- a/backend/tests/unit/test_mail_accounts.py +++ b/backend/tests/unit/test_mail_accounts.py @@ -81,6 +81,7 @@ def _make_account(**overrides) -> MagicMock: check_interval_minutes=5, max_emails_per_check=50, delete_after_forward=True, + debug_logging=False, provider_name="Gmail", auto_detected=False, total_emails_processed=100, diff --git a/docs/TODO.md b/docs/TODO.md index f19c0fd..58113c4 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -4,6 +4,25 @@ Comprehensive task breakdown for repository improvements and production readines ## ✅ Recently Completed +- [x] **IMAP/POP3 diagnostics — Step 1: Friendly error messages**: Added + `_format_connection_error()` helper that converts raw OS/socket/SSL/POP3/IMAP + exceptions into human-readable sentences with host:port context. Applied at + every `raise MailFetchError`/`MailConnectionError` site. Fixes blank "IMAP + fetch error:" messages and cryptic DNS errno strings. + +- [x] **IMAP/POP3 diagnostics — Step 2: Auto-clear stale error state**: Successful + fetches now always clear `last_error_message`/`last_error_at` and set + `status=ACTIVE`, even when some individual email forwards fail. Added + `POST /mail-accounts/{id}/clear-error` endpoint and "Clear" buttons on the + Accounts and Mailbox Activity pages. + +- [x] **IMAP/POP3 diagnostics — Step 3: Per-account debug logging**: Added + `debug_logging` boolean column (migration `0002`), `MailDebugRecorder` class, + instrumented all connection phases (connect, auth, select, search, fetch UIDs, + per-message fetch, logout), persisted as `ProcessingLog[level=DEBUG]`. + Auto-disables after 5 runs in 24 h. Toggle in account edit form. Connection + trace viewer in Mailbox Activity logs page. + - [x] **Google OAuth consent screen legal compliance**: Added English Privacy Policy (`/privacy`) with Google API Limited Use Disclosure, Terms of Service (`/terms`), legal footer links on the home page (resolves Google's "homepage has no privacy policy link" verification rejection), login page, and register page (consent text). Cross-link from `/datenschutz` to `/privacy` added. - [x] **Fix Pydantic V2 deprecation warnings**: Replaced `.dict()` with `.model_dump()` in `admin.py` and `notifications.py`. Fixed `RuntimeWarning: coroutine never awaited` for `db.add()` in test mocks (`test_tasks.py`, `test_config_service.py`). diff --git a/frontend/src/app/accounts/page.tsx b/frontend/src/app/accounts/page.tsx index 6409e1a..03eadd4 100644 --- a/frontend/src/app/accounts/page.tsx +++ b/frontend/src/app/accounts/page.tsx @@ -4,7 +4,7 @@ import { AuthGuard } from '@/components/AuthGuard'; import { DashboardLayout } from '@/components/DashboardLayout'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { mailAccountsApi, MailAccount } from '@/lib/api'; -import { Plus, Edit2, Trash2, CheckCircle, XCircle, AlertTriangle, Power, RefreshCw } from 'lucide-react'; +import { Plus, Edit2, Trash2, CheckCircle, XCircle, AlertTriangle, Power, RefreshCw, RotateCcw } from 'lucide-react'; import { useState } from 'react'; import Image from 'next/image'; import { AddMailAccountModal } from '@/components/AddMailAccountModal'; @@ -115,6 +115,13 @@ export default function AccountsPage() { }, }); + const clearErrorMutation = useMutation({ + mutationFn: mailAccountsApi.clearError, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['mail-accounts'] }); + }, + }); + const handleEdit = (account: MailAccount) => { setEditingAccount(account); setIsModalOpen(true); @@ -253,9 +260,20 @@ export default function AccountsPage() { {account.last_error_message && (
-
- -

{account.last_error_message}

+
+
+ +

{account.last_error_message}

+
+
)} diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index 47c6c8e..83a22dd 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -2,7 +2,7 @@ import { AuthGuard } from '@/components/AuthGuard'; import { DashboardLayout } from '@/components/DashboardLayout'; -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { mailAccountsApi, MailAccount } from '@/lib/api'; import { formatRelative } from '@/lib/date-utils'; import Link from 'next/link'; @@ -15,6 +15,7 @@ import { XCircle, AlertTriangle, Inbox, + RotateCcw, } from 'lucide-react'; interface StatCardProps { @@ -43,6 +44,14 @@ function StatCard({ title, value, icon: Icon, iconColor }: StatCardProps) { function AccountStatusRow({ account }: { account: MailAccount }) { const hasError = !!account.last_error_message; const lastChecked = account.last_check_at; + const queryClient = useQueryClient(); + + const clearErrorMutation = useMutation({ + mutationFn: () => mailAccountsApi.clearError(account.id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['mail-accounts'] }); + }, + }); return (
@@ -84,7 +93,16 @@ function AccountStatusRow({ account }: { account: MailAccount }) { {hasError && (
-

{account.last_error_message}

+

{account.last_error_message}

+
)} diff --git a/frontend/src/app/logs/page.tsx b/frontend/src/app/logs/page.tsx index 749685b..9caf659 100644 --- a/frontend/src/app/logs/page.tsx +++ b/frontend/src/app/logs/page.tsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import { AuthGuard } from '@/components/AuthGuard'; import { DashboardLayout } from '@/components/DashboardLayout'; -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { processingRunsApi, mailAccountsApi, MailAccount, ProcessingRun, ProcessingLog } from '@/lib/api'; import { formatRelative, formatDate, formatDuration } from '@/lib/date-utils'; import { @@ -17,11 +17,14 @@ import { ChevronDown, ChevronUp, AlertTriangle, + RotateCcw, + Bug, } from 'lucide-react'; function RunDetailRow({ run }: { run: ProcessingRun }) { const [expanded, setExpanded] = useState(false); const [logsPage, setLogsPage] = useState(1); + const [traceExpanded, setTraceExpanded] = useState(false); const { data: logsData, isLoading: logsLoading } = useQuery({ queryKey: ['run-logs', run.id, logsPage], @@ -29,6 +32,10 @@ function RunDetailRow({ run }: { run: ProcessingRun }) { enabled: expanded, }); + // Separate DEBUG-level connection trace entries from email-level logs + const debugLogs = (logsData?.items ?? []).filter((l: ProcessingLog) => l.level === 'DEBUG'); + const emailLogs = (logsData?.items ?? []).filter((l: ProcessingLog) => l.level !== 'DEBUG'); + return ( <> )} + + {/* Connection trace (DEBUG-level logs) */} + {!logsLoading && debugLogs.length > 0 && ( +
+ + {traceExpanded && debugLogs.map((log: ProcessingLog) => { + const trace = (log.error_details as { trace?: unknown[]; truncated?: boolean } | null)?.trace ?? []; + const truncated = (log.error_details as { trace?: unknown[]; truncated?: boolean } | null)?.truncated ?? false; + return ( +
+ {(trace as { ts: string; phase: string; msg: string; data?: Record }[]).map((entry, i) => ( +
+ {new Date(entry.ts).toISOString().slice(11, 23)} + [{entry.phase}] + {entry.msg} + {entry.data && ( + {JSON.stringify(entry.data)} + )} +
+ ))} + {truncated && ( +
⚠ Trace was truncated (size limit reached)
+ )} +
+ ); + })} +
+ )} + {logsLoading ? (
Loading…
- ) : logsData && logsData.items.length > 0 ? ( + ) : emailLogs.length > 0 ? ( <> @@ -78,7 +121,7 @@ function RunDetailRow({ run }: { run: ProcessingRun }) { - {logsData.items.map((log: ProcessingLog) => ( + {emailLogs.map((log: ProcessingLog) => (
{formatDate(log.timestamp)} @@ -100,7 +143,7 @@ function RunDetailRow({ run }: { run: ProcessingRun }) { ))}
- {logsData.pages > 1 && ( + {logsData && logsData.pages > 1 && (
)} - ) : ( + ) : !logsLoading && debugLogs.length === 0 ? (

No per-email logs for this run.

- )} + ) : null} )} @@ -146,6 +189,14 @@ function MailboxCard({ const [showHistory, setShowHistory] = useState(false); const hasError = !!account.last_error_message; const lastChecked = account.last_check_at; + const queryClient = useQueryClient(); + + const clearErrorMutation = useMutation({ + mutationFn: () => mailAccountsApi.clearError(account.id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['mail-accounts'] }); + }, + }); return (
@@ -186,7 +237,16 @@ function MailboxCard({ {hasError && (
-

{account.last_error_message}

+

{account.last_error_message}

+
)} diff --git a/frontend/src/components/AddMailAccountModal.tsx b/frontend/src/components/AddMailAccountModal.tsx index 01a2211..94bdcef 100644 --- a/frontend/src/components/AddMailAccountModal.tsx +++ b/frontend/src/components/AddMailAccountModal.tsx @@ -39,6 +39,7 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro check_interval_minutes: account?.check_interval_minutes || 5, max_emails_per_check: account?.max_emails_per_check || 50, delete_after_forward: account?.delete_after_forward ?? true, + debug_logging: account?.debug_logging ?? false, provider_name: account?.provider_name ?? null, }); @@ -188,6 +189,7 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro check_interval_minutes: formData.check_interval_minutes, max_emails_per_check: formData.max_emails_per_check, delete_after_forward: formData.delete_after_forward, + debug_logging: formData.debug_logging, }; if (formData.password) { updateData.password = formData.password; @@ -383,6 +385,20 @@ export function AddMailAccountModal({ account, onClose }: AddMailAccountModalPro Enabled
+
+ + +
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 1d544bc..deb9df0 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -66,6 +66,7 @@ export interface MailAccount { check_interval_minutes: number; max_emails_per_check: number; delete_after_forward: boolean; + debug_logging: boolean; status: string; provider_name?: string | null; auto_detected: boolean; @@ -95,6 +96,7 @@ export interface MailAccountCreate { check_interval_minutes?: number; max_emails_per_check?: number; delete_after_forward?: boolean; + debug_logging?: boolean; provider_name?: string | null; } @@ -114,6 +116,7 @@ export interface MailAccountUpdate { check_interval_minutes?: number; max_emails_per_check?: number; delete_after_forward?: boolean; + debug_logging?: boolean; provider_name?: string | null; } @@ -361,6 +364,11 @@ export const mailAccountsApi = { }>("/mail-accounts/auto-detect", { email_address: emailAddress }); return response.data; }, + + async clearError(id: number): Promise { + const response = await api.post(`/mail-accounts/${id}/clear-error`); + return response.data; + }, }; // ── Processing Runs API ─────────────────────────────────────────────────