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 ,
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" ;
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 { fanOutMajorIfPrimary } 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" ;
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 > ,
recalcOptions? : Parameters < typeof recalculateAffectedLeagues > [ 2 ]
) : Promise < void > {
await processQualifyingBracketEvent ( event . id , db ) ;
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.
await fanOutMajorIfPrimary ( event , { markComplete : false } ) ;
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" } ;
}
}
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" } ;
}
// 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.
await scoreQualifyingBracket ( event , db , {
eventId : event.id ,
eventName : event.name ? ? undefined ,
matchIds : [ matchId ] ,
} ) ;
} 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 [ ] = [ ] ;
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 ) ;
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-06-19 01:25:17 +00:00
// Qualifying majors: derive QP from the full bracket once for the batch.
if ( event . isQualifyingEvent ) {
await processQualifyingBracketEvent ( event . id , db ) ;
}
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 {
// Shared major primary window: propagate this round to siblings.
await fanOutMajorIfPrimary ( event , { markComplete : false } ) ;
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 } ) ;
}
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
// Re-propagate corrected QP to sibling windows (data-correction; not final).
await fanOutMajorIfPrimary ( event , { markComplete : false } ) ;
2026-06-19 01:25:17 +00:00
return {
success : ` Reprocessed qualifying bracket: cleared stale fantasy points and recomputed QP ( ${ completed . length } completed match(es)). ` ,
} ;
}
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 20:40:47 +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" } ;
}