feat: Dashboard-Metriken nach Mailpiler-Vorbild (Uptime, Aktivität, Prognose)

- storage_stats.go (neu): MailActivityStats (60min/24h/7d/30d), StorageEstimateStats
- dashboard_handlers.go: Uptime (/proc/uptime), activity + estimate in System-Stats-Response
- DashboardTab: Uptime in API-Kachel, neue Kacheln "Mail-Eingang" + "Speicherprognose"
- Warnung (Badge "Knapp!") wenn Partition in <90 Tagen voll

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-03-31 10:44:02 +02:00
parent 5bbf6d0ff3
commit 58bcfb1586
6 changed files with 224 additions and 6 deletions
+87 -1
View File
@@ -4,6 +4,8 @@ import {
type SMTPStatus,
type StorageStats,
type SystemStats,
type SystemStatsActivity,
type SystemStatsEstimate,
} from "@/lib/api";
import { type User } from "@/lib/api";
import { Button } from "@/components/ui/button";
@@ -20,6 +22,73 @@ function formatBytes(bytes: number): string {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
function formatUptime(seconds: number): string {
const d = Math.floor(seconds / 86400);
const h = Math.floor((seconds % 86400) / 3600);
const m = Math.floor((seconds % 3600) / 60);
if (d > 0) return `${d}d ${h}h ${m}m`;
if (h > 0) return `${h}h ${m}m`;
return `${m}m`;
}
function formatDaysUntilFull(days: number): string {
if (days < 0) return "∞";
if (days < 30) return `${days} Tage`;
if (days < 365) return `${Math.round(days / 30)} Monate`;
return `${(days / 365).toFixed(1)} Jahre`;
}
function ActivityCard({ activity }: { activity: SystemStatsActivity }) {
return (
<Card>
<CardContent className="pt-6 space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-muted-foreground">Mail-Eingang</span>
</div>
<Separator />
<div className="grid grid-cols-2 gap-1 text-sm">
<span className="text-muted-foreground">Letzte 60 min</span>
<span className="font-semibold">{activity.last_60_min.toLocaleString("de-DE")}</span>
<span className="text-muted-foreground">Letzte 24 h</span>
<span className="font-semibold">{activity.last_24h.toLocaleString("de-DE")}</span>
<span className="text-muted-foreground">Letzte 7 Tage</span>
<span className="font-semibold">{activity.last_7d.toLocaleString("de-DE")}</span>
<span className="text-muted-foreground">Letzte 30 Tage</span>
<span className="font-semibold">{activity.last_30d.toLocaleString("de-DE")}</span>
</div>
</CardContent>
</Card>
);
}
function EstimateCard({ estimate }: { estimate: SystemStatsEstimate }) {
return (
<Card>
<CardContent className="pt-6 space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-muted-foreground">Speicherprognose</span>
{estimate.days_until_full >= 0 && estimate.days_until_full < 90 && (
<Badge variant="destructive">Knapp!</Badge>
)}
</div>
<Separator />
<div className="grid grid-cols-2 gap-1 text-sm">
<span className="text-muted-foreground">Ø Mails/Tag</span>
<span className="font-semibold">{estimate.avg_mails_per_day.toFixed(1)}</span>
<span className="text-muted-foreground">Ø Mail-Größe</span>
<span>{formatBytes(estimate.avg_mail_bytes)}</span>
<span className="text-muted-foreground">Archiv-Alter</span>
<span>{estimate.archive_age_days} Tage</span>
<span className="text-muted-foreground">Partition voll in</span>
<span className={estimate.days_until_full >= 0 && estimate.days_until_full < 90 ? "font-semibold text-destructive" : "font-semibold"}>
{formatDaysUntilFull(estimate.days_until_full)}
</span>
</div>
</CardContent>
</Card>
);
}
interface DashboardTabProps {
isSuperAdmin: boolean;
smtpStatus: SMTPStatus | null;
@@ -74,7 +143,7 @@ export function DashboardTab({
{/* Status-Kacheln */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{/* API */}
{/* API + Uptime */}
<Card>
<CardContent className="pt-6 space-y-3">
<div className="flex items-center justify-between">
@@ -89,6 +158,12 @@ export function DashboardTab({
<span className="font-mono">:8080</span>
<span className="text-muted-foreground">Protokoll</span>
<span>HTTP</span>
{systemStats?.uptime && (
<>
<span className="text-muted-foreground">System-Uptime</span>
<span>{formatUptime(systemStats.uptime.seconds)}</span>
</>
)}
</div>
</CardContent>
</Card>
@@ -293,6 +368,17 @@ export function DashboardTab({
</Card>
</div>
{/* Mail-Aktivität + Speicherprognose */}
{(systemStats.activity || systemStats.estimate) && (
<div className="space-y-2">
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">Archiv-Statistik</h3>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{systemStats.activity && <ActivityCard activity={systemStats.activity} />}
{systemStats.estimate && <EstimateCard estimate={systemStats.estimate} />}
</div>
</div>
)}
{/* Festplatten */}
{systemStats.disks.length > 0 && (
<div className="space-y-2">
+2
View File
@@ -123,6 +123,8 @@ export type {
SystemStatsRAM,
SystemStatsDisk,
SystemStatsMailInfo,
SystemStatsActivity,
SystemStatsEstimate,
SystemStats,
SecurityCheck,
SecurityAuditResult,
+17
View File
@@ -83,14 +83,31 @@ export interface SystemStatsMailInfo {
subject: string;
}
export interface SystemStatsActivity {
last_60_min: number;
last_24h: number;
last_7d: number;
last_30d: number;
}
export interface SystemStatsEstimate {
avg_mails_per_day: number;
avg_mail_bytes: number;
days_until_full: number; // -1 = unknown
archive_age_days: number;
}
export interface SystemStats {
cpu: SystemStatsCPU;
ram: SystemStatsRAM;
disks: SystemStatsDisk[];
uptime: { seconds: number };
archive: {
first_mail: SystemStatsMailInfo | null;
last_mail: SystemStatsMailInfo | null;
};
activity: SystemStatsActivity;
estimate: SystemStatsEstimate;
}
export interface SecurityCheck {