import { Form, Link, redirect, useActionData, useNavigation } from "react-router"; import type { Route } from "./+types/admin.sports-seasons.$id.simulator"; import { AlertCircle, ArrowLeft, CheckCircle2, Loader2, Play, Save, SlidersHorizontal } from "lucide-react"; import { Badge } from "~/components/ui/badge"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "~/components/ui/card"; import { Textarea } from "~/components/ui/textarea"; import { findParticipantsBySportsSeasonId } from "~/models/season-participant"; import { findSportsSeasonById } from "~/models/sports-season"; import { batchUpsertParticipantSimulatorInputs, getParticipantSimulatorInputs, getSportsSeasonSimulatorConfig, upsertSportsSeasonSimulatorConfig, validateSimulatorReadiness, type UpsertParticipantSimulatorInput, } from "~/models/simulator"; import { normalizeName } from "~/lib/fuzzy-match"; import { getSimulatorInputPolicy, type MissingEloStrategy, type MissingRatingStrategy, } from "~/services/simulations/input-policy"; import { runSportsSeasonSimulation } from "~/services/simulations/runner"; export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { return [{ title: `Simulator Setup - ${data?.sportsSeason?.name ?? "Sports Season"} - Brackt Admin` }]; } export async function loader({ params }: Route.LoaderArgs) { const sportsSeason = await findSportsSeasonById(params.id); if (!sportsSeason) { throw new Response("Sports season not found", { status: 404 }); } const [participants, config, inputs, readiness] = await Promise.all([ findParticipantsBySportsSeasonId(params.id), getSportsSeasonSimulatorConfig(params.id), getParticipantSimulatorInputs(params.id), validateSimulatorReadiness(params.id), ]); if (!config) { throw new Response("This sports season does not have a simulator configured.", { status: 404 }); } const inputMap = new Map(inputs.map((input) => [input.participantId, input])); const inputRows = participants.map((participant) => ({ participant, input: inputMap.get(participant.id) ?? null, })); const inputPolicy = getSimulatorInputPolicy(config.config); return { sportsSeason, participants, config, inputRows, readiness, inputPolicy }; } interface ActionData { success?: boolean; message: string; } function parseOptionalNumber(value: string | undefined): number | null { if (value === undefined || value.trim() === "") return null; const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; } function parsePolicyNumber(formData: FormData, key: string, fallback: number): number { const value = formData.get(key); if (typeof value !== "string" || value.trim() === "") return fallback; const parsed = Number(value); return Number.isFinite(parsed) ? parsed : fallback; } function parseMissingEloStrategy(value: FormDataEntryValue | null): MissingEloStrategy { return value === "fallbackElo" || value === "averageKnown" || value === "worstKnownMinus" ? value : "block"; } function parseMissingRatingStrategy(value: FormDataEntryValue | null): MissingRatingStrategy { return value === "fallbackRating" || value === "averageKnown" || value === "worstKnownMinus" ? value : "block"; } function findParticipantId(name: string, participants: Array<{ id: string; name: string }>): string | null { const normalizedInput = normalizeName(name); const normalized = participants.map((participant) => ({ participant, normalized: normalizeName(participant.name), })); return ( normalized.find((candidate) => candidate.normalized === normalizedInput)?.participant.id ?? normalized.find((candidate) => candidate.normalized.includes(normalizedInput) || normalizedInput.includes(candidate.normalized) )?.participant.id ?? null ); } function parseInputCsv( text: string, sportsSeasonId: string, participants: Array<{ id: string; name: string }> ): { inputs: UpsertParticipantSimulatorInput[]; unmatched: string[] } { const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean); if (lines.length === 0) return { inputs: [], unmatched: [] }; const header = lines[0].split(",").map((value) => value.trim()); const indexes = new Map(header.map((value, index) => [value, index])); const nameIndex = indexes.get("name"); if (nameIndex === undefined) { throw new Error("Bulk input CSV must include a `name` column."); } const inputs: UpsertParticipantSimulatorInput[] = []; const unmatched: string[] = []; for (const line of lines.slice(1)) { const cols = line.split(",").map((value) => value.trim()); const name = cols[nameIndex]; if (!name) continue; const participantId = findParticipantId(name, participants); if (!participantId) { unmatched.push(name); continue; } inputs.push({ participantId, sportsSeasonId, sourceElo: parseOptionalNumber(cols[indexes.get("sourceElo") ?? -1]) ?? undefined, sourceOdds: parseOptionalNumber(cols[indexes.get("sourceOdds") ?? -1]) ?? undefined, worldRanking: parseOptionalNumber(cols[indexes.get("worldRanking") ?? -1]) ?? undefined, rating: parseOptionalNumber(cols[indexes.get("rating") ?? -1]) ?? undefined, projectedWins: parseOptionalNumber(cols[indexes.get("projectedWins") ?? -1]) ?? undefined, projectedTablePoints: parseOptionalNumber(cols[indexes.get("projectedTablePoints") ?? -1]) ?? undefined, seed: parseOptionalNumber(cols[indexes.get("seed") ?? -1]) ?? undefined, region: cols[indexes.get("region") ?? -1] || undefined, }); } return { inputs, unmatched }; } export async function action({ request, params }: Route.ActionArgs): Promise { const formData = await request.formData(); const intent = formData.get("intent"); const sportsSeasonId = params.id; if (intent === "run") { try { await runSportsSeasonSimulation(sportsSeasonId); return redirect(`/admin/sports-seasons/${sportsSeasonId}/expected-values`); } catch (error) { return { success: false, message: error instanceof Error ? error.message : "Simulation failed." }; } } if (intent === "save-config") { const rawConfig = formData.get("config"); const currentConfig = await getSportsSeasonSimulatorConfig(sportsSeasonId); if (!currentConfig) return { success: false, message: "Simulator config not found." }; try { const parsed = typeof rawConfig === "string" && rawConfig.trim() ? JSON.parse(rawConfig) as Record : {}; // Preserve an existing inputPolicy if the submitted JSON omits it, so // editing other config keys doesn't silently wipe a previously configured // missing-Elo strategy. const config = "inputPolicy" in parsed ? parsed : { ...parsed, ...(currentConfig.config.inputPolicy !== undefined ? { inputPolicy: currentConfig.config.inputPolicy } : {}) }; await upsertSportsSeasonSimulatorConfig({ sportsSeasonId, simulatorType: currentConfig.simulatorType, config, }); return { success: true, message: "Simulator config saved." }; } catch (error) { return { success: false, message: error instanceof Error ? error.message : "Invalid config JSON." }; } } if (intent === "save-input-policy") { const currentConfig = await getSportsSeasonSimulatorConfig(sportsSeasonId); if (!currentConfig) return { success: false, message: "Simulator config not found." }; const currentPolicy = getSimulatorInputPolicy(currentConfig.config); await upsertSportsSeasonSimulatorConfig({ sportsSeasonId, simulatorType: currentConfig.simulatorType, config: { ...currentConfig.config, inputPolicy: { missingEloStrategy: parseMissingEloStrategy(formData.get("missingEloStrategy")), missingRatingStrategy: parseMissingRatingStrategy(formData.get("missingRatingStrategy")), // Stored as-is; getSimulatorInputPolicy clamps to [0,1] on read. oddsWeight: parsePolicyNumber(formData, "oddsWeight", currentPolicy.oddsWeight), fallbackElo: parsePolicyNumber(formData, "fallbackElo", currentPolicy.fallbackElo), fallbackEloDelta: parsePolicyNumber(formData, "fallbackEloDelta", currentPolicy.fallbackEloDelta), eloMin: parsePolicyNumber(formData, "eloMin", currentPolicy.eloMin), eloMax: parsePolicyNumber(formData, "eloMax", currentPolicy.eloMax), fallbackRating: parsePolicyNumber(formData, "fallbackRating", currentPolicy.fallbackRating), fallbackRatingDelta: parsePolicyNumber(formData, "fallbackRatingDelta", currentPolicy.fallbackRatingDelta), ratingMin: parsePolicyNumber(formData, "ratingMin", currentPolicy.ratingMin), ratingMax: parsePolicyNumber(formData, "ratingMax", currentPolicy.ratingMax), }, }, }); return { success: true, message: "Simulator input policy saved." }; } if (intent === "save-inputs") { const rawInputs = formData.get("bulkInputs"); if (typeof rawInputs !== "string" || rawInputs.trim() === "") { return { success: false, message: "Paste at least one input row before saving." }; } const participants = await findParticipantsBySportsSeasonId(sportsSeasonId); try { const parsed = parseInputCsv(rawInputs, sportsSeasonId, participants); if (parsed.inputs.length === 0) { return { success: false, message: "No matching participants found in the pasted inputs." }; } await batchUpsertParticipantSimulatorInputs(parsed.inputs); const suffix = parsed.unmatched.length > 0 ? ` ${parsed.unmatched.length} row(s) were unmatched: ${parsed.unmatched.join(", ")}.` : ""; return { success: true, message: `Saved ${parsed.inputs.length} simulator input row(s).${suffix}` }; } catch (error) { return { success: false, message: error instanceof Error ? error.message : "Failed to save simulator inputs." }; } } return { success: false, message: "Unknown simulator setup action." }; } export default function AdminSportsSeasonSimulator({ loaderData }: Route.ComponentProps) { const { sportsSeason, config, inputRows, readiness, inputPolicy } = loaderData; const actionData = useActionData(); const navigation = useNavigation(); const isSubmitting = navigation.state === "submitting"; const setupSections = config.profile.setupSections; const sourceEloAlternatives = config.profile.derivableInputs?.sourceElo ?? []; const ratingAlternatives = config.profile.derivableInputs?.rating ?? []; const showsInputPolicy = config.profile.requiredInputs.includes("sourceElo") || config.profile.requiredInputs.includes("rating"); return (

Simulator Setup

{sportsSeason.sport.name} - {sportsSeason.name}

{actionData && ( {actionData.message} )}
{config.profile.displayName} {config.profile.description}
{readiness.status === "ready" ? ( Ready ) : ( Needs setup )}
Simulator Type
{config.simulatorType}
Participant Inputs
{readiness.participantInputCount}/{readiness.participantCount}
Simulation Status
{sportsSeason.simulationStatus}
{readiness.missingInputs.length > 0 && (
Missing: {readiness.missingInputs.join(", ")}
)} {readiness.warnings.length > 0 && (
{readiness.warnings.join(" ")}
)}
{setupSections.includes("futuresOdds") && } {setupSections.includes("eloRatings") && } {setupSections.includes("surfaceElo") && } {setupSections.includes("golfSkills") && } {setupSections.includes("regularStandings") && } {setupSections.includes("events") && }
{showsInputPolicy && ( Input Policy Direct inputs win. This simulator can derive Elo from{" "} {sourceEloAlternatives.length > 0 ? sourceEloAlternatives.join(", ") : "no alternate Elo inputs"} {ratingAlternatives.length > 0 ? ` and ratings from ${ratingAlternatives.join(", ")}` : ""}. Tail fallbacks are explicit so low-impact missing participants do not silently get invented ratings.

Every source (raw Elo, projections, futures odds) becomes an Elo, then they blend into the single Elo that feeds the simulator. This is the weight given to futures odds: 0 = Elo / projections only, 1 = futures fully override, in between = blend (e.g. 0.3 = 70% Elo / 30% futures).

{config.profile.requiredInputs.includes("sourceElo") && ( <>
)} {config.profile.requiredInputs.includes("rating") && ( <>
)}
)} Season Config JSON overrides for this specific sports season. Defaults come from the simulator profile.