Initial commit: Minecraft dashboard
Next.js 16 + Tailwind v4 + shadcn v4 dashboard for managing a modded Forge 1.20.1 server. Includes server controls, player management, mod manager with Modrinth search and dependency resolution, world backups, snapshots, analytics, logs, and chat bridge. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
dd69c17c3b
77 changed files with 7007 additions and 0 deletions
177
components/Analytics.tsx
Normal file
177
components/Analytics.tsx
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
|
||||
type MetricEntry = {
|
||||
ts: string;
|
||||
tps: number;
|
||||
ramUsedMB: number;
|
||||
ramTotalMB: number;
|
||||
cpuPercent: number;
|
||||
playersOnline: number;
|
||||
};
|
||||
|
||||
function Sparkline({
|
||||
data,
|
||||
color,
|
||||
max,
|
||||
height = 80,
|
||||
label,
|
||||
unit,
|
||||
currentValue,
|
||||
}: {
|
||||
data: number[];
|
||||
color: string;
|
||||
max?: number;
|
||||
height?: number;
|
||||
label: string;
|
||||
unit: string;
|
||||
currentValue: string;
|
||||
}) {
|
||||
if (data.length < 2) {
|
||||
return (
|
||||
<div className="rounded-lg bg-muted p-4">
|
||||
<div className="flex items-baseline justify-between mb-2">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">{label}</p>
|
||||
<p className="text-lg font-bold">
|
||||
{currentValue}
|
||||
<span className="text-xs text-muted-foreground ml-1">{unit}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Skeleton className="w-full" style={{ height }} />
|
||||
<p className="text-xs text-muted-foreground mt-2">Collecting data...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const dataMax = max || Math.max(...data, 1);
|
||||
const w = 300;
|
||||
const points = data.map((v, i) => {
|
||||
const x = (i / (data.length - 1)) * w;
|
||||
const y = height - (v / dataMax) * (height - 10) - 5;
|
||||
return `${x},${y}`;
|
||||
});
|
||||
|
||||
const pathD = `M${points.join(" L")}`;
|
||||
const areaD = `${pathD} L${w},${height} L0,${height} Z`;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg bg-muted p-4">
|
||||
<div className="flex items-baseline justify-between mb-2">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">{label}</p>
|
||||
<p className="text-lg font-bold">
|
||||
{currentValue}
|
||||
<span className="text-xs text-muted-foreground ml-1">{unit}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<svg viewBox={`0 0 ${w} ${height}`} className="w-full" preserveAspectRatio="none">
|
||||
<defs>
|
||||
<linearGradient id={`grad-${label}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor={color} stopOpacity="0.3" />
|
||||
<stop offset="100%" stopColor={color} stopOpacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d={areaD} fill={`url(#grad-${label})`} />
|
||||
<path d={pathD} fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Analytics() {
|
||||
const [hours, setHours] = useState(6);
|
||||
|
||||
const { data: metrics = [] } = useQuery<MetricEntry[]>({
|
||||
queryKey: ["analytics", hours],
|
||||
queryFn: () =>
|
||||
fetch(`/api/analytics?hours=${hours}`).then((r) => r.json()),
|
||||
refetchInterval: 60_000,
|
||||
});
|
||||
|
||||
const latest = metrics.length > 0 ? metrics[metrics.length - 1] : null;
|
||||
|
||||
const ranges = [
|
||||
{ label: "1h", value: 1 },
|
||||
{ label: "6h", value: 6 },
|
||||
{ label: "24h", value: 24 },
|
||||
];
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||||
<div>
|
||||
<CardTitle>Server Analytics</CardTitle>
|
||||
<CardDescription>
|
||||
{metrics.length} data points
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex gap-1 bg-muted p-1 rounded-lg w-fit shrink-0">
|
||||
{ranges.map((r) => (
|
||||
<button
|
||||
key={r.value}
|
||||
onClick={() => setHours(r.value)}
|
||||
className={`px-3 py-1 rounded-md text-xs font-medium transition ${
|
||||
hours === r.value
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
{r.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<Sparkline
|
||||
data={metrics.map((m) => m.tps)}
|
||||
color="#4ade80"
|
||||
max={22}
|
||||
label="TPS"
|
||||
unit=""
|
||||
currentValue={latest ? latest.tps.toFixed(1) : "-"}
|
||||
/>
|
||||
<Sparkline
|
||||
data={metrics.map((m) => m.ramUsedMB)}
|
||||
color="#60a5fa"
|
||||
label="RAM"
|
||||
unit="MB"
|
||||
currentValue={
|
||||
latest ? `${(latest.ramUsedMB / 1024).toFixed(1)} GB` : "-"
|
||||
}
|
||||
/>
|
||||
<Sparkline
|
||||
data={metrics.map((m) => m.cpuPercent)}
|
||||
color="#f59e0b"
|
||||
max={100}
|
||||
label="CPU"
|
||||
unit="%"
|
||||
currentValue={latest ? latest.cpuPercent.toFixed(0) : "-"}
|
||||
/>
|
||||
<Sparkline
|
||||
data={metrics.map((m) => m.playersOnline)}
|
||||
color="#a78bfa"
|
||||
label="Players"
|
||||
unit=""
|
||||
currentValue={latest ? latest.playersOnline.toString() : "0"}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue