diff --git a/pb_migrations/1787443200_badge_progress_unique_index.js b/pb_migrations/1787443200_badge_progress_unique_index.js new file mode 100644 index 0000000..72f2ff9 --- /dev/null +++ b/pb_migrations/1787443200_badge_progress_unique_index.js @@ -0,0 +1,36 @@ +/// +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) +}) diff --git a/src/features/admin/components/admin-page.tsx b/src/features/admin/components/admin-page.tsx index 91acb29..ed784b7 100644 --- a/src/features/admin/components/admin-page.tsx +++ b/src/features/admin/components/admin-page.tsx @@ -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" && ( + + + Recalculating badges in the background ({migrationStatus.playersProcessed}/{migrationStatus.totalPlayers}) + + + )} + {migrationStatus?.state === "completed" && ( + + + Last recalculation:{" "} + + + + )} + {migrationStatus?.state === "failed" && ( + + Badge recalculation failed. Check the server logs. + + )} 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 }; +}; diff --git a/src/features/badges/server.ts b/src/features/badges/server.ts index 1b9ad54..a1dc509 100644 --- a/src/features/badges/server.ts +++ b/src/features/badges/server.ts @@ -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() diff --git a/src/lib/pocketbase/services/badges.ts b/src/lib/pocketbase/services/badges.ts index 5eb5e83..5378ec3 100644 --- a/src/lib/pocketbase/services/badges.ts +++ b/src/lib/pocketbase/services/badges.ts @@ -70,16 +70,6 @@ export function createBadgesService(pb: PocketBase) { return true; }, - async clearAllBadgeProgress(): Promise { - 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 { 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(); + const failedKeys = new Set(); + 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(); + 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", }; }, }; diff --git a/src/locales/de/messages.po b/src/locales/de/messages.po index 76ba343..234e6e6 100644 --- a/src/locales/de/messages.po +++ b/src/locales/de/messages.po @@ -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" diff --git a/src/locales/en/messages.po b/src/locales/en/messages.po index 6fa5db5..706241e 100644 --- a/src/locales/en/messages.po +++ b/src/locales/en/messages.po @@ -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" diff --git a/src/locales/es/messages.po b/src/locales/es/messages.po index 92cc0f4..660003e 100644 --- a/src/locales/es/messages.po +++ b/src/locales/es/messages.po @@ -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" diff --git a/src/locales/ja/messages.po b/src/locales/ja/messages.po index 64466e7..ca7c8f6 100644 --- a/src/locales/ja/messages.po +++ b/src/locales/ja/messages.po @@ -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 "最近終了した試合"