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,107 @@
import { useState } from "react";
import { Trans, useLingui } from "@lingui/react/macro";
import {
Badge,
Box,
Button,
Code,
Collapse,
Group,
Stack,
Text,
UnstyledButton,
} from "@mantine/core";
import { useQueryClient } from "@tanstack/react-query";
import { useServerMutation } from "@/lib/tanstack-query/hooks";
import { resolveClientErrorGroup } from "../../server";
import { telemetryKeys } from "../../queries";
import type { ClientErrorGroup } from "../../types";
import { RelativeTime } from "../shared";
export const ErrorGroupRow = ({ group }: { group: ClientErrorGroup }) => {
const { t } = useLingui();
const [expanded, setExpanded] = useState(false);
const queryClient = useQueryClient();
const resolveMutation = useServerMutation({
mutationFn: (variables: { groupHash: string; resolved: boolean }) =>
resolveClientErrorGroup({ data: variables }),
successMessage: group.resolved ? t`Error group reopened` : t`Error group resolved`,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: telemetryKeys.all });
},
});
return (
<Box>
<UnstyledButton
w="100%"
p="md"
onClick={() => setExpanded((prev) => !prev)}
style={{ borderRadius: 0 }}
>
<Stack gap={4}>
<Group gap="xs" wrap="nowrap">
<Badge color={group.resolved ? "gray" : "red"} variant="light" size="sm">
{group.count.toLocaleString()}
</Badge>
<Text size="sm" fw={600} lineClamp={2} style={{ minWidth: 0 }}>
{group.sample.message}
</Text>
</Group>
<Group gap="md">
{group.sample.source && (
<Text size="xs" c="dimmed">
{group.sample.source}
</Text>
)}
<Text size="xs" c="dimmed">
<RelativeTime date={group.lastSeen} />
</Text>
{group.resolved && (
<Text size="xs" c="dimmed">
<Trans>Resolved</Trans>
</Text>
)}
</Group>
</Stack>
</UnstyledButton>
<Collapse in={expanded} transitionDuration={200}>
<Stack gap="sm" px="md" pb="md">
{group.sample.path && (
<Text size="xs" c="dimmed">
{group.sample.path}
</Text>
)}
{group.sample.stack && (
<Code
block
style={{ fontSize: "11px", maxHeight: 240, overflow: "auto" }}
>
{group.sample.stack}
</Code>
)}
<Group justify="flex-end">
<Button
size="xs"
variant="light"
color={group.resolved ? "yellow" : "green"}
loading={resolveMutation.isPending}
onClick={() =>
resolveMutation.mutate({
groupHash: group.group_hash,
resolved: !group.resolved,
})
}
>
{group.resolved ? <Trans>Reopen</Trans> : <Trans>Mark resolved</Trans>}
</Button>
</Group>
</Stack>
</Collapse>
</Box>
);
};
export default ErrorGroupRow;