brackt/app/routes/admin.sports-seasons.$id.simulator.tsx

592 lines
27 KiB
TypeScript
Raw Normal View History

Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
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";
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
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 };
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
}
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";
}
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
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
);
}
Centralize futures odds onto the simulator page and fix bulk import (#117) Futures odds entry was split across two unrelated places: a standalone /futures-odds page (which auto-ran the simulation on save and surfaced a "Simulator is not ready" run failure as if the save itself had failed) and the Bulk Simulator Inputs CSV importer on the simulator setup page. Consolidate everything onto the Bulk Simulator Inputs card: - batchUpsertParticipantSimulatorInputs now COALESCE-wraps every conflict update column, so a partial paste (e.g. odds-only) updates just the columns it provides instead of nulling out previously stored Elo/rating/etc. - The bulk importer accepts sportsbook-style paste (one team per line ending in American odds) when no CSV header is present, reusing the existing fuzzy matcher, and auto-runs the simulation on save. A not-ready run is reported as a successful save plus the readiness gap, never as a failed save. - Retire the standalone /futures-odds page (now redirects to the simulator setup page) and drop the redundant Futures links. - Remove the now-dead futures-only model paths (batchSaveSourceOdds, clearSourceOddsForParticipants, batchSaveFuturesOddsForSimulator, batchSaveParticipantSimulatorSourceOdds); the EV-table bridge is preserved by batchUpsertParticipantSimulatorInputs. - Repoint the test to the consolidated path and assert the non-destructive COALESCE behaviour. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BHnoQ7myY3PzNdTnd7iGNE Co-authored-by: Claude <noreply@anthropic.com> Reviewed-on: https://forge.brackt.com/chrisp/brackt/pulls/117
2026-06-30 17:05:52 +00:00
const ODDS_LINE_PATTERN = /^(.+?)\s+([+-]\d{2,6})\s*$/;
function parseOddsLines(
lines: string[],
sportsSeasonId: string,
participants: Array<{ id: string; name: string }>
): { inputs: UpsertParticipantSimulatorInput[]; unmatched: string[] } {
const inputs: UpsertParticipantSimulatorInput[] = [];
const unmatched: string[] = [];
const seen = new Set<string>();
for (const line of lines) {
const match = ODDS_LINE_PATTERN.exec(line);
if (!match) continue;
const name = match[1].trim();
const sourceOdds = Number(match[2]);
if (!Number.isFinite(sourceOdds)) continue;
const participantId = findParticipantId(name, participants);
if (!participantId) {
unmatched.push(name);
continue;
}
if (seen.has(participantId)) continue;
seen.add(participantId);
inputs.push({ participantId, sportsSeasonId, sourceOdds });
}
return { inputs, unmatched };
}
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
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) {
Centralize futures odds onto the simulator page and fix bulk import (#117) Futures odds entry was split across two unrelated places: a standalone /futures-odds page (which auto-ran the simulation on save and surfaced a "Simulator is not ready" run failure as if the save itself had failed) and the Bulk Simulator Inputs CSV importer on the simulator setup page. Consolidate everything onto the Bulk Simulator Inputs card: - batchUpsertParticipantSimulatorInputs now COALESCE-wraps every conflict update column, so a partial paste (e.g. odds-only) updates just the columns it provides instead of nulling out previously stored Elo/rating/etc. - The bulk importer accepts sportsbook-style paste (one team per line ending in American odds) when no CSV header is present, reusing the existing fuzzy matcher, and auto-runs the simulation on save. A not-ready run is reported as a successful save plus the readiness gap, never as a failed save. - Retire the standalone /futures-odds page (now redirects to the simulator setup page) and drop the redundant Futures links. - Remove the now-dead futures-only model paths (batchSaveSourceOdds, clearSourceOddsForParticipants, batchSaveFuturesOddsForSimulator, batchSaveParticipantSimulatorSourceOdds); the EV-table bridge is preserved by batchUpsertParticipantSimulatorInputs. - Repoint the test to the consolidated path and assert the non-destructive COALESCE behaviour. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BHnoQ7myY3PzNdTnd7iGNE Co-authored-by: Claude <noreply@anthropic.com> Reviewed-on: https://forge.brackt.com/chrisp/brackt/pulls/117
2026-06-30 17:05:52 +00:00
// No CSV header — treat the paste as sportsbook futures odds, one team per
// line ending in American odds (e.g. `Kansas City Chiefs +450`). This is the
// friendly bulk-futures path; team names are fuzzy-matched to participants.
return parseOddsLines(lines, sportsSeasonId, participants);
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
}
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<ActionData | Response> {
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<string, unknown>
: {};
// 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),
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
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),
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
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(", ")}.`
: "";
Centralize futures odds onto the simulator page and fix bulk import (#117) Futures odds entry was split across two unrelated places: a standalone /futures-odds page (which auto-ran the simulation on save and surfaced a "Simulator is not ready" run failure as if the save itself had failed) and the Bulk Simulator Inputs CSV importer on the simulator setup page. Consolidate everything onto the Bulk Simulator Inputs card: - batchUpsertParticipantSimulatorInputs now COALESCE-wraps every conflict update column, so a partial paste (e.g. odds-only) updates just the columns it provides instead of nulling out previously stored Elo/rating/etc. - The bulk importer accepts sportsbook-style paste (one team per line ending in American odds) when no CSV header is present, reusing the existing fuzzy matcher, and auto-runs the simulation on save. A not-ready run is reported as a successful save plus the readiness gap, never as a failed save. - Retire the standalone /futures-odds page (now redirects to the simulator setup page) and drop the redundant Futures links. - Remove the now-dead futures-only model paths (batchSaveSourceOdds, clearSourceOddsForParticipants, batchSaveFuturesOddsForSimulator, batchSaveParticipantSimulatorSourceOdds); the EV-table bridge is preserved by batchUpsertParticipantSimulatorInputs. - Repoint the test to the consolidated path and assert the non-destructive COALESCE behaviour. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BHnoQ7myY3PzNdTnd7iGNE Co-authored-by: Claude <noreply@anthropic.com> Reviewed-on: https://forge.brackt.com/chrisp/brackt/pulls/117
2026-06-30 17:05:52 +00:00
const saved = `Saved ${parsed.inputs.length} simulator input row(s).${suffix}`;
// Auto-run the simulation so saved inputs immediately drive the standings.
// The save itself already succeeded, so a run that never started (e.g.
// participants missing required inputs) is reported as success with the
// readiness gap — never as a failed save.
try {
await runSportsSeasonSimulation(sportsSeasonId);
return redirect(`/admin/sports-seasons/${sportsSeasonId}/expected-values`);
} catch (runError) {
const reason = runError instanceof Error ? runError.message : "could not run.";
// Distinguish "saved but never ran" (readiness/already-running) from
// "started running and failed mid-run": the runner only flips the season
// to status 'failed' once the simulation itself throws.
const season = await findSportsSeasonById(sportsSeasonId);
if (season?.simulationStatus === "failed") {
return { success: false, message: `${saved} The simulation failed: ${reason}` };
}
return { success: true, message: `${saved} Simulation not run yet: ${reason}` };
}
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
} 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;
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
const actionData = useActionData<ActionData>();
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");
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
return (
<div className="container mx-auto p-6 space-y-6">
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" asChild>
<Link to={`/admin/sports-seasons/${sportsSeason.id}`}>
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Sports Season
</Link>
</Button>
<Button variant="ghost" size="sm" asChild>
<Link to="/admin/simulators">All Simulators</Link>
</Button>
</div>
<div>
<h1 className="text-3xl font-bold flex items-center gap-2">
<SlidersHorizontal className="h-7 w-7" />
Simulator Setup
</h1>
<p className="text-muted-foreground mt-1">
{sportsSeason.sport.name} - {sportsSeason.name}
</p>
</div>
{actionData && (
<Card className={actionData.success ? "border-emerald-500/40" : "border-destructive/40"}>
<CardContent className="py-4 text-sm">{actionData.message}</CardContent>
</Card>
)}
<Card>
<CardHeader>
<div className="flex items-center justify-between gap-4">
<div>
<CardTitle>{config.profile.displayName}</CardTitle>
<CardDescription>{config.profile.description}</CardDescription>
</div>
{readiness.status === "ready" ? (
<Badge className="gap-1"><CheckCircle2 className="h-3 w-3" /> Ready</Badge>
) : (
<Badge variant="secondary" className="gap-1"><AlertCircle className="h-3 w-3" /> Needs setup</Badge>
)}
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-3 md:grid-cols-3">
<div className="rounded-md border p-3">
<div className="text-sm text-muted-foreground">Simulator Type</div>
<code className="text-sm">{config.simulatorType}</code>
</div>
<div className="rounded-md border p-3">
<div className="text-sm text-muted-foreground">Participant Inputs</div>
<div className="font-medium">{readiness.participantInputCount}/{readiness.participantCount}</div>
</div>
<div className="rounded-md border p-3">
<div className="text-sm text-muted-foreground">Simulation Status</div>
<div className="font-medium">{sportsSeason.simulationStatus}</div>
</div>
</div>
{readiness.missingInputs.length > 0 && (
<div className="rounded-md border border-amber-500/30 bg-amber-500/10 p-3 text-sm">
Missing: {readiness.missingInputs.join(", ")}
</div>
)}
{readiness.warnings.length > 0 && (
<div className="rounded-md border border-sky-500/30 bg-sky-500/10 p-3 text-sm">
{readiness.warnings.join(" ")}
</div>
)}
<div className="flex flex-wrap gap-2">
Centralize futures odds onto the simulator page and fix bulk import (#117) Futures odds entry was split across two unrelated places: a standalone /futures-odds page (which auto-ran the simulation on save and surfaced a "Simulator is not ready" run failure as if the save itself had failed) and the Bulk Simulator Inputs CSV importer on the simulator setup page. Consolidate everything onto the Bulk Simulator Inputs card: - batchUpsertParticipantSimulatorInputs now COALESCE-wraps every conflict update column, so a partial paste (e.g. odds-only) updates just the columns it provides instead of nulling out previously stored Elo/rating/etc. - The bulk importer accepts sportsbook-style paste (one team per line ending in American odds) when no CSV header is present, reusing the existing fuzzy matcher, and auto-runs the simulation on save. A not-ready run is reported as a successful save plus the readiness gap, never as a failed save. - Retire the standalone /futures-odds page (now redirects to the simulator setup page) and drop the redundant Futures links. - Remove the now-dead futures-only model paths (batchSaveSourceOdds, clearSourceOddsForParticipants, batchSaveFuturesOddsForSimulator, batchSaveParticipantSimulatorSourceOdds); the EV-table bridge is preserved by batchUpsertParticipantSimulatorInputs. - Repoint the test to the consolidated path and assert the non-destructive COALESCE behaviour. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BHnoQ7myY3PzNdTnd7iGNE Co-authored-by: Claude <noreply@anthropic.com> Reviewed-on: https://forge.brackt.com/chrisp/brackt/pulls/117
2026-06-30 17:05:52 +00:00
{setupSections.includes("eloRatings") &&<Button variant="outline" size="sm" asChild><Link to={`/admin/sports-seasons/${sportsSeason.id}/elo-ratings`}>Elo Ratings</Link></Button>}
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
{setupSections.includes("surfaceElo") && <Button variant="outline" size="sm" asChild><Link to={`/admin/sports-seasons/${sportsSeason.id}/surface-elo`}>Surface Elo</Link></Button>}
{setupSections.includes("golfSkills") && <Button variant="outline" size="sm" asChild><Link to={`/admin/sports-seasons/${sportsSeason.id}/golf-skills`}>Golf Skills</Link></Button>}
{setupSections.includes("regularStandings") && <Button variant="outline" size="sm" asChild><Link to={`/admin/sports-seasons/${sportsSeason.id}/regular-standings`}>Regular Standings</Link></Button>}
{setupSections.includes("events") && <Button variant="outline" size="sm" asChild><Link to={`/admin/sports-seasons/${sportsSeason.id}/events`}>Events</Link></Button>}
<Button variant="outline" size="sm" asChild><Link to={`/admin/sports-seasons/${sportsSeason.id}/expected-values`}>Expected Values</Link></Button>
</div>
<Form method="post">
<input type="hidden" name="intent" value="run" />
<Button type="submit" disabled={!readiness.canRun || sportsSeason.simulationStatus === "running" || isSubmitting}>
{isSubmitting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Play className="mr-2 h-4 w-4" />}
Run Simulation
</Button>
</Form>
</CardContent>
</Card>
{showsInputPolicy && (
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
<Card>
<CardHeader>
<CardTitle>Input Policy</CardTitle>
<CardDescription>
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.
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
</CardDescription>
</CardHeader>
<CardContent>
<Form method="post" className="grid gap-4 md:grid-cols-5">
<input type="hidden" name="intent" value="save-input-policy" />
<div className="space-y-2 md:col-span-5">
<Label htmlFor="oddsWeight">Futures vs. Elo Odds Blend Weight (01)</Label>
<Input id="oddsWeight" name="oddsWeight" type="number" step="0.05" min="0" max="1" defaultValue={inputPolicy.oddsWeight} />
<p className="text-xs text-muted-foreground">
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:
<strong> 0</strong> = Elo / projections only, <strong>1</strong> = futures fully override,
in between = blend (e.g. 0.3 = 70% Elo / 30% futures).
</p>
</div>
{config.profile.requiredInputs.includes("sourceElo") && (
<>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="missingEloStrategy">Missing Elo Strategy</Label>
<select
id="missingEloStrategy"
name="missingEloStrategy"
className="h-9 w-full rounded-md border bg-background px-3 text-sm"
defaultValue={inputPolicy.missingEloStrategy}
>
<option value="block">Block until complete</option>
<option value="fallbackElo">Use fixed fallback Elo</option>
<option value="averageKnown">Use average known Elo</option>
<option value="worstKnownMinus">Use worst known Elo minus delta</option>
</select>
</div>
<div className="space-y-2">
<Label htmlFor="fallbackElo">Fallback Elo</Label>
<Input id="fallbackElo" name="fallbackElo" type="number" defaultValue={inputPolicy.fallbackElo} />
</div>
<div className="space-y-2">
<Label htmlFor="fallbackEloDelta">Worst Elo Delta</Label>
<Input id="fallbackEloDelta" name="fallbackEloDelta" type="number" defaultValue={inputPolicy.fallbackEloDelta} />
</div>
<div className="space-y-2">
<Label htmlFor="eloMin">Elo Floor</Label>
<Input id="eloMin" name="eloMin" type="number" defaultValue={inputPolicy.eloMin} />
</div>
<div className="space-y-2">
<Label htmlFor="eloMax">Elo Ceiling</Label>
<Input id="eloMax" name="eloMax" type="number" defaultValue={inputPolicy.eloMax} />
</div>
</>
)}
{config.profile.requiredInputs.includes("rating") && (
<>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="missingRatingStrategy">Missing Rating Strategy</Label>
<select
id="missingRatingStrategy"
name="missingRatingStrategy"
className="h-9 w-full rounded-md border bg-background px-3 text-sm"
defaultValue={inputPolicy.missingRatingStrategy}
>
<option value="block">Block until complete</option>
<option value="fallbackRating">Use fixed fallback rating</option>
<option value="averageKnown">Use average known rating</option>
<option value="worstKnownMinus">Use worst known rating minus delta</option>
</select>
</div>
<div className="space-y-2">
<Label htmlFor="fallbackRating">Fallback Rating</Label>
<Input id="fallbackRating" name="fallbackRating" type="number" step="0.01" defaultValue={inputPolicy.fallbackRating} />
</div>
<div className="space-y-2">
<Label htmlFor="fallbackRatingDelta">Worst Rating Delta</Label>
<Input id="fallbackRatingDelta" name="fallbackRatingDelta" type="number" step="0.01" defaultValue={inputPolicy.fallbackRatingDelta} />
</div>
</>
)}
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
<div className="space-y-2">
<Label htmlFor="ratingMin">Rating Floor</Label>
<Input id="ratingMin" name="ratingMin" type="number" step="0.01" defaultValue={inputPolicy.ratingMin} />
</div>
<div className="space-y-2">
<Label htmlFor="ratingMax">Rating Ceiling</Label>
<Input id="ratingMax" name="ratingMax" type="number" step="0.01" defaultValue={inputPolicy.ratingMax} />
</div>
<div className="md:col-span-5">
<Button type="submit" disabled={isSubmitting}>
<Save className="mr-2 h-4 w-4" />
Save Input Policy
</Button>
</div>
</Form>
</CardContent>
</Card>
)}
<Card>
<CardHeader>
<CardTitle>Season Config</CardTitle>
<CardDescription>
JSON overrides for this specific sports season. Defaults come from the simulator profile.
</CardDescription>
</CardHeader>
<CardContent>
<Form method="post" className="space-y-4">
<input type="hidden" name="intent" value="save-config" />
<Textarea
name="config"
rows={10}
className="font-mono text-sm"
defaultValue={JSON.stringify(config.config, null, 2)}
/>
<Button type="submit" disabled={isSubmitting}>
<Save className="mr-2 h-4 w-4" />
Save Config
</Button>
</Form>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Bulk Simulator Inputs</CardTitle>
<CardDescription>
Centralize futures odds onto the simulator page and fix bulk import (#117) Futures odds entry was split across two unrelated places: a standalone /futures-odds page (which auto-ran the simulation on save and surfaced a "Simulator is not ready" run failure as if the save itself had failed) and the Bulk Simulator Inputs CSV importer on the simulator setup page. Consolidate everything onto the Bulk Simulator Inputs card: - batchUpsertParticipantSimulatorInputs now COALESCE-wraps every conflict update column, so a partial paste (e.g. odds-only) updates just the columns it provides instead of nulling out previously stored Elo/rating/etc. - The bulk importer accepts sportsbook-style paste (one team per line ending in American odds) when no CSV header is present, reusing the existing fuzzy matcher, and auto-runs the simulation on save. A not-ready run is reported as a successful save plus the readiness gap, never as a failed save. - Retire the standalone /futures-odds page (now redirects to the simulator setup page) and drop the redundant Futures links. - Remove the now-dead futures-only model paths (batchSaveSourceOdds, clearSourceOddsForParticipants, batchSaveFuturesOddsForSimulator, batchSaveParticipantSimulatorSourceOdds); the EV-table bridge is preserved by batchUpsertParticipantSimulatorInputs. - Repoint the test to the consolidated path and assert the non-destructive COALESCE behaviour. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BHnoQ7myY3PzNdTnd7iGNE Co-authored-by: Claude <noreply@anthropic.com> Reviewed-on: https://forge.brackt.com/chrisp/brackt/pulls/117
2026-06-30 17:05:52 +00:00
Two ways to paste, then the simulation re-runs automatically on save:
<strong> CSV</strong> with a header row supported columns: name, sourceElo, sourceOdds,
worldRanking, rating, projectedWins, projectedTablePoints, seed, region (values must not
contain commas); or <strong>futures odds</strong>, one team per line ending in American
odds (e.g. <code>Kansas City Chiefs +450</code>). Team names are fuzzy-matched to
participants. A partial paste only updates the columns it includes other inputs are kept.
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Form method="post" className="space-y-4">
<input type="hidden" name="intent" value="save-inputs" />
<Textarea
name="bulkInputs"
rows={8}
className="font-mono text-sm"
Centralize futures odds onto the simulator page and fix bulk import (#117) Futures odds entry was split across two unrelated places: a standalone /futures-odds page (which auto-ran the simulation on save and surfaced a "Simulator is not ready" run failure as if the save itself had failed) and the Bulk Simulator Inputs CSV importer on the simulator setup page. Consolidate everything onto the Bulk Simulator Inputs card: - batchUpsertParticipantSimulatorInputs now COALESCE-wraps every conflict update column, so a partial paste (e.g. odds-only) updates just the columns it provides instead of nulling out previously stored Elo/rating/etc. - The bulk importer accepts sportsbook-style paste (one team per line ending in American odds) when no CSV header is present, reusing the existing fuzzy matcher, and auto-runs the simulation on save. A not-ready run is reported as a successful save plus the readiness gap, never as a failed save. - Retire the standalone /futures-odds page (now redirects to the simulator setup page) and drop the redundant Futures links. - Remove the now-dead futures-only model paths (batchSaveSourceOdds, clearSourceOddsForParticipants, batchSaveFuturesOddsForSimulator, batchSaveParticipantSimulatorSourceOdds); the EV-table bridge is preserved by batchUpsertParticipantSimulatorInputs. - Repoint the test to the consolidated path and assert the non-destructive COALESCE behaviour. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BHnoQ7myY3PzNdTnd7iGNE Co-authored-by: Claude <noreply@anthropic.com> Reviewed-on: https://forge.brackt.com/chrisp/brackt/pulls/117
2026-06-30 17:05:52 +00:00
placeholder={`name,sourceElo,sourceOdds,worldRanking,rating\n${inputRows[0]?.participant.name ?? "Team Name"},1500,+2500,12,28.5\n\n— or paste futures odds only —\n${inputRows[0]?.participant.name ?? "Team Name"} +2500`}
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
/>
<Button type="submit" disabled={isSubmitting}>
<Save className="mr-2 h-4 w-4" />
Save Inputs
</Button>
</Form>
<div className="rounded-md border">
<div className="grid grid-cols-6 gap-2 border-b px-3 py-2 text-xs font-medium text-muted-foreground">
<div className="col-span-2">Participant</div>
<div>Elo</div>
<div>Odds</div>
<div>Rank</div>
<div>Rating</div>
</div>
{inputRows.slice(0, 20).map(({ participant, input }) => (
<div key={participant.id} className="grid grid-cols-6 gap-2 border-b last:border-b-0 px-3 py-2 text-sm">
<div className="col-span-2 font-medium">{participant.name}</div>
<div>{input?.sourceElo ?? "—"}</div>
<div>{input?.sourceOdds ?? "—"}</div>
<div>{input?.worldRanking ?? "—"}</div>
<div>{input?.rating ?? "—"}</div>
</div>
))}
{inputRows.length > 20 && (
<div className="px-3 py-2 text-xs text-muted-foreground">
Showing 20 of {inputRows.length} participants
</div>
)}
</div>
</CardContent>
</Card>
</div>
);
}