feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
import type { Route } from "./+types/admin.sports-seasons.$id.events.$eventId.bracket" ;
2026-03-21 09:44:05 -07:00
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
import { findSportsSeasonById } from "~/models/sports-season" ;
Auto-populate & auto-score tennis Grand Slam brackets from Wikipedia
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 00:32:35 -07:00
import {
findParticipantsBySportsSeasonId ,
createParticipant ,
updateParticipant ,
} from "~/models/season-participant" ;
Unify majors: score once, fan out across windows + tennis bracket EV
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
import { getScoringEventById , updateScoringEvent , isReadOnlySibling } from "~/models/scoring-event" ;
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
import {
findPlayoffMatchesByEventId ,
Lay out brackets from the feeder graph
The LLWS bracket didn't read as a bracket: cards sat above games that
don't feed them, connectors joined the wrong pairs, and several games had
no line at all.
The stored data was correct — LLWS_ADVANCEMENT already matches the
official 2026 LLBWS bracket game for game. The renderer was the problem.
TreeColumns placed cards at `index * (height / roundSize)` and
ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds
only for an exact halving. The LLWS winners bracket is not one: two of
the four Opening Round games skip Winners Round 2 and go straight to the
semifinals, so those two got stranded in column one with nothing beside
them, and the halving branch drew confident, wrong connectors for the
rest.
Lay out from the graph instead. app/lib/bracket-layout.ts inverts a
template's advancement into "what fills each slot", then assigns columns
by depth from the group's final, orders each column by the parent's slot
order, and centres each card on its feeders. Counting back from the final
is what makes a printed bracket line up: a team entering late is drawn in
the column where it actually plays. This reproduces the official
International bracket exactly, and fixes Elimination Round 3, where the
official bracket prints the later game on top but match-number sort put
it below.
Because column is depth, every in-group edge spans exactly one gutter, so
connectors now draw for unplayed games too. Cards also take a fixed
height rather than stretching to fill their column, which is what made a
lone final tower over the rest.
Empty slots name their source — "Loser of Winners SF 1" rather than
"TBD". That is the only way to show the feeds crossing between the
winners and elimination brackets, which render as separate trees.
Also:
- Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer
can import it without pulling the database context into the browser
bundle; models/playoff-match re-exports it.
- Page the mobile view one group at a time, matching desktop. A whole
double-elimination phase is a DAG, not a tree, so its columns would be
arbitrary.
- Add a clear-bracket admin action. Nothing else could rewrite a match's
participants, so a mis-seeded bracket had no repair path at all.
- Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the
routing and layout tests check against one copy of the official bracket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
deletePlayoffMatchesByEventId ,
2025-11-04 22:09:44 -08:00
generateBracketFromTemplate ,
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
setMatchWinner ,
2025-11-04 22:09:44 -08:00
advanceWinnerTemplate ,
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
findPlayoffMatchById ,
2026-02-14 22:30:12 -08:00
assignParticipantsToKnockout ,
2026-04-14 22:16:57 -07:00
doesLoserAdvance ,
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
} from "~/models/playoff-match" ;
Add playoff match game scheduling and odds management (#135)
* Add playoff match games and odds storage
Introduces two new tables for bracket matchup detail storage:
- `playoff_match_games`: tracks individual game schedules within a
series matchup (game number, scheduledAt, status, per-game scores,
winner). Supports scheduled/complete/postponed status enum.
- `playoff_match_odds`: stores moneyline odds per participant per
matchup (single upsert record, no isLatest complexity).
Includes:
- Drizzle schema + relations with CASCADE deletes from playoff_matches
- Migration 0040_fat_puma.sql
- playoff-match-game.ts model with pure helpers: computeSeriesScore,
isSeriesComplete, getSeriesLeader — plus full CRUD
- playoff-match-odds.ts model with pure helpers: americanToImpliedProbability,
impliedProbabilityToAmerican, normalizeOdds — plus upsert/read/delete
- findPlayoffMatchesByEventId and findPlayoffMatchById updated to
include games and odds in their query results
- Bracket server route: add-game, update-game, delete-game,
upsert-odds, delete-odds actions
- Bracket admin UI: expandable per-match panel for game schedule
management and moneyline odds entry
- 41 new unit tests (18 game + 23 odds), all 810 tests passing
https://claude.ai/code/session_01Twt3D1bsEK3eXUhMaz6ee7
* Code review fixes: type safety, abstraction, and React correctness
- Derive PlayoffMatchGameStatus from schema enum instead of hardcoding
the string union, eliminating the duplicate source of truth
- updateGame now returns PlayoffMatchGame | undefined to reflect reality
when no row matches the ID
- Remove TOCTOU check-then-act in update-game action: call updateGame
directly and check the return value instead of a pre-flight findGameById
- Add status enum validation before the cast in update-game action
- Move impliedProbability computation inside upsertMatchOdds so callers
only provide moneylineOdds; the model owns the derivation
- Remove unnecessary dynamic import of americanToImpliedProbability in
the upsert-odds action (was already imported from the same module)
- Fix React list reconciliation bug: replace bare <> fragment with
<Fragment key={match.id}> so React can correctly track rows
https://claude.ai/code/session_01Twt3D1bsEK3eXUhMaz6ee7
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 14:17:43 -07:00
import {
createGame ,
updateGame ,
deleteGame ,
type PlayoffMatchGameStatus ,
} from "~/models/playoff-match-game" ;
import {
upsertMatchOdds ,
deleteOddsForParticipant ,
} from "~/models/playoff-match-odds" ;
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
import {
processPlayoffEvent ,
processMatchResult ,
2026-06-19 01:25:17 +00:00
processQualifyingBracketEvent ,
processQualifyingEvent ,
finalizeQualifyingPoints ,
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
recalculateAffectedLeagues ,
2026-03-18 22:15:28 -07:00
recalculateStandings ,
2026-06-12 22:35:35 +00:00
autoCompleteRoundIfDone ,
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
} from "~/models/scoring-calculator" ;
import { updateProbabilitiesAfterResult } from "~/services/probability-updater" ;
2026-03-15 21:52:47 -07:00
import { getBracketTemplate , ALL_16_SEEDS , type BracketRegion } from "~/lib/bracket-templates" ;
2026-06-19 01:25:17 +00:00
import {
setParticipantResult ,
findParticipantResultsBySportsSeasonId ,
deleteParticipantResultsBySportsSeasonId ,
} from "~/models/participant-result" ;
2026-03-18 22:15:28 -07:00
import { findSeasonSportsBySportsSeasonId } from "~/models/season-sport" ;
import { createDailySnapshot } from "~/models/standings" ;
2026-02-14 22:30:12 -08:00
import {
createGroupsForEvent ,
addMembersToGroup ,
findGroupsByEventId ,
toggleMemberEliminated ,
getAdvancingParticipantIds ,
getEliminatedParticipantIds ,
} from "~/models/tournament-group" ;
Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator (#242)
* 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>
2026-03-29 10:27:47 -07:00
import {
createGroupStageMatches ,
generateRoundRobinPairings ,
updateGroupStageMatchResult ,
updateGroupStageMatchSchedule ,
findMatchesByGroupId ,
} from "~/models/group-stage-match" ;
2026-03-21 13:41:39 -07:00
import { logger } from "~/lib/logger" ;
2025-11-03 13:16:37 -08:00
import { database } from "~/database/context" ;
import * as schema from "~/database/schema" ;
2026-03-21 09:44:05 -07:00
import { eq } from "drizzle-orm" ;
2026-05-04 20:31:44 -07:00
import { maybeResolveCompletedBracktForSportsSeason } from "~/services/brackt.server" ;
Fix reprocess bracket to score mirror windows like the primary
Reprocessing a tennis (qualifying) major recomputed the primary window's
QP correctly (R16 players = 1.5, the average of the 9th–16th values) but
left mirror/sibling windows showing 2 QP. Two defects:
1. The mirror fan-out ran through fanOutMajorIfPrimary, which swallows
every error and returns void, so reprocess reported a green "success"
even when mirrors were never re-scored. The reprocess qualifying path
now calls syncMajorFromPrimaryEvent directly and folds the SyncReport
(windows synced / failed) into the response, surfacing failures instead
of hiding them.
2. Mirror windows split QP by counting canonical tournament_results rows
at each placement, which only equals the round's structural tie span
when placements are final. Mid-tournament, players floored at a tier
make the row-count diverge from the tier size, so mirrors split
differently than the primary. syncMajorFromPrimaryEvent now derives the
structural span (R16 = 8, QF = 4, SF = 2, Final = 1) from the primary
bracket via deriveBracketQualifyingStates and merges it over the
canonical counts, so every window splits identically to the primary at
every stage. Golf and CS2 Swiss-exit placements keep their canonical
count via the merge.
Adds a fan-out test covering an R16-in-progress bracket where only 4 rows
sit at placement 9: the mirror is scored with the structural span (8), not
the live count (4).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UkZVYgLmquWV2xDn349T2
2026-07-04 01:45:53 +00:00
import { fanOutMajorIfPrimary , syncMajorFromPrimaryEvent } from "~/services/sync-tournament-results" ;
Auto-populate & auto-score tennis Grand Slam brackets from Wikipedia
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 00:32:35 -07:00
import { syncTennisDraw , previewTennisDraw } from "~/services/match-sync" ;
import { articleTitleFromInput } from "~/services/match-sync/wikipedia-tennis" ;
2026-07-04 18:19:52 +00:00
import { newlyDecidedLosers } from "./admin.sports-seasons.$id.events.$eventId.bracket.helpers" ;
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
export async function loader ( { params } : Route . LoaderArgs ) {
const sportsSeason = await findSportsSeasonById ( params . id ) ;
if ( ! sportsSeason ) {
throw new Response ( "Sports season not found" , { status : 404 } ) ;
}
const event = await getScoringEventById ( params . eventId ) ;
if ( ! event ) {
throw new Response ( "Event not found" , { status : 404 } ) ;
}
2026-06-15 22:05:25 +00:00
if ( event . eventType !== "playoff_game" && event . eventType !== "major_tournament" ) {
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
throw new Response ( "This event is not a playoff event" , { status : 400 } ) ;
}
const participants = await findParticipantsBySportsSeasonId ( params . id ) ;
const matches = await findPlayoffMatchesByEventId ( params . eventId ) ;
Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator (#242)
* 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>
2026-03-29 10:27:47 -07:00
// Fetch tournament groups with their round-robin matches
const tournamentGroupsRaw = await findGroupsByEventId ( params . eventId ) ;
const tournamentGroups = await Promise . all (
tournamentGroupsRaw . map ( async ( group ) = > ( {
. . . group ,
groupMatches : await findMatchesByGroupId ( group . id ) ,
} ) )
) ;
2026-02-14 22:30:12 -08:00
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
// The matches already include participant relations from the model query
return {
sportsSeason : sportsSeason as typeof sportsSeason & {
sport : { id : string ; name : string ; type : string ; slug : string } ;
} ,
event ,
participants ,
matches : matches as Array < typeof matches [ 0 ] & {
participant1 : { id : string ; name : string } | null ;
participant2 : { id : string ; name : string } | null ;
winner : { id : string ; name : string } | null ;
loser : { id : string ; name : string } | null ;
2026-03-21 09:44:05 -07:00
games : Array < { id : string ; gameNumber : number ; scheduledAt : Date | null ; status : string ; winner ? : { name : string } | null } > ;
odds : Array < { id : string ; participantId : string ; moneylineOdds : number ; impliedProbability : string ; oddsSource? : string | null ; participant : { id : string ; name : string } | null } > ;
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
} > ,
2026-02-14 22:30:12 -08:00
tournamentGroups ,
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
} ;
}
2026-06-19 01:25:17 +00:00
/ * *
* Score a qualifying - event bracket ( e . g . CS2 Champions Stage ) : derive each team ' s
* guaranteed - minimum QP from the bracket and refresh affected league standings .
*
* Qualifying brackets award QUALIFYING POINTS , not fantasy points , so this never
* writes seasonParticipantResults and never records team_score_events — match
* results surface via the QP Standings and the Discord standings update . Final
* fantasy placements come from finalizeQualifyingPoints across all majors .
* /
async function scoreQualifyingBracket (
Unify majors: score once, fan out across windows + tennis bracket EV
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
event : {
id : string ;
sportsSeasonId : string ;
name : string | null ;
isPrimary : boolean ;
tournamentId : string | null ;
} ,
2026-06-19 01:25:17 +00:00
db : ReturnType < typeof database > ,
2026-07-04 18:19:52 +00:00
recalcOptions? : Parameters < typeof recalculateAffectedLeagues > [ 2 ] ,
/ * *
* Season_participant ids of players knocked out for the first time by this
* operation ( losers of matches that just reached completion ) . Forwarded to the
* fan - out so every mirror window announces the "Knocked Out" section — a
* non - scoring - round exit earns no QP and is otherwise invisible to the mirror .
* Empty for re - scores / reprocesses , which decide no new losers .
* /
newlyEliminatedParticipantIds? : Set < string >
2026-06-19 01:25:17 +00:00
) : Promise < void > {
2026-07-09 10:04:22 -07:00
// processQualifyingEvent derives the bracket QP (via processQualifyingBracketEvent),
// recalcs participant QP totals, AND announces the QP change to this window's leagues.
// Calling processQualifyingBracketEvent directly here would score silently — the QP
// Discord notification only fires from processQualifyingEvent. The fan-out below skips
// this (primary) window via skipEventId, so mirror windows are announced separately
// with no double-post.
await processQualifyingEvent ( event . id , db , { newlyEliminatedParticipantIds } ) ;
2026-06-19 01:25:17 +00:00
await recalculateAffectedLeagues (
event . sportsSeasonId ,
db ,
recalcOptions ? ? { eventId : event.id , eventName : event.name ? ? undefined }
) ;
Unify majors: score once, fan out across windows + tennis bracket EV
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
// If this is the shared major's primary window, propagate to siblings.
// Mid-tournament (a single round): don't mark complete yet.
2026-07-04 18:19:52 +00:00
await fanOutMajorIfPrimary ( event , {
markComplete : false ,
newlyEliminatedParticipantIds ,
} ) ;
2026-06-19 01:25:17 +00:00
}
2026-06-28 19:36:33 +00:00
/ * *
* Mark the given participants as eliminated ( finalPosition = 0 ) and , for fantasy
* ( non - qualifying ) events , announce the teams newly eliminated by this run to the
* affected leagues ' Discord channels . Returns the number of participants marked .
*
* "Newly eliminated" = participants with no prior result row , so re - running a
* generation step never re - announces the same teams . The announcement is a
* best - effort side effect : a failure must not fail the generation action , since
* the eliminations themselves are already committed . eventId is deliberately
* omitted from the recalc call so the announcement doesn ' t pull in unrelated
* completed matches as "Scored Matches" .
* /
async function markEliminatedAndAnnounce (
event : { id : string ; name : string | null ; sportsSeasonId : string ; isQualifyingEvent : boolean } ,
participantIds : string [ ]
) : Promise < number > {
const existingResults = await findParticipantResultsBySportsSeasonId ( event . sportsSeasonId ) ;
const alreadyHadResult = new Set ( existingResults . map ( ( r ) = > r . participantId ) ) ;
const newlyEliminatedIds = participantIds . filter ( ( id ) = > ! alreadyHadResult . has ( id ) ) ;
for ( const participantId of participantIds ) {
await setParticipantResult ( participantId , event . sportsSeasonId , 0 ) ;
}
// QPs (e.g. tennis/CS2 majors) don't get elimination announcements.
if ( ! event . isQualifyingEvent && newlyEliminatedIds . length > 0 ) {
try {
await recalculateAffectedLeagues ( event . sportsSeasonId , database ( ) , {
eventName : event.name ? ? undefined ,
eliminatedParticipantIds : newlyEliminatedIds ,
} ) ;
} catch ( err ) {
logger . error ( "[Eliminations] Discord announcement failed:" , err ) ;
}
}
return participantIds . length ;
}
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
export async function action ( { request , params } : Route . ActionArgs ) {
const formData = await request . formData ( ) ;
const intent = formData . get ( "intent" ) ;
Unify majors: score once, fan out across windows + tennis bracket EV
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
// Brackets are built/scored only on the major's primary window. A
// tournament-linked, non-primary event is a read-only mirror that receives
// results via fan-out, so reject every mutating action here.
{
const ev = await getScoringEventById ( params . eventId ) ;
if ( ev && isReadOnlySibling ( ev ) ) {
return {
error :
"This bracket belongs to a shared major. Build and score it on the primary window (linked from Admin → Tournaments); results fan out here automatically." ,
} ;
}
}
Auto-populate & auto-score tennis Grand Slam brackets from Wikipedia
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 00:32:35 -07:00
if ( intent === "create-draw-participant" ) {
const name = ( formData . get ( "name" ) as string | null ) ? . trim ( ) ;
const externalId = ( formData . get ( "externalId" ) as string | null ) ? . trim ( ) || null ;
if ( ! name ) return { error : "Participant name is required" } ;
try {
await createParticipant ( { sportsSeasonId : params.id , name , externalId } ) ;
return { success : ` Created " ${ name } ". Re-run Preview or Sync Draw to apply. ` } ;
} catch ( error ) {
return { error : error instanceof Error ? error . message : "Failed to create participant" } ;
}
}
if ( intent === "relink-draw-participant" ) {
const participantId = formData . get ( "participantId" ) as string | null ;
const name = ( formData . get ( "name" ) as string | null ) ? . trim ( ) ;
const externalId = ( formData . get ( "externalId" ) as string | null ) ? . trim ( ) || null ;
if ( ! participantId || ! name ) return { error : "Participant and name are required" } ;
try {
await updateParticipant ( participantId , { name , externalId } ) ;
return { success : ` Renamed and linked to " ${ name } ". Re-run Preview or Sync Draw to apply. ` } ;
} catch ( error ) {
return { error : error instanceof Error ? error . message : "Failed to update participant" } ;
}
}
if ( intent === "preview-draw" ) {
const rawInput = ( formData . get ( "externalSourceKey" ) as string | null ) ? . trim ( ) ;
const articleTitle = rawInput ? articleTitleFromInput ( rawInput ) : "" ;
// Persist the article so the user can preview, then sync, without re-entering.
if ( articleTitle ) {
await updateScoringEvent ( params . eventId , { externalSourceKey : articleTitle } ) ;
}
try {
const preview = await previewTennisDraw ( params . eventId ) ;
return { drawPreview : preview } ;
} catch ( error ) {
logger . error ( "[preview-draw] error:" , error ) ;
return { error : error instanceof Error ? error . message : "Failed to preview draw" } ;
}
}
if ( intent === "sync-draw" ) {
const rawInput = ( formData . get ( "externalSourceKey" ) as string | null ) ? . trim ( ) ;
// Accept a pasted Wikipedia URL or a plain article title; store the title.
const articleTitle = rawInput ? articleTitleFromInput ( rawInput ) : "" ;
if ( articleTitle ) {
await updateScoringEvent ( params . eventId , { externalSourceKey : articleTitle } ) ;
}
try {
const result = await syncTennisDraw ( params . eventId ) ;
const reviewNote =
result . unmatched . length > 0
? ` ${ result . unmatched . length } auto-created player(s) need a quick review for possible duplicates. `
: "" ;
return {
success :
` Synced draw: ${ result . matchesWritten } matches written ` +
` ( ${ result . completed } completed), ${ result . participantsCreated } participant(s) added. ${ reviewNote } ` ,
drawSyncResult : result ,
} ;
} catch ( error ) {
logger . error ( "[sync-draw] error:" , error ) ;
return { error : error instanceof Error ? error . message : "Failed to sync draw" } ;
}
}
Lay out brackets from the feeder graph
The LLWS bracket didn't read as a bracket: cards sat above games that
don't feed them, connectors joined the wrong pairs, and several games had
no line at all.
The stored data was correct — LLWS_ADVANCEMENT already matches the
official 2026 LLBWS bracket game for game. The renderer was the problem.
TreeColumns placed cards at `index * (height / roundSize)` and
ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds
only for an exact halving. The LLWS winners bracket is not one: two of
the four Opening Round games skip Winners Round 2 and go straight to the
semifinals, so those two got stranded in column one with nothing beside
them, and the halving branch drew confident, wrong connectors for the
rest.
Lay out from the graph instead. app/lib/bracket-layout.ts inverts a
template's advancement into "what fills each slot", then assigns columns
by depth from the group's final, orders each column by the parent's slot
order, and centres each card on its feeders. Counting back from the final
is what makes a printed bracket line up: a team entering late is drawn in
the column where it actually plays. This reproduces the official
International bracket exactly, and fixes Elimination Round 3, where the
official bracket prints the later game on top but match-number sort put
it below.
Because column is depth, every in-group edge spans exactly one gutter, so
connectors now draw for unplayed games too. Cards also take a fixed
height rather than stretching to fill their column, which is what made a
lone final tower over the rest.
Empty slots name their source — "Loser of Winners SF 1" rather than
"TBD". That is the only way to show the feeds crossing between the
winners and elimination brackets, which render as separate trees.
Also:
- Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer
can import it without pulling the database context into the browser
bundle; models/playoff-match re-exports it.
- Page the mobile view one group at a time, matching desktop. A whole
double-elimination phase is a DAG, not a tree, so its columns would be
arbitrary.
- Add a clear-bracket admin action. Nothing else could rewrite a match's
participants, so a mis-seeded bracket had no repair path at all.
- Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the
routing and layout tests check against one copy of the official bracket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
// The only way to repair a mis-seeded bracket: nothing else can rewrite a match's
// participants. Clearing brings back the setup form, so the admin re-seeds from there.
if ( intent === "clear-bracket" ) {
try {
const event = await getScoringEventById ( params . eventId ) ;
if ( ! event ) return { error : "Event not found" } ;
const existing = await findPlayoffMatchesByEventId ( params . eventId ) ;
if ( existing . length === 0 ) {
return { error : "This event has no bracket to clear" } ;
}
// Clearing discards recorded results, so make the admin confirm once games have
// actually been played.
const completed = existing . filter ( ( m ) = > m . isComplete ) . length ;
if ( completed > 0 && formData . get ( "confirm" ) !== "true" ) {
return {
error : ` This bracket has ${ completed } completed match(es). Confirm to discard those results. ` ,
} ;
}
await deletePlayoffMatchesByEventId ( params . eventId ) ;
// Placements were derived from the matches just deleted; leaving them behind would
// keep stale points on the standings.
await deleteParticipantResultsBySportsSeasonId ( event . sportsSeasonId ) ;
await recalculateAffectedLeagues ( event . sportsSeasonId , database ( ) , { skipDiscord : true } ) ;
return {
success : ` Bracket cleared ( ${ existing . length } match(es) removed). Set it up again below. ` ,
} ;
} catch ( error ) {
logger . error ( "Error clearing bracket:" , error ) ;
return {
error : error instanceof Error ? error . message : "Failed to clear bracket" ,
} ;
}
}
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
if ( intent === "generate-bracket" ) {
2025-11-04 22:09:44 -08:00
const templateId = formData . get ( "templateId" ) ;
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
2025-11-04 22:09:44 -08:00
if ( typeof templateId !== "string" || ! templateId ) {
return { error : "Template ID is required" } ;
}
const template = getBracketTemplate ( templateId ) ;
if ( ! template ) {
return { error : "Invalid bracket template" } ;
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
}
2026-03-15 21:52:47 -07:00
// Parse per-event region config for NCAA-style brackets
let regionOverride : BracketRegion [ ] | undefined ;
if ( template . regions && template . regions . length > 0 ) {
const regions : BracketRegion [ ] = [ ] ;
for ( let r = 0 ; r < template . regions . length ; r ++ ) {
const name = ( formData . get ( ` regionName ${ r } ` ) as string | null ) ? . trim ( )
|| ` Region ${ r + 1 } ` ;
const seedsRaw = ( formData . get ( ` regionPlayInSeeds ${ r } ` ) as string | null ) || "" ;
const playInSeeds = seedsRaw
. split ( "," )
. map ( ( s ) = > parseInt ( s . trim ( ) , 10 ) )
. filter ( ( n ) = > ! isNaN ( n ) && n >= 1 && n <= 16 ) ;
const playInSet = new Set ( playInSeeds ) ;
regions . push ( {
name ,
directSeeds : ALL_16_SEEDS.filter ( ( s ) = > ! playInSet . has ( s ) ) ,
playIns : playInSeeds.map ( ( s ) = > ( { seedSlot : s , teams : 2 as const } ) ) ,
} ) ;
}
// Validate total = template.totalTeams
const total = regions . reduce (
( sum , r ) = > sum + r . directSeeds . length + r . playIns . length * 2 ,
0
) ;
if ( total !== template . totalTeams ) {
return {
error : ` Region config accounts for ${ total } team slots but template requires ${ template . totalTeams } ` ,
} ;
}
regionOverride = regions ;
}
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
const participantIds : string [ ] = [ ] ;
2025-11-04 22:09:44 -08:00
for ( let i = 0 ; i < template . totalTeams ; i ++ ) {
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
const participantId = formData . get ( ` participant ${ i } ` ) ;
if ( typeof participantId !== "string" || ! participantId ) {
return { error : ` Participant ${ i + 1 } is required ` } ;
}
participantIds . push ( participantId ) ;
}
// Check for duplicates
const uniqueParticipants = new Set ( participantIds ) ;
if ( uniqueParticipants . size !== participantIds . length ) {
return { error : "Each participant can only be selected once" } ;
}
try {
2026-03-15 21:52:47 -07:00
await generateBracketFromTemplate ( params . eventId , templateId , participantIds , regionOverride ) ;
2025-11-04 22:09:44 -08:00
2026-06-28 19:36:33 +00:00
// PHASE 5.3: Mark participants NOT in the bracket as eliminated (and announce).
2025-11-21 22:05:50 -08:00
const event = await getScoringEventById ( params . eventId ) ;
if ( event ) {
const allParticipants = await findParticipantsBySportsSeasonId ( params . id ) ;
const participantsInBracket = new Set ( participantIds ) ;
2026-06-28 19:36:33 +00:00
const toEliminate = allParticipants
. filter ( ( p ) = > ! participantsInBracket . has ( p . id ) )
. map ( ( p ) = > p . id ) ;
const eliminatedCount = await markEliminatedAndAnnounce ( event , toEliminate ) ;
logger . log ( ` [BracketGeneration] Marked ${ eliminatedCount } participants as eliminated ` ) ;
2025-11-21 22:05:50 -08:00
}
2026-03-15 21:52:47 -07:00
// Update the event to store the template ID, scoring start round, and region config
2025-11-04 22:09:44 -08:00
await updateScoringEvent ( params . eventId , {
bracketTemplateId : templateId ,
scoringStartsAtRound : template.scoringStartsAtRound ,
2026-03-15 21:52:47 -07:00
bracketRegionConfig : regionOverride ,
2025-11-04 22:09:44 -08:00
} ) ;
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
return { success : "Bracket generated successfully" } ;
} catch ( error ) {
2026-03-21 13:41:39 -07:00
logger . error ( "Error generating bracket:" , error ) ;
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
return {
error :
error instanceof Error
? error . message
: "Failed to generate bracket" ,
} ;
}
}
if ( intent === "set-winner" ) {
const matchId = formData . get ( "matchId" ) ;
const winnerId = formData . get ( "winnerId" ) ;
if ( typeof matchId !== "string" || ! matchId ) {
return { error : "Match ID is required" } ;
}
if ( typeof winnerId !== "string" || ! winnerId ) {
return { error : "Winner ID is required" } ;
}
try {
const match = await findPlayoffMatchById ( matchId ) ;
if ( ! match ) {
return { error : "Match not found" } ;
}
2025-11-04 22:09:44 -08:00
// Get the event to determine the template
const event = await getScoringEventById ( match . scoringEventId ) ;
if ( ! event ) {
return { error : "Event not found" } ;
}
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
// Determine loser
const loserId =
match . participant1Id === winnerId
? match . participant2Id
: match . participant1Id ;
if ( ! loserId ) {
return { error : "Could not determine loser" } ;
}
2026-07-04 18:19:52 +00:00
// A knockout is "newly decided" only when the match wasn't already complete
// (mirrors populateBracketFromDraw's first-completion rule) — a re-score/
// correction of an already-finished match must not re-announce the exit.
const setWinnerNewlyEliminated = new Set (
newlyDecidedLosers ( [ { wasComplete : match.isComplete , loserId } ] )
) ;
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
// Set the winner
await setMatchWinner ( matchId , winnerId , loserId ) ;
2025-11-04 22:09:44 -08:00
// Try to advance the winner to the next round using template
2026-04-15 11:27:00 -07:00
const setWinnerTemplate = event . bracketTemplateId ? getBracketTemplate ( event . bracketTemplateId ) : null ;
if ( setWinnerTemplate ) {
try {
await advanceWinnerTemplate ( matchId , winnerId , setWinnerTemplate ) ;
} catch ( error ) {
// Only ignore "already filled" errors, re-throw unexpected errors
if (
error instanceof Error &&
error . message . includes ( "already filled" )
) {
logger . warn ( "Match already filled, skipping advancement" ) ;
} else {
throw error ; // Re-throw unexpected errors
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
}
}
}
2026-03-17 12:34:10 -07:00
const db = database ( ) ;
2026-06-19 01:25:17 +00:00
if ( event . isQualifyingEvent ) {
// Qualifying major (e.g. CS2): bracket results award QUALIFYING POINTS, not
// fantasy points. matchIds scopes the Discord notification to just this match.
2026-07-04 18:19:52 +00:00
await scoreQualifyingBracket (
event ,
db ,
{
eventId : event.id ,
eventName : event.name ? ? undefined ,
matchIds : [ matchId ] ,
} ,
setWinnerNewlyEliminated
) ;
2026-06-19 01:25:17 +00:00
} else {
// Immediately score this match: loser gets their final placement,
// winner gets provisional floor points (isPartialScore=true).
// Prefer the template-defined isScoring over the DB field: the DB column
// defaults to true, so legacy play-in rows may be incorrectly marked as scoring.
const setWinnerRoundIsScoring = setWinnerTemplate ? . rounds . find ( ( r ) = > r . name === match . round ) ? . isScoring ;
await processMatchResult ( {
round : match.round ,
winnerId ,
loserId ,
isScoring : setWinnerRoundIsScoring !== undefined ? setWinnerRoundIsScoring : ( match . isScoring ? ? true ) ,
sportsSeasonId : event.sportsSeasonId ,
bracketTemplateId : event.bracketTemplateId ,
eventId : event.id ,
eventName : event.name ? ? undefined ,
matchId ,
loserAdvances : doesLoserAdvance ( match . round , match . matchNumber , event . bracketTemplateId ? ? "" ) ,
} ) ;
await autoCompleteRoundIfDone ( event . id , match . round , event . sportsSeasonId , db ) ;
}
2026-03-17 12:34:10 -07:00
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
return { success : "Winner set successfully" } ;
} catch ( error ) {
2026-03-21 13:41:39 -07:00
logger . error ( "Error setting winner:" , error ) ;
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
return {
error :
error instanceof Error ? error . message : "Failed to set winner" ,
} ;
}
}
2025-11-04 22:09:44 -08:00
if ( intent === "set-round-winners" ) {
const round = formData . get ( "round" ) ;
if ( typeof round !== "string" || ! round ) {
return { error : "Round is required" } ;
}
try {
// Get all winner assignments from form data
const winnerAssignments : Array < { matchId : string ; winnerId : string } > = [ ] ;
for ( const [ key , value ] of formData . entries ( ) ) {
if ( key . startsWith ( "winner-" ) && typeof value === "string" ) {
const matchId = key . replace ( "winner-" , "" ) ;
winnerAssignments . push ( { matchId , winnerId : value } ) ;
}
}
if ( winnerAssignments . length === 0 ) {
return { error : "No winners selected" } ;
}
// Get the event to determine the template
const event = await getScoringEventById ( params . eventId ) ;
if ( ! event ) {
return { error : "Event not found" } ;
}
const template = event . bracketTemplateId ? getBracketTemplate ( event . bracketTemplateId ) : null ;
2026-04-15 11:27:00 -07:00
// Prefer template-defined isScoring over the DB field (DB defaults to true,
// so legacy play-in rows may be incorrectly marked as scoring).
const roundIsScoring = new Map < string , boolean > (
template ? . rounds . map ( ( r ) = > [ r . name , r . isScoring ] ) ? ? [ ]
) ;
2025-11-04 22:09:44 -08:00
// Process each winner assignment
let successCount = 0 ;
const errors : string [ ] = [ ] ;
2026-03-17 12:34:10 -07:00
const processedMatchIds : string [ ] = [ ] ;
2026-07-04 18:19:52 +00:00
// Per-match completion + loser, collected so newlyDecidedLosers() can pick out
// the batch's first-time knockouts to fan out to mirror windows (a non-scoring-
// round exit earns no QP and is otherwise invisible to the mirror).
const decidedEntries : Array < { wasComplete : boolean ; loserId : string | null } > = [ ] ;
2025-11-04 22:09:44 -08:00
for ( const { matchId , winnerId } of winnerAssignments ) {
try {
const match = await findPlayoffMatchById ( matchId ) ;
if ( ! match ) {
errors . push ( ` Match ${ matchId } not found ` ) ;
continue ;
}
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
// Guard: ensure the match actually belongs to the submitted round.
if ( match . round !== round ) {
errors . push ( ` Match ${ match . matchNumber } is in round " ${ match . round } ", not " ${ round } " ` ) ;
continue ;
}
2025-11-04 22:09:44 -08:00
// Determine loser
const loserId =
match . participant1Id === winnerId
? match . participant2Id
: match . participant1Id ;
if ( ! loserId ) {
errors . push ( ` Could not determine loser for match ${ match . matchNumber } ` ) ;
continue ;
}
// Set the winner
await setMatchWinner ( matchId , winnerId , loserId ) ;
// Try to advance the winner to the next round using template
if ( template ) {
try {
await advanceWinnerTemplate ( matchId , winnerId , template ) ;
} catch ( error ) {
// Only ignore "already filled" errors
if (
error instanceof Error &&
error . message . includes ( "already filled" )
) {
2026-03-21 13:41:39 -07:00
logger . warn ( "Match already filled, skipping advancement" ) ;
2025-11-04 22:09:44 -08:00
} else {
throw error ;
}
}
}
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
// Score this match without triggering standings/probability recalc yet —
// we batch those side effects into a single call after the loop.
2026-06-19 01:25:17 +00:00
// Qualifying majors derive QP from the whole bracket once after the loop
// (processQualifyingBracketEvent), so skip the per-match fantasy scoring here.
if ( ! event . isQualifyingEvent ) {
await processMatchResult ( {
round : match.round ,
winnerId ,
loserId ,
isScoring : roundIsScoring.get ( match . round ) ? ? ( match . isScoring ? ? true ) ,
sportsSeasonId : event.sportsSeasonId ,
bracketTemplateId : event.bracketTemplateId ,
eventId : event.id ,
eventName : event.name ? ? undefined ,
matchId ,
skipSideEffects : true ,
loserAdvances : doesLoserAdvance ( match . round , match . matchNumber , event . bracketTemplateId ? ? "" ) ,
} ) ;
}
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
2025-11-04 22:09:44 -08:00
successCount ++ ;
2026-03-17 12:34:10 -07:00
processedMatchIds . push ( matchId ) ;
2026-07-04 18:19:52 +00:00
// Only after the match fully succeeded: record its prior completion so
// newlyDecidedLosers() announces this loser only if it's a first-time exit
// (and never for a match whose write failed above).
decidedEntries . push ( { wasComplete : match.isComplete , loserId } ) ;
2025-11-04 22:09:44 -08:00
} catch ( error ) {
2026-03-21 13:41:39 -07:00
logger . error ( ` Error setting winner for match ${ matchId } : ` , error ) ;
2025-11-04 22:09:44 -08:00
errors . push (
` Match ${ matchId } : ${ error instanceof Error ? error . message : "Unknown error" } `
) ;
}
}
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
// Run side effects once for the whole batch (avoids N redundant recalcs).
2026-03-17 12:34:10 -07:00
// Pass matchIds so Discord only shows the matches from this submission, not all
// previously completed matches in the event.
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
if ( successCount > 0 ) {
const db = database ( ) ;
2026-07-09 10:04:22 -07:00
// Qualifying majors: derive QP from the full bracket once for the batch, and
// announce the QP change to this window's leagues. processQualifyingEvent (not
// processQualifyingBracketEvent) is what sends the QP Discord notification; the
// fan-out below skips this window (skipEventId) so mirrors don't double-post.
2026-06-19 01:25:17 +00:00
if ( event . isQualifyingEvent ) {
2026-07-09 10:04:22 -07:00
await processQualifyingEvent ( event . id , db , {
newlyEliminatedParticipantIds : new Set ( newlyDecidedLosers ( decidedEntries ) ) ,
} ) ;
2026-06-19 01:25:17 +00:00
}
2026-05-27 04:28:10 +00:00
// Update probabilities first so recalculateAffectedLeagues reads fresh EVs
// when computing projected points.
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
try {
await updateProbabilitiesAfterResult ( event . sportsSeasonId , true ) ;
} catch ( error ) {
2026-03-21 13:41:39 -07:00
logger . error ( ` Error updating probabilities after batch round winners: ` , error ) ;
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
}
2026-05-27 04:28:10 +00:00
await recalculateAffectedLeagues ( event . sportsSeasonId , db , { eventId : event.id , eventName : event.name ? ? undefined , matchIds : processedMatchIds } ) ;
2026-06-19 01:25:17 +00:00
// autoCompleteRoundIfDone runs processPlayoffEvent (fantasy path); skip for
// qualifying majors, which are scored via processQualifyingBracketEvent above.
if ( ! event . isQualifyingEvent ) {
await autoCompleteRoundIfDone ( event . id , round , event . sportsSeasonId , db ) ;
Unify majors: score once, fan out across windows + tennis bracket EV
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
} else {
2026-07-04 18:19:52 +00:00
// Shared major primary window: propagate this round to siblings, carrying
// the batch's newly-decided knockouts so mirrors announce them too.
await fanOutMajorIfPrimary ( event , {
markComplete : false ,
newlyEliminatedParticipantIds : new Set ( newlyDecidedLosers ( decidedEntries ) ) ,
} ) ;
2026-06-19 01:25:17 +00:00
}
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00
}
2025-11-04 22:09:44 -08:00
if ( errors . length > 0 && successCount === 0 ) {
return { error : ` Failed to set winners: ${ errors . join ( ", " ) } ` } ;
}
if ( errors . length > 0 ) {
return {
success : ` Set ${ successCount } winner(s) successfully ` ,
error : ` Some errors occurred: ${ errors . join ( ", " ) } ` ,
} ;
}
return { success : ` Successfully set ${ successCount } winner(s) for ${ round } ` } ;
} catch ( error ) {
2026-03-21 13:41:39 -07:00
logger . error ( "Error setting round winners:" , error ) ;
2025-11-04 22:09:44 -08:00
return {
error :
error instanceof Error ? error . message : "Failed to set winners" ,
} ;
}
}
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
if ( intent === "complete-round" ) {
const round = formData . get ( "round" ) ;
2025-11-04 22:09:44 -08:00
if ( typeof round !== "string" || ! round ) {
return { error : "Round is required" } ;
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
}
try {
// Get the event and update playoffRound to the completed round
const event = await getScoringEventById ( params . eventId ) ;
if ( ! event ) {
return { error : "Event not found" } ;
}
// Verify all matches in this round are complete
const matches = await findPlayoffMatchesByEventId ( params . eventId ) ;
2025-11-04 22:09:44 -08:00
// Validate that the round exists in this event's matches
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
const roundMatches = matches . filter ( ( m ) = > m . round === round ) ;
2025-11-04 22:09:44 -08:00
if ( roundMatches . length === 0 ) {
return { error : ` Round " ${ round } " not found in this bracket ` } ;
}
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
const allComplete = roundMatches . every ( ( m ) = > m . isComplete ) ;
if ( ! allComplete ) {
return { error : ` Not all matches in ${ round } are complete ` } ;
}
2026-06-19 01:25:17 +00:00
// Qualifying majors (e.g. CS2): QP is derived directly from the bracket via
// processQualifyingBracketEvent — there are no seasonParticipantResults rows to
// validate against, and final fantasy placements come from finalizeQualifyingPoints.
if ( event . isQualifyingEvent ) {
await scoreQualifyingBracket ( event , database ( ) ) ;
return { success : ` ${ round } completed and qualifying points updated ` } ;
}
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
// Validate round order: ensure previous rounds are complete
const existingResults = await findParticipantResultsBySportsSeasonId (
params . id
) ;
if ( round === "Finals" ) {
// Finals requires Semifinals to be complete (3rd/4th place results exist)
const hasSemifinalResults = existingResults . some (
( r ) = > r . finalPosition === 3 || r . finalPosition === 4
) ;
if ( ! hasSemifinalResults ) {
return {
error :
"Semifinals must be completed before Finals (no 3rd/4th place results found)" ,
} ;
}
} else if ( round === "Semifinals" ) {
// Semifinals requires Quarterfinals to be complete if QF matches exist
const hasQuarterfinals = matches . some ( ( m ) = > m . round === "Quarterfinals" ) ;
if ( hasQuarterfinals ) {
const hasQuarterfinalsResults = existingResults . some (
( r ) = >
r . finalPosition === 5 ||
r . finalPosition === 6 ||
r . finalPosition === 7 ||
r . finalPosition === 8
) ;
if ( ! hasQuarterfinalsResults ) {
return {
error :
"Quarterfinals must be completed before Semifinals (no 5th-8th place results found)" ,
} ;
}
}
}
// Get the sports season to find a fantasy season for scoring rules
const sportsSeason = await findSportsSeasonById ( params . id ) ;
if ( ! sportsSeason ) {
return { error : "Sports season not found" } ;
}
// Process the playoff event to calculate placements
2025-11-03 13:16:37 -08:00
// TODO Phase 2.7: Refactor processPlayoffEvent to use template system
// For now, we still need to set playoffRound for the old scoring logic
// even though we removed it from the create event UI
const db = database ( ) ;
await db
. update ( schema . scoringEvents )
. set ( { playoffRound : round , updatedAt : new Date ( ) } )
. where ( eq ( schema . scoringEvents . id , params . eventId ) ) ;
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
// Get a fantasy season ID for scoring calculation
const seasonSports = await findSeasonSportsBySportsSeasonId ( params . id ) ;
if ( seasonSports . length === 0 ) {
return {
error : "No fantasy seasons found for this sports season" ,
} ;
}
2026-04-05 15:11:43 -07:00
// Process playoff event to update participant results.
// skipRecalculate=true: match winners were already set via set-winner/set-round-winners,
// which triggered recalculateAffectedLeagues and Discord per match. No need to re-fire.
await processPlayoffEvent ( params . eventId , undefined , { skipRecalculate : true } ) ;
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
return {
success : ` ${ round } completed and placements calculated successfully ` ,
} ;
} catch ( error ) {
2026-03-21 13:41:39 -07:00
logger . error ( "Error completing round:" , error ) ;
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
return {
error :
error instanceof Error ? error . message : "Failed to complete round" ,
} ;
}
}
2026-04-15 14:56:33 -07:00
if ( intent === "reprocess-bracket" ) {
Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator (#242)
* 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>
2026-03-29 10:27:47 -07:00
try {
const event = await getScoringEventById ( params . eventId ) ;
if ( ! event ) return { error : "Event not found" } ;
const matches = await findPlayoffMatchesByEventId ( params . eventId ) ;
const completed = matches . filter ( ( m ) = > m . isComplete && m . winnerId && m . loserId ) ;
2026-06-19 01:25:17 +00:00
// Qualifying majors: this is also the cleanup tool for majors that wrongly banked
// fantasy points under the old path. Delete the stale seasonParticipantResults rows
// (erasing those points), then rebuild correct QP from the bracket. Qualifying sports
// have no legitimate per-major fantasy placements — those come from
// finalizeQualifyingPoints across all majors.
if ( event . isQualifyingEvent ) {
const db = database ( ) ;
await deleteParticipantResultsBySportsSeasonId ( event . sportsSeasonId , db ) ;
await processQualifyingBracketEvent ( params . eventId , db ) ;
// If the season's QP was already finalized, the delete above wiped the final
// placements — recompute them from QP totals so standings aren't left blank.
const sportsSeason = await findSportsSeasonById ( params . id ) ;
if ( sportsSeason ? . qualifyingPointsFinalized ) {
await finalizeQualifyingPoints ( event . sportsSeasonId , db ) ; // recalcs leagues itself
} else {
// skipDiscord: reprocess is a data-correction tool, not a result announcement.
await recalculateAffectedLeagues ( event . sportsSeasonId , db , { skipDiscord : true } ) ;
}
Fix reprocess bracket to score mirror windows like the primary
Reprocessing a tennis (qualifying) major recomputed the primary window's
QP correctly (R16 players = 1.5, the average of the 9th–16th values) but
left mirror/sibling windows showing 2 QP. Two defects:
1. The mirror fan-out ran through fanOutMajorIfPrimary, which swallows
every error and returns void, so reprocess reported a green "success"
even when mirrors were never re-scored. The reprocess qualifying path
now calls syncMajorFromPrimaryEvent directly and folds the SyncReport
(windows synced / failed) into the response, surfacing failures instead
of hiding them.
2. Mirror windows split QP by counting canonical tournament_results rows
at each placement, which only equals the round's structural tie span
when placements are final. Mid-tournament, players floored at a tier
make the row-count diverge from the tier size, so mirrors split
differently than the primary. syncMajorFromPrimaryEvent now derives the
structural span (R16 = 8, QF = 4, SF = 2, Final = 1) from the primary
bracket via deriveBracketQualifyingStates and merges it over the
canonical counts, so every window splits identically to the primary at
every stage. Golf and CS2 Swiss-exit placements keep their canonical
count via the merge.
Adds a fan-out test covering an R16-in-progress bracket where only 4 rows
sit at placement 9: the mirror is scored with the structural span (8), not
the live count (4).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UkZVYgLmquWV2xDn349T2
2026-07-04 01:45:53 +00:00
// Re-propagate corrected QP to sibling/mirror windows (data-correction; not
// final). Call syncMajorFromPrimaryEvent directly rather than the
// swallow-and-log fanOutMajorIfPrimary so the admin actually sees whether the
// mirrors were re-scored — a silent failure here is exactly how mirrors got
// left showing stale QP behind a green "success".
const baseMessage = ` Reprocessed qualifying bracket: cleared stale fantasy points and recomputed QP ( ${ completed . length } completed match(es)). ` ;
if ( event . isPrimary && event . tournamentId ) {
try {
const report = await syncMajorFromPrimaryEvent ( event . id , { markComplete : false } ) ;
2026-07-04 02:06:14 +00:00
// Surface a partial fan-out as an error so it renders as a warning
// banner, not a green success the admin might skim past while some
// mirror windows are left stale.
Fix reprocess bracket to score mirror windows like the primary
Reprocessing a tennis (qualifying) major recomputed the primary window's
QP correctly (R16 players = 1.5, the average of the 9th–16th values) but
left mirror/sibling windows showing 2 QP. Two defects:
1. The mirror fan-out ran through fanOutMajorIfPrimary, which swallows
every error and returns void, so reprocess reported a green "success"
even when mirrors were never re-scored. The reprocess qualifying path
now calls syncMajorFromPrimaryEvent directly and folds the SyncReport
(windows synced / failed) into the response, surfacing failures instead
of hiding them.
2. Mirror windows split QP by counting canonical tournament_results rows
at each placement, which only equals the round's structural tie span
when placements are final. Mid-tournament, players floored at a tier
make the row-count diverge from the tier size, so mirrors split
differently than the primary. syncMajorFromPrimaryEvent now derives the
structural span (R16 = 8, QF = 4, SF = 2, Final = 1) from the primary
bracket via deriveBracketQualifyingStates and merges it over the
canonical counts, so every window splits identically to the primary at
every stage. Golf and CS2 Swiss-exit placements keep their canonical
count via the merge.
Adds a fan-out test covering an R16-in-progress bracket where only 4 rows
sit at placement 9: the mirror is scored with the structural span (8), not
the live count (4).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UkZVYgLmquWV2xDn349T2
2026-07-04 01:45:53 +00:00
if ( report . windowsFailed > 0 ) {
const reasons = report . failures . map ( ( f ) = > f . error ) . join ( "; " ) ;
2026-07-04 02:06:14 +00:00
return {
error : ` ${ baseMessage } Synced ${ report . windowsSynced } mirror window(s), but ${ report . windowsFailed } failed (those windows may be stale): ${ reasons } ` ,
} ;
Fix reprocess bracket to score mirror windows like the primary
Reprocessing a tennis (qualifying) major recomputed the primary window's
QP correctly (R16 players = 1.5, the average of the 9th–16th values) but
left mirror/sibling windows showing 2 QP. Two defects:
1. The mirror fan-out ran through fanOutMajorIfPrimary, which swallows
every error and returns void, so reprocess reported a green "success"
even when mirrors were never re-scored. The reprocess qualifying path
now calls syncMajorFromPrimaryEvent directly and folds the SyncReport
(windows synced / failed) into the response, surfacing failures instead
of hiding them.
2. Mirror windows split QP by counting canonical tournament_results rows
at each placement, which only equals the round's structural tie span
when placements are final. Mid-tournament, players floored at a tier
make the row-count diverge from the tier size, so mirrors split
differently than the primary. syncMajorFromPrimaryEvent now derives the
structural span (R16 = 8, QF = 4, SF = 2, Final = 1) from the primary
bracket via deriveBracketQualifyingStates and merges it over the
canonical counts, so every window splits identically to the primary at
every stage. Golf and CS2 Swiss-exit placements keep their canonical
count via the merge.
Adds a fan-out test covering an R16-in-progress bracket where only 4 rows
sit at placement 9: the mirror is scored with the structural span (8), not
the live count (4).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UkZVYgLmquWV2xDn349T2
2026-07-04 01:45:53 +00:00
}
2026-07-04 02:06:14 +00:00
return { success : ` ${ baseMessage } Synced ${ report . windowsSynced } mirror window(s). ` } ;
Fix reprocess bracket to score mirror windows like the primary
Reprocessing a tennis (qualifying) major recomputed the primary window's
QP correctly (R16 players = 1.5, the average of the 9th–16th values) but
left mirror/sibling windows showing 2 QP. Two defects:
1. The mirror fan-out ran through fanOutMajorIfPrimary, which swallows
every error and returns void, so reprocess reported a green "success"
even when mirrors were never re-scored. The reprocess qualifying path
now calls syncMajorFromPrimaryEvent directly and folds the SyncReport
(windows synced / failed) into the response, surfacing failures instead
of hiding them.
2. Mirror windows split QP by counting canonical tournament_results rows
at each placement, which only equals the round's structural tie span
when placements are final. Mid-tournament, players floored at a tier
make the row-count diverge from the tier size, so mirrors split
differently than the primary. syncMajorFromPrimaryEvent now derives the
structural span (R16 = 8, QF = 4, SF = 2, Final = 1) from the primary
bracket via deriveBracketQualifyingStates and merges it over the
canonical counts, so every window splits identically to the primary at
every stage. Golf and CS2 Swiss-exit placements keep their canonical
count via the merge.
Adds a fan-out test covering an R16-in-progress bracket where only 4 rows
sit at placement 9: the mirror is scored with the structural span (8), not
the live count (4).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UkZVYgLmquWV2xDn349T2
2026-07-04 01:45:53 +00:00
} catch ( error ) {
return {
error : ` Primary window recomputed, but mirror fan-out failed (mirror windows may be stale): ${ error instanceof Error ? error.message : String ( error ) } ` ,
} ;
}
}
return { success : ` ${ baseMessage } No mirror windows to sync. ` } ;
2026-06-19 01:25:17 +00:00
}
Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator (#242)
* 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>
2026-03-29 10:27:47 -07:00
if ( completed . length === 0 ) {
return { error : "No completed matches to reprocess" } ;
}
// Delete ALL results for this sports season and rebuild from scratch.
// Only deleting partial rows leaves stale finalized rows that block
// the "never un-finalize" guard in upsertParticipantResult.
const db = database ( ) ;
2026-06-19 01:25:17 +00:00
await deleteParticipantResultsBySportsSeasonId ( event . sportsSeasonId , db ) ;
Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator (#242)
* 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>
2026-03-29 10:27:47 -07:00
// Replay each completed match in bracket order (earlier rounds first).
const template = event . bracketTemplateId ? getBracketTemplate ( event . bracketTemplateId ) : null ;
const roundOrder = template ? template . rounds . map ( ( r ) = > r . name ) : [ ] ;
2026-04-15 11:27:00 -07:00
// Build a round→isScoring map from the template so we use the template as the
// source of truth rather than the DB column (which defaults to true and may be
// wrong for brackets created before the isScoring column was added).
const templateRoundIsScoring = new Map < string , boolean > (
template ? . rounds . map ( ( r ) = > [ r . name , r . isScoring ] ) ? ? [ ]
) ;
Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator (#242)
* 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>
2026-03-29 10:27:47 -07:00
const sortedMatches = completed . toSorted ( ( a , b ) = > {
const ai = roundOrder . indexOf ( a . round ) ;
const bi = roundOrder . indexOf ( b . round ) ;
return ( ai === - 1 ? 999 : ai ) - ( bi === - 1 ? 999 : bi ) ;
} ) ;
for ( const match of sortedMatches ) {
New design (#309)
* Redesign home page with new layout and component system
- Two-column layout (My Leagues 2/3, Upcoming Events 1/3) with mobile stack
- LeagueRow: square avatar, gradient draft highlight, rank/points display, progress bar
- MyLeaguesCard, CreateLeagueCard with shared SectionCardHeader
- UpcomingEventsCard: vertical timeline with grouped multi-league events
- Shared gradient system: BracktGradients SVG defs, GradientIcon wrapper, brand.ts constants
- Button default variant updated to green→cyan gradient
- Navbar: plain nav links with gradient hover, support/admin icon buttons
- Accessibility fixes: semantic h2 headings, aria-label on LeagueAvatar and nav elements
- Storybook stories for all new components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Responsive league row layout and mobile polish
- League rows stack avatar+name on top, stats full-width below on mobile
- Stats spread to right side on sm+ screens with border separator on mobile
- Tighter padding on mobile (px-3/py-3), full padding on sm+
- Card headers and content use px-3 sm:px-6 to reduce mobile gutters
- Two-column home layout deferred to lg breakpoint (tablet gets stacked)
- Active leagues sorted by completion percentage descending
- Default rank 1 / 0 points for active leagues with no scoring events yet
- Fix ordinal bug for 11th/12th/13th; add aria-labels to rank change indicators
- Remove dead StatDivider className prop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Improve claude file.
* Add StandingsPreview card component with podium row styling
- New StandingsPreview component with gold/silver/bronze row tints for
top 3, team avatar, and LeagueRow-style stat columns (Ranking + Points)
with rank and 7-day point change indicators
- Fix GradientIcon in Storybook by adding BracktGradients decorator to
preview.tsx (renamed from .ts to support JSX)
- Fix degenerate SVG gradient on horizontal strokes by switching
BracktGradients to gradientUnits="userSpaceOnUse" with Lucide-space
coordinates (0→24)
- Revert erroneous fill: url(#gradient) from GradientIcon; stroke-only
fix was sufficient once gradientUnits was corrected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update components on league homepage.
* Finish up league page styling.
* Work on standings page.
* Add story for RecentScoresCard
* Update Point Progression Chart.
* Sort point progression legend by ranking and add team links to standings rows
* Fix standings discrepancy on change.
* Create draft cell component.
* Update draft board page
* Draft room improvements.
* Update some draft room styling.
* Fix context menu missing.
* Move tab navigation and autodraft to header row, narrow sidebar
* Virtualize available participants list, memoize draft room props
Adds @tanstack/react-virtual to replace separate mobile/desktop lists
with a single unified virtual scroll loop. Also memoizes miniDraftGrid
and availableParticipantsSectionProps, and switches pick lookup from
Array.find to a Map for O(1) access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update draft room UI.
* More draft room fixes.
* Draft room tweaks.
* Fix Rosters page.
* Queue Section fixes.
* Mobile Draft fixes.
* Fix draft board page.
* Create bracket look.
* Bracket work.
* Finish bracket page.
* Homepage initial styling
* homepage copy
* Add privacy policy. Fixes #88.
* how to play copy
* rules copy
* Fix brackets on homepage.
* Add footer to website.
* Glow on dots.
* Landing page copy.
* Fix sidebar.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 13:14:55 -07:00
if ( ! match . winnerId || ! match . loserId ) continue ;
2026-04-15 11:27:00 -07:00
const isScoring = templateRoundIsScoring . get ( match . round ) ? ? ( match . isScoring ? ? true ) ;
Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator (#242)
* 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>
2026-03-29 10:27:47 -07:00
await processMatchResult (
{
round : match.round ,
New design (#309)
* Redesign home page with new layout and component system
- Two-column layout (My Leagues 2/3, Upcoming Events 1/3) with mobile stack
- LeagueRow: square avatar, gradient draft highlight, rank/points display, progress bar
- MyLeaguesCard, CreateLeagueCard with shared SectionCardHeader
- UpcomingEventsCard: vertical timeline with grouped multi-league events
- Shared gradient system: BracktGradients SVG defs, GradientIcon wrapper, brand.ts constants
- Button default variant updated to green→cyan gradient
- Navbar: plain nav links with gradient hover, support/admin icon buttons
- Accessibility fixes: semantic h2 headings, aria-label on LeagueAvatar and nav elements
- Storybook stories for all new components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Responsive league row layout and mobile polish
- League rows stack avatar+name on top, stats full-width below on mobile
- Stats spread to right side on sm+ screens with border separator on mobile
- Tighter padding on mobile (px-3/py-3), full padding on sm+
- Card headers and content use px-3 sm:px-6 to reduce mobile gutters
- Two-column home layout deferred to lg breakpoint (tablet gets stacked)
- Active leagues sorted by completion percentage descending
- Default rank 1 / 0 points for active leagues with no scoring events yet
- Fix ordinal bug for 11th/12th/13th; add aria-labels to rank change indicators
- Remove dead StatDivider className prop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Improve claude file.
* Add StandingsPreview card component with podium row styling
- New StandingsPreview component with gold/silver/bronze row tints for
top 3, team avatar, and LeagueRow-style stat columns (Ranking + Points)
with rank and 7-day point change indicators
- Fix GradientIcon in Storybook by adding BracktGradients decorator to
preview.tsx (renamed from .ts to support JSX)
- Fix degenerate SVG gradient on horizontal strokes by switching
BracktGradients to gradientUnits="userSpaceOnUse" with Lucide-space
coordinates (0→24)
- Revert erroneous fill: url(#gradient) from GradientIcon; stroke-only
fix was sufficient once gradientUnits was corrected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update components on league homepage.
* Finish up league page styling.
* Work on standings page.
* Add story for RecentScoresCard
* Update Point Progression Chart.
* Sort point progression legend by ranking and add team links to standings rows
* Fix standings discrepancy on change.
* Create draft cell component.
* Update draft board page
* Draft room improvements.
* Update some draft room styling.
* Fix context menu missing.
* Move tab navigation and autodraft to header row, narrow sidebar
* Virtualize available participants list, memoize draft room props
Adds @tanstack/react-virtual to replace separate mobile/desktop lists
with a single unified virtual scroll loop. Also memoizes miniDraftGrid
and availableParticipantsSectionProps, and switches pick lookup from
Array.find to a Map for O(1) access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update draft room UI.
* More draft room fixes.
* Draft room tweaks.
* Fix Rosters page.
* Queue Section fixes.
* Mobile Draft fixes.
* Fix draft board page.
* Create bracket look.
* Bracket work.
* Finish bracket page.
* Homepage initial styling
* homepage copy
* Add privacy policy. Fixes #88.
* how to play copy
* rules copy
* Fix brackets on homepage.
* Add footer to website.
* Glow on dots.
* Landing page copy.
* Fix sidebar.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 13:14:55 -07:00
winnerId : match.winnerId ,
loserId : match.loserId ,
2026-04-15 11:27:00 -07:00
isScoring ,
Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator (#242)
* 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>
2026-03-29 10:27:47 -07:00
sportsSeasonId : event.sportsSeasonId ,
bracketTemplateId : event.bracketTemplateId ,
New design (#309)
* Redesign home page with new layout and component system
- Two-column layout (My Leagues 2/3, Upcoming Events 1/3) with mobile stack
- LeagueRow: square avatar, gradient draft highlight, rank/points display, progress bar
- MyLeaguesCard, CreateLeagueCard with shared SectionCardHeader
- UpcomingEventsCard: vertical timeline with grouped multi-league events
- Shared gradient system: BracktGradients SVG defs, GradientIcon wrapper, brand.ts constants
- Button default variant updated to green→cyan gradient
- Navbar: plain nav links with gradient hover, support/admin icon buttons
- Accessibility fixes: semantic h2 headings, aria-label on LeagueAvatar and nav elements
- Storybook stories for all new components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Responsive league row layout and mobile polish
- League rows stack avatar+name on top, stats full-width below on mobile
- Stats spread to right side on sm+ screens with border separator on mobile
- Tighter padding on mobile (px-3/py-3), full padding on sm+
- Card headers and content use px-3 sm:px-6 to reduce mobile gutters
- Two-column home layout deferred to lg breakpoint (tablet gets stacked)
- Active leagues sorted by completion percentage descending
- Default rank 1 / 0 points for active leagues with no scoring events yet
- Fix ordinal bug for 11th/12th/13th; add aria-labels to rank change indicators
- Remove dead StatDivider className prop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Improve claude file.
* Add StandingsPreview card component with podium row styling
- New StandingsPreview component with gold/silver/bronze row tints for
top 3, team avatar, and LeagueRow-style stat columns (Ranking + Points)
with rank and 7-day point change indicators
- Fix GradientIcon in Storybook by adding BracktGradients decorator to
preview.tsx (renamed from .ts to support JSX)
- Fix degenerate SVG gradient on horizontal strokes by switching
BracktGradients to gradientUnits="userSpaceOnUse" with Lucide-space
coordinates (0→24)
- Revert erroneous fill: url(#gradient) from GradientIcon; stroke-only
fix was sufficient once gradientUnits was corrected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update components on league homepage.
* Finish up league page styling.
* Work on standings page.
* Add story for RecentScoresCard
* Update Point Progression Chart.
* Sort point progression legend by ranking and add team links to standings rows
* Fix standings discrepancy on change.
* Create draft cell component.
* Update draft board page
* Draft room improvements.
* Update some draft room styling.
* Fix context menu missing.
* Move tab navigation and autodraft to header row, narrow sidebar
* Virtualize available participants list, memoize draft room props
Adds @tanstack/react-virtual to replace separate mobile/desktop lists
with a single unified virtual scroll loop. Also memoizes miniDraftGrid
and availableParticipantsSectionProps, and switches pick lookup from
Array.find to a Map for O(1) access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update draft room UI.
* More draft room fixes.
* Draft room tweaks.
* Fix Rosters page.
* Queue Section fixes.
* Mobile Draft fixes.
* Fix draft board page.
* Create bracket look.
* Bracket work.
* Finish bracket page.
* Homepage initial styling
* homepage copy
* Add privacy policy. Fixes #88.
* how to play copy
* rules copy
* Fix brackets on homepage.
* Add footer to website.
* Glow on dots.
* Landing page copy.
* Fix sidebar.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 13:14:55 -07:00
eventId : event.id ,
eventName : event.name ? ? undefined ,
matchId : match.id ,
Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator (#242)
* 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>
2026-03-29 10:27:47 -07:00
skipSideEffects : true ,
2026-04-15 09:40:53 -07:00
loserAdvances : doesLoserAdvance ( match . round , match . matchNumber , event . bracketTemplateId ? ? "" ) ,
Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator (#242)
* 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>
2026-03-29 10:27:47 -07:00
}
) ;
}
2026-04-15 14:56:33 -07:00
// Mark participants NOT in any bracket match as eliminated (finalPosition = 0).
// This covers teams that didn't make the playoffs/play-in tournament.
2025-11-21 22:05:50 -08:00
const allParticipants = await findParticipantsBySportsSeasonId ( params . id ) ;
2026-04-15 14:56:33 -07:00
const bracketParticipantIds = new Set < string > ( ) ;
2025-11-21 22:05:50 -08:00
for ( const match of matches ) {
2026-04-15 14:56:33 -07:00
if ( match . participant1Id ) bracketParticipantIds . add ( match . participant1Id ) ;
if ( match . participant2Id ) bracketParticipantIds . add ( match . participant2Id ) ;
2025-11-21 22:05:50 -08:00
}
let eliminatedCount = 0 ;
for ( const participant of allParticipants ) {
2026-04-15 14:56:33 -07:00
if ( ! bracketParticipantIds . has ( participant . id ) ) {
2025-11-21 22:05:50 -08:00
await setParticipantResult (
participant . id ,
event . sportsSeasonId ,
2026-04-15 14:56:33 -07:00
0
2025-11-21 22:05:50 -08:00
) ;
eliminatedCount ++ ;
}
}
2026-04-15 14:56:33 -07:00
// skipDiscord: reprocess-bracket is a data-correction tool, not a result announcement.
await recalculateAffectedLeagues ( event . sportsSeasonId , undefined , { skipDiscord : true } ) ;
2025-11-21 22:05:50 -08:00
2026-04-15 14:56:33 -07:00
return { success : ` Reprocessed bracket: ${ sortedMatches . length } match(es) replayed, ${ eliminatedCount } non-bracket participant(s) eliminated ` } ;
2025-11-21 22:05:50 -08:00
} catch ( error ) {
2026-04-15 14:56:33 -07:00
logger . error ( "Error reprocessing bracket:" , error ) ;
2025-11-21 22:05:50 -08:00
return {
2026-04-15 14:56:33 -07:00
error : error instanceof Error ? error . message : "Failed to reprocess bracket" ,
2025-11-21 22:05:50 -08:00
} ;
}
}
2025-11-14 20:01:21 -08:00
if ( intent === "finalize-bracket" ) {
try {
// Get the event
const event = await getScoringEventById ( params . eventId ) ;
if ( ! event ) {
return { error : "Event not found" } ;
}
// Get all matches
const matches = await findPlayoffMatchesByEventId ( params . eventId ) ;
if ( matches . length === 0 ) {
return { error : "No bracket exists for this event" } ;
}
2026-04-05 15:11:43 -07:00
// Verify the event has a valid bracket template configured
2025-11-14 20:01:21 -08:00
const template = event . bracketTemplateId ? getBracketTemplate ( event . bracketTemplateId ) : null ;
if ( ! template ) {
return { error : "Bracket template not found" } ;
}
// Verify ALL matches are complete
const incompleteMatches = matches . filter ( ( m ) = > ! m . isComplete ) ;
if ( incompleteMatches . length > 0 ) {
return {
error : ` Cannot finalize: ${ incompleteMatches . length } match(es) still incomplete ` ,
} ;
}
2026-06-19 01:25:17 +00:00
// Qualifying majors (e.g. CS2): lock in final bracket placements/QP, then run the
// standard qualifying finalizer for THIS event. Do not mark the whole sports season
// completed or recalc fantasy standings — a season spans multiple majors and final
// fantasy placements come from finalizeQualifyingPoints across all of them.
if ( event . isQualifyingEvent ) {
const db = database ( ) ;
2026-07-03 21:40:11 +00:00
// processQualifyingEvent derives the bracket QP (via processQualifyingBracketEvent)
// and recalcs participant QP totals. majorsCompleted is derived on read from
// completed qualifying events (see getMajorsCompleted) — marking this event
// complete below is what advances it. Season-wide fantasy finalization stays with
// finalizeQualifyingPoints across all majors.
2026-06-19 01:25:17 +00:00
await processQualifyingEvent ( params . eventId , db ) ;
await db
. update ( schema . scoringEvents )
. set ( { isComplete : true , completedAt : new Date ( ) , updatedAt : new Date ( ) } )
. where ( eq ( schema . scoringEvents . id , params . eventId ) ) ;
await recalculateAffectedLeagues ( event . sportsSeasonId , db , {
eventId : params.eventId ,
eventName : event.name ? ? undefined ,
} ) ;
Unify majors: score once, fan out across windows + tennis bracket EV
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
// Finalize: propagate to siblings AND mark every window complete.
await fanOutMajorIfPrimary ( event , { markComplete : true } ) ;
2026-06-19 01:25:17 +00:00
return { success : "Major bracket finalized — qualifying points awarded." } ;
}
2025-11-14 20:01:21 -08:00
// Get all participants in this sports season
const allParticipants = await findParticipantsBySportsSeasonId ( params . id ) ;
// Get participants in matches
const participantsInMatches = new Set (
matches . flatMap ( ( m ) = > [ m . participant1Id , m . participant2Id ] . filter ( Boolean ) )
) ;
const db = database ( ) ;
// Assign 0 points to participants not in the bracket (Q20)
for ( const participant of allParticipants ) {
if ( ! participantsInMatches . has ( participant . id ) ) {
await setParticipantResult (
participant . id ,
params . id ,
0 // 0 placement = 0 points
) ;
}
}
// Mark event as complete
await db
. update ( schema . scoringEvents )
. set ( { isComplete : true , completedAt : new Date ( ) , updatedAt : new Date ( ) } )
. where ( eq ( schema . scoringEvents . id , params . eventId ) ) ;
2026-05-04 20:31:44 -07:00
await db
. update ( schema . sportsSeasons )
. set ( { status : "completed" , updatedAt : new Date ( ) } )
. where ( eq ( schema . sportsSeasons . id , params . id ) ) ;
2025-11-14 20:01:21 -08:00
// Recalculate standings for all affected fantasy seasons
const seasonSports = await findSeasonSportsBySportsSeasonId ( params . id ) ;
for ( const seasonSport of seasonSports ) {
await recalculateStandings ( seasonSport . seasonId , db ) ;
2026-03-18 22:15:28 -07:00
await createDailySnapshot ( seasonSport . seasonId , db ) ;
2025-11-14 20:01:21 -08:00
}
2026-05-04 20:31:44 -07:00
await maybeResolveCompletedBracktForSportsSeason ( params . id , db ) ;
2025-11-14 20:01:21 -08:00
return {
success : ` Bracket finalized! All placements calculated and standings updated. ` ,
} ;
} catch ( error ) {
2026-03-21 13:41:39 -07:00
logger . error ( "Error finalizing bracket:" , error ) ;
2025-11-14 20:01:21 -08:00
return {
error :
error instanceof Error ? error . message : "Failed to finalize bracket" ,
} ;
}
}
2026-02-14 22:30:12 -08:00
if ( intent === "generate-groups" ) {
const templateId = formData . get ( "templateId" ) ;
if ( typeof templateId !== "string" || ! templateId ) {
return { error : "Template ID is required" } ;
}
const template = getBracketTemplate ( templateId ) ;
if ( ! template || ! template . groupStage ) {
return { error : "Invalid template or template has no group stage" } ;
}
const { groupStage } = template ;
const totalTeams = groupStage . groupCount * groupStage . teamsPerGroup ;
// Collect participant IDs from form
const participantIds : string [ ] = [ ] ;
for ( let i = 0 ; i < totalTeams ; i ++ ) {
const participantId = formData . get ( ` participant ${ i } ` ) ;
if ( typeof participantId !== "string" || ! participantId ) {
return { error : ` Participant ${ i + 1 } is required ` } ;
}
participantIds . push ( participantId ) ;
}
// Check for duplicates
const uniqueParticipants = new Set ( participantIds ) ;
if ( uniqueParticipants . size !== participantIds . length ) {
return { error : "Each participant can only be selected once" } ;
}
try {
// Update the event with template info
await updateScoringEvent ( params . eventId , {
bracketTemplateId : templateId ,
scoringStartsAtRound : template.scoringStartsAtRound ,
} ) ;
// Create tournament groups
const groups = await createGroupsForEvent ( params . eventId , groupStage . groupLabels ) ;
Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator (#242)
* 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>
2026-03-29 10:27:47 -07:00
// Distribute participants into groups (in selection order) and create round-robin matches
2026-02-14 22:30:12 -08:00
for ( let groupIndex = 0 ; groupIndex < groups . length ; groupIndex ++ ) {
const startIdx = groupIndex * groupStage . teamsPerGroup ;
const groupParticipantIds = participantIds . slice (
startIdx ,
startIdx + groupStage . teamsPerGroup
) ;
await addMembersToGroup ( groups [ groupIndex ] . id , groupParticipantIds ) ;
Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator (#242)
* 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>
2026-03-29 10:27:47 -07:00
// Create the 6 round-robin match pairings for this group
if ( groupParticipantIds . length === 4 ) {
const pairings = generateRoundRobinPairings ( groupParticipantIds ) ;
await createGroupStageMatches ( groups [ groupIndex ] . id , pairings ) ;
}
2026-02-14 22:30:12 -08:00
}
// Generate the empty knockout bracket structure
await generateBracketFromTemplate ( params . eventId , templateId ) ;
2026-04-01 00:40:55 -07:00
// Eliminate participants from this sport season who are not in any group
2026-06-28 19:36:33 +00:00
// (and announce to leagues for fantasy events).
const groupsEvent = await getScoringEventById ( params . eventId ) ;
if ( ! groupsEvent ) {
return { error : "Event not found" } ;
2026-04-01 00:40:55 -07:00
}
2026-06-28 19:36:33 +00:00
const allParticipants = await findParticipantsBySportsSeasonId ( params . id ) ;
const toEliminate = allParticipants
. filter ( ( p ) = > ! uniqueParticipants . has ( p . id ) )
. map ( ( p ) = > p . id ) ;
const eliminatedCount = await markEliminatedAndAnnounce ( groupsEvent , toEliminate ) ;
2026-04-01 00:40:55 -07:00
return {
success : ` Groups and knockout bracket structure created successfully ${ eliminatedCount > 0 ? ` ( ${ eliminatedCount } participant(s) not in any group marked as eliminated) ` : "" } ` ,
} ;
2026-02-14 22:30:12 -08:00
} catch ( error ) {
2026-03-21 13:41:39 -07:00
logger . error ( "Error generating groups:" , error ) ;
2026-02-14 22:30:12 -08:00
return {
error : error instanceof Error ? error . message : "Failed to generate groups" ,
} ;
}
}
if ( intent === "toggle-elimination" ) {
const memberId = formData . get ( "memberId" ) ;
if ( typeof memberId !== "string" || ! memberId ) {
return { error : "Member ID is required" } ;
}
try {
const updated = await toggleMemberEliminated ( memberId ) ;
return {
success : updated.eliminated
? "Team marked as eliminated"
: "Team reinstated" ,
} ;
} catch ( error ) {
2026-03-21 13:41:39 -07:00
logger . error ( "Error toggling elimination:" , error ) ;
2026-02-14 22:30:12 -08:00
return {
error : error instanceof Error ? error . message : "Failed to toggle elimination" ,
} ;
}
}
Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator (#242)
* 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>
2026-03-29 10:27:47 -07:00
if ( intent === "update-group-match" ) {
const matchId = formData . get ( "matchId" ) ;
const p1Score = formData . get ( "participant1Score" ) ;
const p2Score = formData . get ( "participant2Score" ) ;
if ( typeof matchId !== "string" || ! matchId ) return { error : "matchId is required" } ;
if ( typeof p1Score !== "string" || typeof p2Score !== "string" ) {
return { error : "Both scores are required" } ;
}
const s1 = parseInt ( p1Score , 10 ) ;
const s2 = parseInt ( p2Score , 10 ) ;
if ( isNaN ( s1 ) || isNaN ( s2 ) || s1 < 0 || s2 < 0 ) {
return { error : "Scores must be non-negative integers" } ;
}
try {
await updateGroupStageMatchResult ( matchId , s1 , s2 ) ;
return { success : "Match result saved" } ;
} catch ( error ) {
logger . error ( "Error updating group match:" , error ) ;
return { error : error instanceof Error ? error . message : "Failed to update match" } ;
}
}
if ( intent === "update-group-match-schedule" ) {
const matchId = formData . get ( "matchId" ) ;
const scheduledAt = formData . get ( "scheduledAt" ) ;
if ( typeof matchId !== "string" || ! matchId ) return { error : "matchId is required" } ;
try {
const dateValue = typeof scheduledAt === "string" && scheduledAt
? new Date ( scheduledAt )
: null ;
await updateGroupStageMatchSchedule ( matchId , dateValue ) ;
return { success : "Match time saved" } ;
} catch ( error ) {
logger . error ( "Error updating group match schedule:" , error ) ;
return { error : error instanceof Error ? error . message : "Failed to update schedule" } ;
}
}
2026-02-14 22:30:12 -08:00
if ( intent === "populate-knockout" ) {
try {
const event = await getScoringEventById ( params . eventId ) ;
if ( ! event ) {
return { error : "Event not found" } ;
}
// Parse match assignments from form
const assignments : Array < {
matchNumber : number ;
slot : "participant1Id" | "participant2Id" ;
participantId : string ;
} > = [ ] ;
for ( const [ key , value ] of formData . entries ( ) ) {
const matchPattern = /^match-(\d+)-(participant1Id|participant2Id)$/ ;
const match = key . match ( matchPattern ) ;
if ( match && typeof value === "string" && value ) {
assignments . push ( {
matchNumber : parseInt ( match [ 1 ] , 10 ) ,
slot : match [ 2 ] as "participant1Id" | "participant2Id" ,
participantId : value ,
} ) ;
}
}
if ( assignments . length !== 32 ) {
return { error : ` Expected 32 assignments but got ${ assignments . length } ` } ;
}
// Validate all assignments are unique participants
const assignedParticipantIds = new Set ( assignments . map ( ( a ) = > a . participantId ) ) ;
if ( assignedParticipantIds . size !== 32 ) {
return { error : "All 32 knockout slots must have unique participants" } ;
}
// Validate all assigned participants are non-eliminated
const advancingIds = new Set ( await getAdvancingParticipantIds ( params . eventId ) ) ;
for ( const participantId of assignedParticipantIds ) {
if ( ! advancingIds . has ( participantId ) ) {
return { error : "All assigned participants must be non-eliminated group members" } ;
}
}
// Assign participants to knockout bracket
await assignParticipantsToKnockout ( params . eventId , assignments ) ;
2026-06-28 19:36:33 +00:00
// Mark eliminated group participants with finalPosition = 0 (and announce
// the group-stage eliminations to leagues for fantasy events).
2026-02-14 22:30:12 -08:00
const eliminatedIds = await getEliminatedParticipantIds ( params . eventId ) ;
2026-06-28 19:36:33 +00:00
await markEliminatedAndAnnounce ( event , eliminatedIds ) ;
2026-02-14 22:30:12 -08:00
2026-03-21 13:41:39 -07:00
logger . log (
2026-02-14 22:30:12 -08:00
` [PopulateKnockout] Assigned 32 participants to knockout, marked ${ eliminatedIds . length } as eliminated `
) ;
return { success : "Knockout bracket populated successfully" } ;
} catch ( error ) {
2026-03-21 13:41:39 -07:00
logger . error ( "Error populating knockout:" , error ) ;
2026-02-14 22:30:12 -08:00
return {
error : error instanceof Error ? error . message : "Failed to populate knockout" ,
} ;
}
}
Add playoff match game scheduling and odds management (#135)
* Add playoff match games and odds storage
Introduces two new tables for bracket matchup detail storage:
- `playoff_match_games`: tracks individual game schedules within a
series matchup (game number, scheduledAt, status, per-game scores,
winner). Supports scheduled/complete/postponed status enum.
- `playoff_match_odds`: stores moneyline odds per participant per
matchup (single upsert record, no isLatest complexity).
Includes:
- Drizzle schema + relations with CASCADE deletes from playoff_matches
- Migration 0040_fat_puma.sql
- playoff-match-game.ts model with pure helpers: computeSeriesScore,
isSeriesComplete, getSeriesLeader — plus full CRUD
- playoff-match-odds.ts model with pure helpers: americanToImpliedProbability,
impliedProbabilityToAmerican, normalizeOdds — plus upsert/read/delete
- findPlayoffMatchesByEventId and findPlayoffMatchById updated to
include games and odds in their query results
- Bracket server route: add-game, update-game, delete-game,
upsert-odds, delete-odds actions
- Bracket admin UI: expandable per-match panel for game schedule
management and moneyline odds entry
- 41 new unit tests (18 game + 23 odds), all 810 tests passing
https://claude.ai/code/session_01Twt3D1bsEK3eXUhMaz6ee7
* Code review fixes: type safety, abstraction, and React correctness
- Derive PlayoffMatchGameStatus from schema enum instead of hardcoding
the string union, eliminating the duplicate source of truth
- updateGame now returns PlayoffMatchGame | undefined to reflect reality
when no row matches the ID
- Remove TOCTOU check-then-act in update-game action: call updateGame
directly and check the return value instead of a pre-flight findGameById
- Add status enum validation before the cast in update-game action
- Move impliedProbability computation inside upsertMatchOdds so callers
only provide moneylineOdds; the model owns the derivation
- Remove unnecessary dynamic import of americanToImpliedProbability in
the upsert-odds action (was already imported from the same module)
- Fix React list reconciliation bug: replace bare <> fragment with
<Fragment key={match.id}> so React can correctly track rows
https://claude.ai/code/session_01Twt3D1bsEK3eXUhMaz6ee7
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 14:17:43 -07:00
// ── Game scheduling ──────────────────────────────────────────────────────
if ( intent === "add-game" ) {
const matchId = formData . get ( "matchId" ) ;
const gameNumber = formData . get ( "gameNumber" ) ;
const scheduledAt = formData . get ( "scheduledAt" ) ;
const notes = formData . get ( "notes" ) ;
if ( typeof matchId !== "string" || ! matchId ) return { error : "matchId is required" } ;
if ( typeof gameNumber !== "string" || ! gameNumber ) return { error : "gameNumber is required" } ;
try {
await createGame ( {
playoffMatchId : matchId ,
gameNumber : parseInt ( gameNumber , 10 ) ,
scheduledAt : scheduledAt ? new Date ( scheduledAt as string ) : null ,
notes : notes ? ( notes as string ) : null ,
} ) ;
return { success : "Game added" } ;
} catch ( error ) {
return { error : error instanceof Error ? error . message : "Failed to add game" } ;
}
}
if ( intent === "update-game" ) {
const gameId = formData . get ( "gameId" ) ;
const scheduledAt = formData . get ( "scheduledAt" ) ;
const status = formData . get ( "status" ) ;
const participant1Score = formData . get ( "participant1Score" ) ;
const participant2Score = formData . get ( "participant2Score" ) ;
const winnerId = formData . get ( "winnerId" ) ;
const notes = formData . get ( "notes" ) ;
if ( typeof gameId !== "string" || ! gameId ) return { error : "gameId is required" } ;
const validStatuses : PlayoffMatchGameStatus [ ] = [ "scheduled" , "complete" , "postponed" ] ;
if ( status && ! validStatuses . includes ( status as PlayoffMatchGameStatus ) ) {
return { error : ` Invalid status: must be one of ${ validStatuses . join ( ", " ) } ` } ;
}
try {
const updated = await updateGame ( gameId , {
. . . ( scheduledAt !== null && { scheduledAt : scheduledAt ? new Date ( scheduledAt as string ) : null } ) ,
. . . ( status && { status : status as PlayoffMatchGameStatus } ) ,
. . . ( participant1Score !== null && { participant1Score : ( participant1Score as string ) || null } ) ,
. . . ( participant2Score !== null && { participant2Score : ( participant2Score as string ) || null } ) ,
. . . ( winnerId !== null && { winnerId : ( winnerId as string ) || null } ) ,
. . . ( notes !== null && { notes : ( notes as string ) || null } ) ,
} ) ;
if ( ! updated ) return { error : "Game not found" } ;
return { success : "Game updated" } ;
} catch ( error ) {
return { error : error instanceof Error ? error . message : "Failed to update game" } ;
}
}
if ( intent === "delete-game" ) {
const gameId = formData . get ( "gameId" ) ;
if ( typeof gameId !== "string" || ! gameId ) return { error : "gameId is required" } ;
try {
await deleteGame ( gameId ) ;
return { success : "Game deleted" } ;
} catch ( error ) {
return { error : error instanceof Error ? error . message : "Failed to delete game" } ;
}
}
// ── Odds management ──────────────────────────────────────────────────────
if ( intent === "upsert-odds" ) {
const matchId = formData . get ( "matchId" ) ;
const participantId = formData . get ( "participantId" ) ;
const moneylineOdds = formData . get ( "moneylineOdds" ) ;
const oddsSource = formData . get ( "oddsSource" ) ;
if ( typeof matchId !== "string" || ! matchId ) return { error : "matchId is required" } ;
if ( typeof participantId !== "string" || ! participantId ) return { error : "participantId is required" } ;
if ( typeof moneylineOdds !== "string" || ! moneylineOdds ) return { error : "moneylineOdds is required" } ;
const moneylineInt = parseInt ( moneylineOdds , 10 ) ;
if ( isNaN ( moneylineInt ) ) return { error : "moneylineOdds must be an integer" } ;
try {
await upsertMatchOdds ( matchId , participantId , {
moneylineOdds : moneylineInt ,
oddsSource : oddsSource ? ( oddsSource as string ) : undefined ,
} ) ;
return { success : "Odds updated" } ;
} catch ( error ) {
return { error : error instanceof Error ? error . message : "Failed to update odds" } ;
}
}
if ( intent === "delete-odds" ) {
const matchId = formData . get ( "matchId" ) ;
const participantId = formData . get ( "participantId" ) ;
if ( typeof matchId !== "string" || ! matchId ) return { error : "matchId is required" } ;
if ( typeof participantId !== "string" || ! participantId ) return { error : "participantId is required" } ;
try {
await deleteOddsForParticipant ( matchId , participantId ) ;
return { success : "Odds deleted" } ;
} catch ( error ) {
return { error : error instanceof Error ? error . message : "Failed to delete odds" } ;
}
}
feat: Implement bracket expansion plan to support various tournament structures
- 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.
2025-11-03 09:36:16 -08:00
return { error : "Invalid action" } ;
}