Commit graph

518 commits

Author SHA1 Message Date
Chris Parsons
47b8935be6
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
Chris Parsons
0277c4d25e
Add per-event configurable region config for NCAA-style brackets (#149) (#149)
Replaces the hardcoded 2026 Men's region structure with a per-event
configurable system so the same ncaa_68 template works for Women's
March Madness (different region names and play-in placements each year).

- Add `bracket_region_config jsonb` column to `scoring_events`
- Export `ALL_16_SEEDS` and `BracketRegion`/`BracketPlayIn` interfaces
  from bracket-templates.ts so both server and client share the type
- Admin bracket entry form now shows a "Configure Regions" section:
  editable region names + add/remove play-in seed selectors (ShadCN
  Select) per region; slot map updates live as config changes
- Server action parses region config from form, validates total = 68,
  stores it on the event, and passes it to bracket generation
- `advanceFirstFourWinner` reads stored `bracketRegionConfig` from the
  event row first, falls back to template.regions for existing events
- Fix opponentSeed formula (was `seedNum <= 8 ? 17-n : n`, always `17-n`)
- Remove duplicate ALL_SEEDS inline definitions (now a shared constant)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 21:52:47 -07:00
Chris Parsons
67c7de1924
Fix event date timezone bugs (UTC rollover + same-day status) (#148)
* Fix event date display off-by-one for late-night events in UTC-negative timezones

Events saved at 10 PM PDT (UTC-7) cross UTC midnight, so eventDate is stored
as the next UTC calendar day (e.g. March 28 10 PM PDT → eventDate "2026-03-29").
Displaying that date string via parseISO() gives March 29 local midnight, so
the events list showed "Mar 29" when the user expected "Mar 28".

Fix: when eventStartsAt is available, derive the display date from it directly
(format(new Date(eventStartsAt), ...)) rather than from the stored eventDate
string. This correctly converts the UTC timestamp to the user's local calendar
date. Date-only events (no eventStartsAt) are unchanged and continue to use
parseISO(eventDate).

Also tighten the getDisplayDate() guard: replace a misleading try/catch (new
Date() never throws) with an explicit isNaN check, and replace a non-null
assertion (eventDate!) with null-coalescing in the admin events list.

Tests cover both UTC and PDT environments and are timezone-independent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix same-day events incorrectly showing Upcoming instead of Results Pending

String date comparison (eventDate < today) is strictly less-than, so events
on the current day always showed Upcoming regardless of time. When eventStartsAt
is available, use timestamp comparison (new Date(eventStartsAt) < new Date())
for accurate past/future determination.

Fixed in three locations: admin events list (getStatusBadge call site),
admin event detail isPast check, and EventSchedule upcoming badge.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 12:28:39 -07:00
Chris Parsons
d546585fb6
Fix event date display off-by-one for late-night events in UTC-negative timezones (#147)
Events saved at 10 PM PDT (UTC-7) cross UTC midnight, so eventDate is stored
as the next UTC calendar day (e.g. March 28 10 PM PDT → eventDate "2026-03-29").
Displaying that date string via parseISO() gives March 29 local midnight, so
the events list showed "Mar 29" when the user expected "Mar 28".

Fix: when eventStartsAt is available, derive the display date from it directly
(format(new Date(eventStartsAt), ...)) rather than from the stored eventDate
string. This correctly converts the UTC timestamp to the user's local calendar
date. Date-only events (no eventStartsAt) are unchanged and continue to use
parseISO(eventDate).

Also tighten the getDisplayDate() guard: replace a misleading try/catch (new
Date() never throws) with an explicit isNaN check, and replace a non-null
assertion (eventDate!) with null-coalescing in the admin events list.

Tests cover both UTC and PDT environments and are timezone-independent.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 11:18:16 -07:00
Chris Parsons
21597ba57b Filter unnecessary sentry css and js errors. 2026-03-15 10:24:57 -07:00
Chris Parsons
dc13d99f39
Add event_starts_at timestamp to scoring events for sub-day ordering (#146)
- New `event_starts_at timestamptz` column on `scoring_events`; backfilled
  from `event_date` (midnight UTC) via migration for all existing rows
- Admin create/edit forms consolidated from separate Date + Time inputs into
  a single `datetime-local` field; server derives `eventDate` from the UTC
  date portion of `eventStartsAt` so calendar-window queries continue to work
- Edit form pre-fill computed client-side via useEffect to avoid server-timezone
  mismatch for non-UTC admins
- Sort fix: replaced `eventDate ?? earliestGameTime.split("T")[0]` with
  `toEventSortKey` helper that prefers `earliestGameTime ?? eventDate` so
  bracket events with only per-game timestamps sort in the correct calendar position
- DB sort queries updated to use `eventStartsAt` as a tiebreaker after `eventDate`
- F1/IndyCar/golf events now populate `earliestGameTime` from `eventStartsAt`,
  giving time display in `UpcomingCalendarPanel`
- Bulk import gains optional `HH:MM` (UTC) third column
- New unit tests for `toEventSortKey` and `eventStartsAt` model behavior

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 10:22:42 -07:00
Chris Parsons
4fe76b6845
Display upcoming matches (#141) 2026-03-12 10:12:38 -07:00
Chris Parsons
f97ee4e4d8
Add NProgress navigation progress bar (#139)
Installs nprogress and wires it to React Router's useNavigation() hook
so a progress bar appears at the top of the page during route transitions.
Themed to the site's --electric CSS variable with the spinner disabled.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 22:02:48 -07:00
Chris Parsons
3dc6549640 MCP update 2026-03-11 21:45:19 -07:00
Chris Parsons
ed4e99b40e
Fix datetime-local to UTC conversion for game scheduling (#138)
* fix: use hidden input for UTC date value in add-game form

The onSubmit handler was setting the datetime-local input's value to an
ISO UTC string (e.g. "2026-03-11T17:00:00.000Z"). datetime-local inputs
only accept the format "YYYY-MM-DDTHH:MM" — values with a 'Z' timezone
suffix are invalid and the browser silently clears the field to "". This
caused the form to submit an empty scheduledAt, which the server treated
as null, so games were always saved without a date.

Fix by keeping the datetime-local input for user interaction (renamed
scheduledAtLocal, not submitted) and writing the converted UTC ISO string
into a separate hidden input named scheduledAt in the onSubmit handler.

Also adds app/lib/date-utils.ts with a localDateTimeToUtcIso helper and
matching tests to document and verify the datetime-local ↔ ISO conversion
behaviour.

https://claude.ai/code/session_0148fgZiXpyvGX8ZX3BFRGCs

* refactor: use localDateTimeToUtcIso in component and clean up tests

- Import and call localDateTimeToUtcIso in the add-game onSubmit handler
  instead of duplicating the conversion inline; also applies the null/
  invalid-date safety from the utility to the component
- Remove the redundant "incompatible with datetime-local" test case (its
  intent is better expressed as a code comment than a test assertion)
- Move the datetime-local incompatibility explanation into a NOTE comment
  on the utility function's JSDoc

https://claude.ai/code/session_0148fgZiXpyvGX8ZX3BFRGCs

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 16:22:29 -07:00
Chris Parsons
c1270f6c9c
Fix game datetime timezone handling in bracket admin (#137)
- Convert datetime-local input value to UTC ISO string on form submit
  so server correctly parses local time instead of treating it as UTC
- Show local timezone abbreviation (e.g. PDT) in the field label
- Hoist timezone abbreviation computation out of the matches render loop

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 15:13:50 -07:00
Chris Parsons
8666ff73c3 Found it in a second spot. 2026-03-11 14:47:19 -07:00
Chris Parsons
9d76178f56 Fix bad migration. 2026-03-11 14:38:45 -07:00
Chris Parsons
b1f778bab2 Fixes missing icon. 2026-03-11 14:20:53 -07:00
Chris Parsons
2f14565d43
Add playoff match game scheduling and odds management (#135)
* Add playoff match games and odds storage

Introduces two new tables for bracket matchup detail storage:

- `playoff_match_games`: tracks individual game schedules within a
  series matchup (game number, scheduledAt, status, per-game scores,
  winner). Supports scheduled/complete/postponed status enum.
- `playoff_match_odds`: stores moneyline odds per participant per
  matchup (single upsert record, no isLatest complexity).

Includes:
- Drizzle schema + relations with CASCADE deletes from playoff_matches
- Migration 0040_fat_puma.sql
- playoff-match-game.ts model with pure helpers: computeSeriesScore,
  isSeriesComplete, getSeriesLeader — plus full CRUD
- playoff-match-odds.ts model with pure helpers: americanToImpliedProbability,
  impliedProbabilityToAmerican, normalizeOdds — plus upsert/read/delete
- findPlayoffMatchesByEventId and findPlayoffMatchById updated to
  include games and odds in their query results
- Bracket server route: add-game, update-game, delete-game,
  upsert-odds, delete-odds actions
- Bracket admin UI: expandable per-match panel for game schedule
  management and moneyline odds entry
- 41 new unit tests (18 game + 23 odds), all 810 tests passing

https://claude.ai/code/session_01Twt3D1bsEK3eXUhMaz6ee7

* Code review fixes: type safety, abstraction, and React correctness

- Derive PlayoffMatchGameStatus from schema enum instead of hardcoding
  the string union, eliminating the duplicate source of truth
- updateGame now returns PlayoffMatchGame | undefined to reflect reality
  when no row matches the ID
- Remove TOCTOU check-then-act in update-game action: call updateGame
  directly and check the return value instead of a pre-flight findGameById
- Add status enum validation before the cast in update-game action
- Move impliedProbability computation inside upsertMatchOdds so callers
  only provide moneylineOdds; the model owns the derivation
- Remove unnecessary dynamic import of americanToImpliedProbability in
  the upsert-odds action (was already imported from the same module)
- Fix React list reconciliation bug: replace bare <> fragment with
  <Fragment key={match.id}> so React can correctly track rows

https://claude.ai/code/session_01Twt3D1bsEK3eXUhMaz6ee7

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 14:17:43 -07:00
Chris Parsons
c8b0da5cad Fix instrument file not being added to docker image 2026-03-11 09:18:08 -07:00
Chris Parsons
cbae4cc0c4
fix: replace react-router-serve with node dist/server.js in start script (#133)
The project uses a custom Express server (built to dist/server.js) rather
than the react-router-serve CLI, which was never installed. Also adds the
start:production alias referenced in CLAUDE.md.

https://claude.ai/code/session_011hnBY83ui5P5MzWt3iYppc

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 09:06:11 -07:00
Chris Parsons
35a3b71579
feat: add Sentry error monitoring (#132)
* feat: add Sentry error monitoring (#77)

Installs and configures @sentry/react-router with server and client
instrumentation. Disabled in development to avoid noise; only active
in production.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: add VSCode Sentry MCP server config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: pass vite env to sentryReactRouter plugin

sentryReactRouter requires the ConfigEnv as a second argument to read
the vite `command` property.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 23:35:33 -07:00
Chris Parsons
70c22d03fc
feat: add UCL bracket Monte Carlo simulator (#131)
Implements a 16-team UEFA Champions League knockout bracket simulator
using blended Elo + futures odds probabilities (70/30 split). Tracks
integer placement counts per tier to avoid fractional accumulation
drift, ensuring total EV sums to exactly 340 by construction.

- New UCLSimulator class reading live playoffMatches from DB
- Respects completed match results (eliminated teams get exact EV)
- New ucl_bracket simulator_type enum value + migration
- Registered in simulator registry; added to admin sport selector
- 18 unit tests covering math, bracket path, and integration scenarios

Fixes #113

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 23:04:51 -07:00
Chris Parsons
80bdbbc3bb
fix: clean up derived data when deleting scoring events (#130)
- Bracket events (playoff_game): delete all participantResults for the
  sports season atomically with the event, then recalculate league standings
- Qualifying events: capture affected participant IDs before cascade
  deletes eventResults, recalculate participantQualifyingTotals from
  remaining events, decrement majorsCompleted if QP had been processed,
  and recalculate league standings

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 21:47:47 -07:00
Chris Parsons
50722531ca
Create label-subissues.yaml 2026-03-10 13:40:44 -07:00
Chris Parsons
d784571f29
feat: add meta title to all route pages (#109)
Every route now exports a meta function so the browser title bar reflects
the current page. Static pages use fixed titles; dynamic pages pull names
from loader data with a sensible fallback (e.g. league name, sport season
name, team name).

Titles follow the pattern "Page Name - Brackt" for user-facing routes and
"Page Name - Brackt Admin" for admin routes.

Fixes #74

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 12:10:52 -07:00
Chris Parsons
cf8ac8a765
feat: progressive floor scoring for playoff brackets (#100)
When a participant wins a bracket round, they immediately earn provisional
"floor" points (the averaged minimum they'd receive if eliminated next round).
These update as they advance and are replaced by finalized scores on elimination.

Key changes:
- Add `is_partial_score` column to `participant_results` (migration 0038)
- `processPlayoffEvent`: assign provisional position 5 to non-scoring round
  winners; assign round-appropriate floors to scoring round winners via
  `getGuaranteedMinimumPosition`; add catch-all for unrecognized round names
- `upsertParticipantResult`: guard against un-finalizing rows (never overwrite
  isPartialScore=false with true)
- `calculateBracketPoints`: new function averaging tied bracket tiers
  (5-8 → 20 pts, 3-4 → avg, 1-2 solo); used in `calculateTeamScore` for
  playoff_bracket pattern (pattern-aware, doesn't affect F1/golf scoring)
- `PlayoffBracket`: "In Contention" table for still-active participants;
  AFL double-chance fix (participants who won a later match excluded from
  earlier round's loser list); correct `nextRank` starting position
- Server loader: batch owner DB queries (one query vs N+1); deduplicate
  participantPoints; use calculateBracketPoints for bracket point display
- Clean up Phase/Q-number tracking comments throughout scoring-calculator.ts
- 3 new tests for non-scoring round provisional floor behavior

Also includes a dev admin bypass via DEV_ADMIN_CLERK_ID env var (separate
change on this branch, not part of floor scoring feature).

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 10:27:58 -07:00
Chris Parsons
d88be08deb
User/chris/bracket UI redesign (#95)
* feat: redesign playoff bracket UI with compact layout and owner display

- Replace per-match Card wrappers with compact left-vs-right rows (stacks on mobile)
- Add TeamOwnerBadge component (colored avatar + team name + username), matching the standings "Drafted By" style
- Show owner info below each participant name in bracket matches
- Add TBD forward-reference labels ("Winner of Quarterfinals M1") computed via buildFeederMap
- Add Final Rankings table below bracket showing all participants ranked by elimination round
- Fix round ordering in loader: sort by match count descending (more matches = earlier round)
- Add loser relation to playoff matches query for elimination tracking
- Extract getAvatarColor to app/lib/color-hash.ts (shared across GroupStageDisplay, SeasonStandings, TeamOwnerBadge)
- Extract groupMatchesByRound helper; share grouped map between feeder computation and render
- Remove dead getTeamAvatar from SeasonStandings after TeamOwnerBadge adoption
- Add tests for groupMatchesByRound and buildFeederMap (7 tests)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add draft order card to league home page

Show draft order on the league home page when order is set and season
is in pre_draft or draft status. Includes team name, owner name, and
a link to the draft room.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: enhance playoff bracket with eliminated teams, points, and ownership highlights

- Show eliminated teams (including group-stage losers) in Final Rankings card
- Display computed fantasy points per participant in rankings table
- Card title switches between "Eliminated Teams" and "Final Rankings" based on bracket completion
- Highlight owned participants with electric blue name, dot indicator, and card border in bracket matches
- Highlight owned rows with electric border/background in rankings table
- Suppress EventSchedule for playoff_bracket sports seasons
- Fix title redundancy: bracket section title no longer repeats sport name
- Two-column match grid on desktop (md:grid-cols-2)
- Code review fixes: parallel DB fetches, targeted query for pre-eliminated, single-pass parseFloat, remove IIFE

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 22:44:33 -07:00
Chris Parsons
c6ba59b0e6
User/chris/ev f1 framework (#93)
* feat: EV simulation framework with F1 Monte Carlo simulator

- Add EV snapshot tables (participant_ev_snapshots, team_ev_snapshots) and simulation_status column on sports seasons
- Add ev-snapshot model with upsert and history query functions
- Add simulator framework: types, bracket/F1/golf simulators, registry
- F1 simulator: vig-removed ICM weighted draw (pre-season) + race-by-race Monte Carlo from current standings (in-season); per-position column normalization to prevent floating-point EV drift
- Add admin simulate route and Run Simulation button on sports season page
- Rework futures-odds admin page to save odds then run simulation in one action
- Remove recalculate-probabilities route (superseded by simulate route)
- Remove EV trend chart panel and associated DB queries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: map simulators to sports via simulatorType field

Adds a `simulator_type` enum column to the `sports` table so each sport
can be assigned a specific simulation algorithm rather than deriving it
from the sports season's scoring pattern.

- Add `simulatorTypeEnum` (f1_standings, indycar_standings,
  golf_qualifying_points, playoff_bracket) + `simulatorType` nullable
  column on `sports` table; migration 0037
- Rewrite simulator registry to key off `SimulatorType` instead of
  `ScoringPattern`; indycar_standings shares F1Simulator for now
- `findSportsSeasonById` now returns `SportsSeasonWithSport` so callers
  have typed access to `sport.simulatorType`
- Simulate and futures-odds actions read `sport.simulatorType`; guard
  fires before setting `simulationStatus: running`
- Admin sport edit page gains a Simulator Type dropdown

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 15:34:31 -07:00
Chris Parsons
e72c33d5b5
refactor: fix event edit page code review issues (#87)
- Extract EditEventCard component to eliminate duplicated edit form
- Add edit form to non-schedule event types (bracket events)
- Move actionData feedback above debug cards for better UX
- Guard "Add Result" form from showing on playoff_game events
- Fix isPast date comparison to be safe for Date or string types
- Remove unused qpConfig/qpStandings destructuring

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 23:43:24 -08:00
Chris Parsons
447546a4c2
fix: add statement-breakpoint to schedule_event enum migration (#86)
ALTER TYPE ADD VALUE cannot run inside a transaction in older PostgreSQL
versions. The missing breakpoint caused migration 0033 to be skipped in
production, leaving the 'schedule_event' enum value absent.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 22:45:59 -08:00
Chris Parsons
35fe84a1dd
Update claude.md to push more tests. (#84)
* Update claude.md to push more tests.

* Archiving old plans.

* fix: run DB migrations programmatically on server startup

Replace unreliable drizzle-kit CLI migration with drizzle-orm's built-in
migrator running in the server process before accepting connections. This
ensures migrations are always applied on deploy and fails fast if they error.

- Add runMigrations() to server.ts using drizzle-orm/postgres-js/migrator
- Skip migrations in development (handled manually via db:migrate)
- Add DATABASE_URL guard with a clear error message
- Remove start:production script (now identical to start)
- Update Dockerfile CMD to use npm run start

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 22:31:04 -08:00
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
Chris Parsons
b0960c3c30
feat: improve league home standings and sports seasons UX (#65)
- Sort sports seasons by status (active → upcoming → completed), with
  season_standings pattern last within active, then alphabetical
- Fix rank display for teams without standings: T1 when no scores exist,
  T{n+1} when some teams are scored (avoids misleading T1 for unranked teams)
- Extract rank logic to getDisplayRank() in standings-display.ts with tests
- Pre-compute standingsMap and sortedTeams to eliminate O(n²) render lookups
- Fix invite URL SSR flash by deriving origin in the loader
- Guard toast useEffect with processedParamsRef to prevent double-fire
- Hide Teams Filled and Draft Order Set post-draft; show Standings sidebar
  link only when active/completed
- Show "Final standings" label when season is completed
- Remove duplicate Season Status from sidebar; add Flex Spots description
- Fix status type cast to occur at the model mapping layer, not in JSX

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 15:57:51 -08:00
Chris Parsons
8c707d2e79
League page fixes (#64) 2026-03-05 10:07:15 -08:00
Chris Parsons
472ceca92d
feat: implement real-time queue updates via socket events in queue actions (#63) 2026-03-04 21:39:54 -08:00
Chris Parsons
435acdeef0
Add favicons and logo (#62) 2026-03-04 00:23:45 -08:00
Chris Parsons
40167cfa5e
feat: enhance autodraft functionality with detailed settings and commissioner controls (#61) 2026-03-03 20:14:38 -08:00
Chris Parsons
d53bd271c6
feat: add totalRounds prop to DraftSummaryView and pass from DraftRoom (#60) 2026-03-02 22:19:19 -08:00
Chris Parsons
16aa450d63
feat: proactively prune ineligible queue items after each pick (#59)
After every pick, recalculate draft eligibility for all teams and
remove any queued participants whose sport is no longer eligible
(e.g. a team queued a snooker player but just filled their last flex
slot). Previously this was only caught lazily when autodraft fired,
which could pause the draft or pick an unwanted player.

- Add getAllQueuesForSeason to draft-queue.ts — fetches all queue rows
  for a season in one query (Map<teamId, QueueItem[]>) instead of N+1
  per-team queries
- Add pruneIneligibleQueueItems to draft-utils.ts — uses Promise.all
  for the four required data fetches, collects ineligible items in a
  single loop pass, warns on orphaned participant references
- Call from both pick paths: executeAutoPick and draft.make-pick.ts
- Emit queue-eligibility-pruned socket event per affected team so the
  client updates the queue UI in real time
- Add 5 tests covering: single ineligible removal, all eligible (no-op),
  empty queues (no delete called, getTeamQueue never called), mixed
  queue (only ineligible item removed), and unknown participant (warn)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 22:07:22 -08:00
Chris Parsons
09918f4b38
fix: improve JWT expiry handling and revalidation logic in DraftRoom … (#58)
* fix: improve JWT expiry handling and revalidation logic in DraftRoom component

* fix: ensure authRecoveryTimerRef is initialized to undefined to prevent potential errors
2026-03-02 20:55:42 -08:00
Chris Parsons
599bba8949
refactor: address code review findings in Roster/Summary draft views (#57)
- Extract groupPicksByTeamAndSport into shared picks-utils.ts to remove
  duplication between TeamRosterView and DraftSummaryView
- Fix TeamRosterView: make teamPicks a proper useMemo dep on selectedTeamId,
  add useEffect to reset stale selection if draftSlots changes
- Add accessible label/htmlFor to TeamRosterView team Select
- Remove dead `season` prop from DraftSummaryView (was inherited from
  TeamsDraftedGrid but never used)
- Fix DraftSummaryView: sport name cells are now <th scope="row">, column
  headers have scope="col", Total header uses solid bg-muted to match Sport
- Replace || null with explicit > 0 check in Total cell for clarity
- Consolidate to summaryViewProps/rosterViewProps in route for consistency

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 16:46:35 -08:00
Chris Parsons
46332ea735
feat: enhance autodraft settings with detailed option descriptions an… (#56)
* feat: enhance autodraft settings with detailed option descriptions and improved state handling

* test: update AutodraftSettings tests for button interactions and API payloads
2026-03-02 16:26:18 -08:00
Chris Parsons
ca2fd288ab
perf: fix draft room lag from excessive re-renders and listener leaks (#54)
Primary fix: setTeamTimers now bails out with `return prev` when the
value hasn't changed, preventing a full DraftRoom re-render on every
1-second timer tick (was 33% of profiler samples).

Memoization: wrap AvailableParticipantsSection, TeamsDraftedGrid,
QueueSection, SidebarRecentPicks, and DraftGridSection in React.memo
so timer ticks don't cascade into heavy components that don't use
timer state.

Stable refs: wrap nine action handlers in useCallback and extract two
inline arrow functions from props objects so memo() comparisons
actually bail out. Memoize the { numFlexPicks } object passed to
TeamsDraftedGrid.

socketVersion: expose an incrementing counter from useDraftSocket so
the socket handler effect re-registers on socket recreation.

Async cleanup: add abort flag + in-flight guard to the visibilitychange
JWT refresh handler to prevent concurrent executions and stale state
updates after unmount. Add abort flag to useDraftNotifications
permissions.query() to prevent dangling onchange if unmounted
mid-promise.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 13:18:47 -08:00
Chris Parsons
7b491d86d2
refactor: integrate sport filter dropdown with drafted sports toggle (#53)
- Move "Show drafted sports" into the sport filter dropdown (sheet/popover)
  instead of a standalone checkbox in the filter bar
- When unchecked, drafted sports are hidden from the dropdown list entirely;
  when checked, they appear in alphabetical order with muted text
- Auto-deselect drafted sport filters when hiding drafted sports
- Reset button also restores "Show drafted sports" to checked
- Extract SportFilterContent component to eliminate sheet/popover duplication
- Make userDraftedSportNames required; add useCallback for handlers;
  memoize triggerText/triggerAriaLabel; unify all checkboxes to ShadCN Checkbox;
  replace empty div divider with hr

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 10:13:51 -08:00
Chris Parsons
b5a6741d45
feat: add public read-only draft state API endpoint (#52)
Adds GET /api/seasons/:seasonId/draft returning current pick number,
who's on the clock, and the full picks list with usernames and sport.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 00:35:23 -08:00
Chris Parsons
da0e0f2be8
feat: convert sport filter to multi-select with mobile bottom sheet (#51)
Replaces the single-sport native <select> with a popover (desktop) and
bottom sheet (mobile) checkbox multi-select. No sports selected shows all
participants; selecting one or more narrows the list. Trigger label adapts
to reflect current filter state and a Reset button appears when filters
are active.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 00:29:54 -08:00
Chris Parsons
5f23885097
fix: prevent silent logout during long-running draft sessions (#49)
Clerk's short-lived JWTs (60s) can expire when browser tabs are
backgrounded and the SDK's refresh interval is throttled. This left
users silently logged out — actions returned 401s shown as generic
error toasts with no recovery path.

Prevention: refresh the Clerk JWT on tab visibility change so the
token is always fresh when the user returns from a backgrounded tab.

Detection: replace all 12 raw fetch() calls with an authFetch wrapper
that catches 401 responses and sets an authDegraded state, triggering
a blocking recovery overlay instead of confusing toasts.

Recovery: AuthRecoveryOverlay (mirrors ConnectionOverlay pattern)
prompts the user to reload the page, which re-authenticates via
Clerk's long-lived session cookie.

Also adds missing try/catch to handleStartDraft, handleMakePick,
handleForceAutopick, handleForceManualPick, and handleReplacePick
which previously had no network error handling.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 19:33:03 -08:00
Chris Parsons
6c480d47a0
fix: guard against stale revalidation overwriting fresh socket data
Add a reference-equality check so that if HTTP revalidation fails (network
error, expired token), the sync effect does not overwrite fresh data that
draft-state-sync already applied. Also adds missing dependency array entries
(userQueue, timers, autodraftSettings) to the revalidation sync effect.

https://claude.ai/code/session_01JxJ1CYTiFWV4KTPxTnjrms (#48)

* fix: sync all draft state on mobile reconnection

After a mobile browser returns from a long background period, the draft
room had stale picks, wrong "on the clock" display, and inaccurate
available player lists. The root cause was that reconnection relied
solely on an HTTP revalidation that could fail (expired JWT, flaky
network), and the timer-update handler ignored currentPickNumber.

Changes:
- Server emits draft-state-sync on join-draft with full picks, timers,
  and season state, giving the client an immediate socket-based sync
  path that doesn't depend on HTTP revalidation
- timer-update handler now syncs currentPickNumber, fixing the "on the
  clock" display within 1 second of reconnection
- Revalidation retry with 3s delay ensures the HTTP path succeeds even
  when the network is slow to stabilize on mobile return
- Revalidation completion now also syncs teamTimers and autodraftStatus
- Added draft-state-sync client handler that applies the server snapshot
  immediately (skipped when revalidation is in-flight to avoid conflicts)

Tests: 32 new tests covering reconnection sync, pick buffering/merge,
timer-update currentPickNumber sync, draft-state-sync handling,
available player filtering, on-the-clock correctness, and revalidation
retry logic.

https://claude.ai/code/session_01JxJ1CYTiFWV4KTPxTnjrms

* fix: guard against stale revalidation overwriting fresh socket data

Add a reference-equality check so that if HTTP revalidation fails (network
error, expired token), the sync effect does not overwrite fresh data that
draft-state-sync already applied. Also adds missing dependency array entries
(userQueue, timers, autodraftSettings) to the revalidation sync effect.

https://claude.ai/code/session_01JxJ1CYTiFWV4KTPxTnjrms

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-01 12:23:24 -08:00
Chris Parsons
9b6617309d
feat: add hide completed sports filter to AvailableParticipantsSection (#47) 2026-02-28 23:16:06 -08:00
Chris Parsons
0ac9ef4ff8
fix: eliminate queue tab flash on mobile by using useSyncExternalStore (#46)
* fix: eliminate queue tab flash on mobile by using useSyncExternalStore

useMediaQuery initialized to false and updated in useEffect, causing a
paint where isMobile was false on mobile devices. This rendered the
desktop layout first, hiding the queue tab until the effect fired.

useSyncExternalStore reads matchMedia synchronously on the client so
the correct layout is used on the first render with no extra paint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: revalidate draft room when Clerk JWT expires at load time

Clerk's short-lived JWTs (~1 min) can expire between navigations.
Server-side clerkMiddleware can't refresh them for client-side loader
fetches, so getAuth() returns userId=null — causing userTeam=undefined,
the queue button to disappear, and the tab to default to "board".

Added a useEffect that detects the mismatch: if the Clerk client SDK
has a valid userId but the loader ran without one, trigger revalidate()
so the loader re-runs with a fresh token and returns correct
userTeam/userQueue data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: prevent infinite revalidation loop in auth guard

If the server consistently fails to populate currentUserId after JWT
refresh (e.g. misconfigured CLERK_SECRET_KEY), the previous effect
would loop indefinitely. A one-shot ref ensures we attempt auth
recovery at most once per mount.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 09:54:14 -08:00
Chris Parsons
2df47658fd
fix: prevent iOS Safari viewport zoom on draft room form inputs (#45)
Add text-base md:text-sm to all native <input> and <select> elements
in the draft room (including the ShadCN SelectTrigger) so iOS Safari
doesn't auto-zoom when focused.

Also extract the two near-duplicate participant selection dialogs into
a shared ParticipantSelectionDialog component (-236 lines), and replace
O(n) queue.find() scans with a memoized Map in AvailableParticipantsSection.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 23:46:09 -08:00
Chris Parsons
4e447c30ab
feat: show username and team name on the clock display (#44)
- Show owner username as primary label in "On the Clock" section (mobile bar + desktop sidebar), with team name as a secondary label below
- Memoize currentDraftSlot to avoid O(n) array scan on every timer tick
- Derive currentDraftSlotOwnerName once instead of looking up ownerMap four times per render

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 23:21:08 -08:00
Chris Parsons
4547a7a4e7
refactor: default draft room to board tab and clean up tab section (#43)
- Change default desktop tab from "participants" to "board"
- Extract mobileTabs/mobileColCount before JSX to eliminate IIFE pattern
- Remove redundant h-full wrapper divs from TabsContent for consistency

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 23:12:21 -08:00