- 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>
320 lines
15 KiB
Markdown
320 lines
15 KiB
Markdown
# 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
|
|
|
|
3. **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.
|
|
|
|
4. **`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.
|
|
|
|
5. **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.
|
|
|
|
6. **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
|
|
|
|
7. **`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`.
|
|
|
|
8. **Simulation async model**: Monte Carlo simulations could take 30+ seconds. Must run asynchronously with status tracking, not in an HTTP request.
|
|
|
|
9. **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
|
|
|
|
10. **`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.
|
|
|
|
11. **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.ts` — `Simulator` 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.
|
|
|
|
```typescript
|
|
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
|