mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-05 05:18:12 +03:00
feat: recalc profiles endpoint, frontend rivals fix, ublock origin fix
This commit is contained in:
@@ -71,7 +71,7 @@ function DashboardLoggedIn({ user }: { user: UserDocument }) {
|
||||
<UGPTProfiles />
|
||||
</Route>
|
||||
<Route exact path="/global-activity">
|
||||
<Activity url="/activity" />
|
||||
<Activity url="/ublock-blocks-this" />
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
@@ -239,7 +239,7 @@ function DashboardNotLoggedIn() {
|
||||
<div className="text-center">
|
||||
Nosey? Here's what our users are up to.
|
||||
<div style={{ fontSize: "1rem" }}>
|
||||
<Activity url="/activity" />
|
||||
<Activity url="/ublock-blocks-this" />
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
+4
-17
@@ -168,7 +168,10 @@ function PreferencesForm({
|
||||
defaultTable: settings.preferences.defaultTable,
|
||||
preferredDefaultEnum:
|
||||
settings.preferences.preferredDefaultEnum ?? gameConfig.preferredDefaultEnum,
|
||||
preferredRanking: settings.preferences.preferredRanking ?? "global",
|
||||
preferredRanking:
|
||||
settings.preferences.preferredRanking === "rival"
|
||||
? "global"
|
||||
: (settings.preferences.preferredRanking ?? "global"),
|
||||
},
|
||||
onSubmit: async (values) => {
|
||||
const rj = await APIFetchV1<UserDocument>(
|
||||
@@ -292,22 +295,6 @@ function PreferencesForm({
|
||||
What should {TachiConfig.NAME} default to showing you about folders?
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
{settings.rivals.length !== 0 && (
|
||||
<Form.Group className={formGroupClassNames}>
|
||||
<Form.Label>Preferred Ranking</Form.Label>
|
||||
<Form.Select
|
||||
id="preferredRanking"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.preferredRanking}
|
||||
>
|
||||
<option value="global">Global Rankings</option>
|
||||
<option value="rival">Rival Rankings</option>
|
||||
</Form.Select>
|
||||
<Form.Text className="text-body-secondary">
|
||||
What should {TachiConfig.NAME} default to when showing your score rankings?
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
)}
|
||||
<Form.Group className={formGroupClassNames}>
|
||||
<Form.Label>Preferred Table</Form.Label>
|
||||
<Form.Select
|
||||
|
||||
@@ -1,101 +1,28 @@
|
||||
import useLUGPTSettings from "#components/util/useLUGPTSettings";
|
||||
import React from "react";
|
||||
import { type integer } from "rg-stats/js/util/types";
|
||||
import { type PBScoreDocument } from "tachi-common";
|
||||
import { type integer, type PBScoreDocument } from "tachi-common";
|
||||
|
||||
import { rankingColumnTdStyle } from "./ranking-cell-layout";
|
||||
|
||||
export type RankingViewMode = "both-if-self" | "global" | "global-no-switch" | "rival";
|
||||
|
||||
const lineCls = "d-block text-truncate";
|
||||
const lineCls = "d-block";
|
||||
|
||||
export default function RankingCell({
|
||||
rankingData,
|
||||
userID,
|
||||
rankingViewMode,
|
||||
userID: _userID,
|
||||
rankingViewMode: _rankingViewMode,
|
||||
}: {
|
||||
rankingData: PBScoreDocument["rankingData"];
|
||||
rankingViewMode: RankingViewMode;
|
||||
userID: integer;
|
||||
}) {
|
||||
const { settings } = useLUGPTSettings();
|
||||
|
||||
switch (rankingViewMode) {
|
||||
case "global":
|
||||
case "global-no-switch": {
|
||||
const title = `#${rankingData.rank} / ${rankingData.outOf}`;
|
||||
return (
|
||||
<td style={rankingColumnTdStyle} title={title}>
|
||||
<div className={lineCls} style={{ minWidth: 0 }}>
|
||||
<strong>#{rankingData.rank}</strong>
|
||||
<small>/{rankingData.outOf}</small>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
case "rival": {
|
||||
if (!settings) {
|
||||
const err =
|
||||
"No Settings, yet tried to view rival stats? not possible. how'd you get here. report this.";
|
||||
return (
|
||||
<td style={rankingColumnTdStyle} title={err}>
|
||||
<div className={lineCls} style={{ minWidth: 0, fontSize: "0.7rem" }}>
|
||||
{err}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (settings?.userID !== userID) {
|
||||
return (
|
||||
<td style={rankingColumnTdStyle} title="N/A">
|
||||
<div className={lineCls} style={{ minWidth: 0 }}>
|
||||
<strong>N/A</strong>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
const denom = settings.rivals.length + 1;
|
||||
const title = `#${rankingData.rivalRank} / ${denom}`;
|
||||
return (
|
||||
<td style={rankingColumnTdStyle} title={title}>
|
||||
<div className={lineCls} style={{ minWidth: 0 }}>
|
||||
<strong>#{rankingData.rivalRank}</strong>
|
||||
<small>/{denom}</small>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
case "both-if-self": {
|
||||
if (settings?.userID === userID && rankingData.rivalRank !== null) {
|
||||
const globalTitle = `Global #${rankingData.rank} / ${rankingData.outOf}`;
|
||||
const rivalTitle = `Rival #${rankingData.rivalRank} / ${settings.rivals.length + 1}`;
|
||||
const fullTitle = `${globalTitle} — ${rivalTitle}`;
|
||||
return (
|
||||
<td style={rankingColumnTdStyle} title={fullTitle}>
|
||||
<div className={lineCls} style={{ fontSize: "0.76rem", minWidth: 0 }}>
|
||||
<strong>Global #{rankingData.rank}</strong>
|
||||
<small>/{rankingData.outOf}</small>
|
||||
</div>
|
||||
<div className={lineCls} style={{ fontSize: "0.76rem", minWidth: 0 }}>
|
||||
<strong>Rival #{rankingData.rivalRank}</strong>
|
||||
<small>/{settings.rivals.length + 1}</small>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
const title = `#${rankingData.rank} / ${rankingData.outOf}`;
|
||||
return (
|
||||
<td style={rankingColumnTdStyle} title={title}>
|
||||
<div className={lineCls} style={{ minWidth: 0 }}>
|
||||
<strong>#{rankingData.rank}</strong>
|
||||
<small>/{rankingData.outOf}</small>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
}
|
||||
const title = `#${rankingData.rank} / ${rankingData.outOf}`;
|
||||
return (
|
||||
<td style={rankingColumnTdStyle} title={title}>
|
||||
<div className={lineCls}>
|
||||
<strong>#{rankingData.rank}</strong>
|
||||
<small>/{rankingData.outOf}</small>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
/** Body + `<th>` must agree or `auto` tables size the column from the widest header cell. */
|
||||
export const RANKING_CELL_WIDTH_PX = 50;
|
||||
export const RANKING_CELL_WIDTH_PX = 104;
|
||||
|
||||
const rankingColumnBox: Pick<
|
||||
CSSProperties,
|
||||
"boxSizing" | "maxWidth" | "minWidth" | "overflow" | "width"
|
||||
"boxSizing" | "maxWidth" | "minWidth" | "overflow" | "whiteSpace" | "width"
|
||||
> = {
|
||||
boxSizing: "border-box",
|
||||
maxWidth: `${RANKING_CELL_WIDTH_PX}px`,
|
||||
maxWidth: "none",
|
||||
minWidth: `${RANKING_CELL_WIDTH_PX}px`,
|
||||
overflow: "hidden",
|
||||
overflow: "visible",
|
||||
whiteSpace: "nowrap",
|
||||
width: `${RANKING_CELL_WIDTH_PX}px`,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import Icon from "#components/util/Icon";
|
||||
import useLUGPTSettings from "#components/util/useLUGPTSettings";
|
||||
import { type SetState } from "#types/react";
|
||||
import React from "react";
|
||||
|
||||
@@ -8,9 +6,10 @@ import { type RankingViewMode } from "../cells/RankingCell";
|
||||
import SortableTH from "./SortableTH";
|
||||
import { type ZTableTHProps } from "./TachiTable";
|
||||
|
||||
/** Rival ranking scope is temporarily disabled; header is always a single “Ranking” column. */
|
||||
export default function SelectableRanking({
|
||||
rankingViewMode,
|
||||
setRankingViewMode,
|
||||
rankingViewMode: _rankingViewMode,
|
||||
setRankingViewMode: _setRankingViewMode,
|
||||
changeSort,
|
||||
currentSortMode,
|
||||
reverseSort,
|
||||
@@ -18,72 +17,15 @@ export default function SelectableRanking({
|
||||
rankingViewMode: RankingViewMode;
|
||||
setRankingViewMode: SetState<RankingViewMode>;
|
||||
} & ZTableTHProps) {
|
||||
const { settings } = useLUGPTSettings();
|
||||
|
||||
if (
|
||||
rankingViewMode === "both-if-self" ||
|
||||
rankingViewMode === "global-no-switch" ||
|
||||
!settings ||
|
||||
settings.rivals.length === 0
|
||||
) {
|
||||
return (
|
||||
<SortableTH
|
||||
changeSort={changeSort}
|
||||
currentSortMode={currentSortMode}
|
||||
name="Ranking"
|
||||
reverseSort={reverseSort}
|
||||
shortName="Ranking"
|
||||
sortingName="Site Ranking"
|
||||
style={rankingColumnThStyle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const sortHighlighted = currentSortMode === "Site Ranking";
|
||||
|
||||
return (
|
||||
<th style={rankingColumnThStyle}>
|
||||
<div
|
||||
className="vstack gap-0 justify-content-center"
|
||||
style={{ marginInline: "auto", maxWidth: "100%", minWidth: 0 }}
|
||||
>
|
||||
<select
|
||||
aria-label="Ranking scope"
|
||||
className="border-0 fw-bolder rounded focus-ring focus-ring-light bg-transparent text-body p-0"
|
||||
onChange={(v) => setRankingViewMode(v.target.value as RankingViewMode)}
|
||||
style={{
|
||||
display: "block",
|
||||
fontSize: "0.55rem",
|
||||
lineHeight: 1.1,
|
||||
maxWidth: "100%",
|
||||
width: "100%",
|
||||
}}
|
||||
title={rankingViewMode === "rival" ? "Rival Ranking" : "Global Ranking"}
|
||||
value={rankingViewMode}
|
||||
>
|
||||
<option value="global">Global</option>
|
||||
<option value="rival">Rival</option>
|
||||
</select>
|
||||
<div
|
||||
className="d-flex justify-content-center"
|
||||
onClick={() => changeSort("Site Ranking")}
|
||||
>
|
||||
<div className="d-flex justify-content-center gap-0">
|
||||
<Icon
|
||||
className={
|
||||
sortHighlighted && reverseSort ? "opacity-100" : "opacity-25"
|
||||
}
|
||||
type="arrow-up"
|
||||
/>
|
||||
<Icon
|
||||
className={
|
||||
sortHighlighted && !reverseSort ? "opacity-100" : "opacity-25"
|
||||
}
|
||||
type="arrow-down"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<SortableTH
|
||||
changeSort={changeSort}
|
||||
currentSortMode={currentSortMode}
|
||||
name="Ranking"
|
||||
reverseSort={reverseSort}
|
||||
shortName="Ranking"
|
||||
sortingName="Site Ranking"
|
||||
style={rankingColumnThStyle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,9 +23,7 @@ export function CreateRankingHeader<T>(
|
||||
return -Infinity;
|
||||
}
|
||||
|
||||
return rankingViewMode === "rival"
|
||||
? (rankingData.rivalRank ?? -Infinity)
|
||||
: rankingData.rank;
|
||||
return rankingData.rank;
|
||||
}),
|
||||
(thProps: ZTableTHProps) => (
|
||||
<SelectableRanking
|
||||
|
||||
@@ -7,5 +7,7 @@ export default function usePreferredRanking():
|
||||
| null {
|
||||
const { settings } = useLUGPTSettings();
|
||||
|
||||
return settings?.preferences.preferredRanking ?? null;
|
||||
const raw = settings?.preferences.preferredRanking ?? null;
|
||||
// Rival ranking display is disabled; treat stored preference as global.
|
||||
return raw === "rival" ? "global" : raw;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { MakeAction } from "#lib/actions/actions";
|
||||
import { drainGameProfileDirtyFully } from "#lib/jobs/drain-dirty-queues";
|
||||
import { EnqueueAllGameProfilesDirty } from "#utils/calculations/recalc-scores";
|
||||
import { IsUserAdmin } from "#utils/user";
|
||||
import { ExpectedErr } from "bliss";
|
||||
|
||||
export const ACTION_RecalcAllGameProfiles = MakeAction(
|
||||
"RECALC_ALL_GAME_PROFILES",
|
||||
async (taker) => {
|
||||
if (!(await IsUserAdmin(taker.acct.id))) {
|
||||
throw new ExpectedErr(403, "You are not authorized to perform this action.");
|
||||
}
|
||||
|
||||
await EnqueueAllGameProfilesDirty();
|
||||
await drainGameProfileDirtyFully();
|
||||
|
||||
return {};
|
||||
},
|
||||
);
|
||||
@@ -298,6 +298,10 @@ export const ActionSignatures = {
|
||||
rowCount: z.number().int(),
|
||||
}),
|
||||
},
|
||||
RECALC_ALL_GAME_PROFILES: {
|
||||
input: z.object({}),
|
||||
output: z.object({}),
|
||||
},
|
||||
IMPORT_SEEDS: {
|
||||
input: z.object({
|
||||
commitHash: z.string(),
|
||||
|
||||
@@ -29,6 +29,7 @@ import { ACTION_ImportSeeds } from "#actions/import-seeds";
|
||||
import { ACTION_InstallBuiltinClient } from "#actions/install-builtin-client";
|
||||
import { ACTION_MarkAllNotificationsRead } from "#actions/mark-all-notifications-read";
|
||||
import { ACTION_PatchUGPTSettings } from "#actions/patch-ugpt-settings";
|
||||
import { ACTION_RecalcAllGameProfiles } from "#actions/recalc-all-game-profiles";
|
||||
import { ACTION_RebuildFolderChartLookup } from "#actions/rebuild-folder-chart-lookup";
|
||||
import { ACTION_RemoveGoalSubscription } from "#actions/remove-goal-subscription";
|
||||
import { ACTION_ResendVerifyEmail } from "#actions/resend-verify-email";
|
||||
@@ -93,6 +94,7 @@ export const authenticatedActionHandlers = {
|
||||
MARK_ALL_NOTIFICATIONS_READ: ACTION_MarkAllNotificationsRead,
|
||||
PATCH_UGPT_SETTINGS: ACTION_PatchUGPTSettings,
|
||||
REBUILD_FOLDER_CHART_LOOKUP: ACTION_RebuildFolderChartLookup,
|
||||
RECALC_ALL_GAME_PROFILES: ACTION_RecalcAllGameProfiles,
|
||||
REMOVE_GOAL_SUBSCRIPTION: ACTION_RemoveGoalSubscription,
|
||||
RESEND_VERIFY_EMAIL: ACTION_ResendVerifyEmail,
|
||||
RESET_API_CLIENT_SECRET: ACTION_ResetApiClientSecret,
|
||||
|
||||
@@ -243,6 +243,20 @@ export async function drainGameProfileDirty(): Promise<number> {
|
||||
return rows.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drain `game_profile_dirty` until empty (no per-tick row cap). For admin synchronous
|
||||
* profile recalculation.
|
||||
*/
|
||||
export async function drainGameProfileDirtyFully(): Promise<void> {
|
||||
while (true) {
|
||||
const n = await drainGameProfileDirty();
|
||||
|
||||
if (n === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drain `score_rederive`, then `pb_dirty`, then `session_dirty`, then `game_profile_dirty`,
|
||||
* repeating until a full pass does nothing. Each queue has its own per-tick row budget so
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
import { GetRecentActivityForMultipleGames } from "#lib/activity/activity";
|
||||
import { success } from "#lib/router/typed-router";
|
||||
import { ALL_GAMES } from "tachi-common";
|
||||
|
||||
import { API_V1_ROUTER } from "../router";
|
||||
|
||||
async function globalActivityImpl(input: { startTime?: number }) {
|
||||
const data = await GetRecentActivityForMultipleGames(
|
||||
ALL_GAMES,
|
||||
undefined,
|
||||
input.startTime ?? null,
|
||||
);
|
||||
|
||||
return success(`Returned global activity.`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve *all* activity across every game on the site.
|
||||
*
|
||||
* @param session - See CreateActivityRouteHandler
|
||||
* @param startTime - See CreateActivityRouteHandler
|
||||
*/
|
||||
API_V1_ROUTER.add("GET /activity", async ({ input }) => {
|
||||
const data = await GetRecentActivityForMultipleGames(
|
||||
ALL_GAMES,
|
||||
undefined,
|
||||
input.startTime ?? null,
|
||||
);
|
||||
API_V1_ROUTER.add("GET /activity", ({ input }) => globalActivityImpl(input));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
description: `Returned global activity.`,
|
||||
body: data,
|
||||
};
|
||||
});
|
||||
/**
|
||||
* Same behavior as `GET /activity`. Some browser blocklists match `/activity`;
|
||||
* use this path when an ad blocker interferes.
|
||||
*/
|
||||
API_V1_ROUTER.add("GET /ublock-blocks-this", ({ input }) => globalActivityImpl(input));
|
||||
|
||||
@@ -194,6 +194,58 @@ describe("POST /api/v1/admin/recalc", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/v1/admin/recalc-profiles", () => {
|
||||
it("returns 403 when the caller is not an admin", async () => {
|
||||
await seedUser({
|
||||
username: "prof_pleb",
|
||||
email: "prof_pleb@test.com",
|
||||
withCredential: true,
|
||||
withSettings: true,
|
||||
});
|
||||
|
||||
const plebCookie = await loginAs("prof_pleb");
|
||||
|
||||
const res = await mockApi
|
||||
.post("/api/v1/admin/recalc-profiles")
|
||||
.set("Cookie", plebCookie)
|
||||
.send({});
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
|
||||
it("drains game_profile_dirty when the caller is an admin", async () => {
|
||||
const { id: adminId } = await seedUser({
|
||||
username: "prof_admin",
|
||||
email: "prof_admin@test.com",
|
||||
authLevel: "admin",
|
||||
withCredential: true,
|
||||
withSettings: true,
|
||||
});
|
||||
|
||||
const adminCookie = await loginAs("prof_admin");
|
||||
|
||||
await DB.insertInto("game_profile_dirty")
|
||||
.values({ user_id: adminId, game: "iidx-sp" })
|
||||
.onConflict((oc) => oc.doNothing())
|
||||
.execute();
|
||||
|
||||
const res = await mockApi
|
||||
.post("/api/v1/admin/recalc-profiles")
|
||||
.set("Cookie", adminCookie)
|
||||
.send({});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const stillQueued = await DB.selectFrom("game_profile_dirty")
|
||||
.select("game_profile_dirty.user_id")
|
||||
.where("game_profile_dirty.user_id", "=", adminId)
|
||||
.where("game_profile_dirty.game", "=", "iidx-sp")
|
||||
.executeTakeFirst();
|
||||
|
||||
expect(stillQueued).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/v1/admin/recalc-pbs", () => {
|
||||
it("returns 403 when the caller is not an admin", async () => {
|
||||
await seedUser({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ACTION_DeleteScore } from "#actions/delete-score";
|
||||
import { ACTION_DeleteSession } from "#actions/delete-session";
|
||||
import { ACTION_RebuildFolderChartLookup } from "#actions/rebuild-folder-chart-lookup";
|
||||
import { ACTION_RecalcAllGameProfiles } from "#actions/recalc-all-game-profiles";
|
||||
import { ACTION_SetUserQuestSubmitterStatus } from "#actions/set-user-quest-submitter-status";
|
||||
import { ACTION_SetUserSupporterStatus } from "#actions/set-user-supporter-status";
|
||||
import {
|
||||
@@ -126,6 +127,19 @@ API_V1_ROUTER.add("POST /admin/recalc", withAdmin, async () => {
|
||||
);
|
||||
});
|
||||
|
||||
API_V1_ROUTER.add("POST /admin/recalc-profiles", withAdmin, async ({ req }) => {
|
||||
const adminUserID = req[SYMBOL_TACHI_API_AUTH].userID!;
|
||||
const adminUser = await GetUserWithIDGuaranteed(adminUserID);
|
||||
const taker = { acct: { id: adminUser.id, username: adminUser.username }, ip: req.ip };
|
||||
|
||||
await ACTION_RecalcAllGameProfiles(taker, {});
|
||||
|
||||
return success(
|
||||
"Enqueued all game_profile rows and distinct committed score (user, game) pairs into game_profile_dirty, then drained that queue until idle.",
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
API_V1_ROUTER.add("POST /admin/announcement", withAdmin, async ({ input }) => {
|
||||
const game = input.game as V3Game | undefined;
|
||||
|
||||
|
||||
@@ -194,6 +194,13 @@ export const API_V1_SPEC = {
|
||||
output: z.record(z.string(), doc<ActivityPayload>()),
|
||||
},
|
||||
|
||||
"GET /ublock-blocks-this": {
|
||||
description:
|
||||
"Global recent activity across all games (alias of GET /activity for clients affected by blocklists that match `/activity`).",
|
||||
input: z.object({ startTime: z.coerce.number().optional() }),
|
||||
output: z.record(z.string(), doc<ActivityPayload>()),
|
||||
},
|
||||
|
||||
// ────────────────────────────────────────────────
|
||||
// Search
|
||||
// ────────────────────────────────────────────────
|
||||
@@ -1987,6 +1994,13 @@ export const API_V1_SPEC = {
|
||||
output: empty,
|
||||
},
|
||||
|
||||
"POST /admin/recalc-profiles": {
|
||||
description:
|
||||
"Enqueue every `game_profile` row and every distinct committed `(user_id, game)` from `score` into `game_profile_dirty`, then synchronously drain that queue until idle (recomputes ratings/classes from current PBs). No request body.",
|
||||
input: z.object({}),
|
||||
output: empty,
|
||||
},
|
||||
|
||||
"POST /admin/announcement": {
|
||||
description: "Create a site announcement.",
|
||||
input: z.object({
|
||||
|
||||
+1
-5
@@ -14,11 +14,7 @@ import { GetScoreIdsGroupedBySessionId } from "#utils/queries/sessions";
|
||||
import { CheckStrSessionAlg } from "#utils/string-checks";
|
||||
import { ExpectedErr } from "bliss";
|
||||
import { sql } from "kysely";
|
||||
import {
|
||||
type AnySessionRatingAlg,
|
||||
GetGameConfig,
|
||||
LEGACY_GameToGameGroupPT,
|
||||
} from "tachi-common";
|
||||
import { type AnySessionRatingAlg, GetGameConfig, LEGACY_GameToGameGroupPT } from "tachi-common";
|
||||
import { type Game } from "tachi-db";
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,3 +22,27 @@ export async function UpdateAllPBs(): Promise<void> {
|
||||
.onConflict((oc) => oc.doNothing())
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue every existing `game_profile` row and every distinct (user, playtype) with a
|
||||
* committed score into `game_profile_dirty` so workers (or admin drain) will recompute
|
||||
* ratings/classes from current PBs.
|
||||
*/
|
||||
export async function EnqueueAllGameProfilesDirty(): Promise<void> {
|
||||
await DB.insertInto("game_profile_dirty")
|
||||
.expression(
|
||||
DB.selectFrom("game_profile").select(["game_profile.user_id", "game_profile.game"]),
|
||||
)
|
||||
.onConflict((oc) => oc.doNothing())
|
||||
.execute();
|
||||
|
||||
await DB.insertInto("game_profile_dirty")
|
||||
.expression(
|
||||
DB.selectFrom("score")
|
||||
.select(["score.user_id", "score.game"])
|
||||
.where("score.committed", "=", true)
|
||||
.distinct(),
|
||||
)
|
||||
.onConflict((oc) => oc.doNothing())
|
||||
.execute();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user