feat(admin): dashboard home, telemetry section, enrollment ops screen

- mission-control dashboard: now strip, tournament quick actions, usage
  sparklines, error feed, live activity, tools list
- /admin/telemetry with functions/events/errors/players/usage child routes,
  hand-rolled svg chart primitives, decomposed activities table
- error browser with grouping and mark-resolved
- consolidated per-tournament enrollment screen; hub simplified with
  run tournament promoted
- /admin/activities redirects to /admin/telemetry/functions
This commit is contained in:
2026-08-25 21:54:21 -07:00
parent 06fe1f4ed7
commit a52872bea8
46 changed files with 2824 additions and 645 deletions
@@ -0,0 +1,87 @@
import { ReactNode } from "react";
import { Box, Group, Stack, Text, UnstyledButton } from "@mantine/core";
export interface BarListItem {
label: ReactNode;
value: number;
onClick?: () => void;
}
interface BarListProps {
items: BarListItem[];
formatValue?: (value: number) => string;
color?: string;
}
const BarRow = ({
item,
max,
formatValue,
color,
}: {
item: BarListItem;
max: number;
formatValue: (value: number) => string;
color: string;
}) => {
const content = (
<Stack gap={4} w="100%">
<Group justify="space-between" wrap="nowrap" gap="md">
<Text size="sm" truncate style={{ minWidth: 0 }}>
{item.label}
</Text>
<Text size="sm" c="dimmed" style={{ fontVariantNumeric: "tabular-nums" }}>
{formatValue(item.value)}
</Text>
</Group>
<Box
h={8}
w={`${max > 0 ? Math.max((item.value / max) * 100, 1) : 0}%`}
bg={color}
style={{ borderRadius: "0 4px 4px 0" }}
/>
</Stack>
);
if (!item.onClick) {
return <Box py={6}>{content}</Box>;
}
return (
<UnstyledButton w="100%" py={6} onClick={item.onClick}>
{content}
</UnstyledButton>
);
};
export const BarList = ({
items,
formatValue = (value) => value.toLocaleString(),
color = "var(--mantine-color-anchor)",
}: BarListProps) => {
if (items.length === 0) {
return (
<Text c="dimmed" size="sm">
&mdash;
</Text>
);
}
const max = Math.max(...items.map((item) => item.value));
return (
<Stack gap={2}>
{items.map((item, index) => (
<BarRow
key={index}
item={item}
max={max}
formatValue={formatValue}
color={color}
/>
))}
</Stack>
);
};
export default BarList;
@@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest";
import { niceExtent, pointsToPath } from "./chart-path";
describe("pointsToPath", () => {
it("returns an empty string for an empty series", () => {
expect(pointsToPath([], 100, 40)).toBe("");
});
it("renders a single point as a horizontal segment without NaN", () => {
const path = pointsToPath([5], 100, 40, 2);
expect(path).not.toContain("NaN");
expect(path).toBe("M2,20 L98,20");
});
it("centers a flat series vertically without NaN", () => {
const path = pointsToPath([3, 3, 3], 100, 40);
expect(path).not.toContain("NaN");
expect(path).toBe("M0,20 L50,20 L100,20");
});
it("maps min to the bottom and max to the top", () => {
const path = pointsToPath([0, 10], 100, 40);
expect(path).toBe("M0,40 L100,0");
});
it("respects padding", () => {
const path = pointsToPath([0, 10], 100, 40, 4);
expect(path).toBe("M4,36 L96,4");
});
it("uses a shared extent when provided", () => {
const path = pointsToPath([0, 5], 100, 40, 0, [0, 10]);
expect(path).toBe("M0,40 L100,20");
});
});
describe("niceExtent", () => {
it("handles an empty series", () => {
expect(niceExtent([])).toEqual([0, 1]);
});
it("handles an all-zero series", () => {
expect(niceExtent([0, 0, 0])).toEqual([0, 1]);
});
it("spreads a flat non-zero series", () => {
const [lo, hi] = niceExtent([10, 10]);
expect(lo).toBeLessThan(10);
expect(hi).toBeGreaterThan(10);
expect(Number.isNaN(lo)).toBe(false);
expect(Number.isNaN(hi)).toBe(false);
});
it("covers the data with rounded bounds", () => {
const [lo, hi] = niceExtent([3, 47, 12]);
expect(lo).toBeLessThanOrEqual(3);
expect(hi).toBeGreaterThanOrEqual(47);
});
it("never dips below zero for non-negative data", () => {
const [lo] = niceExtent([1, 2, 3]);
expect(lo).toBeGreaterThanOrEqual(0);
});
});
@@ -0,0 +1,61 @@
export function pointsToPath(
values: number[],
width: number,
height: number,
pad = 0,
extent?: [number, number]
): string {
if (values.length === 0) return "";
const [lo, hi] = extent ?? [Math.min(...values), Math.max(...values)];
const span = hi - lo;
const innerWidth = Math.max(width - pad * 2, 0);
const innerHeight = Math.max(height - pad * 2, 0);
const yFor = (value: number) =>
span === 0
? pad + innerHeight / 2
: pad + innerHeight - ((value - lo) / span) * innerHeight;
if (values.length === 1) {
const y = yFor(values[0]);
return `M${pad},${y} L${pad + innerWidth},${y}`;
}
const step = innerWidth / (values.length - 1);
return values
.map((value, index) => {
const command = index === 0 ? "M" : "L";
return `${command}${pad + step * index},${yFor(value)}`;
})
.join(" ");
}
export function niceExtent(values: number[]): [number, number] {
if (values.length === 0) return [0, 1];
let min = Math.min(...values);
let max = Math.max(...values);
if (min === max) {
if (min === 0) return [0, 1];
const spread = Math.abs(min) / 2;
min -= spread;
max += spread;
}
const step = niceStep((max - min) / 4);
let lo = Math.floor(min / step) * step;
const hi = Math.ceil(max / step) * step;
if (min >= 0 && lo < 0) lo = 0;
return [lo, hi];
}
function niceStep(rough: number): number {
const power = 10 ** Math.floor(Math.log10(rough));
const fraction = rough / power;
if (fraction <= 1) return power;
if (fraction <= 2) return 2 * power;
if (fraction <= 5) return 5 * power;
return 10 * power;
}
@@ -0,0 +1,5 @@
export { pointsToPath, niceExtent } from "./chart-path";
export { Sparkline } from "./sparkline";
export { BarList } from "./bar-list";
export type { BarListItem } from "./bar-list";
export { StatTile } from "./stat-tile";
@@ -0,0 +1,77 @@
import { Text } from "@mantine/core";
import { niceExtent, pointsToPath } from "./chart-path";
const VIEW_WIDTH = 240;
const PAD = 3;
interface SparklineProps {
data: number[];
series2?: number[];
height?: number;
showArea?: boolean;
label?: string;
color?: string;
}
export const Sparkline = ({
data,
series2,
height = 48,
showArea = false,
label,
color = "var(--mantine-color-anchor)",
}: SparklineProps) => {
if (data.length === 0) {
return (
<Text c="dimmed" size="sm" aria-label={label}>
&mdash;
</Text>
);
}
const extent = niceExtent(series2?.length ? [...data, ...series2] : data);
const path = pointsToPath(data, VIEW_WIDTH, height, PAD, extent);
const secondPath = series2?.length
? pointsToPath(series2, VIEW_WIDTH, height, PAD, extent)
: null;
const areaPath = showArea
? `${path} L${VIEW_WIDTH - PAD},${height - PAD} L${PAD},${height - PAD} Z`
: null;
return (
<svg
viewBox={`0 0 ${VIEW_WIDTH} ${height}`}
preserveAspectRatio="none"
width="100%"
height={height}
role="img"
aria-label={label}
style={{ display: "block" }}
>
{areaPath && <path d={areaPath} fill={color} fillOpacity={0.1} stroke="none" />}
<path
d={path}
fill="none"
stroke={color}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
vectorEffect="non-scaling-stroke"
/>
{secondPath && (
<path
d={secondPath}
fill="none"
stroke={color}
strokeWidth={2}
strokeDasharray="4 3"
strokeLinecap="round"
strokeLinejoin="round"
vectorEffect="non-scaling-stroke"
/>
)}
</svg>
);
};
export default Sparkline;
@@ -0,0 +1,53 @@
import { ReactNode } from "react";
import { Box, Card, Group, Stack, Text, UnstyledButton } from "@mantine/core";
import Sparkline from "./sparkline";
interface StatTileProps {
label: ReactNode;
value: ReactNode;
delta?: number;
spark?: number[];
caption?: ReactNode;
onClick?: () => void;
}
export const StatTile = ({ label, value, delta, spark, caption, onClick }: StatTileProps) => {
const content = (
<Stack gap={4}>
<Text size="xs" c="dimmed">
{label}
</Text>
<Group gap="xs" align="baseline" wrap="nowrap">
<Text fz={24} fw={600} lh={1.2}>
{value}
</Text>
{delta !== undefined && delta !== 0 && (
<Text size="xs" c={delta > 0 ? "green" : "red"}>
{delta > 0 ? "+" : ""}
{delta.toLocaleString()}
</Text>
)}
</Group>
{caption && <Box>{caption}</Box>}
{spark && spark.length > 0 && <Sparkline data={spark} height={28} />}
</Stack>
);
if (onClick) {
return (
<UnstyledButton onClick={onClick} w="100%">
<Card withBorder radius="md" p="md">
{content}
</Card>
</UnstyledButton>
);
}
return (
<Card withBorder radius="md" p="md">
{content}
</Card>
);
};
export default StatTile;