feat(telemetry): collections, universal server-fn coverage, beacon, client tracker
- activities indexes + client_events/client_errors/rollups/alerts collections - global function middleware via createStart; reads skipped on success - actor stash, denial dedup, control-flow error guards - api route logging, auth event overrides, beacon endpoint, web vitals - centralized admin role checks, shared skeletons, award-badges fix - extracted deriveRunState with tests
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { Outlet, redirect, createFileRoute } from "@tanstack/react-router";
|
||||
import { msg } from "@lingui/core/macro";
|
||||
import type { HeaderConfig } from "@/features/core/types/header-config";
|
||||
import { isAdminRole } from "@/features/core/utils/roles";
|
||||
|
||||
export const Route = createFileRoute("/_authed/admin")({
|
||||
component: Outlet,
|
||||
beforeLoad: ({ context }) => {
|
||||
if (!context.auth?.roles?.includes("Admin")) {
|
||||
if (!isAdminRole(context.auth?.roles)) {
|
||||
throw redirect({ to: "/" });
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
||||
import { ActivitiesTable, activityQueries } from "@/features/activities";
|
||||
import { PlayersActivityTable, playerQueries } from "@/features/players";
|
||||
import { Box, Divider, Group, Skeleton, Stack, Tabs } from "@mantine/core";
|
||||
import { ListSkeleton } from "@/components/skeletons";
|
||||
import { Tabs } from "@mantine/core";
|
||||
import { Suspense, useState } from "react";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { msg } from "@lingui/core/macro";
|
||||
@@ -25,30 +26,6 @@ export const Route = createFileRoute("/_authed/admin/activities")({
|
||||
}),
|
||||
});
|
||||
|
||||
function ActivityRowsSkeleton({ withSearch = false }: { withSearch?: boolean }) {
|
||||
return (
|
||||
<Stack gap={0}>
|
||||
{withSearch && (
|
||||
<Box px="md" pb="sm">
|
||||
<Skeleton height={42} radius="sm" />
|
||||
</Box>
|
||||
)}
|
||||
{Array.from({ length: 8 }).map((_, index) => (
|
||||
<div key={`activity-skeleton-${index}`} style={{ opacity: Math.max(1 - index * 0.1, 0.35) }}>
|
||||
<Group p="md" wrap="nowrap" w="100%" justify="space-between">
|
||||
<Stack gap={6}>
|
||||
<Skeleton height={14} width={160} radius="sm" />
|
||||
<Skeleton height={10} width={100} radius="sm" />
|
||||
</Stack>
|
||||
<Skeleton height={10} width={60} radius="sm" />
|
||||
</Group>
|
||||
<Divider />
|
||||
</div>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function Stats() {
|
||||
const [activeTab, setActiveTab] = useState<string | null>("server-functions");
|
||||
|
||||
@@ -60,13 +37,13 @@ function Stats() {
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value="server-functions">
|
||||
<Suspense fallback={<ActivityRowsSkeleton withSearch />}>
|
||||
<Suspense fallback={<ListSkeleton rows={8} withSearch withSubtitle withTrailing="text" />}>
|
||||
<ActivitiesTable />
|
||||
</Suspense>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="player-activity">
|
||||
<Suspense fallback={<ActivityRowsSkeleton />}>
|
||||
<Suspense fallback={<ListSkeleton rows={8} withSubtitle withTrailing="text" />}>
|
||||
<PlayersActivityTable />
|
||||
</Suspense>
|
||||
</Tabs.Panel>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router";
|
||||
import { tournamentQueries, useFreeAgents, useTournament } from "@/features/tournaments/queries";
|
||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||
import { StatTileSkeleton } from "@/components/skeletons";
|
||||
import { Stack, Text, Button, Alert, LoadingOverlay, Group, Skeleton } from "@mantine/core";
|
||||
import { useState } from "react";
|
||||
import useGenerateRandomTeams from "@/features/tournaments/hooks/use-generate-random-teams";
|
||||
@@ -37,10 +38,7 @@ function AssignPartnersPending() {
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<Stack gap="xs">
|
||||
<Group gap="xs" align="baseline">
|
||||
<Skeleton height={28} width={36} radius="sm" />
|
||||
<Skeleton height={14} width={110} radius="sm" />
|
||||
</Group>
|
||||
<StatTileSkeleton align="row" />
|
||||
<Skeleton height={36} w="100%" radius="sm" style={{ opacity: 0.7 }} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { tournamentQueries } from "@/features/tournaments/queries";
|
||||
import ManageTournament from "@/features/tournaments/components/manage-tournament";
|
||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||
import { Divider, Group, Skeleton, Stack } from "@mantine/core";
|
||||
import { ListSkeleton } from "@/components/skeletons";
|
||||
import { msg } from "@lingui/core/macro";
|
||||
|
||||
export const Route = createFileRoute("/_authed/admin/tournaments/$id/")({
|
||||
@@ -31,18 +31,14 @@ export const Route = createFileRoute("/_authed/admin/tournaments/$id/")({
|
||||
|
||||
function ManageTournamentPending() {
|
||||
return (
|
||||
<Stack gap={0}>
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<div key={`manage-tournament-skeleton-${index}`} style={{ opacity: Math.max(1 - index * 0.14, 0.4) }}>
|
||||
<Group p="md" wrap="nowrap" w="100%">
|
||||
<Skeleton height={20} width={20} radius="sm" />
|
||||
<Skeleton height={16} width="45%" radius="sm" />
|
||||
<Skeleton ml="auto" height={20} width={20} radius="sm" />
|
||||
</Group>
|
||||
<Divider />
|
||||
</div>
|
||||
))}
|
||||
</Stack>
|
||||
<ListSkeleton
|
||||
rows={5}
|
||||
withAvatar
|
||||
avatarSize={20}
|
||||
withTrailing
|
||||
fadeStep={0.14}
|
||||
fadeFloor={0.4}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { tournamentQueries } from "@/features/tournaments/queries";
|
||||
import ManageTeams from "@/features/teams/components/manage-teams";
|
||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||
import { Box, Divider, Group, Skeleton, Stack } from "@mantine/core";
|
||||
import { ListSkeleton } from "@/components/skeletons";
|
||||
import { Box, Skeleton, Stack } from "@mantine/core";
|
||||
import { msg } from "@lingui/core/macro";
|
||||
|
||||
export const Route = createFileRoute("/_authed/admin/tournaments/$id/teams")({
|
||||
@@ -38,21 +39,7 @@ function ManageTeamsPending() {
|
||||
<Box px="md">
|
||||
<Skeleton height={12} width={90} radius="sm" />
|
||||
</Box>
|
||||
<Stack gap={0}>
|
||||
{Array.from({ length: 8 }).map((_, index) => (
|
||||
<div key={`manage-teams-skeleton-${index}`} style={{ opacity: Math.max(1 - index * 0.1, 0.35) }}>
|
||||
<Group p="xs" wrap="nowrap" w="100%">
|
||||
<Skeleton height={40} width={40} radius="sm" />
|
||||
<Skeleton height={16} width="45%" radius="sm" />
|
||||
<Stack ml="auto" gap={6} align="flex-end">
|
||||
<Skeleton height={10} width={90} radius="sm" />
|
||||
<Skeleton height={10} width={70} radius="sm" />
|
||||
</Stack>
|
||||
</Group>
|
||||
<Divider />
|
||||
</div>
|
||||
))}
|
||||
</Stack>
|
||||
<ListSkeleton rows={8} withAvatar avatarSize={40} withTrailing="double" padding="xs" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ManageTournaments from "@/features/admin/components/manage-tournaments";
|
||||
import { tournamentQueries } from "@/features/tournaments/queries";
|
||||
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
||||
import { Divider, Group, Skeleton, Stack } from "@mantine/core";
|
||||
import { ListSkeleton } from "@/components/skeletons";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Suspense } from "react";
|
||||
import { msg } from "@lingui/core/macro";
|
||||
@@ -22,25 +22,13 @@ export const Route = createFileRoute("/_authed/admin/tournaments/")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function TournamentListSkeleton() {
|
||||
return (
|
||||
<Stack gap={0}>
|
||||
{Array.from({ length: 6 }).map((_, index) => (
|
||||
<div key={`manage-tournaments-skeleton-${index}`} style={{ opacity: Math.max(1 - index * 0.12, 0.4) }}>
|
||||
<Group p="md" wrap="nowrap" w="100%">
|
||||
<Skeleton height={16} width="55%" radius="sm" />
|
||||
<Skeleton ml="auto" height={20} width={20} radius="sm" />
|
||||
</Group>
|
||||
<Divider />
|
||||
</div>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<Suspense fallback={<TournamentListSkeleton />}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<ListSkeleton rows={6} titleWidth="55%" withTrailing fadeStep={0.12} fadeFloor={0.4} />
|
||||
}
|
||||
>
|
||||
<ManageTournaments />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -8,12 +8,10 @@ import SeedTournament from "@/features/tournaments/components/seed-tournament";
|
||||
import SetupGroupStage from "@/features/tournaments/components/setup-group-stage";
|
||||
import GroupStageView from "@/features/tournaments/components/group-stage-view";
|
||||
import { Container, Stack, Divider, Title, Box, Card, Group, Skeleton, SimpleGrid } from "@mantine/core";
|
||||
import { useMemo } from "react";
|
||||
import { BracketData } from "@/features/bracket/types";
|
||||
import { Match } from "@/features/matches/types";
|
||||
import BracketView from "@/features/bracket/components/bracket-view";
|
||||
import { SpotifyControlsBar } from "@/features/spotify/components";
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
import { useIsAdmin } from "@/features/core/hooks/use-is-admin";
|
||||
import { useTournamentRunState } from "@/features/tournaments/hooks/use-tournament-run-state";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { msg } from "@lingui/core/macro";
|
||||
|
||||
@@ -83,67 +81,10 @@ function RunTournamentPending() {
|
||||
function RouteComponent() {
|
||||
const { id } = Route.useParams();
|
||||
const { data: tournament } = useTournament(id);
|
||||
const { roles } = useAuth();
|
||||
const isAdmin = roles?.includes('Admin') || false;
|
||||
const isAdmin = useIsAdmin();
|
||||
|
||||
const hasGroupStage = useMemo(() => {
|
||||
return tournament.matches?.some((match) => match.round === -1) || false;
|
||||
}, [tournament.matches]);
|
||||
|
||||
const hasKnockout = useMemo(() => {
|
||||
return tournament.matches?.some((match) => match.round !== -1) || false;
|
||||
}, [tournament.matches]);
|
||||
|
||||
const knockoutBracketPopulated = useMemo(() => {
|
||||
return tournament.matches?.some((match) =>
|
||||
match.round === 0 && match.lid >= 0 && (match.home || match.away)
|
||||
) || false;
|
||||
}, [tournament.matches]);
|
||||
|
||||
const nextUpMatchId = useMemo(() => {
|
||||
const ready = (tournament.matches ?? [])
|
||||
.filter(
|
||||
(match) =>
|
||||
match.status === "ready" && match.home && match.away && !match.bye
|
||||
)
|
||||
.sort((a, b) => a.order - b.order);
|
||||
return ready[0]?.id;
|
||||
}, [tournament.matches]);
|
||||
|
||||
const bracket: BracketData = useMemo(() => {
|
||||
if (!tournament.matches || tournament.matches.length === 0) {
|
||||
return { winners: [], losers: [] };
|
||||
}
|
||||
|
||||
const winnersMap = new Map<number, Match[]>();
|
||||
const losersMap = new Map<number, Match[]>();
|
||||
|
||||
tournament.matches
|
||||
.filter((match) => match.round !== -1)
|
||||
.sort((a, b) => a.lid - b.lid)
|
||||
.forEach((match) => {
|
||||
if (!match.is_losers_bracket) {
|
||||
if (!winnersMap.has(match.round)) {
|
||||
winnersMap.set(match.round, []);
|
||||
}
|
||||
winnersMap.get(match.round)!.push(match);
|
||||
} else {
|
||||
if (!losersMap.has(match.round)) {
|
||||
losersMap.set(match.round, []);
|
||||
}
|
||||
losersMap.get(match.round)!.push(match);
|
||||
}
|
||||
});
|
||||
|
||||
const winners = Array.from(winnersMap.entries())
|
||||
.sort(([a], [b]) => a - b)
|
||||
.map(([, matches]) => matches);
|
||||
|
||||
const losers = Array.from(losersMap.entries())
|
||||
.sort(([a], [b]) => a - b)
|
||||
.map(([, matches]) => matches);
|
||||
return { winners, losers };
|
||||
}, [tournament.matches]);
|
||||
const { hasGroupStage, hasKnockout, knockoutBracketPopulated, nextUpMatchId, bracket } =
|
||||
useTournamentRunState(tournament.matches);
|
||||
|
||||
return (
|
||||
<Container size="md" px={0}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { tournamentQueries } from '@/features/tournaments/queries';
|
||||
import Profile from '@/features/tournaments/components/profile';
|
||||
import { isAdminRole } from '@/features/core/utils/roles';
|
||||
import { z } from "zod";
|
||||
import { prefetchServerQuery } from '@/lib/tanstack-query/utils/prefetch';
|
||||
import { Suspense } from 'react';
|
||||
@@ -20,7 +21,7 @@ export const Route = createFileRoute('/_authed/tournaments/$tournamentId')({
|
||||
header: {
|
||||
collapsed: true,
|
||||
withBackButton: true,
|
||||
settingsLink: context.auth.roles.includes("Admin") ? `/admin/tournaments/${params.tournamentId}` : undefined
|
||||
settingsLink: isAdminRole(context.auth.roles) ? `/admin/tournaments/${params.tournamentId}` : undefined
|
||||
},
|
||||
refresh: [tournamentQueries.details(params.tournamentId).queryKey],
|
||||
withPadding: false
|
||||
|
||||
Reference in New Issue
Block a user