Paginate and sport-tailor the simulator setup participant table (#132)
## What
The participant preview on the simulator setup page (`admin/sports-seasons/:id/simulator`) was capped at the first 20 rows by a hard-coded `.slice(0, 20)`, so most of the field was invisible for seasons with up to ~300 participants (golf, tennis). All rows were already loaded — this was purely a UI limit.
## Changes
- **Search + pagination** — name search box (accent-insensitive via `normalizeName`) plus Prev/Next paging at 50/page, over the already-loaded rows. Page resets on filter change.
- **Sport-aware columns** — columns are derived from each simulator's `requiredInputs` + `optionalInputs` (from the manifest), labeled, with required columns marked. So F1 shows odds, NBA shows Elo, NCAA shows rating, etc.
- **Missing-input filter** — "Only show participants missing a required input" toggle (shown only when the simulator has required inputs) plus a per-row amber marker so gaps are visible at a glance.
- **Tennis/golf link-out** — for sports whose inputs live on a dedicated page (surface Elo, golf skills), a note links there instead of implying they're edited on this page.
## Notes
Column resolution is the only manifest **runtime** call, and it runs in the **loader** — this keeps the simulator manifest/registry out of the client bundle. Importing it into the client component pulled in every simulator and their transitive server-only modules (`scoring-calculator.ts → qualifying-points-discord.server`, `cs2-major-stage.ts`), which broke the Champions League setup page with a "server-only module referenced by client" error.
## Verification
`npm run typecheck`, `oxlint`, and `npm run build` (client + server) all pass with no server-only-leak error.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: https://forge.brackt.com/chrisp/brackt/pulls/132
2026-07-07 06:00:15 +00:00
import { useMemo , useState } from "react" ;
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" ;
Paginate and sport-tailor the simulator setup participant table (#132)
## What
The participant preview on the simulator setup page (`admin/sports-seasons/:id/simulator`) was capped at the first 20 rows by a hard-coded `.slice(0, 20)`, so most of the field was invisible for seasons with up to ~300 participants (golf, tennis). All rows were already loaded — this was purely a UI limit.
## Changes
- **Search + pagination** — name search box (accent-insensitive via `normalizeName`) plus Prev/Next paging at 50/page, over the already-loaded rows. Page resets on filter change.
- **Sport-aware columns** — columns are derived from each simulator's `requiredInputs` + `optionalInputs` (from the manifest), labeled, with required columns marked. So F1 shows odds, NBA shows Elo, NCAA shows rating, etc.
- **Missing-input filter** — "Only show participants missing a required input" toggle (shown only when the simulator has required inputs) plus a per-row amber marker so gaps are visible at a glance.
- **Tennis/golf link-out** — for sports whose inputs live on a dedicated page (surface Elo, golf skills), a note links there instead of implying they're edited on this page.
## Notes
Column resolution is the only manifest **runtime** call, and it runs in the **loader** — this keeps the simulator manifest/registry out of the client bundle. Importing it into the client component pulled in every simulator and their transitive server-only modules (`scoring-calculator.ts → qualifying-points-discord.server`, `cs2-major-stage.ts`), which broke the Champions League setup page with a "server-only module referenced by client" error.
## Verification
`npm run typecheck`, `oxlint`, and `npm run build` (client + server) all pass with no server-only-leak error.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: https://forge.brackt.com/chrisp/brackt/pulls/132
2026-07-07 06:00:15 +00:00
import {
simulatorInputLabel ,
type SimulatorInputKey ,
} from "~/services/simulations/manifest" ;
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 ) ;
Paginate and sport-tailor the simulator setup participant table (#132)
## What
The participant preview on the simulator setup page (`admin/sports-seasons/:id/simulator`) was capped at the first 20 rows by a hard-coded `.slice(0, 20)`, so most of the field was invisible for seasons with up to ~300 participants (golf, tennis). All rows were already loaded — this was purely a UI limit.
## Changes
- **Search + pagination** — name search box (accent-insensitive via `normalizeName`) plus Prev/Next paging at 50/page, over the already-loaded rows. Page resets on filter change.
- **Sport-aware columns** — columns are derived from each simulator's `requiredInputs` + `optionalInputs` (from the manifest), labeled, with required columns marked. So F1 shows odds, NBA shows Elo, NCAA shows rating, etc.
- **Missing-input filter** — "Only show participants missing a required input" toggle (shown only when the simulator has required inputs) plus a per-row amber marker so gaps are visible at a glance.
- **Tennis/golf link-out** — for sports whose inputs live on a dedicated page (surface Elo, golf skills), a note links there instead of implying they're edited on this page.
## Notes
Column resolution is the only manifest **runtime** call, and it runs in the **loader** — this keeps the simulator manifest/registry out of the client bundle. Importing it into the client component pulled in every simulator and their transitive server-only modules (`scoring-calculator.ts → qualifying-points-discord.server`, `cs2-major-stage.ts`), which broke the Champions League setup page with a "server-only module referenced by client" error.
## Verification
`npm run typecheck`, `oxlint`, and `npm run build` (client + server) all pass with no server-only-leak error.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: https://forge.brackt.com/chrisp/brackt/pulls/132
2026-07-07 06:00:15 +00:00
// Sport-aware preview columns: the intersection of the displayable numeric keys
// with this simulator's required + optional inputs, so each season shows exactly
// the inputs its simulator consumes (F1 = odds, NBA = Elo, NCAA = rating, ...).
// Resolved here (server-only) so the client bundle never imports the simulator
// manifest/registry, which transitively pulls in `.server` modules.
const relevantInputs = new Set < SimulatorInputKey > ( [
. . . config . profile . requiredInputs ,
. . . config . profile . optionalInputs ,
] ) ;
const inputColumns = DISPLAY_INPUT_ORDER . filter ( ( key ) = > relevantInputs . has ( key ) ) . map ( ( key ) = > ( {
key ,
label : simulatorInputLabel ( key ) ,
required : config.profile.requiredInputs.includes ( key ) ,
} ) ) ;
return { sportsSeason , participants , config , inputRows , readiness , inputPolicy , inputColumns } ;
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 ;
}
Paginate and sport-tailor the simulator setup participant table (#132)
## What
The participant preview on the simulator setup page (`admin/sports-seasons/:id/simulator`) was capped at the first 20 rows by a hard-coded `.slice(0, 20)`, so most of the field was invisible for seasons with up to ~300 participants (golf, tennis). All rows were already loaded — this was purely a UI limit.
## Changes
- **Search + pagination** — name search box (accent-insensitive via `normalizeName`) plus Prev/Next paging at 50/page, over the already-loaded rows. Page resets on filter change.
- **Sport-aware columns** — columns are derived from each simulator's `requiredInputs` + `optionalInputs` (from the manifest), labeled, with required columns marked. So F1 shows odds, NBA shows Elo, NCAA shows rating, etc.
- **Missing-input filter** — "Only show participants missing a required input" toggle (shown only when the simulator has required inputs) plus a per-row amber marker so gaps are visible at a glance.
- **Tennis/golf link-out** — for sports whose inputs live on a dedicated page (surface Elo, golf skills), a note links there instead of implying they're edited on this page.
## Notes
Column resolution is the only manifest **runtime** call, and it runs in the **loader** — this keeps the simulator manifest/registry out of the client bundle. Importing it into the client component pulled in every simulator and their transitive server-only modules (`scoring-calculator.ts → qualifying-points-discord.server`, `cs2-major-stage.ts`), which broke the Champions League setup page with a "server-only module referenced by client" error.
## Verification
`npm run typecheck`, `oxlint`, and `npm run build` (client + server) all pass with no server-only-leak error.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: https://forge.brackt.com/chrisp/brackt/pulls/132
2026-07-07 06:00:15 +00:00
/ * *
* Input keys the participant preview can render as a numeric column , in the order
* they appear . Keys not listed ( e . g . ` region ` , ` metadata ` ) are not shown as
* columns ; the visible columns for a season are the intersection of this order
* with the simulator ' s required + optional inputs .
* /
const DISPLAY_INPUT_ORDER : SimulatorInputKey [ ] = [
"sourceElo" ,
"sourceOdds" ,
"worldRanking" ,
"rating" ,
"projectedWins" ,
"projectedTablePoints" ,
"seed" ,
] ;
const PARTICIPANT_PAGE_SIZE = 50 ;
2026-06-30 23:24:48 +00:00
/ * *
* Engine knobs that a simulator ( or the shared input - policy resolver ) actually
* reads from config . The structured Engine fields are limited to these so the UI
* never shows a control that silently does nothing — bespoke per - sim constants
* ( e . g . homeFieldElo , eloDivisor , srsEloScale , raceNoise ) that live in a profile
* but are not read from config stay editable only via the raw - JSON escape hatch .
* /
const HONORED_ENGINE_KNOBS = new Set ( [
"iterations" ,
"parityFactor" ,
"seasonGames" ,
"overtimeRate" ,
"matchParityFactor" ,
"averageOpponentElo" ,
"baseDrawRate" ,
"drawDecay" ,
"ratingScaleFactor" ,
] ) ;
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 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." } ;
}
}
2026-06-30 23:24:48 +00:00
if ( intent === "save-configuration" ) {
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 currentConfig = await getSportsSeasonSimulatorConfig ( sportsSeasonId ) ;
if ( ! currentConfig ) return { success : false , message : "Simulator config not found." } ;
2026-06-30 23:24:48 +00:00
// Start from the current merged config so keys not exposed as structured
// fields (e.g. string knobs) are preserved untouched.
const next : Record < string , unknown > = { . . . currentConfig . config } ;
// Engine knobs: every numeric field rendered as `engine.<key>`.
for ( const [ field , value ] of formData . entries ( ) ) {
if ( typeof value !== "string" || ! field . startsWith ( "engine." ) ) continue ;
const key = field . slice ( "engine." . length ) ;
const parsed = Number ( value ) ;
if ( value . trim ( ) !== "" && Number . isFinite ( parsed ) ) next [ key ] = parsed ;
}
// Input-derivation policy (only when the simulator consumes Elo/ratings).
if ( formData . get ( "hasInputPolicy" ) === "1" ) {
const currentPolicy = getSimulatorInputPolicy ( currentConfig . config ) ;
next . inputPolicy = {
. . . currentPolicy ,
missingEloStrategy : parseMissingEloStrategy ( formData . get ( "missingEloStrategy" ) ) ,
missingRatingStrategy : parseMissingRatingStrategy ( formData . get ( "missingRatingStrategy" ) ) ,
// Stored as-is; getSimulatorInputPolicy clamps to [0,1] on read.
oddsWeight : parsePolicyNumber ( formData , "oddsWeight" , currentPolicy . oddsWeight ) ,
fallbackElo : parsePolicyNumber ( formData , "fallbackElo" , currentPolicy . fallbackElo ) ,
fallbackEloDelta : parsePolicyNumber ( formData , "fallbackEloDelta" , currentPolicy . fallbackEloDelta ) ,
eloMin : parsePolicyNumber ( formData , "eloMin" , currentPolicy . eloMin ) ,
eloMax : parsePolicyNumber ( formData , "eloMax" , currentPolicy . eloMax ) ,
fallbackRating : parsePolicyNumber ( formData , "fallbackRating" , currentPolicy . fallbackRating ) ,
fallbackRatingDelta : parsePolicyNumber ( formData , "fallbackRatingDelta" , currentPolicy . fallbackRatingDelta ) ,
ratingMin : parsePolicyNumber ( formData , "ratingMin" , currentPolicy . ratingMin ) ,
ratingMax : parsePolicyNumber ( formData , "ratingMax" , currentPolicy . ratingMax ) ,
} ;
}
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
await upsertSportsSeasonSimulatorConfig ( {
sportsSeasonId ,
simulatorType : currentConfig.simulatorType ,
2026-06-30 23:24:48 +00:00
config : next ,
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
} ) ;
2026-06-30 23:24:48 +00:00
return { success : true , message : "Simulator configuration saved." } ;
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
}
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
2026-06-30 23:24:48 +00:00
// Structured engine knobs: every top-level numeric config key (inputPolicy is a
// nested object edited in its own section). Driving the fields from the merged
// config means each simulator shows exactly the knobs it actually reads.
const engineEntries = Object . entries ( config . config )
. filter ( ( [ key , value ] ) = > HONORED_ENGINE_KNOBS . has ( key ) && typeof value === "number" )
. map ( ( [ key , value ] ) = > [ key , value as unknown as number ] as [ string , number ] ) ;
Paginate and sport-tailor the simulator setup participant table (#132)
## What
The participant preview on the simulator setup page (`admin/sports-seasons/:id/simulator`) was capped at the first 20 rows by a hard-coded `.slice(0, 20)`, so most of the field was invisible for seasons with up to ~300 participants (golf, tennis). All rows were already loaded — this was purely a UI limit.
## Changes
- **Search + pagination** — name search box (accent-insensitive via `normalizeName`) plus Prev/Next paging at 50/page, over the already-loaded rows. Page resets on filter change.
- **Sport-aware columns** — columns are derived from each simulator's `requiredInputs` + `optionalInputs` (from the manifest), labeled, with required columns marked. So F1 shows odds, NBA shows Elo, NCAA shows rating, etc.
- **Missing-input filter** — "Only show participants missing a required input" toggle (shown only when the simulator has required inputs) plus a per-row amber marker so gaps are visible at a glance.
- **Tennis/golf link-out** — for sports whose inputs live on a dedicated page (surface Elo, golf skills), a note links there instead of implying they're edited on this page.
## Notes
Column resolution is the only manifest **runtime** call, and it runs in the **loader** — this keeps the simulator manifest/registry out of the client bundle. Importing it into the client component pulled in every simulator and their transitive server-only modules (`scoring-calculator.ts → qualifying-points-discord.server`, `cs2-major-stage.ts`), which broke the Champions League setup page with a "server-only module referenced by client" error.
## Verification
`npm run typecheck`, `oxlint`, and `npm run build` (client + server) all pass with no server-only-leak error.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: https://forge.brackt.com/chrisp/brackt/pulls/132
2026-07-07 06:00:15 +00:00
// Preview columns are resolved server-side in the loader (see note there) and
// arrive as plain data, so this client component never imports the manifest.
const { inputColumns } = loaderData ;
const requiredInputs = config . profile . requiredInputs ;
const gridTemplate = ` 2fr repeat( ${ Math . max ( inputColumns . length , 1 ) } , 1fr) ` ;
// For this sport the inputs live on a dedicated page, not the shared bulk paste.
const externalInputsSection = requiredInputs . length === 0
? ( setupSections . includes ( "surfaceElo" )
? { label : "Surface Elo" , to : ` /admin/sports-seasons/ ${ sportsSeason . id } /surface-elo ` }
: setupSections . includes ( "golfSkills" )
? { label : "Golf Skills" , to : ` /admin/sports-seasons/ ${ sportsSeason . id } /golf-skills ` }
: null )
: null ;
const isRowIncomplete = ( input : ( typeof inputRows ) [ number ] [ "input" ] ) = >
requiredInputs . some ( ( key ) = > input ? . [ key ] === null || input ? . [ key ] === undefined ) ;
const [ search , setSearch ] = useState ( "" ) ;
const [ onlyMissing , setOnlyMissing ] = useState ( false ) ;
const [ page , setPage ] = useState ( 0 ) ;
const filteredRows = useMemo ( ( ) = > {
const normalizedSearch = normalizeName ( search ) ;
return inputRows . filter ( ( { participant , input } ) = > {
if ( normalizedSearch && ! normalizeName ( participant . name ) . includes ( normalizedSearch ) ) return false ;
if ( onlyMissing && ! isRowIncomplete ( input ) ) return false ;
return true ;
} ) ;
// eslint-disable-next-line react-hooks/exhaustive-deps
} , [ inputRows , search , onlyMissing , requiredInputs ] ) ;
const totalPages = Math . max ( 1 , Math . ceil ( filteredRows . length / PARTICIPANT_PAGE_SIZE ) ) ;
const safePage = Math . min ( page , totalPages - 1 ) ;
const pageStart = safePage * PARTICIPANT_PAGE_SIZE ;
const pageRows = filteredRows . slice ( pageStart , pageStart + PARTICIPANT_PAGE_SIZE ) ;
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-06-30 23:24:48 +00:00
< Card >
< CardHeader >
< CardTitle > Simulator Configuration < / CardTitle >
< CardDescription >
One place for this season ' s settings . < strong > Engine < / strong > controls how the Monte Carlo
runs ; < strong > Input derivation < / strong > controls how raw inputs become the single Elo / rating
the engine consumes . Defaults come from the simulator profile ; values set here override them
for this season only . Both sections write the same stored config .
< / CardDescription >
< / CardHeader >
< CardContent className = "space-y-6" >
< Form method = "post" className = "space-y-6" >
< input type = "hidden" name = "intent" value = "save-configuration" / >
{ showsInputPolicy && < input type = "hidden" name = "hasInputPolicy" value = "1" / > }
< section className = "space-y-3" >
< div >
< h3 className = "text-sm font-semibold" > Engine < / h3 >
2026-06-26 05:16:54 +00:00
< p className = "text-xs text-muted-foreground" >
2026-06-30 23:24:48 +00:00
How the simulation runs . A higher < code > parityFactor < / code > flattens the finish - position
distribution ( favorites win less often ) ; < code > iterations < / code > trades speed for precision .
2026-06-26 05:16:54 +00:00
< / p >
< / div >
2026-06-30 23:24:48 +00:00
{ engineEntries . length > 0 ? (
< div className = "grid gap-4 md:grid-cols-3" >
{ engineEntries . map ( ( [ key , value ] ) = > (
< div key = { key } className = "space-y-2" >
< Label htmlFor = { ` engine. ${ key } ` } className = "font-mono text-xs" > { key } < / Label >
< Input id = { ` engine. ${ key } ` } name = { ` engine. ${ key } ` } type = "number" step = "any" defaultValue = { value } / >
< / div >
) ) }
< / div >
) : (
< p className = "text-xs text-muted-foreground" > This simulator exposes no numeric engine knobs . < / p >
2026-05-12 22:26:25 -07:00
) }
2026-06-30 23:24:48 +00:00
< / section >
{ showsInputPolicy && (
< section className = "space-y-3" >
< div >
< h3 className = "text-sm font-semibold" > Input derivation < / h3 >
< p className = "text-xs text-muted-foreground" >
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 .
< / p >
< / div >
< div className = "grid gap-4 md:grid-cols-5" >
< 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 ) . Odds enter the engine only through
this Elo — they are not blended again per game .
< / p >
2026-05-12 22:26:25 -07:00
< / div >
2026-06-30 23:24:48 +00: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 >
< / >
) }
2026-05-12 22:26:25 -07:00
< div className = "space-y-2" >
2026-06-30 23:24:48 +00:00
< Label htmlFor = "ratingMin" > Rating Floor < / Label >
< Input id = "ratingMin" name = "ratingMin" type = "number" step = "0.01" defaultValue = { inputPolicy . ratingMin } / >
2026-05-12 22:26:25 -07:00
< / div >
< div className = "space-y-2" >
2026-06-30 23:24:48 +00:00
< Label htmlFor = "ratingMax" > Rating Ceiling < / Label >
< Input id = "ratingMax" name = "ratingMax" type = "number" step = "0.01" defaultValue = { inputPolicy . ratingMax } / >
2026-05-12 22:26:25 -07:00
< / div >
2026-06-30 23:24:48 +00:00
< / div >
< / section >
) }
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" / >
2026-06-30 23:24:48 +00:00
Save Configuration
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 >
< / Form >
2026-06-30 23:24:48 +00:00
< details >
< summary className = "cursor-pointer text-sm font-medium" > Advanced : edit raw config JSON < / summary >
< Form method = "post" className = "space-y-3 mt-3" >
< 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 ) }
/ >
< p className = "text-xs text-muted-foreground" >
Power - user escape hatch for keys not shown above . This is the same stored config the fields
edit ; saving here writes the whole object .
< / p >
< Button type = "submit" variant = "outline" disabled = { isSubmitting } >
< Save className = "mr-2 h-4 w-4" / >
Save Raw JSON
< / Button >
< / Form >
< / details >
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
< / 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 >
Paginate and sport-tailor the simulator setup participant table (#132)
## What
The participant preview on the simulator setup page (`admin/sports-seasons/:id/simulator`) was capped at the first 20 rows by a hard-coded `.slice(0, 20)`, so most of the field was invisible for seasons with up to ~300 participants (golf, tennis). All rows were already loaded — this was purely a UI limit.
## Changes
- **Search + pagination** — name search box (accent-insensitive via `normalizeName`) plus Prev/Next paging at 50/page, over the already-loaded rows. Page resets on filter change.
- **Sport-aware columns** — columns are derived from each simulator's `requiredInputs` + `optionalInputs` (from the manifest), labeled, with required columns marked. So F1 shows odds, NBA shows Elo, NCAA shows rating, etc.
- **Missing-input filter** — "Only show participants missing a required input" toggle (shown only when the simulator has required inputs) plus a per-row amber marker so gaps are visible at a glance.
- **Tennis/golf link-out** — for sports whose inputs live on a dedicated page (surface Elo, golf skills), a note links there instead of implying they're edited on this page.
## Notes
Column resolution is the only manifest **runtime** call, and it runs in the **loader** — this keeps the simulator manifest/registry out of the client bundle. Importing it into the client component pulled in every simulator and their transitive server-only modules (`scoring-calculator.ts → qualifying-points-discord.server`, `cs2-major-stage.ts`), which broke the Champions League setup page with a "server-only module referenced by client" error.
## Verification
`npm run typecheck`, `oxlint`, and `npm run build` (client + server) all pass with no server-only-leak error.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: https://forge.brackt.com/chrisp/brackt/pulls/132
2026-07-07 06:00:15 +00:00
{ externalInputsSection && (
< div className = "rounded-md border border-sky-500/30 bg-sky-500/10 p-3 text-sm flex items-center justify-between gap-4" >
< span >
This simulator ' s participant inputs are managed on the { " " }
< strong > { externalInputsSection . label } < / strong > page — the list below is a roster only .
< / span >
< Button variant = "outline" size = "sm" asChild >
< Link to = { externalInputsSection . to } > Go to { externalInputsSection . label } < / Link >
< / Button >
< / div >
) }
< div className = "flex flex-wrap items-center gap-3" >
< Input
type = "search"
placeholder = "Search participants…"
value = { search }
onChange = { ( event ) = > {
setSearch ( event . target . value ) ;
setPage ( 0 ) ;
} }
className = "max-w-xs"
/ >
{ requiredInputs . length > 0 && (
< label className = "flex items-center gap-2 text-sm text-muted-foreground" >
< input
type = "checkbox"
checked = { onlyMissing }
onChange = { ( event ) = > {
setOnlyMissing ( event . target . checked ) ;
setPage ( 0 ) ;
} }
/ >
Only show participants missing a required input
< / label >
) }
< / 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 = "rounded-md border" >
Paginate and sport-tailor the simulator setup participant table (#132)
## What
The participant preview on the simulator setup page (`admin/sports-seasons/:id/simulator`) was capped at the first 20 rows by a hard-coded `.slice(0, 20)`, so most of the field was invisible for seasons with up to ~300 participants (golf, tennis). All rows were already loaded — this was purely a UI limit.
## Changes
- **Search + pagination** — name search box (accent-insensitive via `normalizeName`) plus Prev/Next paging at 50/page, over the already-loaded rows. Page resets on filter change.
- **Sport-aware columns** — columns are derived from each simulator's `requiredInputs` + `optionalInputs` (from the manifest), labeled, with required columns marked. So F1 shows odds, NBA shows Elo, NCAA shows rating, etc.
- **Missing-input filter** — "Only show participants missing a required input" toggle (shown only when the simulator has required inputs) plus a per-row amber marker so gaps are visible at a glance.
- **Tennis/golf link-out** — for sports whose inputs live on a dedicated page (surface Elo, golf skills), a note links there instead of implying they're edited on this page.
## Notes
Column resolution is the only manifest **runtime** call, and it runs in the **loader** — this keeps the simulator manifest/registry out of the client bundle. Importing it into the client component pulled in every simulator and their transitive server-only modules (`scoring-calculator.ts → qualifying-points-discord.server`, `cs2-major-stage.ts`), which broke the Champions League setup page with a "server-only module referenced by client" error.
## Verification
`npm run typecheck`, `oxlint`, and `npm run build` (client + server) all pass with no server-only-leak error.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: https://forge.brackt.com/chrisp/brackt/pulls/132
2026-07-07 06:00:15 +00:00
< div
className = "grid gap-2 border-b px-3 py-2 text-xs font-medium text-muted-foreground"
style = { { gridTemplateColumns : gridTemplate } }
>
< div > Participant < / div >
{ inputColumns . length > 0 ? (
inputColumns . map ( ( column ) = > (
< div key = { column . key } className = "capitalize" >
{ column . label }
{ column . required && < span className = "text-amber-500" > * < / span > }
< / div >
) )
) : (
< div > — < / 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 >
Paginate and sport-tailor the simulator setup participant table (#132)
## What
The participant preview on the simulator setup page (`admin/sports-seasons/:id/simulator`) was capped at the first 20 rows by a hard-coded `.slice(0, 20)`, so most of the field was invisible for seasons with up to ~300 participants (golf, tennis). All rows were already loaded — this was purely a UI limit.
## Changes
- **Search + pagination** — name search box (accent-insensitive via `normalizeName`) plus Prev/Next paging at 50/page, over the already-loaded rows. Page resets on filter change.
- **Sport-aware columns** — columns are derived from each simulator's `requiredInputs` + `optionalInputs` (from the manifest), labeled, with required columns marked. So F1 shows odds, NBA shows Elo, NCAA shows rating, etc.
- **Missing-input filter** — "Only show participants missing a required input" toggle (shown only when the simulator has required inputs) plus a per-row amber marker so gaps are visible at a glance.
- **Tennis/golf link-out** — for sports whose inputs live on a dedicated page (surface Elo, golf skills), a note links there instead of implying they're edited on this page.
## Notes
Column resolution is the only manifest **runtime** call, and it runs in the **loader** — this keeps the simulator manifest/registry out of the client bundle. Importing it into the client component pulled in every simulator and their transitive server-only modules (`scoring-calculator.ts → qualifying-points-discord.server`, `cs2-major-stage.ts`), which broke the Champions League setup page with a "server-only module referenced by client" error.
## Verification
`npm run typecheck`, `oxlint`, and `npm run build` (client + server) all pass with no server-only-leak error.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: https://forge.brackt.com/chrisp/brackt/pulls/132
2026-07-07 06:00:15 +00:00
{ pageRows . length === 0 ? (
< div className = "px-3 py-6 text-center text-sm text-muted-foreground" >
No participants match .
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 >
Paginate and sport-tailor the simulator setup participant table (#132)
## What
The participant preview on the simulator setup page (`admin/sports-seasons/:id/simulator`) was capped at the first 20 rows by a hard-coded `.slice(0, 20)`, so most of the field was invisible for seasons with up to ~300 participants (golf, tennis). All rows were already loaded — this was purely a UI limit.
## Changes
- **Search + pagination** — name search box (accent-insensitive via `normalizeName`) plus Prev/Next paging at 50/page, over the already-loaded rows. Page resets on filter change.
- **Sport-aware columns** — columns are derived from each simulator's `requiredInputs` + `optionalInputs` (from the manifest), labeled, with required columns marked. So F1 shows odds, NBA shows Elo, NCAA shows rating, etc.
- **Missing-input filter** — "Only show participants missing a required input" toggle (shown only when the simulator has required inputs) plus a per-row amber marker so gaps are visible at a glance.
- **Tennis/golf link-out** — for sports whose inputs live on a dedicated page (surface Elo, golf skills), a note links there instead of implying they're edited on this page.
## Notes
Column resolution is the only manifest **runtime** call, and it runs in the **loader** — this keeps the simulator manifest/registry out of the client bundle. Importing it into the client component pulled in every simulator and their transitive server-only modules (`scoring-calculator.ts → qualifying-points-discord.server`, `cs2-major-stage.ts`), which broke the Champions League setup page with a "server-only module referenced by client" error.
## Verification
`npm run typecheck`, `oxlint`, and `npm run build` (client + server) all pass with no server-only-leak error.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: https://forge.brackt.com/chrisp/brackt/pulls/132
2026-07-07 06:00:15 +00:00
) : (
pageRows . map ( ( { participant , input } ) = > {
const incomplete = isRowIncomplete ( input ) ;
return (
< div
key = { participant . id }
className = "grid gap-2 border-b last:border-b-0 px-3 py-2 text-sm"
style = { { gridTemplateColumns : gridTemplate } }
>
< div className = "font-medium flex items-center gap-2" >
{ incomplete && < span className = "h-2 w-2 shrink-0 rounded-full bg-amber-500" title = "Missing a required input" / > }
{ participant . name }
< / div >
{ inputColumns . length > 0 ? (
inputColumns . map ( ( column ) = > {
const value = input ? . [ column . key ] ;
return < div key = { column . key } > { typeof value === "number" || typeof value === "string" ? value : "—" } < / div > ;
} )
) : (
< div > — < / div >
) }
< / 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
) }
Paginate and sport-tailor the simulator setup participant table (#132)
## What
The participant preview on the simulator setup page (`admin/sports-seasons/:id/simulator`) was capped at the first 20 rows by a hard-coded `.slice(0, 20)`, so most of the field was invisible for seasons with up to ~300 participants (golf, tennis). All rows were already loaded — this was purely a UI limit.
## Changes
- **Search + pagination** — name search box (accent-insensitive via `normalizeName`) plus Prev/Next paging at 50/page, over the already-loaded rows. Page resets on filter change.
- **Sport-aware columns** — columns are derived from each simulator's `requiredInputs` + `optionalInputs` (from the manifest), labeled, with required columns marked. So F1 shows odds, NBA shows Elo, NCAA shows rating, etc.
- **Missing-input filter** — "Only show participants missing a required input" toggle (shown only when the simulator has required inputs) plus a per-row amber marker so gaps are visible at a glance.
- **Tennis/golf link-out** — for sports whose inputs live on a dedicated page (surface Elo, golf skills), a note links there instead of implying they're edited on this page.
## Notes
Column resolution is the only manifest **runtime** call, and it runs in the **loader** — this keeps the simulator manifest/registry out of the client bundle. Importing it into the client component pulled in every simulator and their transitive server-only modules (`scoring-calculator.ts → qualifying-points-discord.server`, `cs2-major-stage.ts`), which broke the Champions League setup page with a "server-only module referenced by client" error.
## Verification
`npm run typecheck`, `oxlint`, and `npm run build` (client + server) all pass with no server-only-leak error.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: https://forge.brackt.com/chrisp/brackt/pulls/132
2026-07-07 06:00:15 +00:00
< div className = "flex items-center justify-between gap-4 px-3 py-2 text-xs text-muted-foreground" >
< span >
{ filteredRows . length === 0
? "0 participants"
: ` Showing ${ pageStart + 1 } – ${ pageStart + pageRows . length } of ${ filteredRows . length } ${
filteredRows . length !== inputRows . length ? ` ( ${ inputRows . length } total) ` : ""
} ` }
< / span >
{ totalPages > 1 && (
< div className = "flex items-center gap-2" >
< Button
type = "button"
variant = "outline"
size = "sm"
disabled = { safePage === 0 }
onClick = { ( ) = > setPage ( safePage - 1 ) }
>
Previous
< / Button >
< span >
Page { safePage + 1 } of { totalPages }
< / span >
< Button
type = "button"
variant = "outline"
size = "sm"
disabled = { safePage >= totalPages - 1 }
onClick = { ( ) = > setPage ( safePage + 1 ) }
>
Next
< / Button >
< / div >
) }
< / 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 >
< / CardContent >
< / Card >
< / div >
) ;
}