"use client"; import { useQuery } from "@tanstack/react-query"; import { useMemo, useRef, 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 formatTime(iso: string): string { const d = new Date(iso); if (isNaN(d.getTime())) return ""; const hh = String(d.getHours()).padStart(2, "0"); const mm = String(d.getMinutes()).padStart(2, "0"); return `${hh}:${mm}`; } function Sparkline({ data, color, max, height = 80, label, unit, currentValue, peakIndex, peakLabel, timestamps, formatValue, }: { data: number[]; color: string; max?: number; height?: number; label: string; unit: string; currentValue: string; peakIndex?: number; peakLabel?: string; timestamps?: string[]; formatValue?: (v: number) => string; }) { const svgRef = useRef(null); const [hoverIdx, setHoverIdx] = useState(null); const coords = useMemo(() => { if (data.length < 2) return [] as { x: number; y: number }[]; const dataMax = max || Math.max(...data, 1); const width = 300; return data.map((v, i) => { const x = (i / (data.length - 1)) * width; const y = height - (v / dataMax) * (height - 10) - 5; return { x, y }; }); }, [data, max, height]); const { pathD, areaD, w, peakXY } = useMemo(() => { const width = 300; const pts = coords.map((c) => `${c.x},${c.y}`); const p = pts.length ? `M${pts.join(" L")}` : ""; const peak = typeof peakIndex === "number" && peakIndex >= 0 && peakIndex < coords.length ? coords[peakIndex] : null; return { pathD: p, areaD: p ? `${p} L${width},${height} L0,${height} Z` : "", w: width, peakXY: peak, }; }, [coords, height, peakIndex]); const locate = (clientX: number) => { const svg = svgRef.current; if (!svg || data.length < 2) return; const rect = svg.getBoundingClientRect(); if (rect.width <= 0) return; const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)); setHoverIdx(Math.round(ratio * (data.length - 1))); }; const defaultFormat = (v: number) => Number.isInteger(v) ? String(v) : v.toFixed(1); const fmt = formatValue || defaultFormat; if (data.length < 2) { return (

{label}

{currentValue} {unit}

Collecting data...

); } const hovered = hoverIdx !== null ? coords[hoverIdx] : null; const hoverRatio = hoverIdx !== null ? hoverIdx / (data.length - 1) : 0; const hoverTime = hoverIdx !== null && timestamps?.[hoverIdx] ? formatTime(timestamps[hoverIdx]) : ""; const hoverValue = hoverIdx !== null ? `${fmt(data[hoverIdx])}${unit ? ` ${unit}` : ""}` : ""; return (

{label}

{currentValue} {unit}

locate(e.clientX)} onMouseLeave={() => setHoverIdx(null)} onTouchMove={(e) => { const t = e.touches[0]; if (t) locate(t.clientX); }} onTouchEnd={() => setHoverIdx(null)} role="img" aria-label={`${label} over time`} > {peakXY && ( <> )} {hovered && ( <> )} {hovered && (
0.85 ? "-100%" : hoverRatio < 0.15 ? "0%" : "-50%" }, -100%)`, }} > {hoverTime && ( {hoverTime} )} {hoverValue}
)}
{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)} timestamps={metrics.map((m) => m.ts)} color="#4ade80" max={22} label="TPS" unit="" currentValue={latest ? latest.tps.toFixed(1) : "-"} formatValue={(v) => v.toFixed(1)} /> m.ramUsedMB)} timestamps={metrics.map((m) => m.ts)} color="#60a5fa" label="RAM" unit="GB" currentValue={ latest ? `${(latest.ramUsedMB / 1024).toFixed(1)} GB` : "-" } formatValue={(v) => (v / 1024).toFixed(1)} /> m.cpuPercent)} timestamps={metrics.map((m) => m.ts)} color="#f59e0b" max={100} label="CPU" unit="%" currentValue={latest ? latest.cpuPercent.toFixed(0) : "-"} formatValue={(v) => v.toFixed(0)} /> m.playersOnline)} timestamps={metrics.map((m) => m.ts)} color="#a78bfa" label="Players" unit="" currentValue={latest ? latest.playersOnline.toString() : "0"} peakIndex={peakPlayers > 0 ? peakIndex : undefined} peakLabel={peakPlayers > 0 ? peakPlayers.toString() : undefined} formatValue={(v) => String(Math.round(v))} />
); }