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>
- Fix computeEliminatedByRound() to track participant1Id/participant2Id
in addition to winnerId/loserId, so a 7v8 play-in loser placed in a
PIR2 slot (incomplete match) is not shown as eliminated on the league page
- Replace separate Recalculate Floors and Reprocess Eliminations buttons
with a single Reprocess Bracket action that replays all matches and
re-marks non-bracket participants as eliminated
- Add NBA play-in test cases for the elimination computation logic
* fix: NBA play-in advancement writes E7/W7 to wrong participant slot
advanceNBAPlayInWinner was writing the PIR1 7v8 winner to participant1Id
of First Round M3/M7, but those slots already hold E2/W2 (seeded at
generation time). The "already filled" guard fired, silently swallowing
both the winner advancement and the loser's placement into Play-In
Round 2 — so the 7/8 loser never appeared in the second play-in game.
Fix: write the 7v8 winner to participant2Id (the correct null slot) and
update the guard, doc comment, and load-bearing comment accordingly.
Also swap the p2Override arguments in the simulator and test fixture to
match the corrected slot layout (FR M3: E2=p1, E7=p2).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: hasGroupStage check for templates without groupStage property
undefined !== null is true, so templates that omit groupStage (like
nba_20) were incorrectly treated as group-stage templates, triggering
the generate-groups action and the "Invalid template or template has
no group stage" error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the NBA_20 bracket template (20-team structure with play-in
tournament) and rewrites the NBA simulator to be bracket-aware like UCL.
- Add NBA_20 bracket template: PIR1 (4) → PIR2 (2) → First Round (8)
→ Conference Semifinals → Conference Finals → NBA Finals
- Add participantLabels (East/West 1–10) for admin bracket UI slot labels
- Add generateNBA20Bracket and advanceNBAPlayInWinner in playoff-match.ts
with custom play-in advancement logic (PIR1 winners go to two different
destinations)
- Rewrite NBASimulator with two auto-detected modes:
- Bracket-aware (preferred): reads actual bracket state, simulates only
remaining rounds forward using 50k Monte Carlo iterations
- Season-projection (fallback): existing logic for pre-playoff use
- Guard resolveGame/resolveSeries against null participants (throws with
match ID and slot info instead of silently using empty string)
- Document load-bearing First Round match ordering in generateNBA20Bracket
- Add 25 unit tests covering Elo math, team data, template structure,
and all simulator integration paths
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>
* 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>
* 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>
- NCAAM R64/R32 winners no longer receive T5 floor points; only Sweet
Sixteen winners entering the first scoring round get a floor
- AFL T5-T6 and T7-T8 score as separate tiers (avg([5,6]) and avg([7,8]))
instead of one shared avg([5-8]) pool
- Eliminated teams now always show their correct final rank (e.g. T33 for
NCAAM R64 losers) even during partial round scoring, by deriving the
rank label from the total match count per round rather than the dynamic
still-alive count
- Discord notifications now fire when a drafted participant is eliminated
with 0 points (e.g. R64 losers whose team score doesn't change)
- Discord notifications no longer include all prior matches — scoped to
the current batch via matchIds
- Discord section headers have spacing; rank medals replaced with numbers
- Rounds auto-complete when all matches are marked done; manual Complete
Round dropdown removed
- Fixed double Discord notification when the last match in a round
triggers auto-complete: processPlayoffEvent now accepts skipRecalculate
so the caller controls when the notification fires
- Fixed falsy check on finalPosition=0 in calculateTeamScore
- Undrafted 0-point participants filtered from Eliminated Teams display
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Use bracket template's canonical round order instead of sorting by match
count, which placed First Four (4 matches) after Elite Eight (also 4
matches). Extract getOrderedRoundsFromMatches() into bracket-templates.ts
to eliminate identical logic duplicated in the admin bracket component.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the hardcoded 2026 Men's region structure with a per-event
configurable system so the same ncaa_68 template works for Women's
March Madness (different region names and play-in placements each year).
- Add `bracket_region_config jsonb` column to `scoring_events`
- Export `ALL_16_SEEDS` and `BracketRegion`/`BracketPlayIn` interfaces
from bracket-templates.ts so both server and client share the type
- Admin bracket entry form now shows a "Configure Regions" section:
editable region names + add/remove play-in seed selectors (ShadCN
Select) per region; slot map updates live as config changes
- Server action parses region config from form, validates total = 68,
stores it on the event, and passes it to bracket generation
- `advanceFirstFourWinner` reads stored `bracketRegionConfig` from the
event row first, falls back to template.regions for existing events
- Fix opponentSeed formula (was `seedNum <= 8 ? 17-n : n`, always `17-n`)
- Remove duplicate ALL_SEEDS inline definitions (now a shared constant)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: use hidden input for UTC date value in add-game form
The onSubmit handler was setting the datetime-local input's value to an
ISO UTC string (e.g. "2026-03-11T17:00:00.000Z"). datetime-local inputs
only accept the format "YYYY-MM-DDTHH:MM" — values with a 'Z' timezone
suffix are invalid and the browser silently clears the field to "". This
caused the form to submit an empty scheduledAt, which the server treated
as null, so games were always saved without a date.
Fix by keeping the datetime-local input for user interaction (renamed
scheduledAtLocal, not submitted) and writing the converted UTC ISO string
into a separate hidden input named scheduledAt in the onSubmit handler.
Also adds app/lib/date-utils.ts with a localDateTimeToUtcIso helper and
matching tests to document and verify the datetime-local ↔ ISO conversion
behaviour.
https://claude.ai/code/session_0148fgZiXpyvGX8ZX3BFRGCs
* refactor: use localDateTimeToUtcIso in component and clean up tests
- Import and call localDateTimeToUtcIso in the add-game onSubmit handler
instead of duplicating the conversion inline; also applies the null/
invalid-date safety from the utility to the component
- Remove the redundant "incompatible with datetime-local" test case (its
intent is better expressed as a code comment than a test assertion)
- Move the datetime-local incompatibility explanation into a NOTE comment
on the utility function's JSDoc
https://claude.ai/code/session_0148fgZiXpyvGX8ZX3BFRGCs
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Convert datetime-local input value to UTC ISO string on form submit
so server correctly parses local time instead of treating it as UTC
- Show local timezone abbreviation (e.g. PDT) in the field label
- Hoist timezone abbreviation computation out of the matches render loop
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add playoff match games and odds storage
Introduces two new tables for bracket matchup detail storage:
- `playoff_match_games`: tracks individual game schedules within a
series matchup (game number, scheduledAt, status, per-game scores,
winner). Supports scheduled/complete/postponed status enum.
- `playoff_match_odds`: stores moneyline odds per participant per
matchup (single upsert record, no isLatest complexity).
Includes:
- Drizzle schema + relations with CASCADE deletes from playoff_matches
- Migration 0040_fat_puma.sql
- playoff-match-game.ts model with pure helpers: computeSeriesScore,
isSeriesComplete, getSeriesLeader — plus full CRUD
- playoff-match-odds.ts model with pure helpers: americanToImpliedProbability,
impliedProbabilityToAmerican, normalizeOdds — plus upsert/read/delete
- findPlayoffMatchesByEventId and findPlayoffMatchById updated to
include games and odds in their query results
- Bracket server route: add-game, update-game, delete-game,
upsert-odds, delete-odds actions
- Bracket admin UI: expandable per-match panel for game schedule
management and moneyline odds entry
- 41 new unit tests (18 game + 23 odds), all 810 tests passing
https://claude.ai/code/session_01Twt3D1bsEK3eXUhMaz6ee7
* Code review fixes: type safety, abstraction, and React correctness
- Derive PlayoffMatchGameStatus from schema enum instead of hardcoding
the string union, eliminating the duplicate source of truth
- updateGame now returns PlayoffMatchGame | undefined to reflect reality
when no row matches the ID
- Remove TOCTOU check-then-act in update-game action: call updateGame
directly and check the return value instead of a pre-flight findGameById
- Add status enum validation before the cast in update-game action
- Move impliedProbability computation inside upsertMatchOdds so callers
only provide moneylineOdds; the model owns the derivation
- Remove unnecessary dynamic import of americanToImpliedProbability in
the upsert-odds action (was already imported from the same module)
- Fix React list reconciliation bug: replace bare <> fragment with
<Fragment key={match.id}> so React can correctly track rows
https://claude.ai/code/session_01Twt3D1bsEK3eXUhMaz6ee7
---------
Co-authored-by: Claude <noreply@anthropic.com>
Every route now exports a meta function so the browser title bar reflects
the current page. Static pages use fixed titles; dynamic pages pull names
from loader data with a sensible fallback (e.g. league name, sport season
name, team name).
Titles follow the pattern "Page Name - Brackt" for user-facing routes and
"Page Name - Brackt Admin" for admin routes.
Fixes#74
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add FIFA_48 bracket template with 12 groups of 4, knockout stage of 32
- Add tournament groups schema, model, and GroupStageDisplay component
- Add projected/expected value scoring to standings and team breakdowns
- Add docker-compose for local PostgreSQL development
- Move DRAFT_ORDER_IMPLEMENTATION.md to plans directory
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Added loader and action functions for managing expected values in sports seasons.
- Implemented UI for recalculating probabilities based on participant results.
- Created a service to update probabilities after results are finalized, including handling finished and unfinished participants.
- Developed tests for the probability updater service to ensure correct functionality.
- Introduced a preview feature to show potential changes before applying updates.
- Created `TeamScoreBreakdown` component to display all drafted participants, their placements, and points.
- Added new route for team breakdown at `/leagues/:leagueId/standings/:seasonId/teams/:teamId`.
- Enhanced `getTeamScoreBreakdown` model to include `sportsSeasonId` for grouping.
- Updated `StandingsTable` to include clickable team name links.
- Added functionality to finalize brackets in playoff events, including error handling and recalculating standings.
- Implemented tests for `TeamScoreBreakdown` component to ensure proper rendering and functionality.
- Updated routing to support new team breakdown feature and ensure seamless navigation.
- Introduced `generateStandardSeeding()` function to create proper tournament matchups (e.g., 1v16, 8v9).
- Added comprehensive tests for 4, 8, 16, and 32-team brackets to ensure correct seeding and balance.
- Updated testing instructions to reflect new features and improvements in bracket generation.
- Fixed issues with sequential seeding and ensured all matchups sum to n+1 for balance.
- Implemented batch winner selection and duplicate participant prevention for improved user experience.
- Enhanced fantasy points display on event pages for better visibility of participant results.
- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats.
- Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets.
- Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly.
- Enhanced database schema to accommodate new scoring rules and bracket templates.
- Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly.
- Documented implementation phases for gradual rollout of new features.
- Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.