270 lines
8.6 KiB
TypeScript
270 lines
8.6 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState, useRef } from "react";
|
|
import { io } from "socket.io-client";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import {
|
|
LineChart,
|
|
Line,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
ResponsiveContainer,
|
|
AreaChart,
|
|
Area,
|
|
} from "recharts";
|
|
import { Activity, HardDrive, Clock, Cpu, Server } from "lucide-react";
|
|
|
|
interface DataPoint {
|
|
time: string;
|
|
cpu: number;
|
|
memory: number;
|
|
players: number;
|
|
}
|
|
|
|
interface MonitoringData {
|
|
system: {
|
|
cpuPercent: number;
|
|
totalMemMb: number;
|
|
usedMemMb: number;
|
|
loadAvg: number[];
|
|
uptime: number;
|
|
};
|
|
server: { running: boolean; uptime?: number };
|
|
timestamp: number;
|
|
}
|
|
|
|
const MAX_HISTORY = 60; // 60 data points (e.g. 2 minutes at 2s intervals)
|
|
|
|
function formatUptime(s: number) {
|
|
const d = Math.floor(s / 86400);
|
|
const h = Math.floor((s % 86400) / 3600);
|
|
const m = Math.floor((s % 3600) / 60);
|
|
if (d > 0) return `${d}d ${h}h ${m}m`;
|
|
if (h > 0) return `${h}h ${m}m`;
|
|
return `${m}m ${s % 60}s`;
|
|
}
|
|
|
|
export default function MonitoringPage() {
|
|
const [data, setData] = useState<MonitoringData | null>(null);
|
|
const [history, setHistory] = useState<DataPoint[]>([]);
|
|
const socketRef = useRef<ReturnType<typeof io> | null>(null);
|
|
|
|
useEffect(() => {
|
|
// Use REST polling (Socket.io monitoring namespace is optional here)
|
|
const poll = async () => {
|
|
try {
|
|
const res = await fetch("/api/monitoring");
|
|
if (!res.ok) return;
|
|
const json: MonitoringData = await res.json();
|
|
setData(json);
|
|
const time = new Date(json.timestamp).toLocaleTimeString("en", {
|
|
hour12: false,
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit",
|
|
});
|
|
setHistory((prev) => {
|
|
const updated = [
|
|
...prev,
|
|
{
|
|
time,
|
|
cpu: json.system.cpuPercent,
|
|
memory: Math.round(
|
|
(json.system.usedMemMb / json.system.totalMemMb) * 100,
|
|
),
|
|
players: 0,
|
|
},
|
|
];
|
|
return updated.length > MAX_HISTORY
|
|
? updated.slice(-MAX_HISTORY)
|
|
: updated;
|
|
});
|
|
} catch {}
|
|
};
|
|
|
|
poll();
|
|
const interval = setInterval(poll, 2000);
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
const memPercent = data
|
|
? Math.round((data.system.usedMemMb / data.system.totalMemMb) * 100)
|
|
: 0;
|
|
|
|
const chartTheme = {
|
|
grid: "#27272a",
|
|
axis: "#52525b",
|
|
tooltip: { bg: "#18181b", border: "#3f3f46", text: "#fff" },
|
|
};
|
|
|
|
return (
|
|
<div className="p-6 space-y-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-white">Monitoring</h1>
|
|
<p className="text-zinc-400 text-sm mt-1">
|
|
Real-time system and server performance metrics
|
|
</p>
|
|
</div>
|
|
|
|
{/* Summary cards */}
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{[
|
|
{
|
|
title: "CPU",
|
|
value: `${data?.system.cpuPercent ?? 0}%`,
|
|
icon: Cpu,
|
|
color: "text-blue-500",
|
|
bg: "bg-blue-500/10",
|
|
},
|
|
{
|
|
title: "Memory",
|
|
value: `${memPercent}%`,
|
|
icon: HardDrive,
|
|
color: "text-emerald-500",
|
|
bg: "bg-emerald-500/10",
|
|
sub: `${data?.system.usedMemMb ?? 0} / ${data?.system.totalMemMb ?? 0} MB`,
|
|
},
|
|
{
|
|
title: "Load Avg",
|
|
value: data?.system.loadAvg[0].toFixed(2) ?? "—",
|
|
icon: Activity,
|
|
color: "text-amber-500",
|
|
bg: "bg-amber-500/10",
|
|
},
|
|
{
|
|
title: "Uptime",
|
|
value: data ? formatUptime(data.system.uptime) : "—",
|
|
icon: Clock,
|
|
color: "text-violet-500",
|
|
bg: "bg-violet-500/10",
|
|
},
|
|
].map(({ title, value, icon: Icon, color, bg, sub }) => (
|
|
<Card key={title} className="bg-zinc-900 border-zinc-800">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-start justify-between mb-3">
|
|
<p className="text-sm text-zinc-400">{title}</p>
|
|
<div className={`p-2 rounded-lg ${bg}`}>
|
|
<Icon className={`w-4 h-4 ${color}`} />
|
|
</div>
|
|
</div>
|
|
<p className="text-2xl font-bold text-white">{value}</p>
|
|
{sub && <p className="text-xs text-zinc-500 mt-1">{sub}</p>}
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
{/* CPU chart */}
|
|
<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">
|
|
<Cpu className="w-4 h-4 text-blue-500" />
|
|
CPU Usage Over Time
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ResponsiveContainer width="100%" height={200}>
|
|
<AreaChart data={history}>
|
|
<defs>
|
|
<linearGradient id="cpuGrad" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3} />
|
|
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
|
|
</linearGradient>
|
|
</defs>
|
|
<CartesianGrid stroke={chartTheme.grid} strokeDasharray="3 3" vertical={false} />
|
|
<XAxis
|
|
dataKey="time"
|
|
stroke={chartTheme.axis}
|
|
tick={{ fontSize: 11, fill: "#71717a" }}
|
|
interval="preserveStartEnd"
|
|
/>
|
|
<YAxis
|
|
domain={[0, 100]}
|
|
stroke={chartTheme.axis}
|
|
tick={{ fontSize: 11, fill: "#71717a" }}
|
|
tickFormatter={(v) => `${v}%`}
|
|
/>
|
|
<Tooltip
|
|
contentStyle={{
|
|
backgroundColor: chartTheme.tooltip.bg,
|
|
border: `1px solid ${chartTheme.tooltip.border}`,
|
|
borderRadius: 8,
|
|
color: chartTheme.tooltip.text,
|
|
fontSize: 12,
|
|
}}
|
|
formatter={(v) => [`${v}%`, "CPU"]}
|
|
/>
|
|
<Area
|
|
type="monotone"
|
|
dataKey="cpu"
|
|
stroke="#3b82f6"
|
|
strokeWidth={2}
|
|
fill="url(#cpuGrad)"
|
|
dot={false}
|
|
activeDot={{ r: 4, fill: "#3b82f6" }}
|
|
/>
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Memory chart */}
|
|
<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 Over Time
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ResponsiveContainer width="100%" height={200}>
|
|
<AreaChart data={history}>
|
|
<defs>
|
|
<linearGradient id="memGrad" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="5%" stopColor="#10b981" stopOpacity={0.3} />
|
|
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
|
|
</linearGradient>
|
|
</defs>
|
|
<CartesianGrid stroke={chartTheme.grid} strokeDasharray="3 3" vertical={false} />
|
|
<XAxis
|
|
dataKey="time"
|
|
stroke={chartTheme.axis}
|
|
tick={{ fontSize: 11, fill: "#71717a" }}
|
|
interval="preserveStartEnd"
|
|
/>
|
|
<YAxis
|
|
domain={[0, 100]}
|
|
stroke={chartTheme.axis}
|
|
tick={{ fontSize: 11, fill: "#71717a" }}
|
|
tickFormatter={(v) => `${v}%`}
|
|
/>
|
|
<Tooltip
|
|
contentStyle={{
|
|
backgroundColor: chartTheme.tooltip.bg,
|
|
border: `1px solid ${chartTheme.tooltip.border}`,
|
|
borderRadius: 8,
|
|
color: chartTheme.tooltip.text,
|
|
fontSize: 12,
|
|
}}
|
|
formatter={(v) => [`${v}%`, "Memory"]}
|
|
/>
|
|
<Area
|
|
type="monotone"
|
|
dataKey="memory"
|
|
stroke="#10b981"
|
|
strokeWidth={2}
|
|
fill="url(#memGrad)"
|
|
dot={false}
|
|
activeDot={{ r: 4, fill: "#10b981" }}
|
|
/>
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|