badges improvements

This commit is contained in:
2026-08-23 20:31:44 -07:00
parent d9ccd5acf1
commit 3761f8be6f
9 changed files with 366 additions and 76 deletions
@@ -0,0 +1,36 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
// Dedupe before adding the unique index: keep the best row per
// (player, badge) — earned first, then highest progress, then newest.
app.db().newQuery(`
DELETE FROM badge_progress
WHERE id NOT IN (
SELECT id FROM (
SELECT id, ROW_NUMBER() OVER (
PARTITION BY player, badge
ORDER BY earned DESC, progress DESC, updated DESC
) AS rn
FROM badge_progress
)
WHERE rn = 1
)
`).execute()
const collection = app.findCollectionByNameOrId("pbc_1063824264")
unmarshal({
"indexes": [
"CREATE UNIQUE INDEX `idx_badge_progress_player_badge` ON `badge_progress` (`player`, `badge`)"
]
}, collection)
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("pbc_1063824264")
unmarshal({
"indexes": []
}, collection)
return app.save(collection)
})
+51 -8
View File
@@ -1,5 +1,5 @@
import { List } from "@mantine/core";
import { useLingui } from "@lingui/react/macro";
import { List, Text } from "@mantine/core";
import { Plural, Trans, useLingui } from "@lingui/react/macro";
import ListLink from "@/components/list-link";
import {
DatabaseIcon,
@@ -10,19 +10,38 @@ import {
ListIcon,
} from "@phosphor-icons/react";
import ListButton from "@/components/list-button";
import { migrateBadgeProgress } from "@/features/badges/server";
import { useState } from "react";
import { getBadgeMigrationStatus, migrateBadgeProgress } from "@/features/badges/server";
import { useServerQuery } from "@/lib/tanstack-query/hooks";
import toast from "@/lib/sonner";
const AdminPage = () => {
const { t } = useLingui();
const [isMigrating, setIsMigrating] = useState(false);
const { data: migrationStatus, refetch: refetchMigrationStatus } = useServerQuery({
queryKey: ["admin", "badge-migration-status"],
queryFn: () => getBadgeMigrationStatus(),
options: {
refetchInterval: (query) =>
query.state.data?.state === "running" ? 2000 : false,
},
});
const isMigrating = migrationStatus?.state === "running";
const handleMigrateBadges = async () => {
if (isMigrating) return;
setIsMigrating(true);
await migrateBadgeProgress();
setIsMigrating(false);
const result = await migrateBadgeProgress();
if (result.success) {
toast.success(
result.data.started
? t`Badge recalculation started in the background`
: t`Badge recalculation is already running`
);
} else {
toast.error(t`Failed to start badge recalculation`);
}
await refetchMigrationStatus();
};
return (
@@ -43,6 +62,30 @@ const AdminPage = () => {
onClick={handleMigrateBadges}
loading={isMigrating}
/>
{migrationStatus?.state === "running" && (
<Text size="sm" c="dimmed" px="md" py="xs">
<Trans>
Recalculating badges in the background ({migrationStatus.playersProcessed}/{migrationStatus.totalPlayers})
</Trans>
</Text>
)}
{migrationStatus?.state === "completed" && (
<Text size="sm" c="dimmed" px="md" py="xs">
<Trans>
Last recalculation:{" "}
<Plural
value={migrationStatus.result.totalBadgesEarned}
one="# badge earned"
other="# badges earned"
/>
</Trans>
</Text>
)}
{migrationStatus?.state === "failed" && (
<Text size="sm" c="red" px="md" py="xs">
<Trans>Badge recalculation failed. Check the server logs.</Trans>
</Text>
)}
<ListLink
label={t`Activities`}
Icon={ListIcon}
+55
View File
@@ -0,0 +1,55 @@
import { pbAdmin } from "@/lib/pocketbase/client";
import { emitServerEvent } from "@/lib/events/emitter";
import { logger } from "@/lib/logger";
export type BadgeMigrationResult = {
playersProcessed: number;
totalBadgesEarned: number;
created: number;
updated: number;
deleted: number;
averageBadgesPerPlayer: string;
};
export type BadgeMigrationStatus =
| { state: "idle" }
| { state: "running"; startedAt: string; playersProcessed: number; totalPlayers: number }
| { state: "completed"; startedAt: string; finishedAt: string; result: BadgeMigrationResult }
| { state: "failed"; startedAt: string; finishedAt: string; error: string };
// In-process job state — valid only because the app runs a single replica.
let status: BadgeMigrationStatus = { state: "idle" };
export const getBadgeMigrationJobStatus = (): BadgeMigrationStatus => status;
export const startBadgeMigration = (): { started: boolean; status: BadgeMigrationStatus } => {
if (status.state === "running") {
return { started: false, status };
}
const startedAt = new Date().toISOString();
status = { state: "running", startedAt, playersProcessed: 0, totalPlayers: 0 };
void pbAdmin
.migrateBadgeProgress((playersProcessed, totalPlayers) => {
if (status.state === "running") {
status = { ...status, playersProcessed, totalPlayers };
}
})
.then((result) => {
status = { state: "completed", startedAt, finishedAt: new Date().toISOString(), result };
logger.info("Badge migration completed", result);
emitServerEvent({ type: "badge" });
})
.catch((error) => {
status = {
state: "failed",
startedAt,
finishedAt: new Date().toISOString(),
error: error instanceof Error ? error.message : String(error),
};
logger.error("Badge migration failed", error);
});
return { started: true, status };
};
+8 -5
View File
@@ -4,6 +4,7 @@ import { createServerFn } from "@tanstack/react-start";
import { pbAdmin } from "@/lib/pocketbase/client";
import { z } from "zod";
import { emitServerEvent } from "@/lib/events/emitter";
import { getBadgeMigrationJobStatus, startBadgeMigration } from "./migration.server";
export const getPlayerBadges = createServerFn()
.validator(z.string())
@@ -15,11 +16,13 @@ export const getPlayerBadges = createServerFn()
export const migrateBadgeProgress = createServerFn()
.middleware([superTokensAdminFunctionMiddleware])
.handler(async () =>
toServerResult(async () => {
const result = await pbAdmin.migrateBadgeProgress();
emitServerEvent({ type: "badge" });
return result;
})
toServerResult(async () => startBadgeMigration())
);
export const getBadgeMigrationStatus = createServerFn()
.middleware([superTokensAdminFunctionMiddleware])
.handler(async () =>
toServerResult(async () => getBadgeMigrationJobStatus())
);
export const getAllBadges = createServerFn()
+84 -39
View File
@@ -70,16 +70,6 @@ export function createBadgesService(pb: PocketBase) {
return true;
},
async clearAllBadgeProgress(): Promise<number> {
const existingProgress = await pb.collection("badge_progress").getFullList({
filter: 'badge.type != "manual"',
});
for (const progress of existingProgress) {
await pb.collection("badge_progress").delete(progress.id);
}
return existingProgress.length;
},
async calculateBadgeProgress(playerId: string, badge: Badge): Promise<number> {
if (badge.type === "manual") {
return 0;
@@ -651,27 +641,36 @@ export function createBadgesService(pb: PocketBase) {
return transformBadgeProgress(created);
},
async migrateBadgeProgress(): Promise<{
success: boolean;
async migrateBadgeProgress(
onProgress?: (playersProcessed: number, totalPlayers: number) => void
): Promise<{
playersProcessed: number;
progressRecordsCreated: number;
totalBadgesEarned: number;
created: number;
updated: number;
deleted: number;
averageBadgesPerPlayer: string;
}> {
await this.clearAllBadgeProgress();
const badges = await this.listBadges();
const autoBadges = badges.filter((badge) => badge.type !== "manual");
const players = await pb.collection("players").getFullList();
const allPlayers = await pb.collection("players").getFullList();
const uniquePlayers = new Set(allPlayers.map((p: any) => p.id));
// Phase 1: read-only calculation. Existing badge_progress rows stay
// untouched until the full desired state is known.
const desired = new Map<string, {
badge: string;
player: string;
progress: number;
earned: boolean;
}>();
const failedKeys = new Set<string>();
let playersProcessed = 0;
let totalProgressRecords = 0;
let totalBadgesEarned = 0;
for (const playerId of uniquePlayers) {
for (const badge of badges) {
for (const player of players) {
for (const badge of autoBadges) {
const key = `${player.id}|${badge.id}`;
try {
const progress = await this.calculateBadgeProgress(playerId, badge);
const progress = await this.calculateBadgeProgress(player.id, badge);
const target = this.getTargetProgress(badge);
const isPlacementBadge = badge.criteria.placement !== undefined;
@@ -681,31 +680,77 @@ export function createBadgesService(pb: PocketBase) {
: progress === target;
if (progress > 0 || earned) {
await this.createBadgeProgress({
desired.set(key, {
badge: badge.id,
player: playerId,
progress: progress,
earned: earned,
player: player.id,
progress,
earned,
});
totalProgressRecords++;
if (earned) {
totalBadgesEarned++;
}
}
} catch (error: any) {
console.error(`Error processing badge "${badge.name}" for player ${playerId}:`, error.message);
// Keep whatever row already exists when a calculation fails.
failedKeys.add(key);
console.error(`Error processing badge "${badge.name}" for player ${player.id}:`, error.message);
}
}
playersProcessed++;
onProgress?.(playersProcessed, players.length);
}
// Phase 2: apply as a diff — update changed rows, create missing ones,
// delete stale/duplicate ones. No window where all badges are gone.
const existing = await pb.collection("badge_progress").getFullList({
filter: 'badge.type != "manual"',
});
const existingByKey = new Map<string, { id: string; progress: number; earned: boolean }>();
const toDelete: string[] = [];
for (const row of existing) {
const key = `${row.player}|${row.badge}`;
if (existingByKey.has(key)) {
toDelete.push(row.id);
continue;
}
existingByKey.set(key, { id: row.id, progress: row.progress, earned: row.earned });
if (!desired.has(key) && !failedKeys.has(key)) {
toDelete.push(row.id);
}
}
let created = 0;
let updated = 0;
let totalBadgesEarned = 0;
for (const [key, want] of desired) {
if (want.earned) {
totalBadgesEarned++;
}
const row = existingByKey.get(key);
if (!row) {
await this.createBadgeProgress(want);
created++;
} else if (row.progress !== want.progress || row.earned !== want.earned) {
await this.updateBadgeProgress(row.id, {
progress: want.progress,
earned: want.earned,
});
updated++;
}
}
for (const id of toDelete) {
await this.deleteBadgeProgress(id);
}
return {
success: true,
playersProcessed: uniquePlayers.size,
progressRecordsCreated: totalProgressRecords,
totalBadgesEarned: totalBadgesEarned,
averageBadgesPerPlayer: (totalBadgesEarned / uniquePlayers.size).toFixed(2),
playersProcessed: players.length,
totalBadgesEarned,
created,
updated,
deleted: toDelete.length,
averageBadgesPerPlayer: players.length
? (totalBadgesEarned / players.length).toFixed(2)
: "0",
};
},
};
+33 -6
View File
@@ -384,7 +384,7 @@ msgid "Active device: {0}"
msgstr "Aktives Gerät: {0}"
#: src/app/routes/_authed/admin/activities.tsx:21
#: src/features/admin/components/admin-page.tsx:47
#: src/features/admin/components/admin-page.tsx:90
msgid "Activities"
msgstr "Aktivitäten"
@@ -520,7 +520,7 @@ msgstr "Ø Sieg-Marge"
msgid "Award Badge"
msgstr "Abzeichen verleihen"
#: src/features/admin/components/admin-page.tsx:36
#: src/features/admin/components/admin-page.tsx:55
msgid "Award Badges"
msgstr "Abzeichen verleihen"
@@ -554,6 +554,18 @@ msgstr "Zurück"
msgid "Badge awarded to {playerName}"
msgstr "Abzeichen an {playerName} verliehen"
#: src/features/admin/components/admin-page.tsx:86
msgid "Badge recalculation failed. Check the server logs."
msgstr "Abzeichen-Neuberechnung fehlgeschlagen. Prüfe die Server-Logs."
#: src/features/admin/components/admin-page.tsx:39
msgid "Badge recalculation is already running"
msgstr "Abzeichen-Neuberechnung läuft bereits"
#: src/features/admin/components/admin-page.tsx:38
msgid "Badge recalculation started in the background"
msgstr "Abzeichen-Neuberechnung im Hintergrund gestartet"
#: src/features/admin/components/award-badges.tsx:70
msgid "Badge Type"
msgstr "Abzeichentyp"
@@ -581,7 +593,7 @@ msgid "Bracket"
msgstr "Turnierbaum"
#: src/app/routes/_authed/admin/preview.tsx:13
#: src/features/admin/components/admin-page.tsx:59
#: src/features/admin/components/admin-page.tsx:102
msgid "Bracket Preview"
msgstr "Turnierbaum-Vorschau"
@@ -1060,6 +1072,10 @@ msgstr "Aktives Gerät konnte nicht festgelegt werden"
msgid "Failed to set volume"
msgstr "Lautstärke konnte nicht festgelegt werden"
#: src/features/admin/components/admin-page.tsx:42
msgid "Failed to start badge recalculation"
msgstr "Abzeichen-Neuberechnung konnte nicht gestartet werden"
#: src/features/teams/components/team-form/index.tsx:105
msgid "Failed to update team"
msgstr "Team konnte nicht aktualisiert werden"
@@ -1322,6 +1338,11 @@ msgstr "Der Nachname muss 2-20 Zeichen lang sein und darf nur Buchstaben enthalt
msgid "Last name must be 3-20 characters long and contain only letters"
msgstr "Der Nachname muss 3-20 Zeichen lang sein und darf nur Buchstaben enthalten"
#. placeholder {0}: migrationStatus.result.totalBadgesEarned
#: src/features/admin/components/admin-page.tsx:74
msgid "Last recalculation: {0, plural, one {# badge earned} other {# badges earned}}"
msgstr "Letzte Neuberechnung: {0, plural, one {# Abzeichen verdient} other {# Abzeichen verdient}}"
#: src/features/settings/components/color-scheme-picker.tsx:43
msgid "Light"
msgstr "Hell"
@@ -1410,7 +1431,7 @@ msgid "Manage Team Songs/Logos"
msgstr "Team-Songs/Logos verwalten"
#: src/app/routes/_authed/admin/tournaments/index.tsx:17
#: src/features/admin/components/admin-page.tsx:31
#: src/features/admin/components/admin-page.tsx:50
msgid "Manage Tournaments"
msgstr "Turniere verwalten"
@@ -1489,7 +1510,7 @@ msgstr "Matchup-Details"
msgid "Me"
msgstr "Ich"
#: src/features/admin/components/admin-page.tsx:41
#: src/features/admin/components/admin-page.tsx:60
msgid "Migrate Badge Progress"
msgstr "Abzeichen-Fortschritt migrieren"
@@ -1729,7 +1750,7 @@ msgstr "Sobald du angemeldet bist, kannst du andere Free Agents und ihre Telefon
msgid "One time code"
msgstr "Einmalcode"
#: src/features/admin/components/admin-page.tsx:52
#: src/features/admin/components/admin-page.tsx:95
msgid "Open Pocketbase"
msgstr "Pocketbase öffnen"
@@ -1927,6 +1948,12 @@ msgstr "Neu würfeln"
msgid "Reactions"
msgstr "Reaktionen"
#. placeholder {0}: migrationStatus.playersProcessed
#. placeholder {1}: migrationStatus.totalPlayers
#: src/features/admin/components/admin-page.tsx:67
msgid "Recalculating badges in the background ({0}/{1})"
msgstr "Abzeichen werden im Hintergrund neu berechnet ({0}/{1})"
#: src/features/tournaments/components/started-tournament/sections/recent-results.tsx:34
msgid "Recently finished"
msgstr "Kürzlich beendet"
+33 -6
View File
@@ -384,7 +384,7 @@ msgid "Active device: {0}"
msgstr "Active device: {0}"
#: src/app/routes/_authed/admin/activities.tsx:21
#: src/features/admin/components/admin-page.tsx:47
#: src/features/admin/components/admin-page.tsx:90
msgid "Activities"
msgstr "Activities"
@@ -520,7 +520,7 @@ msgstr "Avg Win Margin"
msgid "Award Badge"
msgstr "Award Badge"
#: src/features/admin/components/admin-page.tsx:36
#: src/features/admin/components/admin-page.tsx:55
msgid "Award Badges"
msgstr "Award Badges"
@@ -554,6 +554,18 @@ msgstr "Back"
msgid "Badge awarded to {playerName}"
msgstr "Badge awarded to {playerName}"
#: src/features/admin/components/admin-page.tsx:86
msgid "Badge recalculation failed. Check the server logs."
msgstr "Badge recalculation failed. Check the server logs."
#: src/features/admin/components/admin-page.tsx:39
msgid "Badge recalculation is already running"
msgstr "Badge recalculation is already running"
#: src/features/admin/components/admin-page.tsx:38
msgid "Badge recalculation started in the background"
msgstr "Badge recalculation started in the background"
#: src/features/admin/components/award-badges.tsx:70
msgid "Badge Type"
msgstr "Badge Type"
@@ -581,7 +593,7 @@ msgid "Bracket"
msgstr "Bracket"
#: src/app/routes/_authed/admin/preview.tsx:13
#: src/features/admin/components/admin-page.tsx:59
#: src/features/admin/components/admin-page.tsx:102
msgid "Bracket Preview"
msgstr "Bracket Preview"
@@ -1060,6 +1072,10 @@ msgstr "Failed to set active device"
msgid "Failed to set volume"
msgstr "Failed to set volume"
#: src/features/admin/components/admin-page.tsx:42
msgid "Failed to start badge recalculation"
msgstr "Failed to start badge recalculation"
#: src/features/teams/components/team-form/index.tsx:105
msgid "Failed to update team"
msgstr "Failed to update team"
@@ -1322,6 +1338,11 @@ msgstr "Last name must be 2-20 characters long and contain only letters"
msgid "Last name must be 3-20 characters long and contain only letters"
msgstr "Last name must be 3-20 characters long and contain only letters"
#. placeholder {0}: migrationStatus.result.totalBadgesEarned
#: src/features/admin/components/admin-page.tsx:74
msgid "Last recalculation: {0, plural, one {# badge earned} other {# badges earned}}"
msgstr "Last recalculation: {0, plural, one {# badge earned} other {# badges earned}}"
#: src/features/settings/components/color-scheme-picker.tsx:43
msgid "Light"
msgstr "Light"
@@ -1410,7 +1431,7 @@ msgid "Manage Team Songs/Logos"
msgstr "Manage Team Songs/Logos"
#: src/app/routes/_authed/admin/tournaments/index.tsx:17
#: src/features/admin/components/admin-page.tsx:31
#: src/features/admin/components/admin-page.tsx:50
msgid "Manage Tournaments"
msgstr "Manage Tournaments"
@@ -1489,7 +1510,7 @@ msgstr "Matchup details"
msgid "Me"
msgstr "Me"
#: src/features/admin/components/admin-page.tsx:41
#: src/features/admin/components/admin-page.tsx:60
msgid "Migrate Badge Progress"
msgstr "Migrate Badge Progress"
@@ -1729,7 +1750,7 @@ msgstr "Once enrolled, you can view other free agents and their phone number in
msgid "One time code"
msgstr "One time code"
#: src/features/admin/components/admin-page.tsx:52
#: src/features/admin/components/admin-page.tsx:95
msgid "Open Pocketbase"
msgstr "Open Pocketbase"
@@ -1927,6 +1948,12 @@ msgstr "Re-roll"
msgid "Reactions"
msgstr "Reactions"
#. placeholder {0}: migrationStatus.playersProcessed
#. placeholder {1}: migrationStatus.totalPlayers
#: src/features/admin/components/admin-page.tsx:67
msgid "Recalculating badges in the background ({0}/{1})"
msgstr "Recalculating badges in the background ({0}/{1})"
#: src/features/tournaments/components/started-tournament/sections/recent-results.tsx:34
msgid "Recently finished"
msgstr "Recently finished"
+33 -6
View File
@@ -384,7 +384,7 @@ msgid "Active device: {0}"
msgstr "Dispositivo activo: {0}"
#: src/app/routes/_authed/admin/activities.tsx:21
#: src/features/admin/components/admin-page.tsx:47
#: src/features/admin/components/admin-page.tsx:90
msgid "Activities"
msgstr "Actividades"
@@ -520,7 +520,7 @@ msgstr "Prom. Margen de Victoria"
msgid "Award Badge"
msgstr "Otorgar insignia"
#: src/features/admin/components/admin-page.tsx:36
#: src/features/admin/components/admin-page.tsx:55
msgid "Award Badges"
msgstr "Otorgar insignias"
@@ -554,6 +554,18 @@ msgstr "Atrás"
msgid "Badge awarded to {playerName}"
msgstr "Insignia otorgada a {playerName}"
#: src/features/admin/components/admin-page.tsx:86
msgid "Badge recalculation failed. Check the server logs."
msgstr "Falló el recálculo de insignias. Revisa los registros del servidor."
#: src/features/admin/components/admin-page.tsx:39
msgid "Badge recalculation is already running"
msgstr "El recálculo de insignias ya está en curso"
#: src/features/admin/components/admin-page.tsx:38
msgid "Badge recalculation started in the background"
msgstr "Recálculo de insignias iniciado en segundo plano"
#: src/features/admin/components/award-badges.tsx:70
msgid "Badge Type"
msgstr "Tipo de insignia"
@@ -581,7 +593,7 @@ msgid "Bracket"
msgstr "Bracket"
#: src/app/routes/_authed/admin/preview.tsx:13
#: src/features/admin/components/admin-page.tsx:59
#: src/features/admin/components/admin-page.tsx:102
msgid "Bracket Preview"
msgstr "Vista previa del bracket"
@@ -1060,6 +1072,10 @@ msgstr "No se pudo establecer el dispositivo activo"
msgid "Failed to set volume"
msgstr "No se pudo ajustar el volumen"
#: src/features/admin/components/admin-page.tsx:42
msgid "Failed to start badge recalculation"
msgstr "No se pudo iniciar el recálculo de insignias"
#: src/features/teams/components/team-form/index.tsx:105
msgid "Failed to update team"
msgstr "No se pudo actualizar el equipo"
@@ -1322,6 +1338,11 @@ msgstr "El apellido debe tener entre 2 y 20 caracteres y contener solo letras"
msgid "Last name must be 3-20 characters long and contain only letters"
msgstr "El apellido debe tener entre 3 y 20 caracteres y contener solo letras"
#. placeholder {0}: migrationStatus.result.totalBadgesEarned
#: src/features/admin/components/admin-page.tsx:74
msgid "Last recalculation: {0, plural, one {# badge earned} other {# badges earned}}"
msgstr "Último recálculo: {0, plural, one {# insignia obtenida} other {# insignias obtenidas}}"
#: src/features/settings/components/color-scheme-picker.tsx:43
msgid "Light"
msgstr "Claro"
@@ -1410,7 +1431,7 @@ msgid "Manage Team Songs/Logos"
msgstr "Administrar canciones/logos del equipo"
#: src/app/routes/_authed/admin/tournaments/index.tsx:17
#: src/features/admin/components/admin-page.tsx:31
#: src/features/admin/components/admin-page.tsx:50
msgid "Manage Tournaments"
msgstr "Administrar torneos"
@@ -1489,7 +1510,7 @@ msgstr "Detalles del enfrentamiento"
msgid "Me"
msgstr "Yo"
#: src/features/admin/components/admin-page.tsx:41
#: src/features/admin/components/admin-page.tsx:60
msgid "Migrate Badge Progress"
msgstr "Migrar progreso de insignias"
@@ -1729,7 +1750,7 @@ msgstr "Una vez inscrito, puedes ver a otros agentes libres y su número de tel
msgid "One time code"
msgstr "Código de un solo uso"
#: src/features/admin/components/admin-page.tsx:52
#: src/features/admin/components/admin-page.tsx:95
msgid "Open Pocketbase"
msgstr "Abrir Pocketbase"
@@ -1927,6 +1948,12 @@ msgstr "Volver a sortear"
msgid "Reactions"
msgstr "Reacciones"
#. placeholder {0}: migrationStatus.playersProcessed
#. placeholder {1}: migrationStatus.totalPlayers
#: src/features/admin/components/admin-page.tsx:67
msgid "Recalculating badges in the background ({0}/{1})"
msgstr "Recalculando insignias en segundo plano ({0}/{1})"
#: src/features/tournaments/components/started-tournament/sections/recent-results.tsx:34
msgid "Recently finished"
msgstr "Recién terminados"
+33 -6
View File
@@ -384,7 +384,7 @@ msgid "Active device: {0}"
msgstr "アクティブなデバイス: {0}"
#: src/app/routes/_authed/admin/activities.tsx:21
#: src/features/admin/components/admin-page.tsx:47
#: src/features/admin/components/admin-page.tsx:90
msgid "Activities"
msgstr "アクティビティ"
@@ -520,7 +520,7 @@ msgstr "平均勝差"
msgid "Award Badge"
msgstr "バッジを付与"
#: src/features/admin/components/admin-page.tsx:36
#: src/features/admin/components/admin-page.tsx:55
msgid "Award Badges"
msgstr "バッジを付与"
@@ -554,6 +554,18 @@ msgstr "戻る"
msgid "Badge awarded to {playerName}"
msgstr "{playerName}にバッジを付与しました"
#: src/features/admin/components/admin-page.tsx:86
msgid "Badge recalculation failed. Check the server logs."
msgstr "バッジの再計算に失敗しました。サーバーログを確認してください。"
#: src/features/admin/components/admin-page.tsx:39
msgid "Badge recalculation is already running"
msgstr "バッジの再計算はすでに実行中です"
#: src/features/admin/components/admin-page.tsx:38
msgid "Badge recalculation started in the background"
msgstr "バックグラウンドでバッジの再計算を開始しました"
#: src/features/admin/components/award-badges.tsx:70
msgid "Badge Type"
msgstr "バッジの種類"
@@ -581,7 +593,7 @@ msgid "Bracket"
msgstr "ブラケット"
#: src/app/routes/_authed/admin/preview.tsx:13
#: src/features/admin/components/admin-page.tsx:59
#: src/features/admin/components/admin-page.tsx:102
msgid "Bracket Preview"
msgstr "ブラケットプレビュー"
@@ -1060,6 +1072,10 @@ msgstr "アクティブなデバイスの設定に失敗しました"
msgid "Failed to set volume"
msgstr "音量の設定に失敗しました"
#: src/features/admin/components/admin-page.tsx:42
msgid "Failed to start badge recalculation"
msgstr "バッジの再計算を開始できませんでした"
#: src/features/teams/components/team-form/index.tsx:105
msgid "Failed to update team"
msgstr "チームの更新に失敗しました"
@@ -1322,6 +1338,11 @@ msgstr "姓は2〜20文字の英字のみで入力してください"
msgid "Last name must be 3-20 characters long and contain only letters"
msgstr "姓は3〜20文字の英字のみで入力してください"
#. placeholder {0}: migrationStatus.result.totalBadgesEarned
#: src/features/admin/components/admin-page.tsx:74
msgid "Last recalculation: {0, plural, one {# badge earned} other {# badges earned}}"
msgstr "前回の再計算:{0, plural, one {# 個のバッジを獲得} other {# 個のバッジを獲得}}"
#: src/features/settings/components/color-scheme-picker.tsx:43
msgid "Light"
msgstr "ライト"
@@ -1410,7 +1431,7 @@ msgid "Manage Team Songs/Logos"
msgstr "チームの曲・ロゴを管理"
#: src/app/routes/_authed/admin/tournaments/index.tsx:17
#: src/features/admin/components/admin-page.tsx:31
#: src/features/admin/components/admin-page.tsx:50
msgid "Manage Tournaments"
msgstr "トーナメント管理"
@@ -1489,7 +1510,7 @@ msgstr "対戦カードの詳細"
msgid "Me"
msgstr "自分"
#: src/features/admin/components/admin-page.tsx:41
#: src/features/admin/components/admin-page.tsx:60
msgid "Migrate Badge Progress"
msgstr "バッジ進捗を移行"
@@ -1729,7 +1750,7 @@ msgstr "登録が完了すると、他のフリーエージェントとその電
msgid "One time code"
msgstr "ワンタイムコード"
#: src/features/admin/components/admin-page.tsx:52
#: src/features/admin/components/admin-page.tsx:95
msgid "Open Pocketbase"
msgstr "Pocketbaseを開く"
@@ -1927,6 +1948,12 @@ msgstr "再抽選"
msgid "Reactions"
msgstr "リアクション"
#. placeholder {0}: migrationStatus.playersProcessed
#. placeholder {1}: migrationStatus.totalPlayers
#: src/features/admin/components/admin-page.tsx:67
msgid "Recalculating badges in the background ({0}/{1})"
msgstr "バックグラウンドでバッジを再計算中({0}/{1})"
#: src/features/tournaments/components/started-tournament/sections/recent-results.tsx:34
msgid "Recently finished"
msgstr "最近終了した試合"