Commit graph

191 commits

Author SHA1 Message Date
Claude
ec89e3eb89
Address review: don't auto-promote primary for golf; lighten window count
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 3m26s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m22s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
- Auto-heal now only promotes a new primary when the deleted event was
  itself the primary window. Golf-style shared majors intentionally have
  no primary (scored on the canonical tournament page), so deleting one
  of their windows no longer flips a sibling into a primary and changes
  its scoring/guard behavior. Adds a regression test for that case.
- Replace the relation-heavy getSportsSeasonsByTournament + double
  Array.find in the events loader with a new countWindowsByTournament
  helper (count(distinct sports_season_id)) and a single cached lookup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBzjCLVkVaQMj1MF3K54t2
2026-07-02 05:27:14 +00:00
Claude
7f7ea4e29d
Improve shared-tournament/event management UX
Deleting an event from a season only affects that season's scoring
event; the shared canonical tournament and the other linked seasons
survive. The old flow never communicated this and offered no way to
manage the relationship, so it felt like deleting an event might wipe
the whole shared tournament.

- deleteScoringEvent is now shared-tournament-aware: it auto-heals the
  primary window (promotes the earliest remaining window when the
  primary is removed) and optionally deletes the canonical tournament
  when the last linked window is removed. Returns a result describing
  what happened.
- Add deleteTournament model helper.
- Events page: delete confirmation now explains exactly what a delete
  does (removed from this season only vs. last window), with an opt-in
  checkbox to also remove the orphaned shared tournament.
- Tournament page: add a per-window "Remove" control on the Linked
  Sports Seasons card to unlink a season, reusing the auto-heal path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBzjCLVkVaQMj1MF3K54t2
2026-07-02 05:17:37 +00:00
Claude
77963a13f4
Address code-review findings on draft schedule Gantt
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 3m1s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m18s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
- Use make_interval(months => $n) instead of parameterized interval
  multiplication so Postgres can infer the bound param's type (the prior
  form risked a "could not determine data type of parameter" runtime error)
- Fix subject/verb grammar in the coverage-gap warning for a single sport
  ("1 sport has" vs "N sports have")
- Stack overlapping draft windows for the same sport into separate lanes
  via greedy interval scheduling so bars no longer render on top of one
  another; row height grows with the number of concurrent windows
- Add a test covering overlapping windows rendering as separate bars

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fd51U9DPMNz4KzBeucR6CK
2026-07-02 02:33:15 +00:00
Claude
7f021aa534
Add admin draft schedule Gantt chart
Adds an admin page at /admin/draft-schedule that visualizes sport-season
draft windows (draftOn -> draftOff) as a Gantt-style timeline: Y axis is
the sport, X axis is a 6- or 12-month horizon. A warning panel highlights
sports with no open or upcoming draft window so gaps in coverage are
obvious at a glance.

- New findDraftScheduleForHorizon model query (overlap test against the
  horizon, admin sport-seasons only)
- Presentational DraftScheduleGantt component with month gridlines, a
  "today" marker, and status-colored bars linking to each sport-season
