From dd4532f6604e2e8c1fa27fd3da05cd5e62733842 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 18:45:38 +0000 Subject: [PATCH] feat: mailbox-centric activity view, reduce log noise from empty polling cycles Agent-Logs-Url: https://github.com/christianlouis/InboxConverge/sessions/1a78a249-127f-4a73-a454-3c6bafbf6e58 Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- CHANGELOG.md | 9 + backend/app/api/v1/endpoints/logs.py | 10 +- backend/app/api/v1/endpoints/mail_accounts.py | 6 + docs/TODO.md | 2 + frontend/package-lock.json | 4 +- frontend/src/app/logs/page.tsx | 308 +++++++++++------- frontend/src/components/DashboardLayout.tsx | 4 +- frontend/src/lib/api.ts | 3 +- 8 files changed, 229 insertions(+), 117 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62c2d46..45d96cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- **Mailbox Activity view**: The user-facing "Logs" page has been redesigned to a mailbox-centric + layout (renamed "Mailbox Activity"). Each mail account is shown as a card with its last check + status and error (if any). Only runs that actually fetched emails are shown in the pull history, + eliminating noise from empty polling cycles. This mirrors Gmail's external POP pull UI. +- **Processing runs filter**: Added `has_emails` query parameter to `GET /processing-runs` and + `GET /mail-accounts/{id}/processing-runs`. When `has_emails=true`, only runs with + `emails_fetched > 0` are returned, allowing clients to suppress empty polling noise. + ### Fixed - **Processing log durations**: Runs that were killed by SIGKILL or failed before updating their own status (e.g. missing SMTP credentials) now always record a correct `completed_at` and diff --git a/backend/app/api/v1/endpoints/logs.py b/backend/app/api/v1/endpoints/logs.py index 7d42bc9..41a19e4 100644 --- a/backend/app/api/v1/endpoints/logs.py +++ b/backend/app/api/v1/endpoints/logs.py @@ -61,12 +61,18 @@ async def list_processing_runs( alias="status", description="Filter by run status (completed, failed, partial_failure, running)", ), + has_emails: Optional[bool] = Query( + None, + description="When true, only return runs that fetched at least one email", + ), current_user: User = Depends(get_current_active_user), db: AsyncSession = Depends(get_db), ): """ Return a paginated list of processing runs for all mail accounts owned by - the authenticated user, optionally filtered by account or status. + the authenticated user, optionally filtered by account, status, or whether + any emails were fetched (has_emails=true reduces log noise by hiding empty + polling cycles). """ # Base query: join with MailAccount to enforce ownership base = ( @@ -79,6 +85,8 @@ async def list_processing_runs( base = base.where(ProcessingRun.mail_account_id == account_id) if status_filter: base = base.where(ProcessingRun.status == status_filter) + if has_emails is True: + base = base.where(ProcessingRun.emails_fetched > 0) # Total count count_q = select(func.count()).select_from(base.subquery()) diff --git a/backend/app/api/v1/endpoints/mail_accounts.py b/backend/app/api/v1/endpoints/mail_accounts.py index 508709d..a846041 100644 --- a/backend/app/api/v1/endpoints/mail_accounts.py +++ b/backend/app/api/v1/endpoints/mail_accounts.py @@ -297,6 +297,10 @@ async def list_account_runs( account_id: int, page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), + has_emails: Optional[bool] = Query( + None, + description="When true, only return runs that fetched at least one email", + ), current_user: User = Depends(get_current_active_user), db: AsyncSession = Depends(get_db), ): @@ -314,6 +318,8 @@ async def list_account_runs( ) base = select(ProcessingRun).where(ProcessingRun.mail_account_id == account_id) + if has_emails is True: + base = base.where(ProcessingRun.emails_fetched > 0) total = ( await db.execute(select(func.count()).select_from(base.subquery())) ).scalar_one() diff --git a/docs/TODO.md b/docs/TODO.md index e2fc55e..cebd7e5 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -4,6 +4,8 @@ Comprehensive task breakdown for repository improvements and production readines ## ✅ Recently Completed +- [x] Redesigned user-facing Logs page to mailbox-centric "Mailbox Activity" view: shows last check status per account + only successful pulls, suppressing noise from empty polling cycles. +- [x] Added `has_emails` filter to `GET /processing-runs` and `GET /mail-accounts/{id}/processing-runs` API endpoints. - [x] Rename entire project to **InboxConverge**: all user-visible strings, Docker container/image names, DB defaults, monitoring, and docs updated. - [x] Domain updated to `inboxconverge.com`; contact email defaults to `christian@inboxconverge.com`. - [x] New configurable env vars: `CONTACT_EMAIL`, `APP_URL`, `NEXT_PUBLIC_APP_NAME`. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9f39a28..01e8177 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1724,7 +1724,7 @@ "version": "19.2.10", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -2801,7 +2801,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { diff --git a/frontend/src/app/logs/page.tsx b/frontend/src/app/logs/page.tsx index 4dc8e3c..7212f61 100644 --- a/frontend/src/app/logs/page.tsx +++ b/frontend/src/app/logs/page.tsx @@ -4,9 +4,9 @@ import { useState } from 'react'; import { AuthGuard } from '@/components/AuthGuard'; import { DashboardLayout } from '@/components/DashboardLayout'; import { useQuery } from '@tanstack/react-query'; -import { processingRunsApi, ProcessingRun, ProcessingLog } from '@/lib/api'; +import { processingRunsApi, mailAccountsApi, MailAccount, ProcessingRun, ProcessingLog } from '@/lib/api'; import { - FileText, + Inbox, ChevronLeft, ChevronRight, CheckCircle, @@ -15,20 +15,18 @@ import { RefreshCw, ChevronDown, ChevronUp, + AlertTriangle, } from 'lucide-react'; -const STATUS_STYLES: Record = { - completed: 'bg-green-100 text-green-800', - failed: 'bg-red-100 text-red-800', - partial_failure: 'bg-yellow-100 text-yellow-800', - running: 'bg-blue-100 text-blue-800', -}; - -function formatDuration(seconds?: number | null): string { - if (seconds == null) return '—'; - if (seconds < 60) return `${seconds.toFixed(1)}s`; - const totalSecs = Math.floor(seconds); - return `${Math.floor(totalSecs / 60)}m ${totalSecs % 60}s`; +function formatRelative(iso?: string | null): string { + if (!iso) return 'Never'; + const diff = Date.now() - new Date(iso).getTime(); + const minutes = Math.floor(diff / 60000); + if (minutes < 1) return 'Just now'; + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + return `${Math.floor(hours / 24)}d ago`; } function formatDate(iso: string): string { @@ -38,7 +36,14 @@ function formatDate(iso: string): string { }); } -function RunRow({ run }: { run: ProcessingRun }) { +function formatDuration(seconds?: number | null): string { + if (seconds == null) return '—'; + if (seconds < 60) return `${seconds.toFixed(1)}s`; + const totalSecs = Math.floor(seconds); + return `${Math.floor(totalSecs / 60)}m ${totalSecs % 60}s`; +} + +function RunDetailRow({ run }: { run: ProcessingRun }) { const [expanded, setExpanded] = useState(false); const [logsPage, setLogsPage] = useState(1); @@ -48,57 +53,42 @@ function RunRow({ run }: { run: ProcessingRun }) { enabled: expanded, }); - const statusClass = STATUS_STYLES[run.status] ?? 'bg-gray-100 text-gray-800'; - return ( <> setExpanded((v) => !v)} > - - {formatDate(run.started_at)} - - -
{run.account_name ?? '—'}
-
{run.account_email ?? ''}
- - - - {run.status} - - - - {run.emails_fetched} - - - {run.emails_forwarded} - - - {run.emails_failed > 0 ? run.emails_failed : 0} - - - {formatDuration(run.duration_seconds)} - - - {expanded ? ( - + {formatDate(run.started_at)} + {run.emails_fetched} + {run.emails_forwarded} + + {run.emails_failed > 0 ? ( + {run.emails_failed} ) : ( - + 0 + )} + + {formatDuration(run.duration_seconds)} + + {expanded ? ( + + ) : ( + )} {expanded && ( - + {run.error_message && (
{run.error_message}
)} {logsLoading ? ( -
- Loading logs… +
+ Loading…
) : logsData && logsData.items.length > 0 ? ( <> @@ -106,7 +96,6 @@ function RunRow({ run }: { run: ProcessingRun }) { Time - Level Subject From Status @@ -118,19 +107,6 @@ function RunRow({ run }: { run: ProcessingRun }) { {formatDate(log.timestamp)} - - - {log.level} - - {log.email_subject ?? '—'} @@ -149,7 +125,7 @@ function RunRow({ run }: { run: ProcessingRun }) { {logsData.pages > 1 && ( -
+
- - Page {logsPage} of {logsData.pages} - + Page {logsPage} of {logsData.pages} + + {showHistory && ( +
+ + + + + + + + + + + + {runs.map((run) => ( + + ))} + +
DateFetchedForwardedFailedDuration +
+
+ )} + + )} +
+
+ ); +} + +export default function LogsPage() { + const [runsPage, setRunsPage] = useState(1); + + const { data: accounts, isLoading: accountsLoading } = useQuery({ + queryKey: ['mail-accounts'], + queryFn: mailAccountsApi.list, }); + // Fetch recent successful runs (emails_fetched > 0) across all accounts + const { data: runsData, isLoading: runsLoading, isError, refetch } = useQuery({ + queryKey: ['processing-runs-meaningful', runsPage], + queryFn: () => + processingRunsApi.list({ page: runsPage, page_size: 100, has_emails: true }), + }); + + const isLoading = accountsLoading || runsLoading; + + // Group runs by account id + const runsByAccount = (runsData?.items ?? []).reduce>( + (acc, run) => { + if (!acc[run.mail_account_id]) acc[run.mail_account_id] = []; + acc[run.mail_account_id].push(run); + return acc; + }, + {} + ); + return ( @@ -202,11 +300,11 @@ export default function LogsPage() {

- - Processing Logs + + Mailbox Activity

- History of email processing runs for all your mail accounts. + Last check status and successful email pulls for each mailbox.

- {/* Table */} {isLoading ? (
) : isError ? (
- Failed to load processing logs. Please try again. + Failed to load mailbox activity. Please try again.
- ) : data && data.items.length > 0 ? ( -
-
- - - - - - - - - - - - - - {data.items.map((run) => ( - - ))} - -
StartedAccountStatusFetchedForwardedFailedDuration -
+ ) : accounts && accounts.length > 0 ? ( + <> +
+ {accounts.map((account) => ( + + ))}
- {/* Pagination */} - {data.pages > 1 && ( -
+ {/* Pagination for runs (only shown when there are multiple pages) */} + {runsData && runsData.pages > 1 && ( +
- Page {data.page} of {data.pages} ({data.total} runs) + Showing page {runsData.page} of {runsData.pages} ({runsData.total} successful pulls)
)} -
+ ) : (
-

No processing runs yet

+

No mail accounts yet

- Logs will appear here once your mail accounts start processing emails. + Add a mail account to start seeing activity here.

)} @@ -292,3 +377,4 @@ export default function LogsPage() { ); } + diff --git a/frontend/src/components/DashboardLayout.tsx b/frontend/src/components/DashboardLayout.tsx index e619004..fedc392 100644 --- a/frontend/src/components/DashboardLayout.tsx +++ b/frontend/src/components/DashboardLayout.tsx @@ -18,7 +18,7 @@ import { Users, CreditCard, Bell, - FileText, + Inbox, Activity } from 'lucide-react'; @@ -47,7 +47,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) { { name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard }, { name: 'Mail Accounts', href: '/accounts', icon: Mail }, { name: 'Notifications', href: '/notifications', icon: Bell }, - { name: 'Logs', href: '/logs', icon: FileText }, + { name: 'Mailbox Activity', href: '/logs', icon: Inbox }, { name: 'Settings', href: '/settings', icon: Settings }, ]; diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index aae2a13..8dfc15a 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -357,6 +357,7 @@ export const processingRunsApi = { page_size?: number; account_id?: number; status?: string; + has_emails?: boolean; }): Promise { const response = await api.get("/processing-runs", { params, @@ -382,7 +383,7 @@ export const processingRunsApi = { async listForAccount( accountId: number, - params?: { page?: number; page_size?: number; status?: string } + params?: { page?: number; page_size?: number; status?: string; has_emails?: boolean } ): Promise { const response = await api.get( `/mail-accounts/${accountId}/processing-runs`,