* Use Docker Compose init service pattern for database migrations
Replaces the fragile double-migration approach (server.ts startup +
drizzle-kit CLI in CI) with a one-shot migrate service in Docker Compose
that the app depends on via condition: service_completed_successfully.
- Add scripts/migrate.mjs: programmatic drizzle-orm migration (uses
production deps, not drizzle-kit which is devOnly and absent from image)
- Dockerfile: copy scripts/ into image, remove drizzle.config.ts (only
needed by drizzle-kit CLI)
- server.ts: remove runMigrations() entirely; migrations are now handled
by the migrate container before the app starts
- deploy.yml: remove explicit docker run migration step; replace sleep 10
health check with a poll loop and explicit migrate exit-code check
Production server's docker-compose.yaml needs a one-time manual update
to add the migrate service — see plan for exact config.
https://claude.ai/code/session_01ReaqH3o9NVH4QU4qE9WMMQ
* Move drizzle-kit to devDependencies; use docker compose wait in deploy
drizzle-kit is a dev-only tool (schema generation and local migrations).
Now that production migrations run via the programmatic drizzle-orm API
in the migrate init container, drizzle-kit has no runtime role.
Also replaces the polling health check loop with docker compose wait,
which blocks until the migrate service exits and returns its exit code
cleanly — no sleep or manual status inspection needed.
https://claude.ai/code/session_01ReaqH3o9NVH4QU4qE9WMMQ
---------
Co-authored-by: Claude <noreply@anthropic.com>
Migrations were never being run against production on deploy, causing
schema mismatches when new columns are added. This adds a migration
step using the PROD_DATABASE_URL secret before bringing up the new
container, with set -e to abort the deploy if migrations fail.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes#262
* Replace isDraftable boolean with draftOn/draftOff date fields on sport seasons
Admins can now schedule when a sport season becomes available for drafting
by setting explicit open and close dates instead of a manual toggle.
https://claude.ai/code/session_01LHYgpyimF8v8odUB6kj8Qc
* Address code review feedback on draft window implementation
- sports-data-sync: use unambiguous past placeholder dates for synced seasons,
with a comment explaining the intent
- sports-season model: remove redundant top-level lte/gte imports (callback
form already supplies them)
- _journal.json: add missing trailing newline
- sports-season test: mock ~/database/schema instead of stubbing all drizzle
builder functions, matching the established pattern in the codebase
- admin list: compute today once in component body instead of per-row
https://claude.ai/code/session_01LHYgpyimF8v8odUB6kj8Qc
* Fix: restore lte/gte imports removed in review cleanup
The relational query where-callback in this version of Drizzle does not
expose lte/gte as helper args, so they must be imported from drizzle-orm.
Removing them broke CI TypeScript.
https://claude.ai/code/session_01LHYgpyimF8v8odUB6kj8Qc
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Remove redundant processPlayoffEvent loop from finalize-bracket action
All bracket rounds are already processed (with scoring and Discord
notifications) as each match winner is set via set-winner/set-round-winners.
By the time the Finalize button is clicked, placements are current and
standings are up to date. The re-processing loop was firing recalculations
and Discord notifications once per round needlessly.
The finalize action now only assigns 0 points to non-bracket participants,
marks the event complete, and runs one final standings recalculation.
https://claude.ai/code/session_01RhQS6FQRh6iYtVNaryCEf5
* Fix stale comment in finalize-bracket action
The template is now fetched only as a validity guard, not for round
order iteration (which was removed in the previous commit).
https://claude.ai/code/session_01RhQS6FQRh6iYtVNaryCEf5
* Fix complete-round redundant Discord/recalculation and stale comment
complete-round was calling processPlayoffEvent without skipRecalculate,
firing recalculateAffectedLeagues and Discord even though each match
winner had already triggered those side effects via set-winner. Add
skipRecalculate: true to match the autoCompleteRoundIfDone pattern.
Also fix autoCompleteRoundIfDone comment which incorrectly said
"non-bracket eliminations are recorded" — that's a separate step in
finalize-bracket; processPlayoffEvent records bracket round placements.
https://claude.ai/code/session_01RhQS6FQRh6iYtVNaryCEf5
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add CS2 Major qualifying points simulator
Implements a full CS2 Major tournament simulator with:
- 3-stage Swiss format (Opening Bo1, Elimination Bo1/Bo3, Decider all Bo3)
+ Champions Stage 8-team single-elimination (QF Bo3, SF Bo3, GF Bo5)
- Monte Carlo simulation (10,000 iterations) accumulating QP across 2 majors/season
- Sampled 24-team field per iteration: top 12 guaranteed, remaining weighted by 1/rank
- Stage 3 exits (placements 9-16) sub-ranked by W-L record (2-3 > 1-3 > 0-3)
- Stage assignments stored per-event so actual field composition drives simulation
- Admin CS Elo form for entering team Elo + HLTV world rankings
- Admin CS2 stage setup page for assigning teams to stages and tracking advancement
- Database migration: cs2_major_qualifying_points enum value + cs2_major_stage_results table
- 24 unit tests covering all exported pure functions
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate Elo + ranking input into generic elo-ratings page
The darts-elo and cs-elo pages were unreachable from the admin nav,
which always links to the generic elo-ratings page. Extended elo-ratings
to conditionally show world ranking fields for simulator types that need
it (darts_bracket, cs2_major_qualifying_points), then deleted the
redundant sport-specific pages.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate server postgres connections into one shared pool
Four separate postgres() clients were open simultaneously (app, timer,
snapshots, socket), each defaulting to 10 connections, exhausting the
database's max_connections limit. Replaced with a single shared lazy-
initialized client in server/db.ts using a Proxy to defer the
DATABASE_URL check until first use (preserving test compatibility).
Also bumps the CS2 Champions Stage stochastic test from 200 → 1000
iterations to eliminate flakiness.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix and() bug and add Swiss loop safety guard
- cs2-major-stage.ts: markCs2StageEliminations and setCs2FinalPlacements
were using JS && instead of Drizzle and(), causing WHERE to filter only
by participantId (not scoringEventId), which would update rows across
all events instead of just the target event
- cs-major-simulator.ts: add break guard in simulateSwiss while loop to
prevent infinite loop if pairGroups returns no pairs
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix all remaining code review issues
- cs2-major-stage.ts: use schema column reference for stageEliminated
in markCs2StageEliminations instead of raw SQL string
- cs-major-simulator.ts: simulateOneMajor now locks in known stage
results when a stage is complete (8 recorded eliminations), only
simulating the remaining stages during live events
- admin event page: add CS2 Stage Setup button for cs2_major_qualifying_points
simulator types; expose simulatorType in server loader type cast
- cs2-setup.tsx: replace document.getElementById DOM manipulation with
React state (eliminatedChecked map) for checkbox show/hide logic;
remove unused stageMap and unassignedParticipants variables
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix oxlint errors: non-null assertions, sort→toSorted, unused vars
- cs-major-simulator.ts: replace 5 non-null assertions (!) with safe
optional chaining / if-guards; replace 6 .sort() with .toSorted()
- cs2-major-stage.ts: remove unused `inArray` import
- cs2-setup.tsx: remove unused `assignedIds` variable
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix flaky Champions Stage stochastic test
The makeTeams(8) helper creates only a 70-pt Elo spread (1800→1730).
With the Champions Stage bracket math this gives team-0 a ~19.6% win
rate — right at the 0.2 threshold, causing the test to fail ~63% of
the time in CI despite 1000 iterations.
Use 100-pt steps (1800→1100) instead, giving team-0 a ~40% win rate
and raising the assertion threshold to 0.25 for a clear safety margin.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
---------
Co-authored-by: Claude <noreply@anthropic.com>
Mirrors how DATABASE_URL is loaded, so users can store both in their
local .env instead of passing PROD_DATABASE_URL inline each time.
https://claude.ai/code/session_01EiYh5ZiuWAnpo1PND45Yi1
Co-authored-by: Claude <noreply@anthropic.com>
* Add prod-to-dev database sync script
Adds scripts/sync-prod-db.sh and npm run db:sync-prod to safely copy
production data to the development database. After restoring, the script
nulls all Discord webhook URLs and anonymizes non-admin user PII (email,
name, avatar) to prevent notification leaks and reduce PII exposure on
developer machines. Admin accounts are preserved intact.
https://claude.ai/code/session_01EiYh5ZiuWAnpo1PND45Yi1
* Fix six code review issues in sync-prod-db.sh
- Add upfront check for pg_dump/pg_restore/psql availability (#5)
- Fix password masking to use sed instead of broken bash glob (#6)
- Replace DROP/CREATE DATABASE with pg_restore --clean --if-exists,
which works on managed cloud databases without superuser access (#2)
- Remove dead ADMIN_DB assignment that was immediately overwritten (#3)
- Handle pg_restore exit code 1 (non-fatal warnings) gracefully
instead of aborting via set -e (#1)
- Count webhook URLs before the UPDATE for an accurate cleared count (#4)
https://claude.ai/code/session_01EiYh5ZiuWAnpo1PND45Yi1
---------
Co-authored-by: Claude <noreply@anthropic.com>
Fixes#224
* Add inline edit for participant names on admin sports season page
Adds a pencil icon button next to each participant's delete button. Clicking
it switches the row into edit mode with an input field, a save (checkmark) and
cancel (X) button. Escape also cancels. Submits via intent="update-name" and
validates uniqueness within the season before persisting.
https://claude.ai/code/session_01Q94DWQ1HZEB9PJhGMArU8D
* Simplify edit state and surface edit errors to user
- Combine editingId + editingName into single editing: { id, name } | null state object
- Extract cancelEdit() helper called in 3 places instead of repeating setEditing(null)
- Tag all update-name action responses with intent: "update-name" so errors can be
identified and routed correctly
- Show update-name errors inline below the edit input
- Guard single-add and bulk-add error displays against showing update-name errors
https://claude.ai/code/session_01Q94DWQ1HZEB9PJhGMArU8D
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Fix admin Games to Score panel missing bracket events
The panel was silently dropping bracket events in two cases:
1. When a bracket event had `scoringEvents.eventDate` set to a future
round date (e.g. "April 5") but an individual game's `scheduledAt`
was today, the old dedup code picked `eventDate` first and the
component filter `displayDate === today` never matched.
2. When an event had games on both today and tomorrow, whichever row
the unordered step-1 query returned first "won" — potentially hiding
a today game from the today tab entirely.
Fix: replace the `Map<id, firstDate>` deduplication with
`Map<id, Set<date>>` that collects every requested date an event
qualifies for (via either `eventDate` or `gameDate`). The return uses
`flatMap` to emit one entry per (event, date) pair, so events with
games on multiple days now appear correctly in both tabs.
https://claude.ai/code/session_01QkFxCF4xmKjdVCNPysWDML
* Fix lint errors: replace .sort() with .toSorted() and remove non-null assertion
- Replace Array#sort() with Array#toSorted() in two places (unicorn rule)
- Remove non-null assertion on Map#get(); use early continue instead
https://claude.ai/code/session_01QkFxCF4xmKjdVCNPysWDML
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Switch Docker layer cache from local filesystem to GHA native cache
(type=gha), eliminating the cache-size workaround hack
- Consolidate cancel-previous-runs into a single dedicated job to
prevent race conditions from concurrent API calls
- Remove DATABASE_URL build-arg that was never consumed by the Dockerfile
- Add post-deploy health check that fails the job and prints logs if any
container exits unexpectedly
- Add .nvmrc and use node-version-file instead of hardcoded node-version: 20
Fixes#249
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
When generating groups for a tournament (e.g. FIFA World Cup), any
participants in the sport season who were not assigned to a group are
now automatically marked as eliminated (finalPosition = 0) — the same
result as running "Reprocess Eliminations" manually.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes#123
* Add PDC World Darts Championship simulator (128-player bracket)
- New DartsSimulator: 128-player single-elimination, 7 rounds with
PDC-accurate best-of-sets formats (bo3/bo5/bo5/bo7/bo7/bo11/bo13).
ELO_DIVISOR=500 via set-level Bernoulli model. Two simulation paths:
Path A (bracket drawn) simulates from actual DB matches; Path B
(pre-bracket) seeds top 32 by world ranking in fixed positions and
randomly draws the remaining 96 per simulation run (50,000 iterations).
- darts_bracket added to simulatorTypeEnum and simulator registry.
- world_ranking nullable integer column added to participant_expected_values
(migration 0067); batchSaveSourceElos now accepts and persists it.
- Admin route /admin/sports-seasons/:id/darts-elo: bulk import with format
"Player Name, 2099, 1" (name, Elo, optional world ranking), fuzzy name
matching, auto-runs simulation and updates EV/snapshots on save.
- DARTS_128 bracket template added (scoring starts at Quarterfinals).
- 30 unit tests: math helpers, bracket seeding structure, Path A/B integration.
https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz
* Review fixes: pre-compute hot-loop invariants, import normalizeName
- Pre-compute seededSlots and getSeededMatchOrder(32) before the 50k
simulation loop — was being recomputed every iteration
- Pad unseeded pool to 96 once before the loop instead of inside it
- Inline bracket-building in the hot loop using pre-computed seededSlots;
removes the per-iteration call to buildR1Bracket/getSeededMatchOrder
- Remove dead SEEDED_R1_PAIRS constant (was never referenced)
- Import normalizeName from ~/lib/fuzzy-match instead of redefining it locally
https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz
* Fix lint failures: unused vars, eqeqeq, toSorted
- Remove unused seededSet/unseededSet variables in test
- Replace != null with !== null && !== undefined (eqeqeq rule)
- Replace .sort() with .toSorted() in simulator and route (unicorn/no-array-sort)
https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz
* Fix TS2552: restore seededSet declaration removed during lint fix
https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Exclude scored bracket matches from upcoming events calendar
Add `eq(schema.playoffMatches.isComplete, false)` to the bracket-sports
query in `getUpcomingEventsForDraftedParticipants` so that individual
playoff matches already scored (isComplete = true) no longer appear on
the upcoming events calendar, even when the parent scoring event is still
open.
Also update the schema mock in upcoming-calendar.test.ts to include
`playoffMatches.isComplete` and add a test covering this behaviour.
https://claude.ai/code/session_01GMLmjN9mwoANfpgSciD7Bi
* Fix leaked mockReturnValueOnce in scored-match test
The new test only triggers one selectDistinct call (rows is empty so the
max-game-number lookup is skipped). The second mockReturnValueOnce was
never consumed, leaving a makeMaxGameChain value in the queue that was
then picked up by the subsequent "uses leftJoin" test, causing it to fail
with "leftJoin is not a function".
https://claude.ai/code/session_01GMLmjN9mwoANfpgSciD7Bi
---------
Co-authored-by: Claude <noreply@anthropic.com>
Scope test:run to the unit project only so it doesn't attempt to launch
Playwright/Chromium (needed by the storybook browser project) in environments
where browsers aren't installed. Add test:storybook script for running
storybook stories explicitly when Playwright is available.
https://claude.ai/code/session_01PqDTVMzP7dMtLpkn5Sw442
Co-authored-by: Claude <noreply@anthropic.com>
Placement counts (used to break points ties) previously only included
fully-finalized participants. A team whose events all resolved first would
accumulate real placement counts while tied teams with pending events had
zeros, causing the tie to be broken in favour of whichever team finished
scoring first rather than on merit.
Fix: count a participant's current position toward the tiebreaker regardless
of isPartialScore. Their position is the best available signal; if it changes,
recalculateStandings reruns and ranks update naturally.
Also removes a redundant bounds check (finalPosition > 0 was already
asserted by the outer if-condition).
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Install Storybook 10 with @storybook/react-vite framework
- Fix React Router Vite plugin conflict by filtering it out in viteFinal
(reactRouter() returns a plugin array, so the filter must flatten first)
- Integrate Storybook stories as a vitest browser project via @storybook/addon-vitest
- Point stories glob at app/ instead of scaffold stories/ dir
- Remove unused @chromatic-com/storybook and @storybook/addon-onboarding addons
- Clean up duplicate __dirname in vitest.config.ts
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator, fixes#127
- New `groupStageMatches` table for recording group play results (W/D/L, scores, matchday, schedule)
- `computeGroupStandings()` model function: pts → GD → GF → name tiebreaker ordering
- `GroupStageStandings` component showing all 12 groups with standings table and manager column
- Admin bracket UI: group match score entry, per-group standings, "Recalculate Floors" action
- `WorldCupSimulator`: 50k Monte Carlo covering group stage + best-8 3rd-place + knockout + 3rd place game
- Fuzzy name matching for national team Elo lookup (exact → substring → word-overlap), warns on miss
- Partial group completion: completed matches replayed with real scores, remaining matches simulated
- Elo priority: admin-entered sourceElo > futures odds converted to Elo > hardcoded national team ratings
- `fifa_48` bracket template: added Third Place Game round with `loserFeedsInto` on Semifinals
- Scoring rules: distinct 3rd/4th place for `fifa_48` (not averaged), QF losers share 5th–8th equally
- Floor scoring: SF participants guaranteed 4th (provisional), finalized after 3rd place game
- `recalculate-floors` admin action deletes and replays all results from scratch (fixes stale guard bug)
- Unique index on `(tournamentGroupId, participant1Id, participant2Id)` to prevent duplicate pairings
- Batch `findMatchesByGroupIds()` replacing N sequential queries in the sport season loader
- League home mini-standings now shows `actualPoints` (includes floor) instead of `totalPoints` only
- Elo ratings admin page supports World Cup (same bulk-import flow as snooker)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix TypeScript errors: update GroupStandingData type to use findMatchesByGroupIds
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Increase Node heap to 4GB for unit tests in CI to prevent OOM
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix OOM in CI: make WorldCupSimulator simulation count configurable for tests
Tests now pass numSimulations=500 instead of the production default of 50,000.
Six simulator tests × 50k iterations each was exhausting the 4GB heap on GitHub
Actions runners. Also reduce simGroupMatch stat tests from 50k to 5k iterations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Show tied ranks with T prefix across standings and Discord notifications, fixes#197
- Add buildTiedRankChecker() utility to standings-display.ts; replaces
four copies of inline rank-count logic spread across components,
the league home route, and the Discord service
- Prefix shared ranks with "T" (e.g. "T3") in StandingsTable (league
panel), StandingsTable (full standings), the league home preview, and
Discord webhook notifications
- Add useMemo wrapping in StandingsTable components so tie detection
does not recompute on every render
- Fix compareTeamsForRanking to round total points to hundredths before
comparing, preventing floating-point noise from producing spurious
non-ties in the standings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix discord test to expect escaped period in rank display
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add unique index on (sports_season_id, name) in participants table
- findParticipantByName uses case-insensitive lower() comparison
- Single add: check for existing name before insert, return clear error
- Bulk add: load existing names once upfront (1 query vs N), dedup
input case-insensitively, report skipped names in UI
- Fix golf-skills and surface-elo routes which called createParticipant
without any duplicate guard (would have thrown DB constraint errors)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix UTC midnight rollover bug: server now queries from yesterday UTC
as a buffer; UpcomingCalendarPanel filters to local-today client-side
via useEffect + Intl.DateTimeFormat, removing the need for any
cookie or server-side timezone detection
- Cap homepage and league page panels at 6 events with a "View all" link
- Add /upcoming-events page (60-day view across all leagues)
- Add /leagues/:leagueId/upcoming-events page (60-day per-league view)
- Add emptyMessage prop to UpcomingCalendarPanel for context-specific copy
- Change getUpcomingEventsForDraftedParticipants to accept pre-computed
date strings instead of Date objects
fixes#213
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- New /support route with Discord join button and contact form
- Contact form sends via Resend to support@brackt.com (email stays hidden from users)
- Spam protection: honeypot field + Cloudflare Turnstile verification
- Server-side input length limits (subject: 200, message: 5000 chars)
- Added Support nav link to navbar (desktop + mobile)
Fixes#233
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add WNBA playoff simulator with SRS-based Elo ratings, fixes#125
- New WNBASimulator: Monte Carlo (50k sims) projecting remaining regular
season games → seeding → R1 Bo3 / Semis Bo5 / Finals Bo7 bracket
- Hybrid Elo sourcing: pre-season uses futures odds (ICM); once avg
gamesPlayed ≥ 5, switches automatically to SRS-derived Elo
(elo = 1500 + srs × 20)
- New WnbaStandingsAdapter: fetches ESPN standings + teams endpoints in
parallel; includes zero-records for 2026 expansion teams (Portland
Fire, Toronto Tempo) not yet in standings
- Added srs column to regular_season_standings (migration 0063);
stored as net rating proxy (avgPointsFor − avgPointsAgainst)
- Added wnba_bracket to simulatorTypeEnum
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix missing afterEach import in wnba standings test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Improve admin dashboard stats and fix scoring event date bugs, fixes#92
- Add "Seasons by Status" breakdown card (pre-draft / drafting / active / completed)
- Replace full-table fetches with COUNT queries for sports, sports seasons, and templates
- Fix bracket events with null eventDate disappearing from the Games to Score tabs by
computing a displayDate from matched playoffMatchGames.scheduledAt in getEventsForDates
- Fix Today tab badge showing stale count when all events are scored
- Hide "Getting Started" checklist once the system has sports configured
- Use explicit "en-US" locale in formatEventTime for deterministic output
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix scoring-event-dashboard tests after select/sql changes
- Rename mockSelectDistinct → mockSelect to match implementation change
from .selectDistinct() to .select()
- Add sql to drizzle-orm mock
- Update mock row shapes to include eventDate and gameDate fields
- Add two tests covering displayDate derivation from eventDate and gameDate
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes#227
- Replace hardcoded Elo ratings with 2026 FanGraphs Depth Charts projected
run differential (RDif) for all 30 teams
- Replace Elo formula with Bill James log5 using RDif → win rate conversion
- Restore p_div/p_wc from FanGraphs playoff odds (divTitle/wcTitle) for
seeding draws — previously these were incorrectly derived from raw win
rates, which gave the Dodgers only ~24% division win probability instead
of the correct ~90%
- Add RDIF_DIVISOR constant to compress team strengths toward .500 for
playoff parity; set to 8000 (Dodgers +137 → ~51.7% per-game win rate)
- Fix minimum team check: < 5 → < 6 (need 3 div winners + 3 wild cards)
- Fix Colorado Rockies p_div 0.000 → 0.001 for consistency
- Fix stale comments throughout; update tests
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add MLB playoff simulator with AL/NL division standings, fixes#121
- New `mlb-simulator.ts`: 50k Monte Carlo sim drawing division winners
(weighted by p_div) and 3 WC teams per league, then simulating
WC best-of-3 → DS best-of-5 → LCS best-of-7 → World Series.
Elo parity factor 350; blends Vegas odds 30/70 when sourceOdds available.
- New `standings-sync/mlb.ts`: adapter for free statsapi.mlb.com API,
mapping AL/NL conferences, division ranks, streaks, home/away/L10 splits.
- New `mlb-divisions` display mode in RegularSeasonStandings: shows 1 division
winner per division section + Wild Card section with 3-spot playoff line,
headings read "AL/NL League" instead of "Conference".
- Refactored buildNhlSections/buildMlbSections into shared buildDivisionSections
(divisionSpots + wcSpots params); conferenceLabel now flows through group
objects rather than being derived from displayMode in the render.
- DB migration 0062 adds `mlb_bracket` to simulator_type enum.
- 37 new tests across simulator, adapter, and component.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix flaky tennis simulator test: increase trials and lower threshold
500 trials with a ~3–5% expected win rate had enough variance to
occasionally land below the 0.03 threshold (~2% failure rate).
Bumping to 2000 trials reduces the std dev by 2x; lowering the
threshold to 0.02 keeps the assertion meaningful (still well above
random 0.78%) while eliminating the flakiness.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaced the old inline command (which just printed raw text and exited
non-zero) with a small script at .claude/hooks/lint-on-edit.sh. When
oxlint finds errors, the script outputs JSON with hookSpecificOutput
.additionalContext, which explicitly injects the lint errors into
Claude's context so it fixes them immediately rather than missing a
background notification.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add golf QP simulator with Plackett-Luce model, fixes#120
- New `participant_golf_skills` table (migration 0061) for SG: Total and
per-major American odds per player/season
- New `app/models/golf-skills.ts` with getGolfSkillsMap, getGolfSkillsForSeason,
batchUpsertGolfSkills
- Full `GolfSimulator` implementation replacing the TODO stub: Plackett-Luce
ranking model (PL_BETA=1.5, FIELD_SIZE=156), 10k Monte Carlo iterations,
awards QP by finishing position, ranks by total QP across all 4 majors
- New admin route `sports-seasons/:id/golf-skills` with bulk CSV import,
fuzzy name matching, per-player SG + per-major odds inputs; saves skills
and auto-runs simulation on submit
- Simulator dropdown on sport admin sorted alphabetically; renamed to
"Golf Qualifying Points Monte Carlo"
- Golf Skills button shown on sports season admin when simulator type is
golf_qualifying_points
- Extract normalizeName/diceCoefficient to shared `app/lib/fuzzy-match.ts`,
removing duplication from surface-elo and golf-skills routes
- Parallelize 4 DB queries in GolfSimulator.simulate() with Promise.all
- O(1) field array removal via swap-to-end + pop (was O(N) splice)
- Fix source tag: performance_model (not elo_simulation) for SG-based model
- 23 unit tests covering americanToImplied, getMajorOddsKey, resolveSkill,
simulateMajor, and Monte Carlo calibration properties
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix oxlint errors: no-non-null-assertion and eqeqeq
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The chain (checkAndTriggerNextAutodraft) picks consecutive while_on teams
immediately after a pick, but is capped at totalTeams iterations. Once the
cap is hit, the next while_on team had to wait for the full timer countdown
before their pick fired — which users experienced as autodraft intermittently
not working.
Root cause fix: the timer loop now detects while_on mode and triggers the
pick immediately (≤1 s) regardless of time remaining, bypassing the
countdown. A timer-update with timeRemaining:0 is emitted first so clients
don't see a frozen timer. If the chain already handled the pick, executeAutoPick
returns "Pick already made" and the timer moves on harmlessly.
Additional fixes:
- Add timerTickRunning guard so concurrent setInterval ticks (when a chain
takes >1 s) don't race on the same pick slot
- Use onConflictDoNothing on the draft pick INSERT in executeAutoPick so a
DB unique-constraint collision is treated as "Pick already made" rather
than pausing the draft
- Cache draftSlots per season in the timer loop (slots never change during
an active draft) — eliminates one DB query per tick
- currentPickNumber || 1 → ?? 1
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes#220
When standard draft mode was added, the chess clock increment was
accidentally restricted to owner-only picks. Commissioner, admin, and
auto-picks stopped earning the increment, causing teams that timed out
to freeze at 0s and instant-autopick every subsequent round.
Fix:
- make-pick: all pick types earn the increment in chess clock mode
- force-manual-pick: same; deduplicate standard/chess-clock branches
into a single update with mode-selected SQL; remove now-unused
timerSnapshot query
- draft-utils executeAutoPick: restore increment for chess clock
auto-picks; add missing seed insert when no timer row exists
- server/timer.ts: fix fallback initialization to use draftIncrementTime
in standard mode (was always using draftInitialTime)
- leagues/$leagueId.tsx: show Draft Timer Mode in League Info panel
Tests:
- Update draft.make-pick.timer-mode to cover owner/commissioner/admin
in both modes (commissioner section previously asserted frozen bank)
- Add draft.force-manual-pick.timer-mode for commissioner/admin force
picks in both modes
- Add executeAutoPick.timer for timer-triggered auto-picks in both modes
- Update draft.force-manual-pick to reflect new chess clock behavior
- Replace fragile toHaveBeenCalledTimes(2) assertions with
toHaveBeenCalledWith checks on the timer set call
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements a Monte Carlo simulator for men's/women's tennis seasons scored
on the qualifying_points pattern. Simulates all 4 Grand Slam majors
(Australian Open, French Open, Wimbledon, US Open) using surface-specific
Elo ratings and ATP/WTA world rankings for seeding.
New table: participant_surface_elos — one row per (participant, season)
storing worldRanking, eloHard, eloClay, eloGrass.
Key design decisions:
- Seeding uses ATP/WTA world ranking (not Elo), matching real draw procedure
- Top 32 seeded with standard slot placement (1→0, 2→64, 3-4→quarters, etc.)
- QP per round with tie-splitting pre-applied: W=20, F=14, SF=9, QF=4, R16=1.5
- Completed majors read actual qualifyingPointsAwarded from eventResults
- 10,000 Monte Carlo simulations; column sums naturally 1.0 (no normalization)
Admin UI at /admin/sports-seasons/:id/surface-elo:
- 5-column grid (Player | Rank | Hard | Clay | Grass)
- Bulk import: "Name, ranking, hardElo, clayElo, grassElo" one per line
- Fuzzy name matching (bigram Dice coefficient) with "Did you mean?" suggestions
- Inline participant creation for unmatched names via useFetcher
- Saves Elos and auto-runs simulation on submit
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix upsert standings no-op: use EXCLUDED pseudo-table for conflict update
Drizzle's onConflictDoUpdate set block was referencing the existing table
columns instead of the incoming values, causing every sync to silently
overwrite records with their own current data (no-op).
Fixes#211
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add unit tests for regular-season-standings model
Covers upsertRegularSeasonStandings (empty-array short-circuit, value
mapping, syncedAt behaviour), upsertManualStanding, getRegularSeasonStandings
sorting, getLastSyncedAt, deleteRegularSeasonStandings, and
getParticipantStanding — 19 tests total.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- SnookerSimulator: Monte Carlo simulation of the 32-player World
Championship bracket using per-frame Bernoulli win probabilities
derived from Elo ratings (ELO_DIVISOR=700). Two paths: bracket-
populated (respects completed matches) and pre-bracket (simulates
qualifying, then seeds full draw).
- Admin route /sports-seasons/:id/elo-ratings: bulk-import and per-
player Elo entry with fuzzy name matching; saves ratings and auto-
runs the simulation in one action.
- schema: add snooker_bracket simulator type, source_elo column on
participant_expected_values, unique index on (participant_id,
sports_season_id).
- Fix batchSaveSourceElos to use INSERT ... ON CONFLICT DO UPDATE
instead of N+1 SELECT+UPDATE loop.
- Fix existingElos returned as plain Record (Map doesn't survive JSON
serialization through useLoaderData).
- Fix "How It Works" formula to show correct divisor (700, not 400).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Sort by sport (secondary: participant) or participant name
- Extract SortableHeader component to reduce header button repetition
- Use toSorted() instead of slice().sort() (oxlint fix)
- Use localeCompare with sensitivity: 'base' for case-insensitive string sorts
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add sortable columns to draft picks table on manager page
Pick # column sorts by pick number (asc by default), Points column
sorts by actual points then projected as tiebreaker (desc by default).
Clicking a column header toggles sort direction with visual indicators.
https://claude.ai/code/session_01XBnm7eKxerR7WjwrqqJPwe
* Fix code review issues in TeamScoreBreakdown sortable table
- Move SortIndicator out of render (avoids remount on every render)
- Extract sortPicks helper outside component
- Restore default sort: sport name then pick number (was lost in prior commit)
- Replace unicode arrow chars with Lucide ArrowUp/ArrowDown/ArrowUpDown icons
- Add aria-sort attributes to sortable column headers for accessibility
- Change w-[90px] to min-w-[90px] on Pick # column to avoid clipping
- Flatten nested div inside Points sort button
https://claude.ai/code/session_01XBnm7eKxerR7WjwrqqJPwe
* Fix pick sort to use pure pick number (matches tests)
The sport-grouped tiebreaker conflicted with the test contract for the
pick column. Tests expect ascending pick number = [1, 2, 3] regardless
of sport, so revert sortPicks to sort by pickNumber only for that column.
https://claude.ai/code/session_01XBnm7eKxerR7WjwrqqJPwe
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Don't show Game #1 label for single-game matchups in calendar
When a matchup has only one game, showing "Game #1" is redundant and
unhelpful. Only show game numbers when there are multiple games in a
matchup (gameKeys.size > 1). For single-game matchups, show just the
round name (or null if it matches the event name).
Closes#198https://claude.ai/code/session_01CRrqu43FKHshpJt1NyrDdX
* Show game number for Game #2+ in series, hide Game #1 for single-game matchups
If a matchup has only one game (game #1), the number is redundant — show
just the round name. If game #2 or higher is upcoming, it clearly belongs
to a multi-game series, so show the game number to distinguish it.
https://claude.ai/code/session_01CRrqu43FKHshpJt1NyrDdX
* Fix Game #1 label for multi-game series when only Game #1 is in date window
Previously, Game #1 of a multi-game series showed no game number (e.g.
"Semifinals") if Game #2 fell outside the calendar date range. Since at
least 2 games are always entered for a series from the start, a second
un-date-filtered query now checks the max game number per event. If max
> 1, the event is a series and Game #1 is labelled accordingly
(e.g. "Semifinals Game #1").
https://claude.ai/code/session_01CRrqu43FKHshpJt1NyrDdX
* Show game number when multiple games from same series are in date window
The gameKeys.size > 1 branch previously showed only the round name,
contradicting the rule that multi-game series always show "Game #x".
Now shows the lowest game number visible in the window (e.g. both
Game #1 and #2 upcoming → "Semifinals Game #1").
https://claude.ai/code/session_01CRrqu43FKHshpJt1NyrDdX
* Split series games into individual calendar entries, each showing Game #x
Each game in a multi-game bracket series (e.g. Game #1 on Mar 23,
Game #2 on Mar 25) now appears as its own row on the calendar with its
own date and "Round Game #x" label. Single-game matchups are unchanged.
Grouping key changes from eventId to eventId|gameNumber for series events.
The max-game-number lookup now runs before grouping so the correct key
can be determined while processing each row.
https://claude.ai/code/session_01CRrqu43FKHshpJt1NyrDdX
* Cache isSeries per entry to avoid re-deriving it in the finalization loop
https://claude.ai/code/session_01CRrqu43FKHshpJt1NyrDdX
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Redesign standings page with sortable table, 7-day change, and chart repositioned
- Move point progression chart below the standings table
- Replace separate Points/Projected/Placement columns with:
- Single stacked "actual / projected" Points column (shared PointsDisplay component)
- "7-Day Change" column showing points gained + rank change over past 7 days
- Remove Placement breakdown column
- Sort ties alphabetically by team name (rank sort only)
- All columns are sortable via new reusable useSortableData hook
- Add sevenDayPointChange to TeamStandingWithChange type and model
https://claude.ai/code/session_01CuCKFVYbpsKSQoFDcTYfY7
* Code review fixes: module-level comparators, correct type on SortableHead, handle negative point change, remove redundant spread
- Move comparators object to module level (closes over nothing, no useMemo needed)
- Use SortConfig<StandingRow> on SortableHead instead of inline duplicate type
- SevenDayChange: handle negative pointChange with correct sign and color
- Remove redundant sevenDayPointChange explicit assignment (already in ...standing spread)
https://claude.ai/code/session_01CuCKFVYbpsKSQoFDcTYfY7
* Update StandingsTable tests to match redesigned component
- Remove showPlacementBreakdown prop (no longer exists on component)
- Replace Placement Breakdown test suite with 7-Day Change column tests
- Update header assertion: "Placements" → "7-Day Change"
- Add tests for positive/negative point change display and rank change indicators
- Remove accessibility test for placement title attributes
https://claude.ai/code/session_01CuCKFVYbpsKSQoFDcTYfY7
* Fix movement indicator arrow count assertion to exclude sort header arrows
queryAllByText(/↑|↓/) was matching the active sort column's ↑ indicator.
Narrow to /[↑↓]\d/ so only movement indicators (↑1, ↓1) are counted.
https://claude.ai/code/session_01CuCKFVYbpsKSQoFDcTYfY7
---------
Co-authored-by: Claude <noreply@anthropic.com>
Monte Carlo simulation of the 2026 AFL season and 10-team finals series.
Elo ratings are backsolved from Squiggle's projected season win totals using
the inverse formula: elo = 1500 - 450×log₁₀((1−wins/23)/(wins/23)).
- New `afl_bracket` simulator type (schema + migration)
- `afl-simulator.ts`: projects remaining regular season via Elo, seeds the
AFL_10 finals bracket (Wildcard → QF/EF → SF → PF → Grand Final), and
correctly tracks P5/P6 and P7/P8 as separate scoring tiers
- `afl.ts` standings sync adapter pulling from Squiggle API (no auth required)
- Finals line on standings display set to 10 for AFL seasons
- Substring name matching with longest-key-wins to prevent "Port Adelaide"
colliding with "Adelaide" when matching "Port Adelaide Power"
- 27 unit tests covering bracket logic, column-sum guarantees, and name matching
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 70/30 Elo + Vegas futures blending for per-game win probability,
mirroring the UCL simulator pattern; falls back to Elo-only when no
sourceOdds are stored in participantExpectedValues
- Adjust PARITY_FACTOR to better match Vegas championship implied
probabilities (COL was ~12% vs ~20% market expectation)
- Add 15 unit tests covering name normalization, team data lookup,
parity-factor math, seeding probability sanity, and eliminated-team checks
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace static Basketball-Reference seed probability distributions
(p_1..p_10) with dynamic simulation of each team's remaining games
based on current standings from the DB.
- Load regularSeasonStandings in parallel with participants query
- Compute remainingGames = 82 - gamesPlayed per team
- Pre-compute per-game win probability (Elo vs average opponent 1500)
on TeamEntry at construction time — not inside the hot loop
- Sort conference standings by projected wins to assign seeds, replacing
the drawSeed() weighted-probability approach
- Conference resolved from standings table, falls back to TEAMS_DATA
- Strip all p_1..p_10 seed probability data from TEAMS_DATA
- Move simulateProjectedWins to module scope (no closure captures)
- Remove const N alias; use NUM_SIMULATIONS directly
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add app/lib/logger.ts: dev passes through to console; prod routes errors
to Sentry.captureException and warnings to Sentry.captureMessage, with
extra context preserved. Uses captureMessage (not captureException) for
string-only args to avoid fabricated stack traces.
- Add server/logger.ts: dev passes through; prod silences log/info but
keeps warn/error on stderr (Sentry not initialized in that process).
- Replace all console.* calls across 44 app files and 4 server files.
- Upgrade no-console from warn → error in oxlint; exempt logger files and
scripts/** via overrides.
- Add typescript/no-inferrable-types rule; fix violations in services and
simulators. Exempt test files (intentional string widening for switch/if
tests would break under literal type inference).
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix no-shadow and consistent-function-scoping lint violations
Resolves all 11 no-shadow and 16 consistent-function-scoping oxlint
warnings and promotes both rules to errors in .oxlintrc.json.
no-shadow: renamed Drizzle callback params (sports→s, matches→m,
seasons→s) to avoid shadowing outer imports; removed shadowed
destructures (eq, inArray) from where callbacks; renamed inner
template→bracketTemplate, prev→currentTimers, season→ss, name→teamName
(with name: teamName fix to preserve semantics).
consistent-function-scoping: moved formatDate, getRankBadge,
getMovementIndicator, getPositionBadge, getStatusBadge, toDateStr,
elo (×2), weightedPick, sortByMatchNumber (×2) to module scope;
moved formatTime (×2), isValidLeagueName, getDraftTimes,
makeSeasonQueues to file scope in test files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix no-non-null-assertion lint violations and promote to error
Eliminates all 208 no-non-null-assertion warnings across 38 files.
Promotes typescript/no-non-null-assertion from warn to error in
.oxlintrc.json.
Fix patterns applied:
- Map.get(key)! after .has() check → extract with get() + null guard
- Map.get(key)! on pre-populated count maps → ?? 0 default
- .set(id, map.get(id)! + 1) increment → ?? 0 before adding
- participant1Id!/participant2Id! on DB matches → ?? "" fallback
- array.find()! in tests → guard + throw or expect().toBeDefined()
- bracketTemplateCache.get(id)! → null guard extract
- Various nullable field accesses → optional chain or ?? default
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix prefer-add-event-listener, no-unassigned-import, require-module-specifiers
Resolves all 9 remaining non-console lint warnings and promotes all
three rules to errors in .oxlintrc.json.
- prefer-add-event-listener: converted onchange/onclick/onload
assignments to addEventListener in useDraftNotifications.ts and
admin.data-sync.tsx; stored changeHandler ref for proper cleanup
with removeEventListener
- no-unassigned-import: configured rule with allow list for legitimate
side-effect imports (*.css, @testing-library/jest-dom,
@testing-library/cypress/add-commands)
- require-module-specifiers: removed redundant `export {}` from
cypress/support/e2e.ts (file already has an import)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix TypeScript errors from no-non-null-assertion fixes
Two fixes introduced by the non-null assertion cleanup produced type
errors:
- scoring-event.ts: `?? ""` was wrong type for a participant object map;
restructured to explicit null guards so TypeScript can narrow correctly
- standings-sync/index.ts: `?? null` after name-match lookup lost the
truthy guarantee, causing TS18047 on the write-back block; added
`participant &&` guard before accessing its properties
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add npm run typecheck as Stop hook in Claude settings
Runs a full project typecheck at the end of each Claude turn so type
errors surface as feedback before the next message.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
In dev mode, viteDevServer.ssrLoadModule re-evaluates server/app.ts on
every request, creating a new postgres connection pool each time and
eventually exhausting Postgres's max_connections limit. Cache the client
on globalThis so HMR re-evaluations reuse the same pool.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add oxlint and fix all lint errors
- Install oxlint, add .oxlintrc.json with rules for TypeScript/React
- Add npm run lint / lint:fix scripts
- Add Claude PostToolUse hook to run oxlint on every edited file
- Fix 101 errors: unused vars/imports, eqeqeq, prefer-const, no-new-array
- Fix no-array-index-key (use stable keys or suppress positional cases)
- Fix exhaustive-deps missing dependency in useEffect
- Promote exhaustive-deps and no-array-index-key to errors
- Fix Map.get() !== null bug in $leagueId.server.ts (should be !== undefined)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix no-explicit-any warnings and upgrade tsconfig to ES2023
- Replace all `any` types with proper types or `unknown` across ~20 files
- Add typed socket payload interfaces in draft route and useDraftSocket
- Use any[] with eslint-disable for socket.io callbacks (legitimate escape hatch)
- Bump all tsconfigs from ES2022 → ES2023 to support toSorted/toReversed
- Fix cascading type errors uncovered by removing any: Map.get narrowing,
participant relation types, ChartDataPoint, Partial<NewSeason> indexing
- Add ParticipantResultWithParticipant type to participant-result model
- Fix test fixtures to match updated interfaces (DraftCell, ParticipantResult)
- Fix duplicate getQPStandings import in sportsSeasonId.server.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Promote no-explicit-any to error
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds live standings sync and display for bracket-based sports (NBA/NHL),
so league members can see W/L tables and which teams their opponents drafted
during the regular season — not just after the playoff bracket is set.
- New `regular_season_standings` table with upsert-on-conflict sync
- Standings sync service with NHL (api-web.nhle.com) and NBA (ESPN) adapters,
externalId write-back for future syncs, and unmatched-team resolution UI
- `RegularSeasonStandings` component: flat (NBA) + division/wild-card (NHL) modes,
playoff line, TeamOwnerBadge, projected Brackt points (EV), mobile horizontal scroll
- Admin "Sync Standings" card + "Resolve Unmatched" UI on sports season page
- Admin manual standings edit hatch at /admin/sports-seasons/:id/regular-standings
- Show standings above bracket until matches exist; below once bracket is set
- `normalize-team-name` utility extracted to shared lib
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements a new "standard" timer mode alongside the existing chess clock
mode. In standard mode the per-pick timer resets to a fixed value after
every pick (no carry-over), and the speed selector shows plain time values
instead of named chess-clock presets.
Key changes:
- Add `draft_timer_mode` enum column to `seasons` table (migration 0053)
- `draft.start`: standard mode seeds timers at `draftIncrementTime` (the
per-pick value) rather than `draftInitialTime`
- `draft.make-pick`: three-way branch — standard resets, chess clock
owner earns increment, commissioner/admin pick leaves bank frozen
- `draft.force-manual-pick`: commissioner picks never earn bank time;
chess clock path uses a pre-pick snapshot to avoid a race window with
the 1-second timer loop
- `executeAutoPick` in draft-utils: auto picks never earn bank time;
chess clock path skips the DB update (timer already at 0)
- League creation and settings pages: mode-aware speed selector (raw
seconds for standard, named presets for chess clock); shared
`parseDraftSpeed` utility extracted to `app/lib/draft-timer.ts`
- Tests added for draft.start timer init and make-pick timer mode
behavior; force-manual-pick tests updated for new timer semantics
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>