feat(telemetry): session journey view replaces raw events table
- sessions listed from client_events grouped by session id (7d window) - timeline sheet merges page views, client events, errors, and server-fn activities joined by player within the session window - vitals excluded from timelines; gap markers over 5 minutes
This commit is contained in:
+22
-22
@@ -48,9 +48,9 @@ import { Route as AuthedTournamentsIdPredictionsRouteImport } from './routes/_au
|
||||
import { Route as AuthedTournamentsIdGroupsRouteImport } from './routes/_authed/tournaments/$id.groups'
|
||||
import { Route as AuthedTournamentsIdBracketRouteImport } from './routes/_authed/tournaments/$id.bracket'
|
||||
import { Route as AuthedAdminTelemetryUsageRouteImport } from './routes/_authed/admin/telemetry/usage'
|
||||
import { Route as AuthedAdminTelemetrySessionsRouteImport } from './routes/_authed/admin/telemetry/sessions'
|
||||
import { Route as AuthedAdminTelemetryPlayersRouteImport } from './routes/_authed/admin/telemetry/players'
|
||||
import { Route as AuthedAdminTelemetryFunctionsRouteImport } from './routes/_authed/admin/telemetry/functions'
|
||||
import { Route as AuthedAdminTelemetryEventsRouteImport } from './routes/_authed/admin/telemetry/events'
|
||||
import { Route as AuthedAdminTelemetryErrorsRouteImport } from './routes/_authed/admin/telemetry/errors'
|
||||
import { Route as AuthedAdminTournamentsIdIndexRouteImport } from './routes/_authed/admin/tournaments/$id/index'
|
||||
import { Route as ApiFilesCollectionRecordIdFileRouteImport } from './routes/api/files/$collection/$recordId/$file'
|
||||
@@ -263,6 +263,12 @@ const AuthedAdminTelemetryUsageRoute =
|
||||
path: '/usage',
|
||||
getParentRoute: () => AuthedAdminTelemetryRoute,
|
||||
} as any)
|
||||
const AuthedAdminTelemetrySessionsRoute =
|
||||
AuthedAdminTelemetrySessionsRouteImport.update({
|
||||
id: '/sessions',
|
||||
path: '/sessions',
|
||||
getParentRoute: () => AuthedAdminTelemetryRoute,
|
||||
} as any)
|
||||
const AuthedAdminTelemetryPlayersRoute =
|
||||
AuthedAdminTelemetryPlayersRouteImport.update({
|
||||
id: '/players',
|
||||
@@ -275,12 +281,6 @@ const AuthedAdminTelemetryFunctionsRoute =
|
||||
path: '/functions',
|
||||
getParentRoute: () => AuthedAdminTelemetryRoute,
|
||||
} as any)
|
||||
const AuthedAdminTelemetryEventsRoute =
|
||||
AuthedAdminTelemetryEventsRouteImport.update({
|
||||
id: '/events',
|
||||
path: '/events',
|
||||
getParentRoute: () => AuthedAdminTelemetryRoute,
|
||||
} as any)
|
||||
const AuthedAdminTelemetryErrorsRoute =
|
||||
AuthedAdminTelemetryErrorsRouteImport.update({
|
||||
id: '/errors',
|
||||
@@ -370,9 +370,9 @@ export interface FileRoutesByFullPath {
|
||||
'/admin/': typeof AuthedAdminIndexRoute
|
||||
'/tournaments/': typeof AuthedTournamentsIndexRoute
|
||||
'/admin/telemetry/errors': typeof AuthedAdminTelemetryErrorsRoute
|
||||
'/admin/telemetry/events': typeof AuthedAdminTelemetryEventsRoute
|
||||
'/admin/telemetry/functions': typeof AuthedAdminTelemetryFunctionsRoute
|
||||
'/admin/telemetry/players': typeof AuthedAdminTelemetryPlayersRoute
|
||||
'/admin/telemetry/sessions': typeof AuthedAdminTelemetrySessionsRoute
|
||||
'/admin/telemetry/usage': typeof AuthedAdminTelemetryUsageRoute
|
||||
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
||||
'/tournaments/$id/groups': typeof AuthedTournamentsIdGroupsRoute
|
||||
@@ -420,9 +420,9 @@ export interface FileRoutesByTo {
|
||||
'/admin': typeof AuthedAdminIndexRoute
|
||||
'/tournaments': typeof AuthedTournamentsIndexRoute
|
||||
'/admin/telemetry/errors': typeof AuthedAdminTelemetryErrorsRoute
|
||||
'/admin/telemetry/events': typeof AuthedAdminTelemetryEventsRoute
|
||||
'/admin/telemetry/functions': typeof AuthedAdminTelemetryFunctionsRoute
|
||||
'/admin/telemetry/players': typeof AuthedAdminTelemetryPlayersRoute
|
||||
'/admin/telemetry/sessions': typeof AuthedAdminTelemetrySessionsRoute
|
||||
'/admin/telemetry/usage': typeof AuthedAdminTelemetryUsageRoute
|
||||
'/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
||||
'/tournaments/$id/groups': typeof AuthedTournamentsIdGroupsRoute
|
||||
@@ -474,9 +474,9 @@ export interface FileRoutesById {
|
||||
'/_authed/admin/': typeof AuthedAdminIndexRoute
|
||||
'/_authed/tournaments/': typeof AuthedTournamentsIndexRoute
|
||||
'/_authed/admin/telemetry/errors': typeof AuthedAdminTelemetryErrorsRoute
|
||||
'/_authed/admin/telemetry/events': typeof AuthedAdminTelemetryEventsRoute
|
||||
'/_authed/admin/telemetry/functions': typeof AuthedAdminTelemetryFunctionsRoute
|
||||
'/_authed/admin/telemetry/players': typeof AuthedAdminTelemetryPlayersRoute
|
||||
'/_authed/admin/telemetry/sessions': typeof AuthedAdminTelemetrySessionsRoute
|
||||
'/_authed/admin/telemetry/usage': typeof AuthedAdminTelemetryUsageRoute
|
||||
'/_authed/tournaments/$id/bracket': typeof AuthedTournamentsIdBracketRoute
|
||||
'/_authed/tournaments/$id/groups': typeof AuthedTournamentsIdGroupsRoute
|
||||
@@ -528,9 +528,9 @@ export interface FileRouteTypes {
|
||||
| '/admin/'
|
||||
| '/tournaments/'
|
||||
| '/admin/telemetry/errors'
|
||||
| '/admin/telemetry/events'
|
||||
| '/admin/telemetry/functions'
|
||||
| '/admin/telemetry/players'
|
||||
| '/admin/telemetry/sessions'
|
||||
| '/admin/telemetry/usage'
|
||||
| '/tournaments/$id/bracket'
|
||||
| '/tournaments/$id/groups'
|
||||
@@ -578,9 +578,9 @@ export interface FileRouteTypes {
|
||||
| '/admin'
|
||||
| '/tournaments'
|
||||
| '/admin/telemetry/errors'
|
||||
| '/admin/telemetry/events'
|
||||
| '/admin/telemetry/functions'
|
||||
| '/admin/telemetry/players'
|
||||
| '/admin/telemetry/sessions'
|
||||
| '/admin/telemetry/usage'
|
||||
| '/tournaments/$id/bracket'
|
||||
| '/tournaments/$id/groups'
|
||||
@@ -631,9 +631,9 @@ export interface FileRouteTypes {
|
||||
| '/_authed/admin/'
|
||||
| '/_authed/tournaments/'
|
||||
| '/_authed/admin/telemetry/errors'
|
||||
| '/_authed/admin/telemetry/events'
|
||||
| '/_authed/admin/telemetry/functions'
|
||||
| '/_authed/admin/telemetry/players'
|
||||
| '/_authed/admin/telemetry/sessions'
|
||||
| '/_authed/admin/telemetry/usage'
|
||||
| '/_authed/tournaments/$id/bracket'
|
||||
| '/_authed/tournaments/$id/groups'
|
||||
@@ -948,6 +948,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthedAdminTelemetryUsageRouteImport
|
||||
parentRoute: typeof AuthedAdminTelemetryRoute
|
||||
}
|
||||
'/_authed/admin/telemetry/sessions': {
|
||||
id: '/_authed/admin/telemetry/sessions'
|
||||
path: '/sessions'
|
||||
fullPath: '/admin/telemetry/sessions'
|
||||
preLoaderRoute: typeof AuthedAdminTelemetrySessionsRouteImport
|
||||
parentRoute: typeof AuthedAdminTelemetryRoute
|
||||
}
|
||||
'/_authed/admin/telemetry/players': {
|
||||
id: '/_authed/admin/telemetry/players'
|
||||
path: '/players'
|
||||
@@ -962,13 +969,6 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthedAdminTelemetryFunctionsRouteImport
|
||||
parentRoute: typeof AuthedAdminTelemetryRoute
|
||||
}
|
||||
'/_authed/admin/telemetry/events': {
|
||||
id: '/_authed/admin/telemetry/events'
|
||||
path: '/events'
|
||||
fullPath: '/admin/telemetry/events'
|
||||
preLoaderRoute: typeof AuthedAdminTelemetryEventsRouteImport
|
||||
parentRoute: typeof AuthedAdminTelemetryRoute
|
||||
}
|
||||
'/_authed/admin/telemetry/errors': {
|
||||
id: '/_authed/admin/telemetry/errors'
|
||||
path: '/errors'
|
||||
@@ -1037,18 +1037,18 @@ declare module '@tanstack/react-router' {
|
||||
|
||||
interface AuthedAdminTelemetryRouteChildren {
|
||||
AuthedAdminTelemetryErrorsRoute: typeof AuthedAdminTelemetryErrorsRoute
|
||||
AuthedAdminTelemetryEventsRoute: typeof AuthedAdminTelemetryEventsRoute
|
||||
AuthedAdminTelemetryFunctionsRoute: typeof AuthedAdminTelemetryFunctionsRoute
|
||||
AuthedAdminTelemetryPlayersRoute: typeof AuthedAdminTelemetryPlayersRoute
|
||||
AuthedAdminTelemetrySessionsRoute: typeof AuthedAdminTelemetrySessionsRoute
|
||||
AuthedAdminTelemetryUsageRoute: typeof AuthedAdminTelemetryUsageRoute
|
||||
AuthedAdminTelemetryIndexRoute: typeof AuthedAdminTelemetryIndexRoute
|
||||
}
|
||||
|
||||
const AuthedAdminTelemetryRouteChildren: AuthedAdminTelemetryRouteChildren = {
|
||||
AuthedAdminTelemetryErrorsRoute: AuthedAdminTelemetryErrorsRoute,
|
||||
AuthedAdminTelemetryEventsRoute: AuthedAdminTelemetryEventsRoute,
|
||||
AuthedAdminTelemetryFunctionsRoute: AuthedAdminTelemetryFunctionsRoute,
|
||||
AuthedAdminTelemetryPlayersRoute: AuthedAdminTelemetryPlayersRoute,
|
||||
AuthedAdminTelemetrySessionsRoute: AuthedAdminTelemetrySessionsRoute,
|
||||
AuthedAdminTelemetryUsageRoute: AuthedAdminTelemetryUsageRoute,
|
||||
AuthedAdminTelemetryIndexRoute: AuthedAdminTelemetryIndexRoute,
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ const TAB_ROUTES = {
|
||||
usage: "/admin/telemetry/usage",
|
||||
players: "/admin/telemetry/players",
|
||||
functions: "/admin/telemetry/functions",
|
||||
events: "/admin/telemetry/events",
|
||||
sessions: "/admin/telemetry/sessions",
|
||||
errors: "/admin/telemetry/errors",
|
||||
} as const;
|
||||
|
||||
@@ -50,8 +50,8 @@ function TelemetryLayout() {
|
||||
<Tabs.Tab value="functions">
|
||||
<Trans>Functions</Trans>
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="events">
|
||||
<Trans>Events</Trans>
|
||||
<Tabs.Tab value="sessions">
|
||||
<Trans>Sessions</Trans>
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="errors">
|
||||
<Trans>Errors</Trans>
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Suspense } from "react";
|
||||
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
||||
import { telemetryQueries, defaultClientEventParams, ClientEventsTable } from "@/features/telemetry";
|
||||
import { playerQueries } from "@/features/players";
|
||||
import { ListSkeleton } from "@/components/skeletons";
|
||||
|
||||
export const Route = createFileRoute("/_authed/admin/telemetry/events")({
|
||||
component: EventsTab,
|
||||
beforeLoad: ({ context }) => {
|
||||
const queryClient = context.queryClient;
|
||||
prefetchServerQuery(queryClient, telemetryQueries.events(defaultClientEventParams));
|
||||
prefetchServerQuery(queryClient, playerQueries.list());
|
||||
},
|
||||
loader: () => ({
|
||||
withPadding: false,
|
||||
fullWidth: true,
|
||||
refresh: [telemetryQueries.events(defaultClientEventParams).queryKey],
|
||||
}),
|
||||
});
|
||||
|
||||
function EventsTab() {
|
||||
return (
|
||||
<Suspense fallback={<ListSkeleton rows={8} withSearch withSubtitle withTrailing="text" />}>
|
||||
<ClientEventsTable />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Suspense } from "react";
|
||||
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
||||
import { telemetryQueries, SessionsList, defaultSessionParams } from "@/features/telemetry";
|
||||
import { ListSkeleton } from "@/components/skeletons";
|
||||
|
||||
export const Route = createFileRoute("/_authed/admin/telemetry/sessions")({
|
||||
component: SessionsTab,
|
||||
beforeLoad: ({ context }) => {
|
||||
prefetchServerQuery(context.queryClient, telemetryQueries.sessions(defaultSessionParams));
|
||||
},
|
||||
loader: () => ({
|
||||
withPadding: false,
|
||||
fullWidth: true,
|
||||
refresh: [telemetryQueries.sessions(defaultSessionParams).queryKey],
|
||||
}),
|
||||
});
|
||||
|
||||
function SessionsTab() {
|
||||
return (
|
||||
<Suspense fallback={<ListSkeleton rows={8} withSubtitle withTrailing="text" />}>
|
||||
<SessionsList />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { Trans, Plural, useLingui } from "@lingui/react/macro";
|
||||
import { Code, Container, Stack } from "@mantine/core";
|
||||
import { usePlayers } from "@/features/players/queries";
|
||||
import { useSheet } from "@/hooks/use-sheet";
|
||||
import { useClientEvents } from "../queries";
|
||||
import type { ClientEvent, ClientEventSearchParams } from "../types";
|
||||
import {
|
||||
DetailSheet,
|
||||
PaginatedList,
|
||||
RelativeTime,
|
||||
SearchFilterBar,
|
||||
TelemetryListItem,
|
||||
} from "./shared";
|
||||
|
||||
export const defaultClientEventParams: ClientEventSearchParams = {
|
||||
page: 1,
|
||||
perPage: 50,
|
||||
sortBy: "-created",
|
||||
};
|
||||
|
||||
export const ClientEventsTable = () => {
|
||||
const { t, i18n } = useLingui();
|
||||
const [page, setPage] = useState(1);
|
||||
const [search, setSearch] = useState("");
|
||||
const [playerFilter, setPlayerFilter] = useState<string | null>(null);
|
||||
const [selectedEvent, setSelectedEvent] = useState<ClientEvent | null>(null);
|
||||
const { isOpen: detailsOpened, open: openDetails, close: closeDetails } = useSheet();
|
||||
|
||||
const { data: players } = usePlayers();
|
||||
|
||||
const searchParams: ClientEventSearchParams = useMemo(
|
||||
() => ({
|
||||
page,
|
||||
perPage: 50,
|
||||
name: search || undefined,
|
||||
player: playerFilter || undefined,
|
||||
sortBy: "-created",
|
||||
}),
|
||||
[page, search, playerFilter]
|
||||
);
|
||||
|
||||
const { data: result } = useClientEvents(searchParams);
|
||||
|
||||
const playerName = (event: ClientEvent) =>
|
||||
typeof event.player === "object" && event.player
|
||||
? `${event.player.first_name} ${event.player.last_name}`
|
||||
: t`Anonymous`;
|
||||
|
||||
const handleEventClick = (event: ClientEvent) => {
|
||||
setSelectedEvent(event);
|
||||
openDetails();
|
||||
};
|
||||
|
||||
const handleCloseDetails = () => {
|
||||
setSelectedEvent(null);
|
||||
closeDetails();
|
||||
};
|
||||
|
||||
return (
|
||||
<Container size="100%" px={0}>
|
||||
<Stack gap="xs">
|
||||
<SearchFilterBar
|
||||
searchValue={search}
|
||||
onSearchChange={(value) => {
|
||||
setSearch(value);
|
||||
setPage(1);
|
||||
}}
|
||||
searchPlaceholder={t`Event name`}
|
||||
filters={[
|
||||
{
|
||||
placeholder: t`Player`,
|
||||
value: playerFilter,
|
||||
onChange: (value) => {
|
||||
setPlayerFilter(value);
|
||||
setPage(1);
|
||||
},
|
||||
data: players.map((player) => ({
|
||||
value: player.id,
|
||||
label: `${player.first_name} ${player.last_name}`,
|
||||
})),
|
||||
searchable: true,
|
||||
},
|
||||
]}
|
||||
summary={
|
||||
<Plural value={result.totalItems} one="# total event" other="# total events" />
|
||||
}
|
||||
/>
|
||||
|
||||
<PaginatedList
|
||||
items={result.items}
|
||||
keyOf={(event) => event.id}
|
||||
empty={t`No Events Found`}
|
||||
page={page}
|
||||
totalPages={result.totalPages}
|
||||
onPageChange={setPage}
|
||||
renderItem={(event) => (
|
||||
<TelemetryListItem
|
||||
title={event.name}
|
||||
meta={[
|
||||
playerName(event),
|
||||
event.route_id || event.path || "",
|
||||
<RelativeTime date={event.created} />,
|
||||
]}
|
||||
onClick={() => handleEventClick(event)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<DetailSheet
|
||||
title={t`Event Details`}
|
||||
opened={detailsOpened}
|
||||
onClose={handleCloseDetails}
|
||||
fields={
|
||||
selectedEvent
|
||||
? [
|
||||
{ label: <Trans>Name</Trans>, value: selectedEvent.name },
|
||||
{ label: <Trans>Player</Trans>, value: playerName(selectedEvent) },
|
||||
...(selectedEvent.path
|
||||
? [{ label: <Trans>Path</Trans>, value: selectedEvent.path }]
|
||||
: []),
|
||||
...(selectedEvent.route_id
|
||||
? [{ label: <Trans>Route</Trans>, value: selectedEvent.route_id }]
|
||||
: []),
|
||||
...(selectedEvent.session_id
|
||||
? [{ label: <Trans>Session</Trans>, value: selectedEvent.session_id }]
|
||||
: []),
|
||||
...(selectedEvent.value !== undefined && selectedEvent.value !== null
|
||||
? [{ label: <Trans>Value</Trans>, value: selectedEvent.value }]
|
||||
: []),
|
||||
...(selectedEvent.props
|
||||
? [
|
||||
{
|
||||
label: <Trans>Props</Trans>,
|
||||
value: (
|
||||
<Code block style={{ fontSize: "11px" }}>
|
||||
{JSON.stringify(selectedEvent.props, null, 2)}
|
||||
</Code>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(selectedEvent.user_agent
|
||||
? [{ label: <Trans>User Agent</Trans>, value: selectedEvent.user_agent }]
|
||||
: []),
|
||||
{
|
||||
label: <Trans>Created</Trans>,
|
||||
value: new Date(selectedEvent.created).toLocaleString(i18n.locale),
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Fragment } from "react";
|
||||
import { Box, Group, Stack, Text } from "@mantine/core";
|
||||
import { Plural, useLingui } from "@lingui/react/macro";
|
||||
import { useSessionTimeline } from "../../queries";
|
||||
import type { TimelineEntry } from "../../types";
|
||||
|
||||
const GAP_MARKER_MS = 5 * 60 * 1000;
|
||||
|
||||
const parseTs = (ts: string) => Date.parse(ts.replace(" ", "T"));
|
||||
|
||||
const dotColor = (entry: TimelineEntry): string => {
|
||||
if (entry.kind === "error" || entry.success === false) return "var(--mantine-color-red-6)";
|
||||
if (entry.kind === "server_fn") return "var(--mantine-color-green-6)";
|
||||
return "var(--mantine-color-dimmed)";
|
||||
};
|
||||
|
||||
const Dot = ({ color }: { color: string }) => (
|
||||
<Box
|
||||
w={6}
|
||||
h={6}
|
||||
style={{ borderRadius: "50%", backgroundColor: color, flexShrink: 0 }}
|
||||
/>
|
||||
);
|
||||
|
||||
export const SessionTimelineView = ({ sessionId }: { sessionId: string }) => {
|
||||
const { i18n } = useLingui();
|
||||
const { data } = useSessionTimeline(sessionId);
|
||||
|
||||
const clock = (ts: string) =>
|
||||
i18n.date(new Date(parseTs(ts)), { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||
|
||||
return (
|
||||
<Stack gap={0}>
|
||||
<Group gap="md" pb="sm">
|
||||
<Text size="xs" c="dimmed">
|
||||
{i18n.date(new Date(parseTs(data.session.started)), {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
<Plural value={data.session.pageViews} one="# page" other="# pages" />
|
||||
</Text>
|
||||
{data.session.errors > 0 && (
|
||||
<Text size="xs" c="red">
|
||||
<Plural value={data.session.errors} one="# error" other="# errors" />
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
{data.entries.map((entry, index) => {
|
||||
const prev = data.entries[index - 1];
|
||||
const gapMs = prev ? parseTs(entry.ts) - parseTs(prev.ts) : 0;
|
||||
return (
|
||||
<Fragment key={`${entry.ts}-${index}`}>
|
||||
{gapMs > GAP_MARKER_MS && (
|
||||
<Text size="xs" c="dimmed" py={4} pl={46}>
|
||||
+{Math.round(gapMs / 60000)}m
|
||||
</Text>
|
||||
)}
|
||||
<Group gap="xs" py={5} wrap="nowrap" align="center">
|
||||
<Text
|
||||
size="xs"
|
||||
c="dimmed"
|
||||
w={64}
|
||||
style={{ flexShrink: 0, fontVariantNumeric: "tabular-nums" }}
|
||||
>
|
||||
{clock(entry.ts)}
|
||||
</Text>
|
||||
<Dot color={dotColor(entry)} />
|
||||
<Text
|
||||
size="sm"
|
||||
lineClamp={1}
|
||||
c={entry.kind === "error" ? "red" : undefined}
|
||||
style={{ minWidth: 0, flex: 1 }}
|
||||
>
|
||||
{entry.label}
|
||||
</Text>
|
||||
{entry.kind === "server_fn" && entry.success === false && (
|
||||
<Text size="xs" c="red" style={{ flexShrink: 0 }}>
|
||||
✗
|
||||
</Text>
|
||||
)}
|
||||
{entry.kind === "server_fn" && typeof entry.duration === "number" && entry.duration > 0 && (
|
||||
<Text size="xs" c="dimmed" style={{ flexShrink: 0, fontVariantNumeric: "tabular-nums" }}>
|
||||
{entry.duration}ms
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SessionTimelineView;
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Suspense, useState } from "react";
|
||||
import { Plural, Trans, useLingui } from "@lingui/react/macro";
|
||||
import { Text } from "@mantine/core";
|
||||
import Sheet from "@/components/sheet/sheet";
|
||||
import { ListSkeleton } from "@/components/skeletons";
|
||||
import { useSessions } from "../../queries";
|
||||
import type { SessionSummary } from "../../types";
|
||||
import { PaginatedList, RelativeTime, TelemetryListItem } from "../shared";
|
||||
import SessionTimelineView from "./session-timeline";
|
||||
|
||||
export const defaultSessionParams = { days: 7, page: 1 } as const;
|
||||
|
||||
const playerName = (player?: SessionSummary["player"]): string | null => {
|
||||
if (!player) return null;
|
||||
return [player.first_name, player.last_name].filter(Boolean).join(" ") || null;
|
||||
};
|
||||
|
||||
const durationLabel = (session: SessionSummary): string => {
|
||||
const ms =
|
||||
Date.parse(session.lastSeen.replace(" ", "T")) -
|
||||
Date.parse(session.started.replace(" ", "T"));
|
||||
const minutes = Math.round(ms / 60000);
|
||||
return minutes < 1 ? "<1m" : `${minutes}m`;
|
||||
};
|
||||
|
||||
export const SessionsList = () => {
|
||||
const { t } = useLingui();
|
||||
const [page, setPage] = useState(1);
|
||||
const [selected, setSelected] = useState<SessionSummary | null>(null);
|
||||
|
||||
const { data } = useSessions({ days: 7, page });
|
||||
|
||||
return (
|
||||
<>
|
||||
<PaginatedList
|
||||
items={data.items}
|
||||
keyOf={(session) => session.session_id}
|
||||
empty={t`No sessions yet`}
|
||||
page={data.page}
|
||||
totalPages={data.totalPages}
|
||||
onPageChange={setPage}
|
||||
renderItem={(session) => (
|
||||
<TelemetryListItem
|
||||
title={playerName(session.player) ?? t`Anonymous`}
|
||||
meta={[
|
||||
<RelativeTime key="time" date={session.lastSeen} />,
|
||||
<Plural key="pages" value={session.pageViews} one="# page" other="# pages" />,
|
||||
durationLabel(session),
|
||||
]}
|
||||
error={
|
||||
session.errors > 0 ? (
|
||||
<Plural value={session.errors} one="# error" other="# errors" />
|
||||
) : undefined
|
||||
}
|
||||
onClick={() => setSelected(session)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Sheet
|
||||
title={selected ? (playerName(selected.player) ?? t`Anonymous`) : ""}
|
||||
opened={selected !== null}
|
||||
onChange={() => setSelected(null)}
|
||||
>
|
||||
{selected && (
|
||||
<Suspense fallback={<ListSkeleton rows={8} />}>
|
||||
<SessionTimelineView sessionId={selected.session_id} />
|
||||
</Suspense>
|
||||
)}
|
||||
</Sheet>
|
||||
|
||||
{data.totalItems > 0 && (
|
||||
<Text size="xs" c="dimmed" px="md" py="sm">
|
||||
<Trans>Last 7 days</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SessionsList;
|
||||
@@ -3,6 +3,7 @@ export * from "./queries";
|
||||
export * from "./range";
|
||||
export * from "./components/charts";
|
||||
export * from "./components/shared";
|
||||
export { ClientEventsTable, defaultClientEventParams } from "./components/client-events-table";
|
||||
export { SessionsList, defaultSessionParams } from "./components/sessions/sessions-list";
|
||||
export { SessionTimelineView } from "./components/sessions/session-timeline";
|
||||
export { ErrorBrowser } from "./components/error-browser";
|
||||
export { UsageScreen, USAGE_METRICS } from "./components/usage-screen";
|
||||
|
||||
@@ -2,7 +2,9 @@ import { useServerSuspenseQuery } from "@/lib/tanstack-query/hooks";
|
||||
import {
|
||||
getClientErrorGroups,
|
||||
getRollupRange,
|
||||
getSessionTimeline,
|
||||
getTelemetryRuntimeStatus,
|
||||
listSessions,
|
||||
listTelemetryAlerts,
|
||||
searchClientErrors,
|
||||
searchClientEvents,
|
||||
@@ -30,6 +32,10 @@ export const telemetryKeys = {
|
||||
errorGroups: (params: ErrorGroupParams) => ["telemetry", "errorGroups", params] as const,
|
||||
rollups: (params: RollupRangeParams) => ["telemetry", "rollups", params] as const,
|
||||
alerts: (page: number) => ["telemetry", "alerts", page] as const,
|
||||
sessions: (params: { days?: number; page?: number }) =>
|
||||
["telemetry", "sessions", params] as const,
|
||||
sessionTimeline: (sessionId: string) =>
|
||||
["telemetry", "sessionTimeline", sessionId] as const,
|
||||
runtimeStatus: ["telemetry", "runtimeStatus"] as const,
|
||||
};
|
||||
|
||||
@@ -54,6 +60,14 @@ export const telemetryQueries = {
|
||||
queryKey: telemetryKeys.alerts(page),
|
||||
queryFn: () => listTelemetryAlerts({ data: { page } }),
|
||||
}),
|
||||
sessions: (params: { days?: number; page?: number } = {}) => ({
|
||||
queryKey: telemetryKeys.sessions(params),
|
||||
queryFn: () => listSessions({ data: params }),
|
||||
}),
|
||||
sessionTimeline: (sessionId: string) => ({
|
||||
queryKey: telemetryKeys.sessionTimeline(sessionId),
|
||||
queryFn: () => getSessionTimeline({ data: { sessionId } }),
|
||||
}),
|
||||
runtimeStatus: () => ({
|
||||
queryKey: telemetryKeys.runtimeStatus,
|
||||
queryFn: () => getTelemetryRuntimeStatus(),
|
||||
@@ -72,5 +86,11 @@ export const useClientErrorGroups = (params: ErrorGroupParams) =>
|
||||
export const useRollupRange = (params: RollupRangeParams) =>
|
||||
useServerSuspenseQuery(telemetryQueries.rollups(params));
|
||||
|
||||
export const useSessions = (params: { days?: number; page?: number } = {}) =>
|
||||
useServerSuspenseQuery(telemetryQueries.sessions(params));
|
||||
|
||||
export const useSessionTimeline = (sessionId: string) =>
|
||||
useServerSuspenseQuery(telemetryQueries.sessionTimeline(sessionId));
|
||||
|
||||
export const useTelemetryRuntimeStatus = () =>
|
||||
useServerSuspenseQuery(telemetryQueries.runtimeStatus());
|
||||
|
||||
@@ -15,9 +15,12 @@ import type {
|
||||
ClientErrorGroup,
|
||||
ClientEvent,
|
||||
RollupRow,
|
||||
SessionSummary,
|
||||
SessionTimeline,
|
||||
TelemetryAlert,
|
||||
TelemetryListResult,
|
||||
TelemetryRuntimeStatus,
|
||||
TimelineEntry,
|
||||
} from "./types";
|
||||
|
||||
const clientEventSearchSchema = z.object({
|
||||
@@ -155,6 +158,214 @@ export const listTelemetryAlerts = createServerFn()
|
||||
)
|
||||
);
|
||||
|
||||
const pbTime = (date: Date): string => date.toISOString().replace("T", " ");
|
||||
|
||||
const playerInfoById = async (ids: Set<string>) => {
|
||||
if (ids.size === 0) return new Map<string, { id: string; first_name?: string; last_name?: string }>();
|
||||
const players = await pbAdmin.listPlayers();
|
||||
return new Map(
|
||||
players
|
||||
.filter((player) => ids.has(player.id))
|
||||
.map((player) => [
|
||||
player.id,
|
||||
{ id: player.id, first_name: player.first_name, last_name: player.last_name },
|
||||
])
|
||||
);
|
||||
};
|
||||
|
||||
export const listSessions = createServerFn()
|
||||
.validator(
|
||||
z.object({
|
||||
days: z.number().min(1).max(90).optional(),
|
||||
page: z.number().optional(),
|
||||
perPage: z.number().max(100).optional(),
|
||||
})
|
||||
)
|
||||
.middleware([superTokensAdminFunctionMiddleware])
|
||||
.handler(async ({ data }) =>
|
||||
toServerResult<TelemetryListResult<SessionSummary>>(async () => {
|
||||
const { days = 7, page = 1, perPage = 30 } = data;
|
||||
const from = pbTime(new Date(Date.now() - days * 86400000));
|
||||
|
||||
const sessions = new Map<
|
||||
string,
|
||||
{ playerId?: string; started: string; lastSeen: string; pageViews: number; events: number; errors: number; user_agent?: string }
|
||||
>();
|
||||
|
||||
await pbAdmin.pageCollection<{
|
||||
session_id?: string;
|
||||
player?: string;
|
||||
name: string;
|
||||
user_agent?: string;
|
||||
created: string;
|
||||
}>(
|
||||
"client_events",
|
||||
{ filter: `created >= '${from}'`, fields: "session_id,player,name,user_agent,created" },
|
||||
(rows) => {
|
||||
for (const row of rows) {
|
||||
if (!row.session_id) continue;
|
||||
const session = sessions.get(row.session_id) ?? {
|
||||
playerId: row.player,
|
||||
started: row.created,
|
||||
lastSeen: row.created,
|
||||
pageViews: 0,
|
||||
events: 0,
|
||||
errors: 0,
|
||||
user_agent: row.user_agent,
|
||||
};
|
||||
session.playerId ||= row.player;
|
||||
if (row.created < session.started) session.started = row.created;
|
||||
if (row.created > session.lastSeen) session.lastSeen = row.created;
|
||||
if (row.name === "page_view") session.pageViews++;
|
||||
else if (!row.name.startsWith("vital.")) session.events++;
|
||||
sessions.set(row.session_id, session);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await pbAdmin.pageCollection<{ session_id?: string; created: string }>(
|
||||
"client_errors",
|
||||
{ filter: `created >= '${from}'`, fields: "session_id,created" },
|
||||
(rows) => {
|
||||
for (const row of rows) {
|
||||
if (!row.session_id) continue;
|
||||
const session = sessions.get(row.session_id);
|
||||
if (session) session.errors++;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const playerIds = new Set(
|
||||
[...sessions.values()].map((s) => s.playerId).filter((id): id is string => !!id)
|
||||
);
|
||||
const players = await playerInfoById(playerIds);
|
||||
|
||||
const all: SessionSummary[] = [...sessions.entries()]
|
||||
.map(([session_id, s]) => ({
|
||||
session_id,
|
||||
player: s.playerId ? players.get(s.playerId) : undefined,
|
||||
started: s.started,
|
||||
lastSeen: s.lastSeen,
|
||||
pageViews: s.pageViews,
|
||||
events: s.events,
|
||||
errors: s.errors,
|
||||
user_agent: s.user_agent,
|
||||
}))
|
||||
.sort((a, b) => (a.lastSeen < b.lastSeen ? 1 : -1));
|
||||
|
||||
const start = (page - 1) * perPage;
|
||||
return {
|
||||
items: all.slice(start, start + perPage),
|
||||
page,
|
||||
perPage,
|
||||
totalPages: Math.max(1, Math.ceil(all.length / perPage)),
|
||||
totalItems: all.length,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const SESSION_WINDOW_PAD_MS = 30_000;
|
||||
const MAX_TIMELINE_ENTRIES = 500;
|
||||
|
||||
export const getSessionTimeline = createServerFn()
|
||||
.validator(z.object({ sessionId: z.string().min(8).max(64) }))
|
||||
.middleware([superTokensAdminFunctionMiddleware])
|
||||
.handler(async ({ data }) =>
|
||||
toServerResult<SessionTimeline>(async () => {
|
||||
const events = await pbAdmin.searchClientEvents({
|
||||
sessionId: data.sessionId,
|
||||
perPage: 200,
|
||||
sortBy: "created",
|
||||
});
|
||||
const errors = await pbAdmin.searchClientErrors({
|
||||
sessionId: data.sessionId,
|
||||
perPage: 200,
|
||||
sortBy: "created",
|
||||
});
|
||||
|
||||
const sessionErrors = errors.items;
|
||||
if (events.items.length === 0 && sessionErrors.length === 0) {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
|
||||
const timestamps = [
|
||||
...events.items.map((e) => e.created),
|
||||
...sessionErrors.map((e) => e.created),
|
||||
].sort();
|
||||
const started = timestamps[0];
|
||||
const lastSeen = timestamps[timestamps.length - 1];
|
||||
|
||||
const playerId = events.items.find((e) => typeof e.player === "string" && e.player)
|
||||
?.player as string | undefined;
|
||||
|
||||
const entries: TimelineEntry[] = [];
|
||||
|
||||
for (const event of events.items) {
|
||||
if (event.name.startsWith("vital.")) continue;
|
||||
entries.push({
|
||||
ts: event.created,
|
||||
kind: event.name === "page_view" ? "page_view" : "event",
|
||||
label: event.name === "page_view" ? event.path || event.route_id || "?" : event.name,
|
||||
detail: event.name === "page_view" ? undefined : event.path,
|
||||
});
|
||||
}
|
||||
|
||||
for (const error of sessionErrors) {
|
||||
entries.push({
|
||||
ts: error.created,
|
||||
kind: "error",
|
||||
label: error.message,
|
||||
detail: error.path,
|
||||
success: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Server-fn activities have no session id; join by player inside the
|
||||
// session's padded time window.
|
||||
if (playerId) {
|
||||
const windowFrom = pbTime(new Date(Date.parse(started.replace(" ", "T")) - SESSION_WINDOW_PAD_MS));
|
||||
const windowTo = pbTime(new Date(Date.parse(lastSeen.replace(" ", "T")) + SESSION_WINDOW_PAD_MS));
|
||||
const activities = await pbAdmin.searchActivities({
|
||||
player: playerId,
|
||||
from: windowFrom,
|
||||
to: windowTo,
|
||||
perPage: 200,
|
||||
sortBy: "created",
|
||||
});
|
||||
for (const activity of activities.items) {
|
||||
entries.push({
|
||||
ts: activity.created,
|
||||
kind: "server_fn",
|
||||
label: activity.name,
|
||||
detail: activity.error,
|
||||
success: activity.success,
|
||||
duration: activity.duration,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
entries.sort((a, b) => (a.ts < b.ts ? -1 : 1));
|
||||
|
||||
const players = await playerInfoById(new Set(playerId ? [playerId] : []));
|
||||
|
||||
return {
|
||||
session: {
|
||||
session_id: data.sessionId,
|
||||
player: playerId ? players.get(playerId) : undefined,
|
||||
started,
|
||||
lastSeen,
|
||||
pageViews: events.items.filter((e) => e.name === "page_view").length,
|
||||
events: events.items.filter(
|
||||
(e) => e.name !== "page_view" && !e.name.startsWith("vital.")
|
||||
).length,
|
||||
errors: sessionErrors.length,
|
||||
user_agent: events.items[0]?.user_agent,
|
||||
},
|
||||
entries: entries.slice(0, MAX_TIMELINE_ENTRIES),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
export const getTelemetryRuntimeStatus = createServerFn()
|
||||
.middleware([superTokensAdminFunctionMiddleware])
|
||||
.handler(async () =>
|
||||
|
||||
@@ -22,6 +22,33 @@ export interface ClientErrorGroup {
|
||||
};
|
||||
}
|
||||
|
||||
export interface SessionSummary {
|
||||
session_id: string;
|
||||
player?: { id: string; first_name?: string; last_name?: string };
|
||||
started: string;
|
||||
lastSeen: string;
|
||||
pageViews: number;
|
||||
events: number;
|
||||
errors: number;
|
||||
user_agent?: string;
|
||||
}
|
||||
|
||||
export type TimelineKind = "page_view" | "event" | "error" | "server_fn";
|
||||
|
||||
export interface TimelineEntry {
|
||||
ts: string;
|
||||
kind: TimelineKind;
|
||||
label: string;
|
||||
detail?: string;
|
||||
success?: boolean;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export interface SessionTimeline {
|
||||
session: SessionSummary;
|
||||
entries: TimelineEntry[];
|
||||
}
|
||||
|
||||
export interface TelemetryRuntimeStatus {
|
||||
health: {
|
||||
status: "ok" | "degraded";
|
||||
|
||||
@@ -40,6 +40,8 @@ export interface ActivitySearchParams {
|
||||
name?: string;
|
||||
player?: string;
|
||||
success?: boolean;
|
||||
from?: string;
|
||||
to?: string;
|
||||
sortBy?: string;
|
||||
}
|
||||
|
||||
@@ -57,6 +59,8 @@ export function createActivitiesService(pb: PocketBase) {
|
||||
name,
|
||||
player,
|
||||
success,
|
||||
from,
|
||||
to,
|
||||
sortBy = "-created"
|
||||
} = params;
|
||||
|
||||
@@ -74,6 +78,14 @@ export function createActivitiesService(pb: PocketBase) {
|
||||
filters.push(pbFilter(pb, "success = {:success}", { success }));
|
||||
}
|
||||
|
||||
if (from) {
|
||||
filters.push(pbFilter(pb, "created >= {:from}", { from }));
|
||||
}
|
||||
|
||||
if (to) {
|
||||
filters.push(pbFilter(pb, "created < {:to}", { to }));
|
||||
}
|
||||
|
||||
const filterString = filters.length > 0 ? filters.join(" && ") : "";
|
||||
|
||||
const result = await pb.collection("activities").getList<Activity>(page, perPage, {
|
||||
|
||||
@@ -125,6 +125,7 @@ export interface ClientErrorSearchParams {
|
||||
groupHash?: string;
|
||||
resolved?: boolean;
|
||||
player?: string;
|
||||
sessionId?: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
sortBy?: string;
|
||||
@@ -175,12 +176,13 @@ export function createTelemetryService(pb: PocketBase) {
|
||||
async searchClientErrors(
|
||||
params: ClientErrorSearchParams = {}
|
||||
): Promise<TelemetryListResult<ClientErrorRecord>> {
|
||||
const { page = 1, perPage = 100, groupHash, resolved, player, from, to, sortBy = "-created" } = params;
|
||||
const { page = 1, perPage = 100, groupHash, resolved, player, sessionId, from, to, sortBy = "-created" } = params;
|
||||
|
||||
const filters: string[] = [];
|
||||
if (groupHash) filters.push(pbFilter(pb, "group_hash = {:groupHash}", { groupHash }));
|
||||
if (resolved !== undefined) filters.push(pbFilter(pb, "resolved = {:resolved}", { resolved }));
|
||||
if (player) filters.push(pbFilter(pb, "player = {:player}", { player }));
|
||||
if (sessionId) filters.push(pbFilter(pb, "session_id = {:sessionId}", { sessionId }));
|
||||
if (from) filters.push(pbFilter(pb, "created >= {:from}", { from }));
|
||||
if (to) filters.push(pbFilter(pb, "created < {:to}", { to }));
|
||||
|
||||
|
||||
+25
-47
@@ -35,6 +35,20 @@ msgstr "(Aktiv)"
|
||||
msgid "{0, plural, one {# bracket in} other {# brackets in}}"
|
||||
msgstr "{0, plural, one {# Bracket in} other {# Brackets in}}"
|
||||
|
||||
#. placeholder {0}: data.session.errors
|
||||
#. placeholder {0}: session.errors
|
||||
#: src/features/telemetry/components/sessions/session-timeline.tsx:48
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:52
|
||||
msgid "{0, plural, one {# error} other {# errors}}"
|
||||
msgstr "{0, plural, one {# Fehler} other {# Fehler}}"
|
||||
|
||||
#. placeholder {0}: data.session.pageViews
|
||||
#. placeholder {0}: session.pageViews
|
||||
#: src/features/telemetry/components/sessions/session-timeline.tsx:44
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:47
|
||||
msgid "{0, plural, one {# page} other {# pages}}"
|
||||
msgstr "{0, plural, one {# Seite} other {# Seiten}}"
|
||||
|
||||
#. placeholder {0}: enrolledPlayers.length
|
||||
#. placeholder {0}: players.length
|
||||
#: src/features/players/components/players-activity-table.tsx:100
|
||||
@@ -57,11 +71,6 @@ msgstr "{0, plural, one {# Team} other {# Teams}}"
|
||||
msgid "{0, plural, one {# total activity} other {# total activities}}"
|
||||
msgstr "{0, plural, one {# Aktivität insgesamt} other {# Aktivitäten insgesamt}}"
|
||||
|
||||
#. placeholder {0}: result.totalItems
|
||||
#: src/features/telemetry/components/client-events-table.tsx:86
|
||||
msgid "{0, plural, one {# total event} other {# total events}}"
|
||||
msgstr "{0, plural, one {# Ereignis insgesamt} other {# Ereignisse insgesamt}}"
|
||||
|
||||
#. placeholder {0}: selectedConfig.total_group_matches
|
||||
#: src/features/tournaments/components/setup-group-stage.tsx:249
|
||||
msgid "{0, plural, one {# total group stage match} other {# total group stage matches}}"
|
||||
@@ -480,7 +489,8 @@ msgstr "Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es erneut."
|
||||
msgid "Announce matchup"
|
||||
msgstr "Matchup ansagen"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:48
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:44
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:61
|
||||
msgid "Anonymous"
|
||||
msgstr "Anonym"
|
||||
|
||||
@@ -846,7 +856,6 @@ msgid "Create Tournament"
|
||||
msgstr "Turnier erstellen"
|
||||
|
||||
#: src/features/activities/components/activities-table.tsx:198
|
||||
#: src/features/telemetry/components/client-events-table.tsx:148
|
||||
msgid "Created"
|
||||
msgstr "Erstellt"
|
||||
|
||||
@@ -1101,18 +1110,6 @@ msgstr "Fehler: {errorMessage}"
|
||||
msgid "Errors"
|
||||
msgstr "Fehler"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:112
|
||||
msgid "Event Details"
|
||||
msgstr "Ereignisdetails"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:69
|
||||
msgid "Event name"
|
||||
msgstr "Ereignisname"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:54
|
||||
msgid "Events"
|
||||
msgstr "Ereignisse"
|
||||
|
||||
#: src/features/tournaments/components/group-stage-view.tsx:368
|
||||
msgid "Expand standings"
|
||||
msgstr "Tabelle erweitern"
|
||||
@@ -1429,6 +1426,10 @@ msgstr "N"
|
||||
msgid "Language"
|
||||
msgstr "Sprache"
|
||||
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:74
|
||||
msgid "Last 7 days"
|
||||
msgstr "Letzte 7 Tage"
|
||||
|
||||
#: src/features/login/components/name-prompt.tsx:43
|
||||
#: src/features/players/components/profile/name-form.tsx:68
|
||||
msgid "Last Name"
|
||||
@@ -1638,7 +1639,6 @@ msgid "MMR Calculation:"
|
||||
msgstr "MMR-Berechnung:"
|
||||
|
||||
#: src/features/teams/components/team-form/index.tsx:200
|
||||
#: src/features/telemetry/components/client-events-table.tsx:118
|
||||
#: src/features/tournaments/components/tournament-form.tsx:137
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
@@ -1734,10 +1734,6 @@ msgstr "Keine Abzeichen verfügbar"
|
||||
msgid "No devices found."
|
||||
msgstr "Keine Geräte gefunden."
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:93
|
||||
msgid "No Events Found"
|
||||
msgstr "Keine Ereignisse gefunden"
|
||||
|
||||
#: src/features/tournaments/components/group-stage-view.tsx:294
|
||||
msgid "No groups have been created yet"
|
||||
msgstr "Es wurden noch keine Gruppen erstellt"
|
||||
@@ -1776,6 +1772,10 @@ msgstr "Keine Tipps"
|
||||
msgid "No results found"
|
||||
msgstr "Keine Ergebnisse gefunden"
|
||||
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:38
|
||||
msgid "No sessions yet"
|
||||
msgstr "Noch keine Sitzungen"
|
||||
|
||||
#: src/app/routes/api/spotify/resume.ts:37
|
||||
msgid "No snapshot provided"
|
||||
msgstr "Kein Snapshot angegeben"
|
||||
@@ -1949,10 +1949,6 @@ msgstr "Partner-Zuweisungen"
|
||||
msgid "Partners will be randomly assigned when enrollment closes"
|
||||
msgstr "Partner werden nach Anmeldeschluss zufällig zugewiesen"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:121
|
||||
msgid "Path"
|
||||
msgstr "Pfad"
|
||||
|
||||
#: src/features/spotify/components/spotify-controls-bar.tsx:116
|
||||
msgid "Pause"
|
||||
msgstr "Pause"
|
||||
@@ -1995,8 +1991,6 @@ msgstr "Wiedergabesteuerung fehlgeschlagen"
|
||||
#: src/features/activities/components/activities-table.tsx:98
|
||||
#: src/features/activities/components/activities-table.tsx:188
|
||||
#: src/features/admin/components/award-badges.tsx:31
|
||||
#: src/features/telemetry/components/client-events-table.tsx:72
|
||||
#: src/features/telemetry/components/client-events-table.tsx:119
|
||||
msgid "Player"
|
||||
msgstr "Spieler"
|
||||
|
||||
@@ -2076,10 +2070,6 @@ msgstr "Profil"
|
||||
msgid "Progress "
|
||||
msgstr "Fortschritt "
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:135
|
||||
msgid "Props"
|
||||
msgstr "Props"
|
||||
|
||||
#: src/app/routes/_authed/tournaments/$id.predictions_.$playerId.tsx:106
|
||||
#: src/features/predictions/components/prediction-leaderboard.tsx:261
|
||||
msgid "PTS"
|
||||
@@ -2167,10 +2157,6 @@ msgstr "rofl"
|
||||
msgid "Round {0}"
|
||||
msgstr "Runde {0}"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:124
|
||||
msgid "Route"
|
||||
msgstr "Route"
|
||||
|
||||
#: src/features/tournaments/components/started-tournament/sections/nav-grid.tsx:123
|
||||
#: src/features/tournaments/components/upcoming-tournament/rules-list-button.tsx:23
|
||||
msgid "Rules"
|
||||
@@ -2351,10 +2337,7 @@ msgstr "Serverfunktionen"
|
||||
msgid "serverFn name"
|
||||
msgstr "serverFn-Name"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:127
|
||||
msgid "Session"
|
||||
msgstr "Sitzung"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:54
|
||||
#: src/features/telemetry/components/usage-screen.tsx:177
|
||||
msgid "Sessions"
|
||||
msgstr "Sitzungen"
|
||||
@@ -2809,14 +2792,9 @@ msgid "Usage"
|
||||
msgstr "Nutzung"
|
||||
|
||||
#: src/features/activities/components/activities-table.tsx:202
|
||||
#: src/features/telemetry/components/client-events-table.tsx:145
|
||||
msgid "User Agent"
|
||||
msgstr "User-Agent"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:130
|
||||
msgid "Value"
|
||||
msgstr "Wert"
|
||||
|
||||
#: src/features/bracket/components/match-card.tsx:201
|
||||
#: src/features/bracket/components/match-card.tsx:226
|
||||
msgid "versus"
|
||||
|
||||
+25
-47
@@ -35,6 +35,20 @@ msgstr "(Active)"
|
||||
msgid "{0, plural, one {# bracket in} other {# brackets in}}"
|
||||
msgstr "{0, plural, one {# bracket in} other {# brackets in}}"
|
||||
|
||||
#. placeholder {0}: data.session.errors
|
||||
#. placeholder {0}: session.errors
|
||||
#: src/features/telemetry/components/sessions/session-timeline.tsx:48
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:52
|
||||
msgid "{0, plural, one {# error} other {# errors}}"
|
||||
msgstr "{0, plural, one {# error} other {# errors}}"
|
||||
|
||||
#. placeholder {0}: data.session.pageViews
|
||||
#. placeholder {0}: session.pageViews
|
||||
#: src/features/telemetry/components/sessions/session-timeline.tsx:44
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:47
|
||||
msgid "{0, plural, one {# page} other {# pages}}"
|
||||
msgstr "{0, plural, one {# page} other {# pages}}"
|
||||
|
||||
#. placeholder {0}: enrolledPlayers.length
|
||||
#. placeholder {0}: players.length
|
||||
#: src/features/players/components/players-activity-table.tsx:100
|
||||
@@ -57,11 +71,6 @@ msgstr "{0, plural, one {# team} other {# teams}}"
|
||||
msgid "{0, plural, one {# total activity} other {# total activities}}"
|
||||
msgstr "{0, plural, one {# total activity} other {# total activities}}"
|
||||
|
||||
#. placeholder {0}: result.totalItems
|
||||
#: src/features/telemetry/components/client-events-table.tsx:86
|
||||
msgid "{0, plural, one {# total event} other {# total events}}"
|
||||
msgstr "{0, plural, one {# total event} other {# total events}}"
|
||||
|
||||
#. placeholder {0}: selectedConfig.total_group_matches
|
||||
#: src/features/tournaments/components/setup-group-stage.tsx:249
|
||||
msgid "{0, plural, one {# total group stage match} other {# total group stage matches}}"
|
||||
@@ -480,7 +489,8 @@ msgstr "An unexpected error occurred. Please try again."
|
||||
msgid "Announce matchup"
|
||||
msgstr "Announce matchup"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:48
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:44
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:61
|
||||
msgid "Anonymous"
|
||||
msgstr "Anonymous"
|
||||
|
||||
@@ -846,7 +856,6 @@ msgid "Create Tournament"
|
||||
msgstr "Create Tournament"
|
||||
|
||||
#: src/features/activities/components/activities-table.tsx:198
|
||||
#: src/features/telemetry/components/client-events-table.tsx:148
|
||||
msgid "Created"
|
||||
msgstr "Created"
|
||||
|
||||
@@ -1101,18 +1110,6 @@ msgstr "Error: {errorMessage}"
|
||||
msgid "Errors"
|
||||
msgstr "Errors"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:112
|
||||
msgid "Event Details"
|
||||
msgstr "Event Details"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:69
|
||||
msgid "Event name"
|
||||
msgstr "Event name"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:54
|
||||
msgid "Events"
|
||||
msgstr "Events"
|
||||
|
||||
#: src/features/tournaments/components/group-stage-view.tsx:368
|
||||
msgid "Expand standings"
|
||||
msgstr "Expand standings"
|
||||
@@ -1429,6 +1426,10 @@ msgstr "L"
|
||||
msgid "Language"
|
||||
msgstr "Language"
|
||||
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:74
|
||||
msgid "Last 7 days"
|
||||
msgstr "Last 7 days"
|
||||
|
||||
#: src/features/login/components/name-prompt.tsx:43
|
||||
#: src/features/players/components/profile/name-form.tsx:68
|
||||
msgid "Last Name"
|
||||
@@ -1638,7 +1639,6 @@ msgid "MMR Calculation:"
|
||||
msgstr "MMR Calculation:"
|
||||
|
||||
#: src/features/teams/components/team-form/index.tsx:200
|
||||
#: src/features/telemetry/components/client-events-table.tsx:118
|
||||
#: src/features/tournaments/components/tournament-form.tsx:137
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
@@ -1734,10 +1734,6 @@ msgstr "No Badges Available"
|
||||
msgid "No devices found."
|
||||
msgstr "No devices found."
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:93
|
||||
msgid "No Events Found"
|
||||
msgstr "No Events Found"
|
||||
|
||||
#: src/features/tournaments/components/group-stage-view.tsx:294
|
||||
msgid "No groups have been created yet"
|
||||
msgstr "No groups have been created yet"
|
||||
@@ -1776,6 +1772,10 @@ msgstr "No predictions"
|
||||
msgid "No results found"
|
||||
msgstr "No results found"
|
||||
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:38
|
||||
msgid "No sessions yet"
|
||||
msgstr "No sessions yet"
|
||||
|
||||
#: src/app/routes/api/spotify/resume.ts:37
|
||||
msgid "No snapshot provided"
|
||||
msgstr "No snapshot provided"
|
||||
@@ -1949,10 +1949,6 @@ msgstr "Partner Assignments"
|
||||
msgid "Partners will be randomly assigned when enrollment closes"
|
||||
msgstr "Partners will be randomly assigned when enrollment closes"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:121
|
||||
msgid "Path"
|
||||
msgstr "Path"
|
||||
|
||||
#: src/features/spotify/components/spotify-controls-bar.tsx:116
|
||||
msgid "Pause"
|
||||
msgstr "Pause"
|
||||
@@ -1995,8 +1991,6 @@ msgstr "Playback control failed"
|
||||
#: src/features/activities/components/activities-table.tsx:98
|
||||
#: src/features/activities/components/activities-table.tsx:188
|
||||
#: src/features/admin/components/award-badges.tsx:31
|
||||
#: src/features/telemetry/components/client-events-table.tsx:72
|
||||
#: src/features/telemetry/components/client-events-table.tsx:119
|
||||
msgid "Player"
|
||||
msgstr "Player"
|
||||
|
||||
@@ -2076,10 +2070,6 @@ msgstr "Profile"
|
||||
msgid "Progress "
|
||||
msgstr "Progress "
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:135
|
||||
msgid "Props"
|
||||
msgstr "Props"
|
||||
|
||||
#: src/app/routes/_authed/tournaments/$id.predictions_.$playerId.tsx:106
|
||||
#: src/features/predictions/components/prediction-leaderboard.tsx:261
|
||||
msgid "PTS"
|
||||
@@ -2167,10 +2157,6 @@ msgstr "rofl"
|
||||
msgid "Round {0}"
|
||||
msgstr "Round {0}"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:124
|
||||
msgid "Route"
|
||||
msgstr "Route"
|
||||
|
||||
#: src/features/tournaments/components/started-tournament/sections/nav-grid.tsx:123
|
||||
#: src/features/tournaments/components/upcoming-tournament/rules-list-button.tsx:23
|
||||
msgid "Rules"
|
||||
@@ -2351,10 +2337,7 @@ msgstr "Server functions"
|
||||
msgid "serverFn name"
|
||||
msgstr "serverFn name"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:127
|
||||
msgid "Session"
|
||||
msgstr "Session"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:54
|
||||
#: src/features/telemetry/components/usage-screen.tsx:177
|
||||
msgid "Sessions"
|
||||
msgstr "Sessions"
|
||||
@@ -2809,14 +2792,9 @@ msgid "Usage"
|
||||
msgstr "Usage"
|
||||
|
||||
#: src/features/activities/components/activities-table.tsx:202
|
||||
#: src/features/telemetry/components/client-events-table.tsx:145
|
||||
msgid "User Agent"
|
||||
msgstr "User Agent"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:130
|
||||
msgid "Value"
|
||||
msgstr "Value"
|
||||
|
||||
#: src/features/bracket/components/match-card.tsx:201
|
||||
#: src/features/bracket/components/match-card.tsx:226
|
||||
msgid "versus"
|
||||
|
||||
+25
-47
@@ -35,6 +35,20 @@ msgstr "(Activo)"
|
||||
msgid "{0, plural, one {# bracket in} other {# brackets in}}"
|
||||
msgstr "{0, plural, one {# bracket en} other {# brackets en}}"
|
||||
|
||||
#. placeholder {0}: data.session.errors
|
||||
#. placeholder {0}: session.errors
|
||||
#: src/features/telemetry/components/sessions/session-timeline.tsx:48
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:52
|
||||
msgid "{0, plural, one {# error} other {# errors}}"
|
||||
msgstr "{0, plural, one {# error} other {# errores}}"
|
||||
|
||||
#. placeholder {0}: data.session.pageViews
|
||||
#. placeholder {0}: session.pageViews
|
||||
#: src/features/telemetry/components/sessions/session-timeline.tsx:44
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:47
|
||||
msgid "{0, plural, one {# page} other {# pages}}"
|
||||
msgstr "{0, plural, one {# página} other {# páginas}}"
|
||||
|
||||
#. placeholder {0}: enrolledPlayers.length
|
||||
#. placeholder {0}: players.length
|
||||
#: src/features/players/components/players-activity-table.tsx:100
|
||||
@@ -57,11 +71,6 @@ msgstr "{0, plural, one {# equipo} other {# equipos}}"
|
||||
msgid "{0, plural, one {# total activity} other {# total activities}}"
|
||||
msgstr "{0, plural, one {# actividad total} other {# actividades totales}}"
|
||||
|
||||
#. placeholder {0}: result.totalItems
|
||||
#: src/features/telemetry/components/client-events-table.tsx:86
|
||||
msgid "{0, plural, one {# total event} other {# total events}}"
|
||||
msgstr "{0, plural, one {# evento total} other {# eventos totales}}"
|
||||
|
||||
#. placeholder {0}: selectedConfig.total_group_matches
|
||||
#: src/features/tournaments/components/setup-group-stage.tsx:249
|
||||
msgid "{0, plural, one {# total group stage match} other {# total group stage matches}}"
|
||||
@@ -480,7 +489,8 @@ msgstr "Ocurrió un error inesperado. Intenta de nuevo."
|
||||
msgid "Announce matchup"
|
||||
msgstr "Anunciar enfrentamiento"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:48
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:44
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:61
|
||||
msgid "Anonymous"
|
||||
msgstr "Anónimo"
|
||||
|
||||
@@ -846,7 +856,6 @@ msgid "Create Tournament"
|
||||
msgstr "Crear torneo"
|
||||
|
||||
#: src/features/activities/components/activities-table.tsx:198
|
||||
#: src/features/telemetry/components/client-events-table.tsx:148
|
||||
msgid "Created"
|
||||
msgstr "Creado"
|
||||
|
||||
@@ -1101,18 +1110,6 @@ msgstr "Error: {errorMessage}"
|
||||
msgid "Errors"
|
||||
msgstr "Errores"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:112
|
||||
msgid "Event Details"
|
||||
msgstr "Detalles del evento"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:69
|
||||
msgid "Event name"
|
||||
msgstr "Nombre del evento"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:54
|
||||
msgid "Events"
|
||||
msgstr "Eventos"
|
||||
|
||||
#: src/features/tournaments/components/group-stage-view.tsx:368
|
||||
msgid "Expand standings"
|
||||
msgstr "Expandir tabla de posiciones"
|
||||
@@ -1429,6 +1426,10 @@ msgstr "D"
|
||||
msgid "Language"
|
||||
msgstr "Idioma"
|
||||
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:74
|
||||
msgid "Last 7 days"
|
||||
msgstr "Últimos 7 días"
|
||||
|
||||
#: src/features/login/components/name-prompt.tsx:43
|
||||
#: src/features/players/components/profile/name-form.tsx:68
|
||||
msgid "Last Name"
|
||||
@@ -1638,7 +1639,6 @@ msgid "MMR Calculation:"
|
||||
msgstr "Cálculo de MMR:"
|
||||
|
||||
#: src/features/teams/components/team-form/index.tsx:200
|
||||
#: src/features/telemetry/components/client-events-table.tsx:118
|
||||
#: src/features/tournaments/components/tournament-form.tsx:137
|
||||
msgid "Name"
|
||||
msgstr "Nombre"
|
||||
@@ -1734,10 +1734,6 @@ msgstr "No hay insignias disponibles"
|
||||
msgid "No devices found."
|
||||
msgstr "No se encontraron dispositivos."
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:93
|
||||
msgid "No Events Found"
|
||||
msgstr "No se encontraron eventos"
|
||||
|
||||
#: src/features/tournaments/components/group-stage-view.tsx:294
|
||||
msgid "No groups have been created yet"
|
||||
msgstr "Aún no se han creado grupos"
|
||||
@@ -1776,6 +1772,10 @@ msgstr "Sin predicciones"
|
||||
msgid "No results found"
|
||||
msgstr "No se encontraron resultados"
|
||||
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:38
|
||||
msgid "No sessions yet"
|
||||
msgstr "Aún no hay sesiones"
|
||||
|
||||
#: src/app/routes/api/spotify/resume.ts:37
|
||||
msgid "No snapshot provided"
|
||||
msgstr "No se proporcionó ninguna instantánea"
|
||||
@@ -1949,10 +1949,6 @@ msgstr "Asignación de compañeros"
|
||||
msgid "Partners will be randomly assigned when enrollment closes"
|
||||
msgstr "Los compañeros se asignarán al azar cuando cierre la inscripción"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:121
|
||||
msgid "Path"
|
||||
msgstr "Ruta"
|
||||
|
||||
#: src/features/spotify/components/spotify-controls-bar.tsx:116
|
||||
msgid "Pause"
|
||||
msgstr "Pausar"
|
||||
@@ -1995,8 +1991,6 @@ msgstr "Falló el control de reproducción"
|
||||
#: src/features/activities/components/activities-table.tsx:98
|
||||
#: src/features/activities/components/activities-table.tsx:188
|
||||
#: src/features/admin/components/award-badges.tsx:31
|
||||
#: src/features/telemetry/components/client-events-table.tsx:72
|
||||
#: src/features/telemetry/components/client-events-table.tsx:119
|
||||
msgid "Player"
|
||||
msgstr "Jugador"
|
||||
|
||||
@@ -2076,10 +2070,6 @@ msgstr "Perfil"
|
||||
msgid "Progress "
|
||||
msgstr "Progreso "
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:135
|
||||
msgid "Props"
|
||||
msgstr "Props"
|
||||
|
||||
#: src/app/routes/_authed/tournaments/$id.predictions_.$playerId.tsx:106
|
||||
#: src/features/predictions/components/prediction-leaderboard.tsx:261
|
||||
msgid "PTS"
|
||||
@@ -2167,10 +2157,6 @@ msgstr "carcajada"
|
||||
msgid "Round {0}"
|
||||
msgstr "Ronda {0}"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:124
|
||||
msgid "Route"
|
||||
msgstr "Ruta"
|
||||
|
||||
#: src/features/tournaments/components/started-tournament/sections/nav-grid.tsx:123
|
||||
#: src/features/tournaments/components/upcoming-tournament/rules-list-button.tsx:23
|
||||
msgid "Rules"
|
||||
@@ -2351,10 +2337,7 @@ msgstr "Funciones del servidor"
|
||||
msgid "serverFn name"
|
||||
msgstr "nombre de serverFn"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:127
|
||||
msgid "Session"
|
||||
msgstr "Sesión"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:54
|
||||
#: src/features/telemetry/components/usage-screen.tsx:177
|
||||
msgid "Sessions"
|
||||
msgstr "Sesiones"
|
||||
@@ -2809,14 +2792,9 @@ msgid "Usage"
|
||||
msgstr "Uso"
|
||||
|
||||
#: src/features/activities/components/activities-table.tsx:202
|
||||
#: src/features/telemetry/components/client-events-table.tsx:145
|
||||
msgid "User Agent"
|
||||
msgstr "User Agent"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:130
|
||||
msgid "Value"
|
||||
msgstr "Valor"
|
||||
|
||||
#: src/features/bracket/components/match-card.tsx:201
|
||||
#: src/features/bracket/components/match-card.tsx:226
|
||||
msgid "versus"
|
||||
|
||||
+25
-47
@@ -35,6 +35,20 @@ msgstr "(アクティブ)"
|
||||
msgid "{0, plural, one {# bracket in} other {# brackets in}}"
|
||||
msgstr "{0, plural, one {#ブラケット参加} other {#ブラケット参加}}"
|
||||
|
||||
#. placeholder {0}: data.session.errors
|
||||
#. placeholder {0}: session.errors
|
||||
#: src/features/telemetry/components/sessions/session-timeline.tsx:48
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:52
|
||||
msgid "{0, plural, one {# error} other {# errors}}"
|
||||
msgstr "{0, plural, one {# 件のエラー} other {# 件のエラー}}"
|
||||
|
||||
#. placeholder {0}: data.session.pageViews
|
||||
#. placeholder {0}: session.pageViews
|
||||
#: src/features/telemetry/components/sessions/session-timeline.tsx:44
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:47
|
||||
msgid "{0, plural, one {# page} other {# pages}}"
|
||||
msgstr "{0, plural, one {# ページ} other {# ページ}}"
|
||||
|
||||
#. placeholder {0}: enrolledPlayers.length
|
||||
#. placeholder {0}: players.length
|
||||
#: src/features/players/components/players-activity-table.tsx:100
|
||||
@@ -57,11 +71,6 @@ msgstr "{0, plural, one {#チーム} other {#チーム}}"
|
||||
msgid "{0, plural, one {# total activity} other {# total activities}}"
|
||||
msgstr "{0, plural, one {アクティビティ合計#件} other {アクティビティ合計#件}}"
|
||||
|
||||
#. placeholder {0}: result.totalItems
|
||||
#: src/features/telemetry/components/client-events-table.tsx:86
|
||||
msgid "{0, plural, one {# total event} other {# total events}}"
|
||||
msgstr "{0, plural, one {イベント合計#件} other {イベント合計#件}}"
|
||||
|
||||
#. placeholder {0}: selectedConfig.total_group_matches
|
||||
#: src/features/tournaments/components/setup-group-stage.tsx:249
|
||||
msgid "{0, plural, one {# total group stage match} other {# total group stage matches}}"
|
||||
@@ -480,7 +489,8 @@ msgstr "予期しないエラーが発生しました。もう一度お試しく
|
||||
msgid "Announce matchup"
|
||||
msgstr "対戦カードを発表"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:48
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:44
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:61
|
||||
msgid "Anonymous"
|
||||
msgstr "匿名"
|
||||
|
||||
@@ -846,7 +856,6 @@ msgid "Create Tournament"
|
||||
msgstr "トーナメントを作成"
|
||||
|
||||
#: src/features/activities/components/activities-table.tsx:198
|
||||
#: src/features/telemetry/components/client-events-table.tsx:148
|
||||
msgid "Created"
|
||||
msgstr "作成日"
|
||||
|
||||
@@ -1101,18 +1110,6 @@ msgstr "エラー: {errorMessage}"
|
||||
msgid "Errors"
|
||||
msgstr "エラー"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:112
|
||||
msgid "Event Details"
|
||||
msgstr "イベント詳細"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:69
|
||||
msgid "Event name"
|
||||
msgstr "イベント名"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:54
|
||||
msgid "Events"
|
||||
msgstr "イベント"
|
||||
|
||||
#: src/features/tournaments/components/group-stage-view.tsx:368
|
||||
msgid "Expand standings"
|
||||
msgstr "順位表を展開"
|
||||
@@ -1429,6 +1426,10 @@ msgstr "L"
|
||||
msgid "Language"
|
||||
msgstr "言語"
|
||||
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:74
|
||||
msgid "Last 7 days"
|
||||
msgstr "過去7日間"
|
||||
|
||||
#: src/features/login/components/name-prompt.tsx:43
|
||||
#: src/features/players/components/profile/name-form.tsx:68
|
||||
msgid "Last Name"
|
||||
@@ -1638,7 +1639,6 @@ msgid "MMR Calculation:"
|
||||
msgstr "MMR計算:"
|
||||
|
||||
#: src/features/teams/components/team-form/index.tsx:200
|
||||
#: src/features/telemetry/components/client-events-table.tsx:118
|
||||
#: src/features/tournaments/components/tournament-form.tsx:137
|
||||
msgid "Name"
|
||||
msgstr "名前"
|
||||
@@ -1734,10 +1734,6 @@ msgstr "利用可能なバッジがありません"
|
||||
msgid "No devices found."
|
||||
msgstr "デバイスが見つかりません。"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:93
|
||||
msgid "No Events Found"
|
||||
msgstr "イベントが見つかりません"
|
||||
|
||||
#: src/features/tournaments/components/group-stage-view.tsx:294
|
||||
msgid "No groups have been created yet"
|
||||
msgstr "まだグループが作成されていません"
|
||||
@@ -1776,6 +1772,10 @@ msgstr "予想はありません"
|
||||
msgid "No results found"
|
||||
msgstr "結果が見つかりません"
|
||||
|
||||
#: src/features/telemetry/components/sessions/sessions-list.tsx:38
|
||||
msgid "No sessions yet"
|
||||
msgstr "まだセッションがありません"
|
||||
|
||||
#: src/app/routes/api/spotify/resume.ts:37
|
||||
msgid "No snapshot provided"
|
||||
msgstr "スナップショットが提供されていません"
|
||||
@@ -1949,10 +1949,6 @@ msgstr "パートナー割り当て"
|
||||
msgid "Partners will be randomly assigned when enrollment closes"
|
||||
msgstr "登録が締め切られると、パートナーはランダムに割り当てられます"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:121
|
||||
msgid "Path"
|
||||
msgstr "パス"
|
||||
|
||||
#: src/features/spotify/components/spotify-controls-bar.tsx:116
|
||||
msgid "Pause"
|
||||
msgstr "一時停止"
|
||||
@@ -1995,8 +1991,6 @@ msgstr "再生コントロールに失敗しました"
|
||||
#: src/features/activities/components/activities-table.tsx:98
|
||||
#: src/features/activities/components/activities-table.tsx:188
|
||||
#: src/features/admin/components/award-badges.tsx:31
|
||||
#: src/features/telemetry/components/client-events-table.tsx:72
|
||||
#: src/features/telemetry/components/client-events-table.tsx:119
|
||||
msgid "Player"
|
||||
msgstr "プレイヤー"
|
||||
|
||||
@@ -2076,10 +2070,6 @@ msgstr "プロフィール"
|
||||
msgid "Progress "
|
||||
msgstr "進行状況 "
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:135
|
||||
msgid "Props"
|
||||
msgstr "プロパティ"
|
||||
|
||||
#: src/app/routes/_authed/tournaments/$id.predictions_.$playerId.tsx:106
|
||||
#: src/features/predictions/components/prediction-leaderboard.tsx:261
|
||||
msgid "PTS"
|
||||
@@ -2167,10 +2157,6 @@ msgstr "大笑い"
|
||||
msgid "Round {0}"
|
||||
msgstr "ラウンド{0}"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:124
|
||||
msgid "Route"
|
||||
msgstr "ルート"
|
||||
|
||||
#: src/features/tournaments/components/started-tournament/sections/nav-grid.tsx:123
|
||||
#: src/features/tournaments/components/upcoming-tournament/rules-list-button.tsx:23
|
||||
msgid "Rules"
|
||||
@@ -2351,10 +2337,7 @@ msgstr "サーバー関数"
|
||||
msgid "serverFn name"
|
||||
msgstr "serverFn名"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:127
|
||||
msgid "Session"
|
||||
msgstr "セッション"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:54
|
||||
#: src/features/telemetry/components/usage-screen.tsx:177
|
||||
msgid "Sessions"
|
||||
msgstr "セッション数"
|
||||
@@ -2809,14 +2792,9 @@ msgid "Usage"
|
||||
msgstr "利用状況"
|
||||
|
||||
#: src/features/activities/components/activities-table.tsx:202
|
||||
#: src/features/telemetry/components/client-events-table.tsx:145
|
||||
msgid "User Agent"
|
||||
msgstr "ユーザーエージェント"
|
||||
|
||||
#: src/features/telemetry/components/client-events-table.tsx:130
|
||||
msgid "Value"
|
||||
msgstr "値"
|
||||
|
||||
#: src/features/bracket/components/match-card.tsx:201
|
||||
#: src/features/bracket/components/match-card.tsx:226
|
||||
msgid "versus"
|
||||
|
||||
Reference in New Issue
Block a user