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" ;
2026-06-26 05:16:54 +00:00
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 ,
} ) ) ;
2026-05-11 22:24:29 -07:00
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" ;
}
2026-05-12 22:26:25 -07:00
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
) ;
}
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 ) {
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" ) ) ,
2026-05-12 22:26:25 -07:00
missingRatingStrategy : parseMissingRatingStrategy ( formData . get ( "missingRatingStrategy" ) ) ,
2026-06-26 05:16:54 +00:00
// 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 ) ,
2026-05-12 22:26:25 -07:00
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 ( ", " ) } . `
: "" ;
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 ) {
2026-05-11 22:24:29 -07:00
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 ? ? [ ] ;
2026-05-12 22:26:25 -07:00
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" >
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 >
2026-05-12 22:26:25 -07:00
{ 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 >
2026-05-12 22:26:25 -07:00
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" / >
2026-06-26 05:16:54 +00:00
< div className = "space-y-2 md:col-span-5" >
< Label htmlFor = "oddsWeight" > Futures vs . Elo — Odds Blend Weight ( 0 – 1 ) < / 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 >
2026-05-12 22:26:25 -07:00
{ 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 >
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"
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 >
) ;
}