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, prefersFuturesOdds, ELO_FIRST_SOURCE_PRIORITY, FUTURES_FIRST_SOURCE_PRIORITY, 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")), sourceEloPriority: formData.get("sourceStrategy") === "futuresFirst" ? FUTURES_FIRST_SOURCE_PRIORITY : ELO_FIRST_SOURCE_PRIORITY, 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.

Controls which signal wins when a participant has both. Futures-first makes freshly entered odds drive the run without discarding stored Elo.

For simulators that blend Elo and odds per match. 1.0 = pure odds; 0 = pure Elo.

{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.