diff --git a/frontend-web/src/core/admin/api.ts b/frontend-web/src/core/admin/api.ts index 70d99b4a..62f1be45 100644 --- a/frontend-web/src/core/admin/api.ts +++ b/frontend-web/src/core/admin/api.ts @@ -247,6 +247,32 @@ export async function getUserLeaderboard( ); } +/** + * Force a synchronous rebuild of *today's* leaderboard snapshot so the latest + * real-time records are included, then return the leaderboard for `range`. + */ +export async function refreshTodayLeaderboard( + range: { + since: string; + until: string; + days?: number; + }, + limit = 20, +): Promise { + const params = new URLSearchParams({ + days: String(range.days ?? 30), + since: range.since, + until: range.until, + limit: String(limit), + }); + return parse( + await apiFetch(`${base()}/leaderboard/refresh-today?${params}`, { + method: "POST", + }), + "Failed to refresh today's leaderboard", + ); +} + export async function getUserAnalytics( userId: string, range: { since: string; until: string; days?: number }, diff --git a/frontend-web/src/core/admin/hooks.ts b/frontend-web/src/core/admin/hooks.ts index cf75c366..228dad1c 100644 --- a/frontend-web/src/core/admin/hooks.ts +++ b/frontend-web/src/core/admin/hooks.ts @@ -9,6 +9,7 @@ import { getUserMemory, getUserAnalytics, getUserLeaderboard, + refreshTodayLeaderboard, listUserQa, listUserThreadMessages, listUserThreads, @@ -41,6 +42,36 @@ export function useUserLeaderboard( }); } +/** + * Force-rebuild today's snapshot, then prime + invalidate the matching + * leaderboard query so the page shows the freshly recomputed numbers. + */ +export function useRefreshTodayLeaderboard() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ + range, + limit = 20, + }: { + range: { since: string; until: string; days?: number }; + limit?: number; + }) => refreshTodayLeaderboard(range, limit), + onSuccess: (data, variables) => { + qc.setQueryData( + [ + "admin-activity", + "leaderboard", + variables.range.since, + variables.range.until, + variables.limit ?? 20, + ], + data, + ); + qc.invalidateQueries({ queryKey: ["admin-activity", "leaderboard"] }); + }, + }); +} + export function useUserAnalytics( userId: string | null, range: { since: string; until: string; days?: number }, diff --git a/frontend-web/src/pages/AdminUserLeaderboardPage.tsx b/frontend-web/src/pages/AdminUserLeaderboardPage.tsx index 62652e7f..de98095f 100644 --- a/frontend-web/src/pages/AdminUserLeaderboardPage.tsx +++ b/frontend-web/src/pages/AdminUserLeaderboardPage.tsx @@ -18,6 +18,7 @@ import { Medal, MessageSquareText, MousePointerClick, + RefreshCw, Search, Settings2, Sparkles, @@ -55,7 +56,7 @@ import type { AdminLeaderboardUser, } from "@/core/admin/api"; import { streamUserLeaderboardReport } from "@/core/admin/api"; -import { useUserAnalytics, useUserLeaderboard } from "@/core/admin/hooks"; +import { useRefreshTodayLeaderboard, useUserAnalytics, useUserLeaderboard } from "@/core/admin/hooks"; import { useModels } from "@/core/models/hooks"; import { downloadMarkdownAsDocx } from "@/core/scheduled-tasks/markdown-docx"; import { cn } from "@/lib/utils"; @@ -1052,6 +1053,7 @@ export default function AdminUserLeaderboardPage() { [range], ); const { data, isLoading, error } = useUserLeaderboard(apiRange, 100); + const refreshToday = useRefreshTodayLeaderboard(); const [reportOpen, setReportOpen] = useState(false); const { models: reportModels, isLoading: reportModelsLoading } = useModels({ enabled: reportOpen }); const [reportModelName, setReportModelName] = useState(DEFAULT_REPORT_MODEL); @@ -1269,6 +1271,30 @@ export default function AdminUserLeaderboardPage() { > +