feat(dashboard): replace per-run table with per-account Mailbox Status view
- Remove noisy "Recent Processing Runs" table from dashboard - Add per-account AccountStatusRow showing: name, email, OK/Error/Pending badge, relative last-check time, inline error message, lifetime counters - Stats cards: "Emails Forwarded Today" → "Emails Processed" (all-time), "Errors" → "Accounts with Errors" (turns gray when 0) - Remove unused processingRunsApi import (saves one API call on page load) - "View activity & history →" link to /logs for full drill-down - Update CHANGELOG.md and docs/TODO.md Agent-Logs-Url: https://github.com/christianlouis/InboxConverge/sessions/0d97741e-e999-49dd-a8f2-dceac2c1da1f Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
<!-- version list -->
|
<!-- version list -->
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Dashboard "Recent Processing Runs" table replaced with a per-account **Mailbox Status** view: each account now shows its last-check status (OK / Error / Pending), relative last-check time, any error message, and lifetime processed/failed counters. The noisy per-run table is gone; full activity history remains available on the Logs page.
|
||||||
|
- Stats cards updated: "Emails Forwarded Today" → "Emails Processed" (all-time total from account records); "Errors" → "Accounts with Errors" (count of accounts currently showing an error).
|
||||||
|
|
||||||
## v0.3.2 (2026-03-28)
|
## v0.3.2 (2026-03-28)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Comprehensive task breakdown for repository improvements and production readines
|
|||||||
|
|
||||||
## ✅ Recently Completed
|
## ✅ Recently Completed
|
||||||
|
|
||||||
|
- [x] **Dashboard redesign**: Replaced noisy "Recent Processing Runs" table with a per-account "Mailbox Status" view showing last-check status (OK/Error/Pending), relative timestamp, error messages, and lifetime counters. Stats cards updated to show all-time processed count and accounts-with-errors count.
|
||||||
- [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] **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] 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.
|
- [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.
|
||||||
|
|||||||
+112
-112
@@ -3,38 +3,44 @@
|
|||||||
import { AuthGuard } from '@/components/AuthGuard';
|
import { AuthGuard } from '@/components/AuthGuard';
|
||||||
import { DashboardLayout } from '@/components/DashboardLayout';
|
import { DashboardLayout } from '@/components/DashboardLayout';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { mailAccountsApi, processingRunsApi } from '@/lib/api';
|
import { mailAccountsApi, MailAccount } from '@/lib/api';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import {
|
import {
|
||||||
Mail,
|
Mail,
|
||||||
Send,
|
Send,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
TrendingUp,
|
Clock,
|
||||||
Clock
|
XCircle,
|
||||||
|
AlertTriangle,
|
||||||
|
Inbox,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
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`;
|
||||||
|
}
|
||||||
|
|
||||||
interface StatCardProps {
|
interface StatCardProps {
|
||||||
title: string;
|
title: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
icon: React.ComponentType<{ className?: string }>;
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
iconColor: string;
|
iconColor: string;
|
||||||
trend?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatCard({ title, value, icon: Icon, iconColor, trend }: StatCardProps) {
|
function StatCard({ title, value, icon: Icon, iconColor }: StatCardProps) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg shadow p-6">
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-600">{title}</p>
|
<p className="text-sm font-medium text-gray-600">{title}</p>
|
||||||
<p className="mt-2 text-3xl font-semibold text-gray-900">{value}</p>
|
<p className="mt-2 text-3xl font-semibold text-gray-900">{value}</p>
|
||||||
{trend && (
|
|
||||||
<div className="mt-2 flex items-center text-sm">
|
|
||||||
<TrendingUp className="h-4 w-4 text-green-500 mr-1" />
|
|
||||||
<span className="text-green-600">{trend}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className={`p-3 rounded-full ${iconColor}`}>
|
<div className={`p-3 rounded-full ${iconColor}`}>
|
||||||
<Icon className="h-8 w-8 text-white" />
|
<Icon className="h-8 w-8 text-white" />
|
||||||
@@ -44,27 +50,78 @@ function StatCard({ title, value, icon: Icon, iconColor, trend }: StatCardProps)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AccountStatusRow({ account }: { account: MailAccount }) {
|
||||||
|
const hasError = !!account.last_error_message;
|
||||||
|
const lastChecked = account.last_check_at;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="px-5 py-4 border-b border-gray-100 last:border-b-0">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
{/* Left: name + email */}
|
||||||
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
|
<Inbox className="h-4 w-4 text-blue-400 shrink-0" />
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-sm font-semibold text-gray-900 truncate">{account.name}</p>
|
||||||
|
<p className="text-xs text-gray-400 truncate">{account.email_address}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: status badge + last check */}
|
||||||
|
<div className="text-right shrink-0">
|
||||||
|
{hasError ? (
|
||||||
|
<span className="inline-flex items-center gap-1 text-xs font-medium text-red-600">
|
||||||
|
<XCircle className="h-3.5 w-3.5" />
|
||||||
|
Error
|
||||||
|
</span>
|
||||||
|
) : lastChecked ? (
|
||||||
|
<span className="inline-flex items-center gap-1 text-xs font-medium text-green-600">
|
||||||
|
<CheckCircle className="h-3.5 w-3.5" />
|
||||||
|
OK
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex items-center gap-1 text-xs font-medium text-gray-400">
|
||||||
|
<Clock className="h-3.5 w-3.5" />
|
||||||
|
Pending
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-gray-400 mt-0.5">
|
||||||
|
{formatRelative(lastChecked)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error message */}
|
||||||
|
{hasError && (
|
||||||
|
<div className="mt-2 flex items-start gap-1.5 p-2 bg-red-50 border border-red-200 rounded">
|
||||||
|
<AlertTriangle className="h-3.5 w-3.5 text-red-500 shrink-0 mt-0.5" />
|
||||||
|
<p className="text-xs text-red-700 line-clamp-2">{account.last_error_message}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Lifetime counters (only when there's activity) */}
|
||||||
|
{(account.total_emails_processed > 0 || account.total_emails_failed > 0) && (
|
||||||
|
<div className="mt-2 flex items-center gap-4 text-xs text-gray-500">
|
||||||
|
<span>{account.total_emails_processed.toLocaleString()} processed</span>
|
||||||
|
{account.total_emails_failed > 0 && (
|
||||||
|
<span className="text-red-500">{account.total_emails_failed.toLocaleString()} failed</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const { data: accounts } = useQuery({
|
const { data: accounts, isLoading: accountsLoading } = useQuery({
|
||||||
queryKey: ['mail-accounts'],
|
queryKey: ['mail-accounts'],
|
||||||
queryFn: mailAccountsApi.list,
|
queryFn: mailAccountsApi.list,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: runs, isLoading: runsLoading } = useQuery({
|
|
||||||
queryKey: ['processing-runs'],
|
|
||||||
queryFn: () => processingRunsApi.list({ page: 1, page_size: 10 }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
totalAccounts: accounts?.length || 0,
|
totalAccounts: accounts?.length || 0,
|
||||||
activeAccounts: accounts?.filter((a) => a.is_enabled).length || 0,
|
activeAccounts: accounts?.filter((a) => a.is_enabled).length || 0,
|
||||||
emailsToday: runs?.items
|
totalProcessed: accounts?.reduce((sum, a) => sum + a.total_emails_processed, 0) || 0,
|
||||||
?.filter((r) => {
|
accountsWithErrors: accounts?.filter((a) => !!a.last_error_message).length || 0,
|
||||||
const today = new Date().toDateString();
|
|
||||||
return new Date(r.started_at).toDateString() === today;
|
|
||||||
})
|
|
||||||
.reduce((sum, r) => sum + r.emails_forwarded, 0) || 0,
|
|
||||||
errors: runs?.items?.filter((r) => r.emails_failed > 0).length || 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -80,8 +137,8 @@ export default function DashboardPage() {
|
|||||||
iconColor="bg-blue-500"
|
iconColor="bg-blue-500"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
title="Emails Forwarded Today"
|
title="Emails Processed"
|
||||||
value={stats.emailsToday}
|
value={stats.totalProcessed.toLocaleString()}
|
||||||
icon={Send}
|
icon={Send}
|
||||||
iconColor="bg-green-500"
|
iconColor="bg-green-500"
|
||||||
/>
|
/>
|
||||||
@@ -92,101 +149,44 @@ export default function DashboardPage() {
|
|||||||
iconColor="bg-purple-500"
|
iconColor="bg-purple-500"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
title="Errors"
|
title="Accounts with Errors"
|
||||||
value={stats.errors}
|
value={stats.accountsWithErrors}
|
||||||
icon={AlertCircle}
|
icon={AlertCircle}
|
||||||
iconColor="bg-red-500"
|
iconColor={stats.accountsWithErrors > 0 ? 'bg-red-500' : 'bg-gray-400'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Recent Processing Runs */}
|
{/* Mailbox Status Overview */}
|
||||||
<div className="bg-white rounded-lg shadow">
|
<div className="bg-white rounded-lg shadow">
|
||||||
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||||
<h3 className="text-lg font-semibold text-gray-900">Recent Processing Runs</h3>
|
<h3 className="text-lg font-semibold text-gray-900">Mailbox Status</h3>
|
||||||
<Link href="/logs" className="text-sm text-blue-600 hover:text-blue-800 font-medium">
|
<Link href="/logs" className="text-sm text-blue-600 hover:text-blue-800 font-medium">
|
||||||
View all logs →
|
View activity & history →
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-x-auto">
|
|
||||||
{runsLoading ? (
|
{accountsLoading ? (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
|
||||||
</div>
|
</div>
|
||||||
) : runs && runs.items && runs.items.length > 0 ? (
|
) : accounts && accounts.length > 0 ? (
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<div>
|
||||||
<thead className="bg-gray-50">
|
{accounts.map((account) => (
|
||||||
<tr>
|
<AccountStatusRow key={account.id} account={account} />
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
))}
|
||||||
Account
|
</div>
|
||||||
</th>
|
) : (
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<div className="text-center py-12">
|
||||||
Started At
|
<Clock className="mx-auto h-10 w-10 text-gray-300 mb-3" />
|
||||||
</th>
|
<p className="text-sm text-gray-500">No mail accounts configured yet.</p>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<Link
|
||||||
Status
|
href="/accounts"
|
||||||
</th>
|
className="mt-3 inline-block text-sm text-blue-600 hover:text-blue-800 font-medium"
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
>
|
||||||
Fetched
|
Add your first account →
|
||||||
</th>
|
</Link>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
</div>
|
||||||
Forwarded
|
)}
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Errors
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
|
||||||
{runs.items.map((run) => {
|
|
||||||
const account = accounts?.find((a) => a.id === run.mail_account_id);
|
|
||||||
return (
|
|
||||||
<tr key={run.id}>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
|
||||||
{run.account_name || account?.name || `Account ${run.mail_account_id}`}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Clock className="h-4 w-4 mr-1 text-gray-400" />
|
|
||||||
{new Date(run.started_at).toLocaleString()}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
|
|
||||||
run.status === 'completed'
|
|
||||||
? 'bg-green-100 text-green-800'
|
|
||||||
: run.status === 'failed'
|
|
||||||
? 'bg-red-100 text-red-800'
|
|
||||||
: 'bg-yellow-100 text-yellow-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{run.status}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
||||||
{run.emails_fetched}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
||||||
{run.emails_forwarded}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
||||||
{run.emails_failed > 0 ? (
|
|
||||||
<span className="text-red-600 font-medium">{run.emails_failed}</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-gray-400">0</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<p className="text-gray-500">No processing runs yet</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
|
|||||||
Reference in New Issue
Block a user