Initial push

This commit is contained in:
2026-03-08 15:49:34 +01:00
parent 8da12bb7d1
commit 47127f276d
101 changed files with 13844 additions and 8 deletions

View File

@@ -0,0 +1,269 @@
"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>
);
}