badges improvements
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user