Performance: RCON pooling, route caching, parallel status probe

Server:
- lib/cache.ts: tiny TTL memo for hot paths.
- lib/rcon.ts: pooled connection with 30s idle close and one-shot retry;
  was opening a fresh TCP per RCON call.
- /api/status: race protocol-ping + RCON with Promise.any (was sequential
  with 5s timeouts) and memo 3s; adds Cache-Control.
- /api/players: memo 10s, invalidated on mutations.
- /api/mods: getModDetails memo 10s (invalidated on install/remove) —
  eliminates per-request readdirSync/statSync/AdmZip parse.
- /api/analytics: reverse-scan JSONL to window cutoff (O(window) vs O(file))
  and memo 30s.
- /api/logs: memo 2s to throttle journalctl spawns during Live mode.

Client:
- StatusCard + ServerControls: staleTime 5s, both poll every 10s, dedup
  via shared query key.
- Analytics: staleTime 30s; memoize sparkline SVG paths.
- Modrinth search icons + mc-heads avatars: width/height + loading=lazy +
  decoding=async.
- next.config.ts: images.remotePatterns for future next/image migration,
  explicit compress=true.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hurkicorgi 2026-04-13 00:59:10 -06:00
parent b6b10159ad
commit b6cf8c7cdc
14 changed files with 220 additions and 82 deletions

View file

@ -1,7 +1,7 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import { useMemo, useState } from "react";
import { Skeleton } from "@/components/ui/skeleton";
import {
Card,
@ -55,16 +55,17 @@ function Sparkline({
);
}
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`;
const { pathD, areaD, w } = useMemo(() => {
const dataMax = max || Math.max(...data, 1);
const width = 300;
const pts = data.map((v, i) => {
const x = (i / (data.length - 1)) * width;
const y = height - (v / dataMax) * (height - 10) - 5;
return `${x},${y}`;
});
const p = `M${pts.join(" L")}`;
return { pathD: p, areaD: `${p} L${width},${height} L0,${height} Z`, w: width };
}, [data, max, height]);
return (
<div className="rounded-lg bg-muted p-4">
@ -99,6 +100,7 @@ export function Analytics() {
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;

View file

@ -489,7 +489,11 @@ export function ModManager() {
<img
src={result.icon_url}
alt=""
className="w-10 h-10 rounded-md shrink-0"
width={40}
height={40}
loading="lazy"
decoding="async"
className="w-10 h-10 rounded-md shrink-0 bg-muted"
/>
) : (
<div className="w-10 h-10 rounded-md bg-muted shrink-0" />

View file

@ -33,6 +33,8 @@ export function PlayerAvatar({
alt=""
width={size}
height={size}
loading="lazy"
decoding="async"
onError={() => setFailed(true)}
className={`rounded shrink-0 bg-muted ${className}`}
/>

View file

@ -39,6 +39,7 @@ export function ServerControls() {
queryKey: ["status"],
queryFn: () => fetch("/api/status").then((r) => r.json()),
refetchInterval: 10000,
staleTime: 5000,
});
const action = useMutation({

View file

@ -20,7 +20,8 @@ export function StatusCard() {
const { data, isLoading } = useQuery<ServerStatus>({
queryKey: ["status"],
queryFn: () => fetch("/api/status").then((r) => r.json()),
refetchInterval: 15000,
refetchInterval: 10000,
staleTime: 5000,
});
const copyIP = () => {