brackt/plans/sport-season-pages.md
Chris Parsons 8aa1726b12
feat: highlight owned participants in standings and fix schedule event badges (#83)
- 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>
2026-03-07 21:59:29 -08:00

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

  1. Dual-mode sports (NBA: standings + bracket): The loader in $leagueId.sports-seasons.$sportsSeasonId.server.ts uses scoringPattern as an exclusive switch — it fetches standings OR bracket data, never both. Adding a phase field alone is insufficient. The loader must be refactored to fetch both data sets for dual-phase sports, with phase controlling which is displayed. Consider a compound scoring pattern like "season_standings_to_playoff" or decoupling data fetching from display via a displayMode derived from (scoringPattern, phase).

  2. QP standings missing team ownership: The SportSeasonDisplay component does NOT pass teamOwnerships to QualifyingPointsStandings. "My participants" highlighting is broken for golf/tennis. Must be fixed.

P1 — Data Integrity & Correctness

  1. Bracket + QP events (Tennis): No code path handles a scoring event that is both a bracket AND a QP event. processPlayoffEvent() and processQualifyingEvent() are separate functions. Need a processQualifyingBracketEvent() that derives QP placements FROM bracket results automatically.

  2. batchUpsertParticipantEVs not transactional: Uses Promise.all in 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.

  3. Simulation concurrency: If admin triggers "Run Simulation" while another admin is entering results, both write to participantExpectedValues simultaneously. The upsertParticipantEV function does SELECT-then-INSERT/UPDATE (not atomic). Need a simulation_in_progress flag or advisory lock.

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

  1. seasonIsFinalized TODO: In $leagueId.sports-seasons.$sportsSeasonId.tsx line 53, this is hardcoded with a // TODO: determine from data comment. Derive from sportsSeason.status === 'completed' or existence of participantResults.

  2. Simulation async model: Monte Carlo simulations could take 30+ seconds. Must run asynchronously with status tracking, not in an HTTP request.

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

  1. scoringType vs scoringPattern duplication: Both columns exist on sportsSeasons. Only scoringPattern is used in loaders. Either deprecate scoringType or document which is authoritative. Adding phase as a third axis without resolving this increases confusion.

  2. 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 eventDate populated 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.events with bulk-create form

Files to modify:

  • app/routes/admin.sports-seasons.$id.events.tsx — add bulk event creation
  • app/models/scoring-event.ts — add bulkCreateScoringEvents(), getUpcomingScoringEvents(sportsSeasonId), getRecentCompletedEvents(sportsSeasonId)

1b. Enhanced Season Standings Display

  • Show current championship standings (existing SeasonStandings component)
  • 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 seasonIsFinalized TODO (edge case #7)

Files to modify:

  • app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts — loader adds upcoming/recent events + user team ownership
  • app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx — pass new data to components, fix finalized TODO
  • app/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 season
  • app/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)
  • probFirst through probEighth, calculatedEV
  • source (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, snapshotDate
  • projectedPoints, actualPoints

Files:

  • database/schema.ts — add snapshot tables
  • app/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.tsSimulator interface, SimulationResult type
  • f1-simulator.ts — F1/IndyCar: based on current standings + remaining races
  • bracket-simulator.ts — Bracket sports: Monte Carlo simulation of remaining matches
  • registry.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 simulationStatus field to sportsSeasons (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 batchUpsertParticipantEVs in a transaction (edge case #4)

Files:

  • app/routes/admin.sports-seasons.$id.simulate.tsx — simulation trigger
  • app/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 teamOwnerships to QualifyingPointsStandings (edge case #2)
  • Add highlighting logic to QP standings component

Files:

  • app/components/scoring/SportSeasonDisplay.tsx — pass ownership data for QP case
  • app/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
  • eventResults can 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 — add getInProgressEvents()
  • 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 phase to control display (edge case #1)
  • Migration: set phase = null for all existing seasons (backwards compatible)

Also address: clarify scoringType vs scoringPattern — consider deprecating scoringType (edge case #10)

Files:

  • database/schema.ts — add phase enum/column
  • app/models/sports-season.ts — phase transition function
  • $leagueId.sports-seasons.$sportsSeasonId.server.ts — refactor to dual-mode data fetching
  • SportSeasonDisplay.tsx — phase-aware rendering
  • Admin UI — phase toggle

4b. Regular Season Standings

  • Show standings using participantSeasonResults during regular_season phase
  • 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:

  1. npm run typecheck — no type errors
  2. npm run test:run — unit tests pass
  3. npm run build — production build succeeds
  4. 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