- Route registered in routes.ts and linked from the admin nav
- Unit tests for the model query and component

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fd51U9DPMNz4KzBeucR6CK
2026-07-02 00:44:23 +00:00
Claude
ef7a71485c
Fix Docker build failure caused by server-only import reaching client bundle
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 3m3s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m19s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
scoring-event.ts exported getEventTypeLabel, a pure display helper used
directly in route components, from the same module that imports
scoring-calculator.ts (which now pulls in the Discord notification
service, a .server-only module). Because that whole chain was
value-imported into the client bundle, Vite's server-only-module guard
failed the build. Move getEventTypeLabel into a new client-safe
scoring-event-types.ts module with no server dependencies, and import
it directly where it's rendered.
2026-07-01 22:36:25 +00:00
6b0e32c3d5 claude/discord-qp-notifications-75wa7k (#122)
Some checks failed
🚀 Deploy / 🧪 Test (push) Successful in 3m6s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m20s
🚀 Deploy / 🐳 Build (push) Failing after 40s
🚀 Deploy / 🚀 Deploy (push) Has been skipped
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed-on: #122
2026-07-01 22:14:51 +00:00
b0c25beb90 claude/discord-qualifying-points-t55s3x (#121)
Some checks failed
🚀 Deploy / 🐳 Build (push) Failing after 43s
🚀 Deploy / 🚀 Deploy (push) Has been skipped
🚀 Deploy / 🧪 Test (push) Successful in 2m53s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m18s
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed-on: #121
2026-07-01 17:29:45 +00:00
64e5cb23ef claude/discord-ping-notification-filter-6m1ymt (#120)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 3m2s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m18s
🚀 Deploy / 🐳 Build (push) Successful in 1m8s
🚀 Deploy / 🚀 Deploy (push) Successful in 10s
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed-on: #120
2026-06-30 23:48:11 +00:00
8efa4aab9e Centralize futures odds onto the simulator page and fix bulk import (#117)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 2m51s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m16s
🚀 Deploy / 🐳 Build (push) Successful in 1m8s
🚀 Deploy / 🚀 Deploy (push) Successful in 12s
Futures odds entry was split across two unrelated places: a standalone
/futures-odds page (which auto-ran the simulation on save and surfaced a
"Simulator is not ready" run failure as if the save itself had failed) and
the Bulk Simulator Inputs CSV importer on the simulator setup page.

Consolidate everything onto the Bulk Simulator Inputs card:

- batchUpsertParticipantSimulatorInputs now COALESCE-wraps every conflict
  update column, so a partial paste (e.g. odds-only) updates just the columns
  it provides instead of nulling out previously stored Elo/rating/etc.
- The bulk importer accepts sportsbook-style paste (one team per line ending
  in American odds) when no CSV header is present, reusing the existing fuzzy
  matcher, and auto-runs the simulation on save. A not-ready run is reported
  as a successful save plus the readiness gap, never as a failed save.
- Retire the standalone /futures-odds page (now redirects to the simulator
  setup page) and drop the redundant Futures links.
- Remove the now-dead futures-only model paths (batchSaveSourceOdds,
  clearSourceOddsForParticipants, batchSaveFuturesOddsForSimulator,
  batchSaveParticipantSimulatorSourceOdds); the EV-table bridge is preserved
  by batchUpsertParticipantSimulatorInputs.
- Repoint the test to the consolidated path and assert the non-destructive
  COALESCE behaviour.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BHnoQ7myY3PzNdTnd7iGNE

Co-authored-by: Claude <noreply@anthropic.com>
Reviewed-on: #117
2026-06-30 17:05:52 +00:00
070b825077 scoring notifications (#115)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 2m48s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m17s
🚀 Deploy / 🐳 Build (push) Successful in 1m11s
🚀 Deploy / 🚀 Deploy (push) Successful in 13s
2026-06-30 01:58:20 +00:00
e3e8485e26 elimination announcements (#114)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 2m53s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m17s
🚀 Deploy / 🐳 Build (push) Successful in 1m9s
🚀 Deploy / 🚀 Deploy (push) Successful in 12s
2026-06-28 19:36:33 +00:00
Chris Parsons
81d063faa1 Auto-populate & auto-score tennis Grand Slam brackets from Wikipedia
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 3m13s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m23s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
Add a per-event "Sync Draw" that pulls a tennis major's full 128-player
draw from its Wikipedia article, auto-creates/links participants (and
propagates them to every linked sibling season), builds the bracket, and
runs the qualifying-points scorer. Re-running advances the bracket and
scoring as matches complete.

Core
- match-sync: WikipediaTennisAdapter + wikitext bracket parser, DrawSync
  DTOs, syncTennisDraw orchestrator, pure draw->rows mapping
- playoff-match: populateBracketFromDraw (idempotent upsert on externalMatchId)
- scoring_events.externalSourceKey column (Wikipedia article; migration 0123)
- admin bracket "Sync Draw" card (accepts URL or title) + cron pass

Scoring fix
- deriveBracketQualifyingStates only floors players who have reached the
  scoring stage; for deep brackets (tennis_128) early-round losers earn 0 QP
  instead of a phantom 9th-place floor. CS2/simple_8 behavior preserved
  (gated on rounds[0].isScoring). Re-sync reconciles stale QP rows in a
  transaction.

Matching & parsing
- accent-folding in normalizeTeamName; strip Wikipedia "(tennis)"
  disambiguators; treat Bye/TBD/Qualifier as TBD; extract wikilink before
  template-stripping so {{nowrap}}-wrapped players parse

Admin UX
- dry-run preview (matched / will-create / possible duplicates / unfilled
  slots) with inline "rename existing" / "create as new" resolution via
  fetcher (no full reload)

Tests: parser vs real 2025 Wimbledon fixture, draw mapping, tennis_128
scoring, accent/disambiguator/nowrap parsing, URL parsing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 21:48:00 -07:00
120056b0bd claude/fix-lint-errors-n48594 (#111)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 2m57s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m21s
🚀 Deploy / 🐳 Build (push) Successful in 1m10s
🚀 Deploy / 🚀 Deploy (push) Successful in 10s
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed-on: #111
2026-06-26 07:26:55 +00:00
ee099c64cd claude/clever-archimedes-dvye42 (#110)
Some checks failed
🚀 Deploy / 🧪 Test (push) Successful in 3m7s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Failing after 50s
🚀 Deploy / 🐳 Build (push) Has been skipped
🚀 Deploy / 🚀 Deploy (push) Has been skipped
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed-on: #110
2026-06-26 05:16:54 +00:00
Claude
88248e349c
Fix futures odds being ignored when stale Elo exists
All checks were successful
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
🚀 Deploy / 🧪 Test (pull_request) Successful in 2m58s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m23s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
When an admin entered futures (preseason) odds for a season that already
had Elo ratings stored, the simulator kept using the old Elo and silently
ignored the new odds. This affected any Elo-based simulator (e.g. NHL).

Root cause: resolveSourceElos() ranks a direct sourceElo above the
sourceOdds -> convertFuturesToElo branch, but batchSaveFuturesOddsForSimulator()
only cleared the bracket-seeding `rating`/`ratingMethod` — never the stale
`sourceElo`/`sourceEloMethod`. A manually entered Elo (method "direct") is not
treated as generated, so it survived and short-circuited the resolver.

Fix:
- batchSaveFuturesOddsForSimulator now also nulls sourceElo and strips
  sourceEloMethod (both the pre-update and upsert-conflict paths), so the
  existing futures -> Elo conversion drives the run.
- resolveSourceElos' sourceOdds branch now guards for >= 2 participants
  (mirroring resolveRatings), so a lone-odds season falls through to the
  configured missing-Elo strategy instead of getting a flat ~1500.
- batchSaveSourceOdds clears the legacy EV sourceElo and marks source as
  futures_odds so the elo-ratings page won't resurrect a stale rating.

Adds unit coverage for odds-derived Elo, the single-participant guard, the
post-clear regression, generated-vs-direct sourceElo suppression, and the
new clearing behavior in batchSaveFuturesOddsForSimulator.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNfUEd9RzD3zm84oLHBHUH
2026-06-25 17:43:31 +00:00
Chris Parsons
9480501932 Unify majors: score once, fan out across windows + tennis bracket EV
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 3m12s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m24s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
🚀 Deploy / 🧪 Test (push) Successful in 3m27s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m25s
🚀 Deploy / 🐳 Build (push) Successful in 1m14s
🚀 Deploy / 🚀 Deploy (push) Successful in 14s
Make a "major" (golf/tennis/CS2) scored once on its canonical tournament
and fan out to every linked sports_season window and league.

Fan-out & completion (app/services/sync-tournament-results.ts):
- syncTournamentResults now marks each synced window's event complete
  (gated by markComplete), recalculates affected leagues, and counts
  recalc failures so a stale league can't hide behind a "completed" badge
- syncMajorFromPrimaryEvent promotes a primary window's derived results to
  canonical tournament_results (deleting rows for dropped placements) and
  fans out to siblings; fanOutMajorIfPrimary guards on the primary
- placement removals now propagate (stale rows reset to filler)

Primary-event model (scoring_events.is_primary, migration 0122):
- getPrimaryEventForTournament / isReadOnlySibling / ensurePrimaryEvent /
  setPrimaryEvent; event creation auto-seeds a primary for bracket majors;
  "Make primary" button on the tournament page
- per-window event/bracket/cs2 pages are read-only for non-primary linked
  events (not-participating stays editable)

Tennis Grand Slam bracket (tennis_128 template + TEMPLATE_ROUND_CONFIG):
- bracket-scored qualifying major via the existing bracket pipeline
- simulator conditions in-progress EV on the real bracket (honoring
  completed matches, walkover for withdrawals), QP derived from config,
  round structure read from the template; CS2 + tennis share resolveStructureSource

Backfill (scripts/backfill-major-linking.ts): one-time idempotent reconcile
of existing majors (link orphans, designate primary, promote canonical, sync).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:32:22 -07:00
Chris Parsons
dfbcb7d953 Add complete CS2 event reset to clear orphaned results/QP
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 3m14s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m41s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
The CS2 Major EV simulation silently discarded the Champions Stage
bracket for IEM Cologne 2026: the bracket seeded "Team Spirit" in QF1
while the stage assignments listed the (different) "Spirit Academy", so
the realBracket gate found an out-of-field participant and rejected the
whole bracket. The phantom "Spirit Academy" also left a stray
event_results row (33 rows for 32 teams).

The existing "Reset all" button only cleared cs2_major_stage_results, so
re-entering couldn't remove the phantom result or its cached QP total.

Add resetCs2Event, which also deletes the event's event_results and
recomputes the affected participants' cached QP totals — all inside a
single transaction so a mid-operation failure can't leave the event in a
torn state. It deliberately preserves the Champions Stage bracket so
re-entry realigns the stage data to it. The reset button now confirms
and states that results/QP are cleared too.

Tests: resetCs2Event clear+recompute behavior, and a simulator
regression pinning that one out-of-field QF participant disables the
whole bracket.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 22:22:11 -07:00
f078eab492 claude/bracket-wins-minimum-qp-el8z14 (#99)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 2m35s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m24s
🚀 Deploy / 🐳 Build (push) Successful in 1m11s
🚀 Deploy / 🚀 Deploy (push) Successful in 11s
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed-on: #99
2026-06-19 01:25:17 +00:00
496ecba464 claude/cool-lamport-etpwsi (#96)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 3m16s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m24s
🚀 Deploy / 🐳 Build (push) Successful in 1m19s
🚀 Deploy / 🚀 Deploy (push) Successful in 13s
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed-on: #96
2026-06-17 19:46:58 +00:00
7da8be9978 claude/charming-tesla-re9ur7 (#93)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 2m48s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m17s
🚀 Deploy / 🐳 Build (push) Successful in 1m8s
🚀 Deploy / 🚀 Deploy (push) Successful in 10s
2026-06-16 22:11:01 +00:00
fe4e1b3f3c claude/beautiful-hawking-a72ilq (#92)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 3m24s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m38s
🚀 Deploy / 🐳 Build (push) Successful in 1m29s
🚀 Deploy / 🚀 Deploy (push) Successful in 9s
2026-06-16 14:11:51 +00:00
1f41748261 claude/funny-maxwell-vcyb19 (#91)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 3m6s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m20s
🚀 Deploy / 🐳 Build (push) Successful in 1m9s
🚀 Deploy / 🚀 Deploy (push) Successful in 11s
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed-on: #91
2026-06-15 22:05:25 +00:00
d0a31f3883 Fix CS2 Stage 3 QP slot assignment (#89)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 2m48s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 5m5s
🚀 Deploy / 🐳 Build (push) Successful in 4m4s
🚀 Deploy / 🚀 Deploy (push) Successful in 14s
## Summary

- 0-3 teams in Stage 3 were being assigned slots 9–10 (high QP) when marked eliminated before other Stage 3 teams, because `computeStage3ExitQP` filled from slot 9 upward using only the teams passed in
- 1-3 teams had the same problem when marked before 2-3 teams
- Fix: gate all Stage 3 QP computation on `stage3Exits.length === STAGE3_TOTAL_EXITS` (8) — partial saves write 0 QP as a placeholder, and correct QP/placements are assigned once all 8 exits are known
- As belt-and-suspenders, `computeStage3ExitQP` now places 0-wins teams from the bottom of the slot range so the function itself is correct even if called with a partial set

## Test plan

- [ ] Run `npm run test:run -- app/models/__tests__/cs2-major-stage.test.ts` — all 17 tests pass
- [ ] Mark 2 Stage 3 teams as 0-3 eliminated and save — confirm they show 0 QP (not 2 QP)
- [ ] Mark all 8 Stage 3 exits and save — confirm 0-3 teams get slots 15–16 QP, 1-3 teams get slots 12–14 QP split correctly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #89
2026-06-14 05:49:06 +00:00
f40144e1e2 claude/great-lovelace-r3bznh (#88)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 2m45s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m24s
🚀 Deploy / 🐳 Build (push) Successful in 1m14s
🚀 Deploy / 🚀 Deploy (push) Successful in 10s
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed-on: #88
2026-06-12 22:35:35 +00:00
0034adaa9a admin fixes (#85)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 2m42s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m24s
🚀 Deploy / 🐳 Build (push) Successful in 1m13s
🚀 Deploy / 🚀 Deploy (push) Successful in 12s
2026-06-11 00:32:22 +00:00
ffb1642ab6 Remove flaky context menu click tests (#81)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 2m46s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m26s
🚀 Deploy / 🐳 Build (push) Successful in 1m29s
🚀 Deploy / 🚀 Deploy (push) Successful in 10s
## Summary

- Deletes 12 `it("calls onX with correct args")` test blocks from `MiniDraftGrid.test.tsx` and `DraftGridSection.test.tsx`
- Removes now-unused `import userEvent` from both files

## Why

`userEvent.setup().click()` hangs indefinitely on Radix UI `ContextMenu` items in jsdom — pointer-event and animation checks stall waiting for CSS transitions that never fire `transitionend` in the test environment. This caused a flaky 5 s timeout in CI.

The deleted tests were verifying that clicking a `ContextMenuItem` fires its `onClick` — React/Radix wiring, not app logic. The remaining presence/absence tests already cover the conditional rendering (which items appear under which conditions), which is where the actual app logic lives.

## Test plan

- [ ] `npm run test:run -- MiniDraftGrid DraftGridSection` — all remaining tests pass, no timeouts

Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #81
2026-06-10 05:59:49 +00:00
0150fb7ab9 sync racing standings (#80)
Some checks failed
🚀 Deploy / 🧪 Test (push) Failing after 2m56s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m27s
🚀 Deploy / 🐳 Build (push) Has been skipped
🚀 Deploy / 🚀 Deploy (push) Has been skipped
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed-on: #80
2026-06-10 04:25:48 +00:00
f96c8f5244 Fix draft timer: broadcasts, increments, reconnect sync, overnight pause (#72)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 2m40s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m26s
🚀 Deploy / 🐳 Build (push) Successful in 1m14s
🚀 Deploy / 🚀 Deploy (push) Successful in 12s
## Summary

- **Timer bank broadcasts**: emit `timer-bank-updated` after every pick so all clients immediately see the updated bank instead of waiting for the next `timer-pick-started`
- **Increment accuracy**: capture `pickMadeAt` at route entry (before auth/DB overhead) and use `Math.ceil` so credited seconds always match the client countdown display
- **Race condition fix**: hold `schedulingInProgress` lock for the full timer callback to prevent the recovery interval from scheduling a duplicate timeout mid-pick
- **force-autopick fix**: call `rescheduleTimer` so the next team's clock starts immediately instead of waiting for the old timeout to naturally expire
- **adjust-time-bank fix**: for on-clock teams, shift `picksExpiresAt` by the adjustment and reschedule so the client countdown updates; block adjustments that would reduce the bank to zero
- **New socket events**: `timer-pick-started`, `timer-overnight-paused`, `timer-bank-updated` with full type definitions; removed dead `timer-update` event
- **Reconnect sync**: `draft-state-sync` now includes `expiresAt` for the active timer and `isOvernightPause` state so reconnecting clients see accurate countdown and pause banner immediately without a page reload
- **Room closure countdown**: capture client-side timestamp when draft completes so the "Room closes in X" countdown actually ticks down before the loader revalidates with `draftCompletedAt`
- **Countdown interval**: run at 500ms with `Math.ceil` to prevent skipped seconds under event loop pressure
- **Overnight pause UX**: `canPick` only blocks on commissioner pause — overnight pause freezes the timer but the on-clock player can still pick early
- **Overnight pause refactor**: extract `checkOvernightPause` to `server/overnight-pause-check.ts`, breaking the `timer↔socket` circular import and sharing the timezone cache across both callers with correct eviction
- **PostgreSQL type fix**: cast `varchar` owner ID to `uuid` in `getTeamTimezone` join

## Test plan

- [ ] Manual pick: all clients see bank increment immediately after pick
- [ ] Timeout pick: all clients see bank update (0 → increment); next clock starts within ~1s
- [ ] Force-autopick: next team's clock starts immediately; no "Pick already made" log
- [ ] Force-manual-pick: all clients see bank increment
- [ ] Pause while clock running: countdown freezes on all clients
- [ ] Resume: clock continues from frozen value
- [ ] adjust-time-bank on on-clock team: countdown shifts immediately
- [ ] adjust-time-bank to zero: returns 400 error
- [ ] Reconnect (socket disconnect/connect): countdown resumes for correct team
- [ ] Hard refresh mid-draft: on-clock indicator and countdown correct immediately
- [ ] Draft complete: "Room closes in X" counts down
- [ ] Overnight pause: banner shows, pick buttons still enabled, timer frozen
- [ ] `npm run test:run` — all 158 files / 2351 tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #72
2026-06-06 05:57:46 +00:00
f657293430 claude/sports-templates-conversion-ILWAM (#71)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 2m32s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m18s
🚀 Deploy / 🐳 Build (push) Successful in 1m10s
🚀 Deploy / 🚀 Deploy (push) Successful in 12s
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed-on: #71
2026-06-04 21:31:49 +00:00
add196902b Fix MLB standings 406 error and refactor ESPN adapter shared code (#62)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 2m21s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m18s
🚀 Deploy / 🔍 Lint (push) Successful in 49s
🚀 Deploy / 🐳 Build (push) Successful in 12m18s
🚀 Deploy / 🚀 Deploy (push) Successful in 12s
## Summary

- **Root cause**: `statsapi.mlb.com` returns 406 (deprecated). Switched MLB to ESPN's free standings API (`site.api.espn.com/apis/v2/sports/baseball/mlb/standings`), consistent with NBA, WNBA, and MLS.
- **Refactor**: Extracted a shared `espn.ts` utility module, eliminating 4× duplication of `statsMap()`, `flattenEspnStandings()`, ESPN interfaces, and the `playoffSeed` → conference rank logic across adapters.
- **Bug fixes** found during review and applied across all affected adapters:
  - `parseConferenceRank`: `|| undefined` falsy-zero bug replaced with `isNaN` guard
  - Sort comparators: stable alphabetical tiebreaker added to MLB, NBA, WNBA
  - `winPct`: falls back to `wins/(wins+losses)` if ESPN omits the stat (was silently 0)
  - statsMap pre-built once per entry before sorting in all adapters (was rebuilt per comparison)
  - WNBA `parseEntry`: accepts pre-computed `sm` instead of rebuilding it internally
  - MLB `gamesBack`: tests updated to reflect ESPN returns numeric `0` for division leaders (old API used `"-"` → `undefined`)

## Test plan

- [ ] All 60 standings-sync unit tests pass (`npm run test:run -- app/services/standings-sync`)
- [ ] Trigger MLB standings sync from admin panel and confirm it returns data without a 406
- [ ] Confirm NBA, WNBA, MLS syncs still work (adapters touched but behaviour unchanged)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #62
2026-06-01 03:31:18 +00:00
ef29c1fcd9 claude/discord-pick-notification-refactor-FeusJ (#58)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 1m36s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m23s
🚀 Deploy / 🔍 Lint (push) Successful in 50s
🚀 Deploy / 🐳 Build (push) Successful in 14m19s
🚀 Deploy / 🚀 Deploy (push) Successful in 13s
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed-on: #58
2026-05-29 16:24:22 +00:00
4a2f542fb1 Fix Discord pick notifications silently dropped during autodraft chains (#57)
All checks were successful
🚀 Deploy / 🔍 Lint (push) Successful in 50s
🚀 Deploy / 🐳 Build (push) Successful in 14m19s
🚀 Deploy / 🚀 Deploy (push) Successful in 11s
🚀 Deploy / 🧪 Test (push) Successful in 1m31s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m21s
## Summary

- **Per-league queue**: replaced the single global notification queue with a \`Map<leagueId, queue>\` so concurrent drafts in different leagues drain independently and don't block each other
- **Deadlock fix**: wrapped the drainer loop in \`try/finally\` so \`drainingLeagues\` is always cleaned up even if a task throws unexpectedly — previously a crash would leave the flag set permanently, silently dropping all future notifications for that league
- **Wait cap fix**: moved \`Math.min\` to wrap \`(retryAfter * (attempt+1))\` so the 10s ceiling applies to the final product, not just the base value — settings actions that directly await webhook calls were previously blockable for up to 30s on a rate-limited third attempt

## Root cause of the original problem

During an autodraft chain, \`notifyPickMadeOnDiscord\` was called fire-and-forget for each pick in the chain, causing all webhook requests to fly concurrently. Discord's per-webhook rate limit (~5 req/2s) rejected most of them; the single retry logic fired for all simultaneously, hit the limit again, and the \`.catch()\` swallowed the errors silently.

## Test plan

- [ ] Enable autodraft for 5+ consecutive teams; trigger the chain; confirm all picks announce in Discord in order
- [ ] Confirm a slow/rate-limited webhook in League A does not delay League B's announcements
- [ ] \`npm run test:run\` — 158 test files, 2336 tests, all pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #57
2026-05-27 16:41:08 +00:00
a9d4954242 Fix World Cup group stage display and upcoming events (#55)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 1m28s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m17s
🚀 Deploy / 🔍 Lint (push) Successful in 49s
🚀 Deploy / 🐳 Build (push) Successful in 14m50s
🚀 Deploy / 🚀 Deploy (push) Successful in 10s
## Summary

- **Group stage match list**: Removed MD 1/2/3 headings; matches now listed chronologically under date separators (e.g. "Jun 14"). Kick-off time appears below the team names instead of between them.
- **Sort order**: \`findMatchesByGroupIds\` / \`findMatchesByEventId\` now order by \`scheduledAt ASC NULLS LAST\` so unscheduled matches always trail scheduled ones.
- **Groups/Bracket toggle**: When both group stage and knockout bracket exist, a toggle appears (mirroring the NBA/AFL standings toggle). Sports with both regular-season standings and a group stage get all views available.
- **Upcoming events**: \`getUpcomingEventsForDraftedParticipants\` now also queries \`groupStageMatches\`, so World Cup group fixtures appear on the home page calendar sorted by kick-off time with a label like "Group A — France vs Germany".
- **\`isAllCompete\` fix**: \`UpcomingEventsCard\` and \`UpcomingCalendarPanel\` now treat \`group_stage_match\` as a bracket-style event, showing team badges instead of "N of your picks".

Fixes #53

## Test plan

- [ ] Navigate to a World Cup sports season page — groups should be listed chronologically under date headers with no MD labels; kick-off time should appear below each match row
- [ ] Once the knockout bracket has matches, confirm the Groups/Bracket toggle appears and both views render correctly
- [ ] On the home page, a user with drafted World Cup participants should see upcoming group fixtures in the calendar panel with individual team badges (not a participant count)
- [ ] NBA/AFL/NHL pages unaffected — their standings/playoffs/finished toggle still works normally
- [ ] \`npm run typecheck\` and \`npm run test:run\` pass

Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #55
2026-05-27 05:43:08 +00:00
a02f603575 Fix slow bracket save and stale projected points (#54)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 1m31s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m20s
🚀 Deploy / 🔍 Lint (push) Successful in 51s
🚀 Deploy / 🐳 Build (push) Successful in 15m9s
🚀 Deploy / 🚀 Deploy (push) Successful in 25s
## Summary
- Swap side-effect order so `updateProbabilitiesAfterResult` runs before `recalculateAffectedLeagues` in `processMatchResult`, `processPlayoffEvent`, and the `set-round-winners` route — projected points now reflect the new result immediately instead of waiting on the next simulation.
- Eliminate the N+1 EV lookup in `calculateTeamProjectedScore` by pre-fetching every EV the season needs in a single `inArray` query inside `recalculateStandings` and passing the map down.
- Add `skipProbabilities` option to `processPlayoffEvent`, used from `autoCompleteRoundIfDone` (callers have already refreshed EVs), avoiding a redundant pass on round-completing saves.

Fixes #31

## Test plan
- [x] `npm run typecheck`
- [x] `npx vitest run app/models/__tests__/team-projected-score.test.ts app/models/__tests__/process-match-result.test.ts app/services/__tests__/probability-updater.test.ts` — 57/57 passing
- [ ] Manual: save a round of bracket winners on a sports season with at least one fantasy league and confirm (a) the request returns faster and (b) the team standings show projected points updated to reflect the result without re-running the simulator

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #54
2026-05-27 04:28:10 +00:00
Chris Parsons
77f520823c Fix Discord pick announcements arriving out of order and add 429 retry
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 1m30s
🚀 Deploy / ʦ TypeScript (pull_request) Successful in 1m16s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 48s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
- Move notifyPickMadeOnDiscord() before checkAndTriggerNextAutodraft() in
  both executeAutoPick() and the manual pick action. Previously the
  triggering pick's announcement fired after all chained autodraft picks
  had already announced, causing Discord to show e.g. pick #106 before
  #105 before #104. Now each pick announces itself before the next pick
  in the chain is triggered.

- Add a single retry with Retry-After delay in sendDiscordWebhook() on
  HTTP 429. A burst of consecutive autodraft picks could hit Discord's
  per-webhook rate limit and silently drop announcements. Use || 1
  (not ?? 1) to handle non-numeric/NaN headers, and cap at 10 s so
  that awaited callers such as the draft-order settings actions are
  never hung for a full global rate-limit window.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 21:29:02 -07:00
Chris Parsons
e3a7a1a9b1 Fix autodraft settings panel not reflecting auto-disable after pick
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 1m24s
🚀 Deploy / ʦ TypeScript (pull_request) Successful in 1m13s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 45s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
When next_queue autodraft fires and turns itself off, two bugs prevented
the UI from behaving correctly:

1. After reconnect, useDraftAuthRecovery synced autodraftStatus (draft
   grid) but not userAutodraft (settings panel), leaving the settings
   badge stale until the next page load.

2. Toast notifications for autodraft auto-disable were unreliable: the
   condition checked prev.mode rather than why the server disabled it,
   causing the wrong toast for manual disables and queue-empty events.

Fix the reconnect sync by calling setUserAutodraft in the revalidation
effect alongside setAutodraftStatus. Fix the toast logic by adding a
reason field ("pick_complete" | "queue_empty") to the server's
autodraft-updated emit, so the client can show the right message without
guessing from local state. Also removes the now-unused userAutodraftRef.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 18:29:13 -07:00
Chris Parsons
c9fdb12a09 Enforce unique team names within a season
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 1m31s
🚀 Deploy / ʦ TypeScript (pull_request) Successful in 1m19s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 48s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
- Add findTeamByNameInSeason (exact case-insensitive match via lower()) to the team model
- Validate uniqueness in the user-facing team settings rename action
- Validate uniqueness in admin assign-owner and remove-owner paths that auto-generate names
- Reject whitespace-only team names that would trim to an empty string
- Add DB-level unique index on (season_id, lower(name)) to close the TOCTOU race

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 16:55:08 -07:00
Chris Parsons
1b51cb554b Fix Discord double-ping when autodraft fires immediately after manual pick
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 1m33s
🚀 Deploy / ʦ TypeScript (pull_request) Successful in 1m16s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 47s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
When a manual pick triggered an immediate autodraft chain, both Discord
announcements re-read currentPickNumber from the DB, which had already
advanced past the chained pick. Both messages showed the same "On the
clock" person, pinging them twice.

Fix: pass nextPickNumber (the pick immediately following each specific
pick, before any chain) as an explicit param to notifyPickMadeOnDiscord
instead of re-reading from the DB. Also removes a now-unnecessary DB
round-trip on every pick announcement.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 20:38:35 -07:00
841760541d Fix snake draft Discord pick numbering and small UX tweaks (#2)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 1m27s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m16s
🚀 Deploy / 🔍 Lint (push) Successful in 1m55s
🚀 Deploy / 🐳 Build (push) Successful in 14m23s
🚀 Deploy / 🚀 Deploy (push) Successful in 10s
## Summary

- **Discord snake draft fix**: pick notifications now show the sequential pick-in-round (e.g. "Round 2, Pick 9") rather than the snake-adjusted slot position ("Round 2, Pick 5"). Also removes the now-unused \`pickInRound\` param from \`notifyPickMadeOnDiscord\` and adds a regression test for the 13-team case.
- **League card**: league name in draft-in-progress cards is now a link to the league homepage (the Enter Draft button still goes to the draft room).
- **Overnight pause label**: shortened to "🌙 Pause" — the "Resumes 4:00 AM" line below already provides context, so "Overnight" was just causing wrapping.
- **Queue & Picks backgrounds**: items use \`bg-card\` instead of \`bg-muted\` for better visual separation from the panel background.

## Test plan

- [x] Discord: in a snake draft, verify pick #22 of 13 shows "Round 2, Pick 9" in Discord
- [x] League card: dashboard with a draft-in-progress league — click name → league homepage, click Enter Draft → draft room
- [x] Overnight pause: pause a draft overnight — cell shows "🌙 Pause" on one line, "Resumes X:XX" below
- [x] Queue/Picks: open draft room and confirm queue items and recent picks visually pop from the sidebar background
- [x] Unit tests: `npm run test:run` passes (13 discord tests)

Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #2
2026-05-22 04:18:11 +00:00
Chris Parsons
fbcecb490e
Add Discord draft pick announcements (#460)
* Add Discord draft pick announcements

Posts a message to the league Discord webhook each time a pick is made, announcing the picked participant and pinging the next team on the clock if their owner has opted into Discord notifications. Enabled via a separate toggle in league settings (independent from standings update notifications).

https://claude.ai/code/session_01Tvwsv3LfL9JUqxoLct8dTn

* Address code review feedback on Discord pick announcements

- Fix "on the clock" timing: read currentPickNumber fresh from DB post-autodraft-chain (matches sendOnTheClockEmail guarantee)
- Remove outer try/catch from notifyPickMadeOnDiscord so callers' .catch() is not dead code
- Add missing draft-discord.server.test.ts with 12 tests covering all early-exit and happy paths
- Fix silent empty-string fallback for missing pickedSlot: warn and skip instead
- Eliminate sequential season→league DB queries by accepting leagueId as a direct param
- Show "save webhook URL to configure options" hint when URL is typed but not yet saved
- Remove block-scope braces at both call sites (plain const declarations)
- Remove redundant "Round N, Pick M" description line (title already carries this info)
- Inline pickInRoundFor helper to avoid circular import with draft-utils

https://claude.ai/code/session_01Tvwsv3LfL9JUqxoLct8dTn

* Fix lint errors from review fixes

- Remove unused logger import (no longer needed after removing try/catch)
- Remove unused OWNER_ID constant in test fixture
- Use toSorted() instead of sort() in sendDraftOrderNotification

https://claude.ai/code/session_01Tvwsv3LfL9JUqxoLct8dTn

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-20 19:55:48 -07:00
Chris Parsons
b8b58ec9f8
Add draft email notifications feature (#459)
* Add on-the-clock email notifications and fix push notification modal

- Add `draft_email_notifications_enabled` column to users table
- New `sendOnTheClockEmail` service sends an email when a user's turn arrives
  in a draft; detects back-to-back picks and sends one combined email instead
  of two; skips notification if the team has autodraft enabled
- Email is triggered after every manual pick (make-pick route) and every
  timer-expiry pick (executeAutoPick) once the full autodraft chain settles,
  so the notified user is always the first human on the clock
- Add email notification toggle to the user settings Notifications section
- Fix push notification dropdown in the draft room: the bare absolute div is
  replaced with a Radix Popover so clicking outside dismisses it without
  toggling notifications off
- Rename draft room label from "Pick Notifications" to "Push Notifications"
  to distinguish from the new email notifications

https://claude.ai/code/session_01LMGxgYvtE3CF8Jf3u2pXgA

* Address code review feedback on email notification feature

- sendOnTheClockEmail now fetches season (and currentPickNumber) internally,
  removing the blocking postChainSeason pre-fetch from both make-pick.ts and
  executeAutoPick; callers are now true fire-and-forget with no IIFE needed
- Parallel Promise.all for team, autodraftSettings, and league queries reduces
  sequential DB round-trips from 5 to 3
- Remove team.ownerId type cast; assign to local after the null guard
- Combine the two calculatePickInfo calls for the same pick into one
- Fix popoverOpen/enabled divergence: remove the {enabled && ...} guard on
  PopoverContent so popoverOpen is the sole visibility control
- Unify NotificationsSection feedback pattern: both Discord and email sections
  now read success/error directly from their respective fetcher data via a
  shared feedbackFromFetcher helper; removes the success/error props and the
  verbose cast that was used for email
- Add 13 unit tests for sendOnTheClockEmail covering: early-exit paths
  (no season, past totalPicks, no owner, notifications disabled, autodraft
  enabled, no league), single vs back-to-back subject selection, draft room
  link presence, error logging without throwing, and parallel query execution

https://claude.ai/code/session_01LMGxgYvtE3CF8Jf3u2pXgA

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-20 15:35:43 -07:00
Chris Parsons
461847c414
Add public sports overview page (#451) 2026-05-19 23:02:58 -07:00
Chris Parsons
0ba2b391f8
Fix draft order initialization for teams added mid-season (#449)
* Fix draft order showing only new team when league size increased before order was set

When a commissioner increased the team count on a league that had teams
but no draft order set yet, the server created draft slots only for the
newly-added teams. This left the DB with N+K teams but only K slots,
causing the drag-and-drop list to display only the K new teams.

Two fixes:
1. Server: only append new draft slots when an order was already set
   (existingSlots.length > 0). If no order exists yet, skip slot creation
   so the page correctly treats the order as unset for all teams.
2. Frontend: buildDraftOrderTeams() appends any unslotted teams after the
   slotted ones, so a partially-corrupt DB state still shows all teams.

https://claude.ai/code/session_01M3H55gnMxRztJK9KMXqqZo

* Address code review feedback on draft order bug fix

- Move buildDraftOrderTeams to app/lib/draft-order.ts so it is importable
  and testable; remove the local copy from the settings component
- Add unit tests for buildDraftOrderTeams covering the empty, full, and
  partial-slot cases
- Tighten the draft slot guard from existingSlots.length > 0 to
  existingSlots.length === currentTeamCount so partial legacy states are
  treated the same as "order not set"
- Condense the 3-line server comment to a single line per project style
- Rename getNumTeamsInSeason → getNumDraftSlotsBySeasonId to reflect what
  the function actually counts, and update its one call site
- Add tests for the server-side slot-creation guard logic

https://claude.ai/code/session_01M3H55gnMxRztJK9KMXqqZo

* Fix lint: move shouldAppendDraftSlots to outer scope

oxlint (consistent-function-scoping) requires functions that don't close
over any variables to be defined at the outer scope rather than inside a
describe block.

https://claude.ai/code/session_01M3H55gnMxRztJK9KMXqqZo

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-19 18:22:47 -07:00
Chris Parsons
43dbdf0040
Add not-participating exclusion support for event results (#446)
* Add notParticipating flag to allow excluding withdrawn participants from qualifying-points simulators

Adds a `not_participating` boolean column to `event_results` so admins can
mark a participant as not competing in a specific upcoming major (e.g. Alcaraz
withdrawing from Wimbledon due to injury). The golf, tennis, and CS2 major
simulators now query this flag for incomplete events and exclude those
participants from the event's draw/field, redistributing probability weight
to the remaining field. Admin UI for qualifying major_tournament events gains
a "Not Participating" card to mark/unmark withdrawals before the event runs.

https://claude.ai/code/session_01HxNPLEXzr5Km3suWrJe2F9

* Address code review feedback on not-participating flag

Security: unmark-not-participating now validates the result exists, belongs
to this event, and is actually a DNP row before deleting. mark-not-participating
now returns a user-friendly error on duplicate-key constraint violations.

Code quality: extract shared getExcludedByEventMap() utility to event-result
model, eliminating the duplicated 20-line exclusion-loading block that was
copy-pasted into all three simulators. Fix hasParticipantResult() to exclude
notParticipating rows so it correctly reflects actual competition participation.
Remove optional chaining on the non-optional notParticipatingIds field in the
admin UI. Fix misleading empty-state message.

Tests: replace the misleading first tennis DNP test (which never used the
activeIds variable it created) with a test that explicitly validates the
fallback behaviour when too few players remain after exclusion. Add three CS2
DNP tests covering the excluded-team-gets-zero-QP path, the redistribution
of wins, and per-event pool independence.

https://claude.ai/code/session_01HxNPLEXzr5Km3suWrJe2F9

* Fix lint errors: replace non-null assertions in CS2 DNP test

https://claude.ai/code/session_01HxNPLEXzr5Km3suWrJe2F9

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-19 11:13:32 -07:00
Chris Parsons
1f0fcf970b
Fix QP tie rounding and finalized scoring (#440) 2026-05-17 21:58:53 -07:00
Chris Parsons
6f70a19b73
Add admin user management interface with search and username editing (#431)
* Add admin users management page with pagination, search, and inline username editing

https://claude.ai/code/session_01E4UUQqQedhFP2F5YcBRArs

* Fix five issues from admin users page code review

- admin.users: use useEffect to close inline edit on success so validation
  errors are not silently discarded when the save button fires onClick
- root: add /admin to ONBOARDING_EXEMPT so admin users without a username
  are not redirect-looped away from admin pages
- settings: skip username validation when the username field is empty so
  updating timezone alone does not break for accounts without a username
- models/user: exclude soft-deleted users from findUsersPaginated
- admin.users: remove unused hasMore from loader return; simplify header div

https://claude.ai/code/session_01E4UUQqQedhFP2F5YcBRArs

* Fix lint: rename shadowed variables in user model

- findUserByUsername: use imported sql directly instead of destructuring
  it from the callback (shadowed the top-level import)
- findUsersPaginated: rename orderBy callback param from users to t
  (shadowed the outer users result variable)

https://claude.ai/code/session_01E4UUQqQedhFP2F5YcBRArs

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-15 12:07:51 -07:00
Chris Parsons
215b0cf6b5
Add user onboarding flow with username selection (#430)
* Add username onboarding prompt for new signups

New users (especially via OAuth/Google) are redirected to /onboarding
to choose a display username before accessing the app. Adds unique
constraint on users.username and case-insensitive uniqueness validation.

https://claude.ai/code/session_01UinBMAy9d6dQzzbcUAdq8J

* Address all code review issues on username onboarding

- Replace case-sensitive unique constraint with functional unique index on
  lower(username) so DB-level uniqueness is case-insensitive (fix #1)
- Share USERNAME_RE from models/user.ts; add same format + uniqueness
  validation to the settings update-profile action (fix #2)
- Wrap updateUser calls in try/catch to handle race-condition DB errors
  gracefully in both onboarding and settings (fix #3)
- Add unit tests for suggestUsername, safeRedirectTo, USERNAME_RE, and
  isOnboardingExempt (fix #4)
- Guard suggestUsername against returning strings shorter than 3 chars (fix #5)
- Preserve the user's intended destination via redirectTo query param
  through the onboarding flow (fix #6)
- Treat empty username in settings as a validation error instead of a
  silent no-op (fix #7)
- Use exact/sub-path matching in isOnboardingExempt to prevent
  false-positive prefix matches like /loginpage (fix #8)

https://claude.ai/code/session_01UinBMAy9d6dQzzbcUAdq8J

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-15 11:17:15 -07:00
Chris Parsons
469cc82e15
Add opt-in Discord pings to standings notifications (#429)
* Add opt-in Discord ping to standings notifications

Users who have linked their Discord account can enable a ping preference
in Settings > Notifications. When enabled, their bracket username is replaced
with a Discord @mention (<@userId>) in league standings update messages,
sending them a push notification and showing their Discord display name.

Adds discordPingEnabled column to users, a findDiscordIdsByUserIds model
helper, allowed_mentions support to the Discord webhook payload, and a
NotificationsSection settings component.

https://claude.ai/code/session_01NUv93WRrufHhpZSMyY4bjs

* Remove Display Name field from user profile settings

Username is the only user-facing name field. The display_name column
remains in the database since BetterAuth writes the OAuth provider name
there, and it serves as a programmatic fallback via getUserDisplayName.

https://claude.ai/code/session_01NUv93WRrufHhpZSMyY4bjs

* Address code review feedback on Discord ping feature

- Fix broken Account settings link in NotificationsSection: replace the
  non-functional ?section=account href with an onNavigateToAccount callback
  that drives the parent's useState-based section switcher
- Replace hand-rolled toggle button with the project's Switch component
  (Radix UI) for consistent sizing, accessibility, and dark-mode support
- Guard update-discord-ping action: return an error if the user attempts
  to enable pings without a Discord account linked
- Surface action error in NotificationsSection UI
- Cap allowed_mentions.users at 100 to avoid Discord rejecting the payload
  in large leagues
- Remove the showWinner alias in scoring-calculator; inline winnerScoreChanged
- Add comment to league settings test notification explaining why discordUserId
  is omitted
- Clarify database schema comment: displayName is write-only from BetterAuth

https://claude.ai/code/session_01NUv93WRrufHhpZSMyY4bjs

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-15 10:06:54 -07:00
Chris Parsons
06339fd4b6
Fix EPL simulator parity config (#421) 2026-05-13 15:02:23 -07:00
Chris Parsons
af64a29cfa
Fix NCAAW futures odds simulation and admin import UX (#420)
- Revert ncaaw-simulator to Barthag win probability formula; set
  realistic rating bounds (ratingMin: 0.70, ratingMax: 0.97) so derived
  ratings stay in the range where the formula behaves well
- Add batchSaveFuturesOddsForSimulator which clears all ratings (manual
  and generated) before upserting sourceOdds, so futures odds always
  drive the simulation rather than being silently overridden by existing
  Barthag ratings from Simulator Setup
- Add clearSourceOddsForParticipants to zero out both tables for
  participants excluded from a bulk import
- Add "Clear existing odds" checkbox to the bulk import card; applies
  client-side on match and server-side on submit
- Fix missing sportsSeasonId filter in batchSaveFuturesOddsForSimulator
  pre-clear UPDATE (could have wiped ratings across other seasons)
- Fix race condition: run batchSaveSourceOdds then
  batchSaveFuturesOddsForSimulator sequentially so the simulator inputs
  table always ends in the correct cleared state
- Fix Math.round in convertFuturesToElo collapsing Barthag-scale ratings
  to 0 or 1; Elo callers already round after clamping
- Handle all-identical-odds edge case in convertFuturesToElo (assign
  midpoint instead of throwing)
- Add missingRatingStrategy: worstKnownMinus to ncaaw_bracket manifest
  so fallbackRatingDelta is live config, not dead
- Log warning in resolveRatings when only 1 participant has odds
- Reset clearExisting checkbox after applyMatches

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 01:06:16 -07:00