"use client"; import { useQuery } from "@tanstack/react-query"; import { useMemo, useState } from "react"; import { Skeleton } from "@/components/ui/skeleton"; import { Badge } from "@/components/ui/badge"; import { PlayerAvatar } from "@/components/PlayerAvatar"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; type MetricEntry = { ts: string; tps: number; ramUsedMB: number; ramTotalMB: number; cpuPercent: number; playersOnline: number; players?: string[]; }; function UptimeRing({ percent, size = 64 }: { percent: number; size?: number }) { const r = size / 2 - 5; const c = 2 * Math.PI * r; const off = c * (1 - percent / 100); const color = percent >= 95 ? "#4ade80" : percent >= 80 ? "#facc15" : "#f87171"; return ( {Math.round(percent)}% ); } function Sparkline({ data, color, max, height = 80, label, unit, currentValue, peakIndex, peakLabel, }: { data: number[]; color: string; max?: number; height?: number; label: string; unit: string; currentValue: string; peakIndex?: number; peakLabel?: string; }) { if (data.length < 2) { return (

{label}

{currentValue} {unit}

Collecting data...

); } const { pathD, areaD, w, peakXY } = useMemo(() => { const dataMax = max || Math.max(...data, 1); const width = 300; const coords = data.map((v, i) => { const x = (i / (data.length - 1)) * width; const y = height - (v / dataMax) * (height - 10) - 5; return { x, y }; }); const pts = coords.map((c) => `${c.x},${c.y}`); const p = `M${pts.join(" L")}`; const peak = typeof peakIndex === "number" && peakIndex >= 0 && peakIndex < coords.length ? coords[peakIndex] : null; return { pathD: p, areaD: `${p} L${width},${height} L0,${height} Z`, w: width, peakXY: peak, }; }, [data, max, height, peakIndex]); return (

{label}

{currentValue} {unit}

{peakXY && ( <> )} {peakLabel && (

peak {peakLabel}

)}
); } export function Analytics() { const [hours, setHours] = useState(6); const { data: metrics = [] } = useQuery({ queryKey: ["analytics", hours], queryFn: () => fetch(`/api/analytics?hours=${hours}`).then((r) => r.json()), refetchInterval: 60_000, staleTime: 30_000, }); const latest = metrics.length > 0 ? metrics[metrics.length - 1] : null; const { uptimePct, peakPlayers, peakIndex, onlineNow } = useMemo(() => { if (metrics.length === 0) { return { uptimePct: 0, peakPlayers: 0, peakIndex: -1, onlineNow: [] as string[] }; } const alive = metrics.filter((m) => m.tps > 0 || m.playersOnline > 0).length; const pct = (alive / metrics.length) * 100; let peak = -Infinity; let idx = -1; metrics.forEach((m, i) => { if (m.playersOnline > peak) { peak = m.playersOnline; idx = i; } }); const online = (latest?.players?.filter((p): p is string => typeof p === "string" && p.length > 0)) || []; return { uptimePct: pct, peakPlayers: peak, peakIndex: idx, onlineNow: online }; }, [metrics, latest]); const ranges = [ { label: "1h", value: 1 }, { label: "6h", value: 6 }, { label: "24h", value: 24 }, ]; return (
Server Analytics {metrics.length} data points
{ranges.map((r) => ( ))}
{metrics.length > 0 && (

Uptime

{uptimePct.toFixed(1)}%{" "} over last {hours}h

Peak players

{peakPlayers >= 0 ? peakPlayers : 0}

Online now ({onlineNow.length})

{onlineNow.length === 0 ? (

) : (
{onlineNow.slice(0, 8).map((name) => ( {name} ))} {onlineNow.length > 8 && ( +{onlineNow.length - 8} )}
)}
)}
m.tps)} color="#4ade80" max={22} label="TPS" unit="" currentValue={latest ? latest.tps.toFixed(1) : "-"} /> m.ramUsedMB)} color="#60a5fa" label="RAM" unit="MB" currentValue={ latest ? `${(latest.ramUsedMB / 1024).toFixed(1)} GB` : "-" } /> m.cpuPercent)} color="#f59e0b" max={100} label="CPU" unit="%" currentValue={latest ? latest.cpuPercent.toFixed(0) : "-"} /> m.playersOnline)} color="#a78bfa" label="Players" unit="" currentValue={latest ? latest.playersOnline.toString() : "0"} peakIndex={peakPlayers > 0 ? peakIndex : undefined} peakLabel={peakPlayers > 0 ? peakPlayers.toString() : undefined} />
); }