//
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Skeleton } from '@/components/ui/skeleton';
import { XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, BarChart, Bar } from 'recharts';
import { Mail, HardDrive, Database, Users, Inbox, Info } from 'lucide-react';
import { formatBytes, formatNumber } from '@/lib/utils';
import { useQuery } from '@tanstack/react-query';
import { get_dashboard_stats, TimeBucket } from '@/api/system/api';
import { Main } from '@/components/layout/main';
import { FixedHeader } from '@/components/layout/fixed-header';
import { useTranslation } from 'react-i18next';
interface DailyActivity {
date: string;
count: number;
}
function convertRecentActivity(timeBuckets: TimeBucket[], locale: string): DailyActivity[] {
const dateFormatter = new Intl.DateTimeFormat(locale, {
month: 'short',
day: 'numeric',
});
return timeBuckets.map(bucket => {
const date = new Date(bucket.timestamp_ms);
return {
date: dateFormatter.format(date),
count: bucket.count,
timestamp_ms: bucket.timestamp_ms,
};
});
}
const formatTooltipDate = (timestamp_ms: number, locale: string): string => {
const date = new Date(timestamp_ms);
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date);
};
const COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444'];
// Skeleton Components
const MetricCardSkeleton = () => (
);
const ChartSkeleton = () => (
{[...Array(6)].map((_, i) => (
))}
);
// Empty State Components
const EmptyChart = ({ title }: { title: string }) => (
);
const EmptyTable = ({ title }: { title: string }) => (
);
export default function MailArchiveDashboard() {
const { data: stats, isLoading } = useQuery({
queryKey: ['dashboard-stats'],
queryFn: get_dashboard_stats,
});
const { t, i18n } = useTranslation();
const currentLocale = i18n.resolvedLanguage || i18n.language || navigator.language;
const totalAttachments = (stats?.with_attachment_count ?? 0) + (stats?.without_attachment_count ?? 0);
const attachmentRatio = totalAttachments > 0 ? (stats?.with_attachment_count ?? 0) / totalAttachments : 0;
const hasRecentActivity = stats?.recent_activity && stats.recent_activity.length > 0;
const hasTopSenders = stats?.top_senders && stats.top_senders.length > 0;
const hasTopEmails = stats?.top_largest_emails && stats.top_largest_emails.length > 0;
const hasTopAccounts = stats?.top_accounts && stats.top_accounts.length > 0;
const attachmentData = totalAttachments > 0
? [
{ name: 'With Attachments', value: attachmentRatio, fill: COLORS[1] },
{ name: 'No Attachments', value: 1 - attachmentRatio, fill: '#e5e7eb' },
]
: [
{ name: 'No Data', value: 1, fill: '#e5e7eb' },
];
if (isLoading) {
return (
{[...Array(6)].map((_, i) => (
))}
);
}
return (
<>
{t('dashboard.mailAccounts')}
{formatNumber(stats!.account_count)}
{t('dashboard.connected')}
{t('dashboard.totalEmails')}
{formatNumber(stats!.email_count)}
{t('dashboard.syncedLocally')}
{t('dashboard.totalEmailSize')}
{formatBytes(stats!.total_size_bytes)}
{t('dashboard.logicalVolume')}
{t('dashboard.localDataFiles')}
{formatBytes(stats!.storage_usage_bytes)}
{t('dashboard.actualDiskUsage')}
{t('dashboard.indexSize')}
{formatBytes(stats!.index_usage_bytes)}
{t('dashboard.tantivyIndex')}
{t('dashboard.systemVersion')}
{stats!.commit_hash ?? 'N/A'}
{t('dashboard.dayTrend')}
{t('dashboard.attachments')}
{t('dashboard.topLists')}
{t('dashboard.newEmails')}
{t('dashboard.messageDistribution')}
{hasRecentActivity ? (
formatNumber(v as number)}
content={({ payload }) => {
if (payload && payload.length) {
const dataPoint = payload[0].payload;
const fullDate = formatTooltipDate(dataPoint.timestamp_ms, currentLocale);
return (
{fullDate}
{t('dashboard.emails')}: {formatNumber(dataPoint.count)}
);
}
return null;
}}
contentStyle={{
backgroundColor: 'hsl(var(--background))',
border: '1px solid hsl(var(--border))',
borderRadius: '8px'
}}
/>
) : (
)}
{/* Attachments */}
{t('dashboard.attachmentRatio')}
{totalAttachments > 0
? t('dashboard.attachmentRatioDesc', { percent: (attachmentRatio * 100).toFixed(1) })
: t('dashboard.noEmailsSynced')}
{attachmentData.map((entry, i) => (
|
))}
totalAttachments > 0 ? `${((v as number) * 100).toFixed(1)}%` : '0%'} />
{totalAttachments > 0 ? (
<>
{t('dashboard.withAttachments')} ({(attachmentRatio * 100).toFixed(1)}%)
{t('dashboard.noAttachments')}
>
) : (
)}
{t('dashboard.top10Senders')}
{t('dashboard.byMessageCount')}
{hasTopSenders ? (
{t('dashboard.sender')}
{t('dashboard.count')}
{stats!.top_senders.map((s) => (
{s.key}
{formatNumber(s.count)}
))}
) : (
)}
{t('dashboard.top10LargestEmails')}
{t('dashboard.byMessageSize')}
{hasTopEmails ? (
{t('dashboard.subject')}
{t('dashboard.size')}
{stats!.top_largest_emails.map((m, index) => (
{m.subject || t('dashboard.noSubject')}
{formatBytes(m.size_bytes)}
))}
) : (
)}
{t('dashboard.top10Accounts')}
{t('dashboard.byMessageCount')}
{hasTopAccounts ? (
{t('dashboard.account')}
{t('dashboard.emails')}
{stats!.top_accounts.map((acc) => (
{acc.key}
{formatNumber(acc.count)}
))}
) : (
)}
{/* Footer / Copyright - New Addition */}
>
);
}