- SeasonStandings: highlight rows for the logged-in user's drafted participants with electric accent (in-points) or muted (outside points) left border + star icon - Loader: derive userParticipantIds from current user's teams and draft picks - EventSchedule: hide type badge for schedule_event; show "Results Pending" only when event is past and not yet marked complete (matches admin page logic) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
15 KiB
Sport Season Pages & Scoring System Enhancement
Context
The league sport season pages currently show basic standings/brackets but lack:
- Schedule visibility: No upcoming events/races/games display
- "My team" highlighting: Users can't easily see which participants are theirs and when they play
- Sport-appropriate layouts: All sports use the same minimal display regardless of type
- EV tracking over time: No historical EV snapshots for trend analysis
- Regular season → playoff transitions: No way for sports like NBA to shift from standings to bracket
- Admin pain: Managing scoring across 4 patterns is tedious and the workflow has too many manual steps
The user wants rich, sport-aware season pages and plans to integrate TypeScript-based EV simulation models.
Sport Type Mental Model
| Category | Sports | How it works | Display |
|---|---|---|---|
| Season Standings | F1, IndyCar | Championship standings update over season; final standings = placements | Standings table + race schedule |
| Qualifying Points | Golf | Multiple discrete events (4 majors), QP per event, final QP rankings = placements | QP standings + event calendar + live event tracking |
| Tournament Bracket | Darts, Snooker | Seeded bracket, results → placements | Bracket visualization |
| Multi-Tournament QP | Tennis, Counter-Strike | Multiple tournament brackets, each awarding QP | QP standings + tournament bracket per event |
| Regular Season + Playoffs | NBA, NFL | Regular season standings → playoff bracket (two phases) | Phase-dependent: standings then bracket |
Known Edge Cases & Risks
These were identified during senior review and must be addressed in the relevant phase.
P0 — Architectural Blockers
-
Dual-mode sports (NBA: standings + bracket): The loader in
$leagueId.sports-seasons.$sportsSeasonId.server.tsusesscoringPatternas an exclusive switch — it fetches standings OR bracket data, never both. Adding aphasefield alone is insufficient. The loader must be refactored to fetch both data sets for dual-phase sports, withphasecontrolling which is displayed. Consider a compound scoring pattern like"season_standings_to_playoff"or decoupling data fetching from display via adisplayModederived from(scoringPattern, phase). -
QP standings missing team ownership: The
SportSeasonDisplaycomponent does NOT passteamOwnershipstoQualifyingPointsStandings. "My participants" highlighting is broken for golf/tennis. Must be fixed.
P1 — Data Integrity & Correctness
-
Bracket + QP events (Tennis): No code path handles a scoring event that is both a bracket AND a QP event.
processPlayoffEvent()andprocessQualifyingEvent()are separate functions. Need aprocessQualifyingBracketEvent()that derives QP placements FROM bracket results automatically. -
batchUpsertParticipantEVsnot transactional: UsesPromise.allin batches of 50 without a wrapping transaction. If it fails midway, some participants have updated EVs and others don't. Must be wrapped in a transaction. -
Simulation concurrency: If admin triggers "Run Simulation" while another admin is entering results, both write to
participantExpectedValuessimultaneously. TheupsertParticipantEVfunction does SELECT-then-INSERT/UPDATE (not atomic). Need asimulation_in_progressflag or advisory lock. -
Initial EV seeding: Participants in not-yet-started sports contribute 0 to team projections. Need to pre-populate EVs for all participants at season start, or clearly label projections as partial.
P2 — Display & UX Gaps
-
seasonIsFinalizedTODO: In$leagueId.sports-seasons.$sportsSeasonId.tsxline 53, this is hardcoded with a// TODO: determine from datacomment. Derive fromsportsSeason.status === 'completed'or existence ofparticipantResults. -
Simulation async model: Monte Carlo simulations could take 30+ seconds. Must run asynchronously with status tracking, not in an HTTP request.
-
EV snapshot retention: Define daily-only snapshots with composite unique index on
(participantId, sportsSeasonId, snapshotDate). Take snapshots from simulation output directly (not re-read from DB) to avoid partial-update captures.
P3 — Tech Debt
-
scoringTypevsscoringPatternduplication: Both columns exist onsportsSeasons. OnlyscoringPatternis used in loaders. Either deprecatescoringTypeor document which is authoritative. Addingphaseas a third axis without resolving this increases confusion. -
No result correction workflow: If admin enters wrong results, there's no undo. Need a "recalculate all from raw data" admin action per sports season.
Phase 1: F1/IndyCar Season Pages (Start Here)
Goal: Rich season standings page with schedule for currently-active sports.
1a. Enhance Scoring Events as Schedule Data
- Ensure scoring events for F1/IndyCar have
eventDatepopulated for all races - Add admin bulk-import: paste race names + dates (one per line, tab or comma separated) to create events
- Admin route: enhance
admin.sports-seasons.$id.eventswith bulk-create form
Files to modify:
app/routes/admin.sports-seasons.$id.events.tsx— add bulk event creationapp/models/scoring-event.ts— addbulkCreateScoringEvents(),getUpcomingScoringEvents(sportsSeasonId),getRecentCompletedEvents(sportsSeasonId)
1b. Enhanced Season Standings Display
- Show current championship standings (existing
SeasonStandingscomponent) - Add upcoming events section: next 3-5 events with dates
- Add recent results section: last completed events
- Highlight participants owned by the current user's team with team color/badge
- Show participant EV alongside standings position
- Fix: resolve
seasonIsFinalizedTODO (edge case #7)
Files to modify:
app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts— loader adds upcoming/recent events + user team ownershipapp/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx— pass new data to components, fix finalized TODOapp/components/sport-season/SeasonStandings.tsx— add schedule section + ownership highlighting- New component:
app/components/sport-season/EventSchedule.tsx
1c. League Home Enhancement
- On the league home page, show next upcoming event for each active sport season card
- Example: "Next: Monaco Grand Prix — May 25"
Files to modify:
app/routes/leagues/$leagueId.tsx— loader fetches next event per sport seasonapp/components/sport-season/SportSeasonCard.tsx— display next event
Phase 2: EV Simulation Framework
Goal: Infrastructure for sport-specific EV simulations in TypeScript, with historical tracking.
2a. EV History Snapshots Table
New table participant_ev_snapshots:
id,participantId,sportsSeasonId,snapshotDate(date)probFirstthroughprobEighth,calculatedEVsource(simulation method used)- Composite unique index on
(participantId, sportsSeasonId, snapshotDate)— one snapshot per participant per day - Retention: add
deleteSnapshotsBefore(date)for cleanup
New table team_ev_snapshots:
id,teamId,seasonId,snapshotDateprojectedPoints,actualPoints
Files:
database/schema.ts— add snapshot tablesapp/models/ev-snapshot.ts— CRUD + query by date range + retention- Migration via
npm run db:generate
2b. Simulation Runner Framework
Create app/services/simulations/ directory:
types.ts—Simulatorinterface,SimulationResulttypef1-simulator.ts— F1/IndyCar: based on current standings + remaining racesbracket-simulator.ts— Bracket sports: Monte Carlo simulation of remaining matchesregistry.ts— Map sport slugs → simulator classes. Return clear error for unmapped sports.
interface Simulator {
simulate(sportsSeasonId: string): Promise<SimulationResult[]>;
}
interface SimulationResult {
participantId: string;
probabilities: ProbabilityDistribution;
source: ProbabilitySource;
}
2c. Simulation Trigger & Admin UI
- Admin action button: "Run Simulation" on sports season page
- Run asynchronously — add
simulationStatusfield tosportsSeasons(idle | running | failed) to prevent concurrent runs (edge case #5) - Take EV snapshot directly from simulation output (not re-read from DB) to avoid partial captures (edge case #9)
- Fix: wrap
batchUpsertParticipantEVsin a transaction (edge case #4)
Files:
app/routes/admin.sports-seasons.$id.simulate.tsx— simulation triggerapp/models/participant-expected-value.ts— add transaction wrapping to batch upsert
2d. Initial EV Seeding
- After draft completes, run initial simulations for all linked sports seasons
- This prevents 0-value projections for unstarted sports (edge case #6)
2e. EV Trend Display
- On sport season detail page, show EV trend chart for top participants
- On team detail page, show projected points trend over time
- Reuse existing chart pattern from standings snapshots
Files:
- Sport season detail route — add EV trends section
- Team detail route — add projected trend section
Phase 3: Golf/Majors-Based Sports
Goal: Event-aware QP display with in-progress tournament tracking.
3a. Fix QP Ownership Display
- Fix: pass
teamOwnershipstoQualifyingPointsStandings(edge case #2) - Add highlighting logic to QP standings component
Files:
app/components/scoring/SportSeasonDisplay.tsx— pass ownership data for QP caseapp/components/sport-season/QualifyingPointsStandings.tsx— add highlighting
3b. Event Calendar View
- Show all QP events (majors) in a timeline/calendar format
- Status per event: upcoming / in-progress / completed
- Show dates and results for completed events
Files:
- New component:
app/components/sport-season/EventCalendar.tsx - Integrate into QP standings display
3c. In-Progress Event Tracking
- During a major, show current leaderboard within the event
eventResultscan have partial/updating data before event completion- Derive "in-progress" state from: has results but
isComplete = false - Admin can update leaderboard nightly
Files:
app/models/scoring-event.ts— addgetInProgressEvents()- Admin event page — easier result entry UX for partial updates
3d. "My Participants" in Events
- When viewing a major leaderboard, highlight user's team's golfers
Phase 4: Sports Season Phases (NBA/NFL)
Goal: Support sports that transition from regular season to playoffs.
4a. Season Phase System
Add phase column to sportsSeasons:
regular_season|playoffs| null (not applicable)- Separate from
status(upcoming/active/completed) - Refactor loader to fetch both standings AND bracket data for phase-capable sports, using
phaseto control display (edge case #1) - Migration: set
phase = nullfor all existing seasons (backwards compatible)
Also address: clarify scoringType vs scoringPattern — consider deprecating scoringType (edge case #10)
Files:
database/schema.ts— addphaseenum/columnapp/models/sports-season.ts— phase transition function$leagueId.sports-seasons.$sportsSeasonId.server.ts— refactor to dual-mode data fetchingSportSeasonDisplay.tsx— phase-aware rendering- Admin UI — phase toggle
4b. Regular Season Standings
- Show standings using
participantSeasonResultsduringregular_seasonphase - Conference/division groupings (future: metadata on participants)
4c. Playoff Bracket
- Phase transitions to
playoffs→ display switches to bracket - Bracket seeded from regular season standings
- Round completion triggers scoring
4d. Upcoming Games (Future)
- Game schedule during regular season
- Highlight user's teams' games
- Requires sport-specific data source
Phase 5: Multi-Tournament Sports (Tennis, CS)
Goal: Support sports with multiple tournament brackets that each award QP.
5a. Qualifying Bracket Event Processing
- Create
processQualifyingBracketEvent()that derives QP placements from bracket results automatically (edge case #3) - When a bracket completes in a QP-bracket sport, translate bracket elimination → event placement → QP award
Files:
app/models/scoring-calculator.ts— new processing function
5b. Tournament-as-Event Display
- Each Grand Slam is a scoring event WITH a bracket
- QP standings show overall rankings
- Click into event → see bracket for that tournament
- Active tournament shows live bracket; completed shows results summary
5c. Event Timeline
- Show all Grand Slams in a timeline with status and QP awarded
Phase 6: Admin UX & Scoring Simplification
Goal: Reduce manual work for commissioners/admins.
6a. Streamlined Result Entry
- Bracket events: click-to-advance UI
- Season standings: paste standings update (position + points per participant)
- QP events: paste leaderboard results
6b. Auto-Finalization
- QP sport: all events complete → prompt to finalize
- Bracket: final complete → auto-calculate placements
- Reduce manual finalize steps
6c. Result Correction Workflow
- Admin "Recalculate All" button per sports season (edge case #11)
- Re-derives all results from raw match/event data
- Re-runs standings calculations and probability updates
6d. Schedule Import
- Per-sport integration for schedule data
- Start with F1 (well-structured data)
- Design extensible interface for new sports
Implementation Order
Phase 1 (F1/IndyCar) ──→ Phase 2 (EV Framework) ──→ Phase 3 (Golf)
──→ Phase 4 (NBA/NFL)
──→ Phase 5 (Tennis/CS)
──→ Phase 6 (Admin UX)
Phase 1 is independent and the quickest win. Phase 2 provides infrastructure used by all subsequent phases. Phases 3-6 can proceed in any order after Phase 2.
Key Existing Code to Reuse
| Code | Location | Used For |
|---|---|---|
getScoringEventsForSportsSeason() |
app/models/scoring-event.ts |
Schedule data |
getSeasonResults() |
app/models/participant-season-result.ts |
F1 standings |
SeasonStandings component |
app/components/sport-season/SeasonStandings.tsx |
Base standings display |
SportSeasonCard component |
app/components/sport-season/SportSeasonCard.tsx |
League home cards |
batchUpsertParticipantEVs() |
app/models/participant-expected-value.ts |
Simulation output |
calculateEV() |
app/services/ev-calculator.ts |
EV calculation |
teamStandingsSnapshots pattern |
app/models/standings.ts |
Snapshot pattern to replicate for EV |
processPlayoffEvent() / processQualifyingEvent() |
app/models/scoring-calculator.ts |
Basis for processQualifyingBracketEvent() |
Verification Plan
For each phase:
npm run typecheck— no type errorsnpm run test:run— unit tests passnpm run build— production build succeeds- Manual testing:
- Phase 1: League → sport season for F1 shows standings + schedule + ownership highlights
- Phase 2: Admin runs simulation → EVs update, snapshot created, trend chart renders
- Phase 3: Golf sport season shows QP standings with ownership + event calendar + in-progress leaderboard
- Phase 4: NBA sport season transitions from regular season standings to playoff bracket
- Phase 5: Tennis shows QP standings + per-Grand-Slam bracket drill-down
- Phase 6: Admin can bulk-enter results, auto-finalize, correct errors