brackt/app/models/simulator.ts
Claude 88248e349c
All checks were successful
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
🚀 Deploy / 🧪 Test (pull_request) Successful in 2m58s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m23s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
Fix futures odds being ignored when stale Elo exists
When an admin entered futures (preseason) odds for a season that already
had Elo ratings stored, the simulator kept using the old Elo and silently
ignored the new odds. This affected any Elo-based simulator (e.g. NHL).

Root cause: resolveSourceElos() ranks a direct sourceElo above the
sourceOdds -> convertFuturesToElo branch, but batchSaveFuturesOddsForSimulator()
only cleared the bracket-seeding `rating`/`ratingMethod` — never the stale
`sourceElo`/`sourceEloMethod`. A manually entered Elo (method "direct") is not
treated as generated, so it survived and short-circuited the resolver.

Fix:
- batchSaveFuturesOddsForSimulator now also nulls sourceElo and strips
  sourceEloMethod (both the pre-update and upsert-conflict paths), so the
  existing futures -> Elo conversion drives the run.
- resolveSourceElos' sourceOdds branch now guards for >= 2 participants
  (mirroring resolveRatings), so a lone-odds season falls through to the
  configured missing-Elo strategy instead of getting a flat ~1500.
- batchSaveSourceOdds clears the legacy EV sourceElo and marks source as
  futures_odds so the elo-ratings page won't resurrect a stale rating.

Adds unit coverage for odds-derived Elo, the single-participant guard, the
post-clear regression, generated-vs-direct sourceElo suppression, and the
new clearing behavior in batchSaveFuturesOddsForSimulator.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNfUEd9RzD3zm84oLHBHUH
2026-06-25 17:43:31 +00:00

719 lines
26 KiB
TypeScript

