Initial push
This commit is contained in:
277
app/(dashboard)/page.tsx
Normal file
277
app/(dashboard)/page.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user