278 lines
8.5 KiB
TypeScript
278 lines
8.5 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Progress } from "@/components/ui/progress";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import {
|
|
Users,
|
|
Puzzle,
|
|
HardDrive,
|
|
Activity,
|
|
Clock,
|
|
Zap,
|
|
TrendingUp,
|
|
AlertTriangle,
|
|
} from "lucide-react";
|
|
|
|
interface MonitoringData {
|
|
system: {
|
|
cpuPercent: number;
|
|
totalMemMb: number;
|
|
usedMemMb: number;
|
|
loadAvg: number[];
|
|
uptime: number;
|
|
};
|
|
server: {
|
|
running: boolean;
|
|
uptime?: number;
|
|
startedAt?: string;
|
|
};
|
|
timestamp: number;
|
|
}
|
|
|
|
interface StatsData {
|
|
totalPlayers: number;
|
|
onlinePlayers: number;
|
|
enabledPlugins: number;
|
|
totalPlugins: number;
|
|
pendingBackups: number;
|
|
recentAlerts: string[];
|
|
}
|
|
|
|
function StatCard({
|
|
title,
|
|
value,
|
|
subtitle,
|
|
icon: Icon,
|
|
accent = "emerald",
|
|
loading = false,
|
|
}: {
|
|
title: string;
|
|
value: string | number;
|
|
subtitle?: string;
|
|
icon: React.ElementType;
|
|
accent?: "emerald" | "blue" | "amber" | "red";
|
|
loading?: boolean;
|
|
}) {
|
|
const colors = {
|
|
emerald: "text-emerald-500 bg-emerald-500/10",
|
|
blue: "text-blue-500 bg-blue-500/10",
|
|
amber: "text-amber-500 bg-amber-500/10",
|
|
red: "text-red-500 bg-red-500/10",
|
|
};
|
|
|
|
return (
|
|
<Card className="bg-zinc-900 border-zinc-800">
|
|
<CardContent className="p-6">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<p className="text-sm text-zinc-400 mb-1">{title}</p>
|
|
{loading ? (
|
|
<Skeleton className="h-8 w-20 bg-zinc-800" />
|
|
) : (
|
|
<p className="text-2xl font-bold text-white">{value}</p>
|
|
)}
|
|
{subtitle && (
|
|
<p className="text-xs text-zinc-500 mt-1">{subtitle}</p>
|
|
)}
|
|
</div>
|
|
<div className={`p-2.5 rounded-lg ${colors[accent]}`}>
|
|
<Icon className="w-5 h-5" />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
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`;
|
|
}
|
|
|
|
export default function DashboardPage() {
|
|
const [monitoring, setMonitoring] = useState<MonitoringData | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const fetchMonitoring = async () => {
|
|
try {
|
|
const res = await fetch("/api/monitoring");
|
|
if (res.ok) setMonitoring(await res.json());
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchMonitoring();
|
|
const interval = setInterval(fetchMonitoring, 5000);
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
const memPercent = monitoring
|
|
? Math.round((monitoring.system.usedMemMb / monitoring.system.totalMemMb) * 100)
|
|
: 0;
|
|
|
|
return (
|
|
<div className="p-6 space-y-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-white">Dashboard</h1>
|
|
<p className="text-zinc-400 text-sm mt-1">
|
|
Real-time overview of your Minecraft server
|
|
</p>
|
|
</div>
|
|
|
|
{/* Status banner */}
|
|
<div
|
|
className={`flex items-center gap-3 p-4 rounded-xl border ${
|
|
monitoring?.server.running
|
|
? "bg-emerald-500/10 border-emerald-500/30 text-emerald-400"
|
|
: "bg-red-500/10 border-red-500/30 text-red-400"
|
|
}`}
|
|
>
|
|
<div
|
|
className={`w-2.5 h-2.5 rounded-full animate-pulse ${
|
|
monitoring?.server.running ? "bg-emerald-500" : "bg-red-500"
|
|
}`}
|
|
/>
|
|
<span className="font-medium">
|
|
Server is {monitoring?.server.running ? "Online" : "Offline"}
|
|
</span>
|
|
{monitoring?.server.running && monitoring.server.uptime && (
|
|
<span className="text-sm opacity-70 ml-auto">
|
|
Up for {formatUptime(monitoring.server.uptime)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Stat cards */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
|
|
<StatCard
|
|
title="CPU Usage"
|
|
value={loading ? "—" : `${monitoring?.system.cpuPercent ?? 0}%`}
|
|
subtitle={`Load avg: ${monitoring?.system.loadAvg[0].toFixed(2) ?? "—"}`}
|
|
icon={Activity}
|
|
accent="blue"
|
|
loading={loading}
|
|
/>
|
|
<StatCard
|
|
title="Memory"
|
|
value={loading ? "—" : `${monitoring?.system.usedMemMb ?? 0} MB`}
|
|
subtitle={`of ${monitoring?.system.totalMemMb ?? 0} MB total`}
|
|
icon={HardDrive}
|
|
accent={memPercent > 85 ? "red" : memPercent > 60 ? "amber" : "emerald"}
|
|
loading={loading}
|
|
/>
|
|
<StatCard
|
|
title="System Uptime"
|
|
value={loading ? "—" : formatUptime(monitoring?.system.uptime ?? 0)}
|
|
icon={Clock}
|
|
accent="emerald"
|
|
loading={loading}
|
|
/>
|
|
<StatCard
|
|
title="Server Status"
|
|
value={monitoring?.server.running ? "Online" : "Offline"}
|
|
icon={Zap}
|
|
accent={monitoring?.server.running ? "emerald" : "red"}
|
|
loading={loading}
|
|
/>
|
|
</div>
|
|
|
|
{/* Resource gauges */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<Card className="bg-zinc-900 border-zinc-800">
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-medium text-zinc-400 flex items-center gap-2">
|
|
<Activity className="w-4 h-4 text-blue-500" />
|
|
CPU Usage
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{loading ? (
|
|
<Skeleton className="h-4 w-full bg-zinc-800" />
|
|
) : (
|
|
<>
|
|
<div className="flex justify-between text-sm mb-2">
|
|
<span className="text-white font-medium">
|
|
{monitoring?.system.cpuPercent ?? 0}%
|
|
</span>
|
|
<span className="text-zinc-500">100%</span>
|
|
</div>
|
|
<Progress
|
|
value={monitoring?.system.cpuPercent ?? 0}
|
|
className="h-2 bg-zinc-800"
|
|
/>
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="bg-zinc-900 border-zinc-800">
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-medium text-zinc-400 flex items-center gap-2">
|
|
<HardDrive className="w-4 h-4 text-emerald-500" />
|
|
Memory Usage
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{loading ? (
|
|
<Skeleton className="h-4 w-full bg-zinc-800" />
|
|
) : (
|
|
<>
|
|
<div className="flex justify-between text-sm mb-2">
|
|
<span className="text-white font-medium">
|
|
{monitoring?.system.usedMemMb ?? 0} MB
|
|
</span>
|
|
<span className="text-zinc-500">
|
|
{monitoring?.system.totalMemMb ?? 0} MB
|
|
</span>
|
|
</div>
|
|
<Progress
|
|
value={memPercent}
|
|
className="h-2 bg-zinc-800"
|
|
/>
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Quick info */}
|
|
<Card className="bg-zinc-900 border-zinc-800">
|
|
<CardHeader>
|
|
<CardTitle className="text-sm font-medium text-zinc-400 flex items-center gap-2">
|
|
<TrendingUp className="w-4 h-4 text-emerald-500" />
|
|
Quick Actions
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
{[
|
|
{ label: "View Console", href: "/console", icon: "⌨️" },
|
|
{ label: "Manage Players", href: "/players", icon: "👥" },
|
|
{ label: "Plugins", href: "/plugins", icon: "🔌" },
|
|
{ label: "Create Backup", href: "/backups", icon: "💾" },
|
|
].map(({ label, href, icon }) => (
|
|
<a
|
|
key={href}
|
|
href={href}
|
|
className="flex items-center gap-2 p-3 rounded-lg bg-zinc-800 hover:bg-zinc-700 transition-colors text-sm text-zinc-300 hover:text-white"
|
|
>
|
|
<span>{icon}</span>
|
|
<span>{label}</span>
|
|
</a>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|