import { and, eq, inArray, sql } from "drizzle-orm";
import { database } from "~/database/context";
import * as schema from "~/database/schema";
import {
getManifestSimulatorProfile,
simulatorInputLabel,
type SimulatorInputKey,
type SimulatorManifestProfile,
} from "~/services/simulations/manifest";
import {
ratingRequirementLabel,
resolveRatings,
resolveSourceElos,
sourceEloRequirementLabel,
} from "~/services/simulations/input-policy";
import { SIMULATOR_TYPES, type SimulatorType } from "~/services/simulations/registry";
export interface SimulatorProfile extends SimulatorManifestProfile {
isActive: boolean;
}
export interface SportsSeasonSimulatorConfig {
sportsSeasonId: string;
simulatorType: SimulatorType;
config: Record<string, unknown>;
profile: SimulatorProfile;
}
export interface ParticipantSimulatorInput {
participantId: string;
sportsSeasonId: string;
sourceOdds: number | null;
sourceElo: number | null;
worldRanking: number | null;
rating: number | null;
projectedWins: number | null;
projectedTablePoints: number | null;
seed: number | null;
region: string | null;
metadata: Record<string, unknown> | null;
}
export interface UpsertParticipantSimulatorInput {
participantId: string;
sportsSeasonId: string;
sourceOdds?: number | null;
sourceElo?: number | null;
worldRanking?: number | null;
rating?: number | null;
projectedWins?: number | null;
projectedTablePoints?: number | null;
seed?: number | null;
region?: string | null;
metadata?: Record<string, unknown> | null;
}
export interface SimulatorReadiness {
status: "ready" | "not_ready";
canRun: boolean;
participantCount: number;
participantInputCount: number;
derivedInputCount: number;
fallbackInputCount: number;
missingInputs: string[];
warnings: string[];
}
export interface SportsSeasonSimulatorSummary {
sportsSeasonId: string;
seasonName: string;
year: number;
seasonStatus: string;
simulationStatus: string;
fantasySeasonId: string | null;
fantasySeasonName: string | null;
leagueName: string | null;
sportName: string;
sportSlug: string;
simulatorType: SimulatorType;
simulatorName: string;
participantCount: number;
participantInputCount: number;
lastSimulatedDate: string | null;
readiness: SimulatorReadiness;
}
function parseDecimal(value: string | null | undefined): number | null {
if (value === null || value === undefined) return null;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
function isSimulatorType(value: string | null | undefined): value is SimulatorType {
return typeof value === "string" && (SIMULATOR_TYPES as readonly string[]).includes(value);
}
function mergeProfile(
simulatorType: SimulatorType,
dbProfile?: typeof schema.simulatorProfiles.$inferSelect | null
): SimulatorProfile {
const manifest = getManifestSimulatorProfile(simulatorType);
if (!dbProfile) return { ...manifest, isActive: true };
return {
...manifest,
displayName: dbProfile.displayName,
description: dbProfile.description ?? manifest.description,
defaultConfig: {
...manifest.defaultConfig,
...dbProfile.defaultConfig,
},
isActive: dbProfile.isActive,
};
}
function hasInputValue(input: ParticipantSimulatorInput, key: SimulatorInputKey): boolean {
const value = input[key];
if (value === null || value === undefined) return false;
if (typeof value === "string") return value.trim().length > 0;
if (typeof value === "object") return Object.keys(value).length > 0;
return true;
}
function isFallbackMethod(method: string): boolean {
return method === "fallbackElo" || method === "fallbackRating" || method === "averageKnown" || method === "worstKnownMinus";
}
const GENERATED_RATING_METHODS = ["sourceOdds", "fallbackRating", "averageKnown", "worstKnownMinus"] as const;
const GENERATED_SOURCE_ELO_METHODS = ["projectedWins", "projectedTablePoints", "sourceOdds", "fallbackElo", "averageKnown", "worstKnownMinus"] as const;
function isGeneratedRatingMethod(method: unknown): boolean {
return typeof method === "string" && GENERATED_RATING_METHODS.includes(method as (typeof GENERATED_RATING_METHODS)[number]);
}
function isGeneratedSourceEloMethod(method: unknown): boolean {
return typeof method === "string" && GENERATED_SOURCE_ELO_METHODS.includes(method as (typeof GENERATED_SOURCE_ELO_METHODS)[number]);
}
function isGeneratedRatingInput(input: typeof schema.seasonParticipantSimulatorInputs.$inferSelect | undefined): boolean {
return isGeneratedRatingMethod(input?.metadata?.ratingMethod);
}
function isGeneratedSourceEloInput(input: typeof schema.seasonParticipantSimulatorInputs.$inferSelect | undefined): boolean {
return isGeneratedSourceEloMethod(input?.metadata?.sourceEloMethod);
}
function inputRequirementLabel(
key: SimulatorInputKey,
profile: SimulatorManifestProfile
): string {
if (key === "sourceElo" && profile.derivableInputs?.sourceElo?.length) {
return sourceEloRequirementLabel(profile);
}
if (key === "rating" && profile.derivableInputs?.rating?.length) {
return ratingRequirementLabel(profile);
}
return simulatorInputLabel(key);
}
export async function getSimulatorProfile(simulatorType: SimulatorType): Promise<SimulatorProfile> {
const db = database();
const dbProfile = await db.query.simulatorProfiles.findFirst({
where: eq(schema.simulatorProfiles.simulatorType, simulatorType),
});
return mergeProfile(simulatorType, dbProfile);
}
export async function upsertSimulatorProfile(
profile: Pick<SimulatorProfile, "simulatorType" | "displayName" | "description" | "defaultConfig"> & {
inputSchema?: Record<string, unknown>;
isActive?: boolean;
}
): Promise<void> {
const db = database();
const now = new Date();
await db
.insert(schema.simulatorProfiles)
.values({
simulatorType: profile.simulatorType,
displayName: profile.displayName,
description: profile.description,
defaultConfig: profile.defaultConfig,
inputSchema: profile.inputSchema ?? {},
isActive: profile.isActive ?? true,
createdAt: now,
updatedAt: now,
})
.onConflictDoUpdate({
target: schema.simulatorProfiles.simulatorType,
set: {
displayName: sql`excluded.display_name`,
description: sql`excluded.description`,
defaultConfig: sql`excluded.default_config`,
inputSchema: sql`excluded.input_schema`,
isActive: sql`excluded.is_active`,
updatedAt: now,
},
});
}
export async function getSportsSeasonSimulatorConfig(
sportsSeasonId: string
): Promise<SportsSeasonSimulatorConfig | null> {
const db = database();
const season = await db.query.sportsSeasons.findFirst({
where: eq(schema.sportsSeasons.id, sportsSeasonId),
with: {
sport: true,
simulatorConfig: true,
},
});
const simulatorType = season?.simulatorConfig?.simulatorType ?? season?.sport?.simulatorType;
if (!season || !isSimulatorType(simulatorType)) return null;
const profile = await getSimulatorProfile(simulatorType);
const config = {
...profile.defaultConfig,
...season.simulatorConfig?.config,
};
return {
sportsSeasonId,
simulatorType,
config,
profile,
};
}
export async function upsertSportsSeasonSimulatorConfig(input: {
sportsSeasonId: string;
simulatorType: SimulatorType;
config?: Record<string, unknown>;
}): Promise<void> {
const db = database();
const now = new Date();
await db
.insert(schema.sportsSeasonSimulatorConfigs)
.values({
sportsSeasonId: input.sportsSeasonId,
simulatorType: input.simulatorType,
config: input.config ?? {},
createdAt: now,
updatedAt: now,
})
.onConflictDoUpdate({
target: schema.sportsSeasonSimulatorConfigs.sportsSeasonId,
set: {
simulatorType: sql`excluded.simulator_type`,
config: sql`excluded.config`,
updatedAt: now,
},
});
}
export async function getParticipantSimulatorInputs(
sportsSeasonId: string
): Promise<ParticipantSimulatorInput[]> {
const db = database();
const [seasonParticipants, simulatorInputs, legacyEvs] = await Promise.all([
db.query.seasonParticipants.findMany({
where: eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId),
columns: { id: true },
}),
db.query.seasonParticipantSimulatorInputs.findMany({
where: eq(schema.seasonParticipantSimulatorInputs.sportsSeasonId, sportsSeasonId),
}),
db.query.seasonParticipantExpectedValues.findMany({
where: eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId),
columns: {
participantId: true,
sourceOdds: true,
sourceElo: true,
worldRanking: true,
},
}),
]);
const simulatorByParticipant = new Map(simulatorInputs.map((row) => [row.participantId, row]));
const legacyByParticipant = new Map(legacyEvs.map((row) => [row.participantId, row]));
return seasonParticipants.map((participant) => {
const input = simulatorByParticipant.get(participant.id);
const legacy = legacyByParticipant.get(participant.id);
return {
participantId: participant.id,
sportsSeasonId,
sourceOdds: input?.sourceOdds ?? legacy?.sourceOdds ?? null,
sourceElo: input
? isGeneratedSourceEloInput(input) ? null : input.sourceElo
: legacy?.sourceElo ?? null,
worldRanking: input?.worldRanking ?? legacy?.worldRanking ?? null,
rating: isGeneratedRatingInput(input) ? null : parseDecimal(input?.rating),
projectedWins: parseDecimal(input?.projectedWins),
projectedTablePoints: parseDecimal(input?.projectedTablePoints),
seed: input?.seed ?? null,
region: input?.region ?? null,
metadata: input?.metadata ?? null,
};
});
}
function generatedRatingMethodSql() {
return sql`${schema.seasonParticipantSimulatorInputs.metadata}->>'ratingMethod' in ('sourceOdds', 'fallbackRating', 'averageKnown', 'worstKnownMinus')`;
}
export async function batchSaveParticipantSimulatorSourceOdds(
inputs: Array<{ participantId: string; sportsSeasonId: string; sourceOdds: number }>
): Promise<void> {
if (inputs.length === 0) return;
const db = database();
const now = new Date();
const seasonIds = [...new Set(inputs.map((input) => input.sportsSeasonId))];
await db.transaction(async (tx) => {
await tx
.update(schema.seasonParticipantSimulatorInputs)
.set({
rating: null,
metadata: sql`coalesce(${schema.seasonParticipantSimulatorInputs.metadata}, '{}'::jsonb) - 'ratingMethod'`,
updatedAt: now,
})
.where(
and(
inArray(schema.seasonParticipantSimulatorInputs.sportsSeasonId, seasonIds),
generatedRatingMethodSql()
)
);
await tx
.insert(schema.seasonParticipantSimulatorInputs)
.values(
inputs.map((input) => ({
participantId: input.participantId,
sportsSeasonId: input.sportsSeasonId,
sourceOdds: input.sourceOdds,
createdAt: now,
updatedAt: now,
}))
)
.onConflictDoUpdate({
target: [
schema.seasonParticipantSimulatorInputs.participantId,
schema.seasonParticipantSimulatorInputs.sportsSeasonId,
],
set: {
sourceOdds: sql`excluded.source_odds`,
rating: sql`case when ${generatedRatingMethodSql()} then null else ${schema.seasonParticipantSimulatorInputs.rating} end`,
metadata: sql`case when ${generatedRatingMethodSql()} then coalesce(${schema.seasonParticipantSimulatorInputs.metadata}, '{}'::jsonb) - 'ratingMethod' else ${schema.seasonParticipantSimulatorInputs.metadata} end`,
updatedAt: now,
},
});
});
}
export async function batchSaveFuturesOddsForSimulator(
inputs: Array<{ participantId: string; sportsSeasonId: string; sourceOdds: number }>
): Promise<void> {
if (inputs.length === 0) return;
const db = database();
const now = new Date();
const participantIds = inputs.map((input) => input.participantId);
const seasonIds = [...new Set(inputs.map((input) => input.sportsSeasonId))];
await db.transaction(async (tx) => {
// Clear ALL ratings AND sourceElo (both manual and generated) so the
// simulation re-derives them from the newly saved futures odds. For
// Elo-based simulators, a stale `sourceElo` (e.g. a manually entered
// "direct" Elo) would otherwise win the resolveSourceElos precedence and
// cause the futures odds to be silently ignored. Nulling it here lets the
// sourceOdds -> convertFuturesToElo branch drive the run.
await tx
.update(schema.seasonParticipantSimulatorInputs)
.set({
rating: null,
sourceElo: null,
metadata: sql`coalesce(${schema.seasonParticipantSimulatorInputs.metadata}, '{}'::jsonb) - 'ratingMethod' - 'sourceEloMethod'`,
updatedAt: now,
})
.where(
and(
inArray(schema.seasonParticipantSimulatorInputs.sportsSeasonId, seasonIds),
inArray(schema.seasonParticipantSimulatorInputs.participantId, participantIds)
)
);
await tx
.insert(schema.seasonParticipantSimulatorInputs)
.values(
inputs.map((input) => ({
participantId: input.participantId,
sportsSeasonId: input.sportsSeasonId,
sourceOdds: input.sourceOdds,
createdAt: now,
updatedAt: now,
}))
)
.onConflictDoUpdate({
target: [
schema.seasonParticipantSimulatorInputs.participantId,
schema.seasonParticipantSimulatorInputs.sportsSeasonId,
],
set: {
sourceOdds: sql`excluded.source_odds`,
rating: null,
sourceElo: null,
metadata: sql`coalesce(${schema.seasonParticipantSimulatorInputs.metadata}, '{}'::jsonb) - 'ratingMethod' - 'sourceEloMethod'`,
updatedAt: now,
},
});
});
}
export async function batchUpsertParticipantSimulatorInputs(
inputs: UpsertParticipantSimulatorInput[]
): Promise<void> {
if (inputs.length === 0) return;
const db = database();
const now = new Date();
await db.transaction(async (tx) => {
await tx
.insert(schema.seasonParticipantSimulatorInputs)
.values(
inputs.map((input) => ({
participantId: input.participantId,
sportsSeasonId: input.sportsSeasonId,
sourceOdds: input.sourceOdds ?? null,
sourceElo: input.sourceElo ?? null,
worldRanking: input.worldRanking ?? null,
rating: input.rating?.toString() ?? null,
projectedWins: input.projectedWins?.toString() ?? null,
projectedTablePoints: input.projectedTablePoints?.toString() ?? null,
seed: input.seed ?? null,
region: input.region ?? null,
metadata: input.metadata ?? null,
createdAt: now,
updatedAt: now,
}))
)
.onConflictDoUpdate({
target: [
schema.seasonParticipantSimulatorInputs.participantId,
schema.seasonParticipantSimulatorInputs.sportsSeasonId,
],
set: {
sourceOdds: sql`excluded.source_odds`,
sourceElo: sql`excluded.source_elo`,
worldRanking: sql`excluded.world_ranking`,
rating: sql`excluded.rating`,
projectedWins: sql`excluded.projected_wins`,
projectedTablePoints: sql`excluded.projected_table_points`,
seed: sql`excluded.seed`,
region: sql`excluded.region`,
metadata: sql`excluded.metadata`,
updatedAt: sql`excluded.updated_at`,
},
});
// Compatibility bridge: keep legacy simulator metadata columns populated until
// every simulator has been migrated to season_participant_simulator_inputs.
await tx
.insert(schema.seasonParticipantExpectedValues)
.values(
inputs.map((input) => ({
participantId: input.participantId,
sportsSeasonId: input.sportsSeasonId,
probFirst: "0",
probSecond: "0",
probThird: "0",
probFourth: "0",
probFifth: "0",
probSixth: "0",
probSeventh: "0",
probEighth: "0",
expectedValue: "0",
source: input.sourceOdds !== null && input.sourceOdds !== undefined ? "futures_odds" as const : "elo_simulation" as const,
sourceOdds: input.sourceOdds ?? null,
sourceElo: input.sourceElo ?? null,
worldRanking: input.worldRanking ?? null,
calculatedAt: now,
updatedAt: now,
}))
)
.onConflictDoUpdate({
target: [
schema.seasonParticipantExpectedValues.participantId,
schema.seasonParticipantExpectedValues.sportsSeasonId,
],
set: {
sourceOdds: sql`COALESCE(excluded.source_odds, ${schema.seasonParticipantExpectedValues.sourceOdds})`,
sourceElo: sql`COALESCE(excluded.source_elo, ${schema.seasonParticipantExpectedValues.sourceElo})`,
worldRanking: sql`COALESCE(excluded.world_ranking, ${schema.seasonParticipantExpectedValues.worldRanking})`,
updatedAt: now,
},
});
});
}
export async function validateSimulatorReadiness(
sportsSeasonId: string
): Promise<SimulatorReadiness> {
const config = await getSportsSeasonSimulatorConfig(sportsSeasonId);
const inputs = await getParticipantSimulatorInputs(sportsSeasonId);
const missingInputs: string[] = [];
const warnings: string[] = [];
let derivedInputCount = 0;
let fallbackInputCount = 0;
if (!config) {
return {
status: "not_ready",
canRun: false,
participantCount: inputs.length,
participantInputCount: 0,
derivedInputCount,
fallbackInputCount,
missingInputs: ["simulator type"],
warnings,
};
}
const participantCount = inputs.length;
if (participantCount === 0) {
missingInputs.push("participants");
}
const requiredInputs = config.profile.requiredInputs;
const resolvedSourceElos = requiredInputs.includes("sourceElo")
? resolveSourceElos(inputs, config.profile, config.config)
: new Map();
const resolvedRatings = requiredInputs.includes("rating")
? resolveRatings(inputs, config.profile, config.config)
: new Map();
derivedInputCount =
[...resolvedSourceElos.values()].filter((input) => input.method !== "direct").length +
[...resolvedRatings.values()].filter((input) => input.method !== "direct").length;
fallbackInputCount =
[...resolvedSourceElos.values()].filter((input) => isFallbackMethod(input.method)).length +
[...resolvedRatings.values()].filter((input) => isFallbackMethod(input.method)).length;
function hasRequiredInput(input: ParticipantSimulatorInput, key: SimulatorInputKey): boolean {
if (hasInputValue(input, key)) return true;
if (key === "sourceElo") return resolvedSourceElos.has(input.participantId);
if (key === "rating") return resolvedRatings.has(input.participantId);
return false;
}
const participantInputCount = requiredInputs.length === 0
? participantCount
: inputs.filter((input) => requiredInputs.every((key) => hasRequiredInput(input, key))).length;
if (requiredInputs.length > 0 && participantInputCount < participantCount) {
for (const key of requiredInputs) {
const missingCount = inputs.filter((input) => !hasRequiredInput(input, key)).length;
if (missingCount > 0) {
missingInputs.push(`${inputRequirementLabel(key, config.profile)} for ${missingCount} participant(s)`);
}
}
}
if (derivedInputCount > 0) {
warnings.push(`Derived simulator inputs will be used for ${derivedInputCount} participant(s).`);
}
if (fallbackInputCount > 0) {
warnings.push(`Configured fallback simulator inputs will be used for ${fallbackInputCount} participant(s).`);
}
if (config.simulatorType === "cs2_major_qualifying_points") {
const missingRankCount = inputs.filter((input) => !hasInputValue(input, "worldRanking")).length;
if (missingRankCount > 0) {
warnings.push(
`CS2 will use rank 9999 for ${missingRankCount} participant(s) without world ranking; this is acceptable for deep-tail teams but less precise.`
);
}
}
if (config.profile.setupSections.includes("regularStandings")) {
warnings.push("Regular-season standings may be needed for in-season accuracy.");
}
if (config.profile.setupSections.includes("bracket")) {
warnings.push("Bracket-aware simulators use completed bracket results when available.");
}
const status = missingInputs.length === 0 ? "ready" : "not_ready";
return {
status,
canRun: status === "ready",
participantCount,
participantInputCount,
derivedInputCount,
fallbackInputCount,
missingInputs,
warnings,
};
}
export async function prepareSimulatorInputsForRun(sportsSeasonId: string): Promise<void> {
const config = await getSportsSeasonSimulatorConfig(sportsSeasonId);
if (!config) return;
const inputs = await getParticipantSimulatorInputs(sportsSeasonId);
const resolvedSourceElos = config.profile.requiredInputs.includes("sourceElo")
? resolveSourceElos(inputs, config.profile, config.config)
: new Map();
const resolvedRatings = config.profile.requiredInputs.includes("rating")
? resolveRatings(inputs, config.profile, config.config)
: new Map();
const rowsToUpsert: UpsertParticipantSimulatorInput[] = [];
for (const input of inputs) {
const resolvedElo = resolvedSourceElos.get(input.participantId);
const resolvedRating = resolvedRatings.get(input.participantId);
if (!resolvedElo && !resolvedRating) continue;
rowsToUpsert.push({
participantId: input.participantId,
sportsSeasonId,
sourceOdds: input.sourceOdds,
sourceElo: resolvedElo?.sourceElo ?? input.sourceElo,
worldRanking: input.worldRanking,
rating: resolvedRating?.rating ?? input.rating,
projectedWins: input.projectedWins,
projectedTablePoints: input.projectedTablePoints,
seed: input.seed,
region: input.region,
metadata: {
...input.metadata,
...(resolvedElo ? { sourceEloMethod: resolvedElo.method } : {}),
...(resolvedRating ? { ratingMethod: resolvedRating.method } : {}),
},
});
}
await batchUpsertParticipantSimulatorInputs(rowsToUpsert);
}
export async function listSportsSeasonSimulatorSummaries(): Promise<SportsSeasonSimulatorSummary[]> {
const db = database();
const seasons = await db.query.sportsSeasons.findMany({
with: {
sport: true,
fantasySeason: { with: { league: true } },
participants: { columns: { id: true } },
simulatorConfig: true,
},
});
const simulatorSeasons = seasons.filter((season) => {
const simulatorType = season.simulatorConfig?.simulatorType ?? season.sport.simulatorType;
if (!isSimulatorType(simulatorType)) return false;
// Brackt's public sports season is a draftable template. The runnable
// simulations are the per-league private Brackt seasons.
if (season.sport.slug === "brackt") return season.fantasySeasonId !== null;
return season.fantasySeasonId === null;
});
const seasonIds = simulatorSeasons.map((season) => season.id);
const snapshotRows = seasonIds.length > 0
? await db
.select({
sportsSeasonId: schema.participantEvSnapshots.sportsSeasonId,
lastSimulatedDate: sql<string>`max(${schema.participantEvSnapshots.snapshotDate})`,
})
.from(schema.participantEvSnapshots)
.where(inArray(schema.participantEvSnapshots.sportsSeasonId, seasonIds))
.groupBy(schema.participantEvSnapshots.sportsSeasonId)
: [];
const lastSnapshotBySeason = new Map(snapshotRows.map((row) => [row.sportsSeasonId, row.lastSimulatedDate]));
// Each season calls getSimulatorProfile + validateSimulatorReadiness (~4 queries each).
// Acceptable for an admin-only page; revisit if season counts grow past ~50.
const summaries = await Promise.all(
simulatorSeasons.map(async (season) => {
const simulatorType = (season.simulatorConfig?.simulatorType ?? season.sport.simulatorType) as SimulatorType;
const profile = await getSimulatorProfile(simulatorType);
const readiness = await validateSimulatorReadiness(season.id);
return {
sportsSeasonId: season.id,
seasonName: season.name,
year: season.year,
seasonStatus: season.status,
simulationStatus: season.simulationStatus,
fantasySeasonId: season.fantasySeasonId,
fantasySeasonName: season.fantasySeason ? `${season.fantasySeason.year} Season` : null,
leagueName: season.fantasySeason?.league?.name ?? null,
sportName: season.sport.name,
sportSlug: season.sport.slug,
simulatorType,
simulatorName: profile.displayName,
participantCount: season.participants.length,
participantInputCount: readiness.participantInputCount,
lastSimulatedDate: lastSnapshotBySeason.get(season.id) ?? null,
readiness,
};
})
);
return summaries.toSorted((a, b) =>
a.sportName.localeCompare(b.sportName) ||
(a.leagueName ?? "").localeCompare(b.leagueName ?? "") ||
b.year - a.year ||
a.seasonName.localeCompare(b.seasonName)
);
}
export async function assertRegistrySchemaDriftFree(): Promise<void> {
const schemaValues = schema.simulatorTypeEnum.enumValues;
const missingFromSchema = SIMULATOR_TYPES.filter((type) => !schemaValues.includes(type));
const missingFromRegistry = schemaValues.filter((type) => !(SIMULATOR_TYPES as readonly string[]).includes(type));
if (missingFromSchema.length > 0 || missingFromRegistry.length > 0) {
throw new Error(
`Simulator registry/schema drift detected. ` +
`Missing from schema: ${missingFromSchema.join(", ") || "none"}. ` +
`Missing from registry: ${missingFromRegistry.join(", ") || "none"}.`
);
}
}