initial commit

This commit is contained in:
SowinskiBraeden committed 2026-07-05 16:54:59 -07:00
commit ce8f719a05
106 files changed
+24584

No files matched your search

+181
View File
@@ -0,0 +1,181 @@
import { Link } from 'react-router-dom';
import type { CurrentUser, ResourceSample } from '@reforger-panel/shared';
import {
useConfiguration,
useResourceHistory,
useServerResources,
useServers,
} from '../api/hooks.js';
import { formatBytes, formatDuration } from '../lib/format.js';
import { Card, Spinner } from '../components/ui.js';
import { TimeSeriesChart } from '../components/charts.js';
import {
ConfigSummaryRows,
CurrentPlayersCard,
OpsHealthCard,
RecentActivityCard,
} from '../components/widgets.js';
export function OverviewPage({ user }: { user: CurrentUser }) {
const { data: serversData, isLoading } = useServers();
const server = serversData?.servers[0];
if (isLoading) return <Spinner label="Loading dashboard…" />;
if (!server) {
return (
<Card title="No servers">
<p className="text-sm text-slate-ink">
No servers found. Run <code className="font-mono text-accent-400">npm run db:seed</code>{' '}
to create the training server.
</p>
</Card>
);
}
return <Dashboard user={user} slug={server.slug} />;
}
function seriesOf(
samples: ResourceSample[] | undefined,
pick: (s: ResourceSample) => number,
): { t: number; v: number }[] {
return (samples ?? []).map((s) => ({ t: s.t, v: pick(s) }));
}
function Dashboard({ user, slug }: { user: CurrentUser; slug: string }) {
const { data: serversData } = useServers();
const server = serversData?.servers.find((s) => s.slug === slug);
const { data: resources } = useServerResources(slug);
const { data: config } = useConfiguration(slug);
const { data: history } = useResourceHistory(slug);
if (!server) return null;
const installedMods = config?.config.mods ?? [];
const samples = history?.samples;
const memoryLimit = resources?.memoryLimitBytes ?? samples?.at(-1)?.memoryLimitBytes ?? null;
const cpuLimit = resources?.cpuLimitPercent ?? samples?.at(-1)?.cpuLimitPercent ?? 100;
return (
<div className="w-full space-y-5">
<div className="grid gap-4 md:grid-cols-3">
<Card title="CPU">
<p className="text-2xl font-semibold text-zinc-100">
{resources ? `${resources.cpuPercent.toFixed(0)}%` : '—'}
<span className="text-sm font-normal text-slate-dim">
{cpuLimit && cpuLimit !== 100 ? ` / ${cpuLimit}%` : ''}
</span>
</p>
<TimeSeriesChart
className="mt-2"
max={cpuLimit}
series={[
{
points: seriesOf(samples, (s) => s.cpuPercent),
color: 'var(--color-accent-400)',
},
]}
/>
</Card>
<Card title="Memory">
<p className="text-2xl font-semibold text-zinc-100">
{resources ? formatBytes(resources.memoryBytes) : '—'}
<span className="text-sm font-normal text-slate-dim">
{memoryLimit ? ` / ${formatBytes(memoryLimit)}` : ''}
</span>
</p>
<TimeSeriesChart
className="mt-2"
max={memoryLimit}
series={[
{
points: seriesOf(samples, (s) => s.memoryBytes),
color: '#7dd3fc',
},
]}
/>
</Card>
<Card title="Network">
<p className="text-sm text-zinc-300">
<span className="text-accent-400">
{formatBytes(samples?.at(-1)?.networkRxRate ?? 0)}/s
</span>
<span className="ml-3 text-warn-400">
{formatBytes(samples?.at(-1)?.networkTxRate ?? 0)}/s
</span>
<span className="ml-3 text-slate-dim">
up{' '}
{resources && resources.uptimeMs > 0
? formatDuration(resources.uptimeMs / 1000)
: '—'}
</span>
</p>
<TimeSeriesChart
className="mt-2"
series={[
{
points: seriesOf(samples, (s) => s.networkRxRate),
color: 'var(--color-accent-400)',
label: 'rx',
},
{
points: seriesOf(samples, (s) => s.networkTxRate),
color: 'var(--color-warn-400)',
fill: false,
label: 'tx',
},
]}
/>
</Card>
</div>
<div className="grid gap-5 lg:grid-cols-3">
<div className="min-w-0 space-y-5 lg:col-span-2">
<CurrentPlayersCard slug={slug} maxPlayers={server.maxPlayers} />
<RecentActivityCard slug={slug} />
</div>
<div className="min-w-0 space-y-5">
<Card
title="Current configuration"
action={
<Link to="/configuration" className="text-xs text-accent-400 hover:underline">
View configuration
</Link>
}
>
{config ? <ConfigSummaryRows config={config} /> : <Spinner />}
</Card>
<Card
title="Installed mods"
action={
<Link to="/mods" className="text-xs text-accent-400 hover:underline">
Manage
</Link>
}
>
{installedMods.length === 0 ? (
<p className="text-sm text-slate-dim">The server runs vanilla (no mods).</p>
) : (
<div>
<p className="text-sm text-zinc-200">
{installedMods.length} mod{installedMods.length === 1 ? '' : 's'} in config.json
</p>
<ul className="mt-2 space-y-1">
{installedMods.slice(0, 5).map((mod) => (
<li key={mod.modId} className="truncate text-xs text-slate-ink">
{mod.name ?? mod.modId}
</li>
))}
{installedMods.length > 5 && (
<li className="text-xs text-slate-dim">+ {installedMods.length - 5} more</li>
)}
</ul>
</div>
)}
</Card>
<OpsHealthCard user={user} slug={slug} />
</div>
</div>
</div>
);
}