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" ;
import { findParticipantsBySportsSeasonId } from "~/models/participant" ;
2025-11-04 22:09:44 -08:00
import { getScoringEventById , updateScoringEvent } 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 ,
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 ,
recalculateAffectedLeagues ,
2026-03-18 22:15:28 -07:00
recalculateStandings ,
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-03-18 22:15:28 -07:00
import { setParticipantResult , findParticipantResultsBySportsSeasonId } from "~/models/participant-result" ;
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" ;
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 } ) ;
}
if ( event . eventType !== "playoff_game" ) {
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
} ;
}
export async function action ( { request , params } : Route . ActionArgs ) {
const formData = await request . formData ( ) ;
const intent = formData . get ( "intent" ) ;
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
2025-11-21 22:05:50 -08:00
// PHASE 5.3: Mark participants NOT in the bracket as eliminated
const event = await getScoringEventById ( params . eventId ) ;
if ( event ) {
// Get all participants in the sports season
const allParticipants = await findParticipantsBySportsSeasonId ( params . id ) ;
// Create a set of participants in the bracket for fast lookup
const participantsInBracket = new Set ( participantIds ) ;
// Mark participants NOT in the bracket as eliminated
for ( const participant of allParticipants ) {
if ( ! participantsInBracket . has ( participant . id ) ) {
await setParticipantResult (
participant . id ,
event . sportsSeasonId ,
0 // 0 = didn't make playoffs, eliminated
) ;
}
}
2026-03-21 13:41:39 -07:00
logger . log ( ` [BracketGeneration] Marked ${ allParticipants . length - participantIds . length } 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" ,
} ;
}
}
2026-03-17 12:34:10 -07:00
/ * *
* Auto - complete a round when every match in it is marked complete .
* Updates scoringEvents . playoffRound and calls processPlayoffEvent so that
2026-04-05 15:11:43 -07:00
* round placements are recorded and probabilities are refreshed .
2026-03-17 12:34:10 -07:00
* This replaces the manual "Complete Round" button .
*
* skipRecalculate = true because the caller already sent a Discord notification
* scoped to the current batch of matches — we don ' t want a second one that
* would show all previously - completed matches in the event .
* /
async function autoCompleteRoundIfDone (
eventId : string ,
round : string ,
sportsSeasonId : string ,
db : ReturnType < typeof database >
) : Promise < void > {
const allMatches = await findPlayoffMatchesByEventId ( eventId ) ;
const roundMatches = allMatches . filter ( ( m ) = > m . round === round ) ;
if ( roundMatches . length === 0 ) return ;
const allDone = roundMatches . every ( ( m ) = > m . isComplete ) ;
if ( ! allDone ) return ;
await db
. update ( schema . scoringEvents )
. set ( { playoffRound : round , updatedAt : new Date ( ) } )
. where ( eq ( schema . scoringEvents . id , eventId ) ) ;
await processPlayoffEvent ( eventId , db , { skipRecalculate : true } ) ;
2026-03-21 13:41:39 -07:00
logger . log ( ` [AutoComplete] Round " ${ round } " auto-completed for event ${ eventId } ` ) ;
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
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
if ( event . bracketTemplateId ) {
const template = getBracketTemplate ( event . bracketTemplateId ) ;
if ( template ) {
try {
await advanceWinnerTemplate ( matchId , winnerId , template ) ;
} catch ( error ) {
// Only ignore "already filled" errors, re-throw unexpected 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 ; // 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
}
}
}
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
// Immediately score this match: loser gets their final placement,
// winner gets provisional floor points (isPartialScore=true).
// isScoring ?? true: default to true for legacy rows pre-dating the isScoring column.
await processMatchResult ( {
round : match.round ,
winnerId ,
loserId ,
isScoring : match.isScoring ? ? true ,
sportsSeasonId : event.sportsSeasonId ,
bracketTemplateId : event.bracketTemplateId ,
2026-03-17 11:16:36 -07:00
eventId : event.id ,
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
eventName : event.name ? ? undefined ,
2026-03-17 12:34:10 -07:00
matchId ,
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-03-17 12:34:10 -07:00
const db = database ( ) ;
await autoCompleteRoundIfDone ( event . id , match . round , event . sportsSeasonId , db ) ;
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 ;
// 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.
// isScoring ?? true: default to true for legacy rows pre-dating the isScoring column.
await processMatchResult ( {
round : match.round ,
winnerId ,
loserId ,
isScoring : match.isScoring ? ? true ,
sportsSeasonId : event.sportsSeasonId ,
bracketTemplateId : event.bracketTemplateId ,
eventName : event.name ? ? undefined ,
skipSideEffects : true ,
} ) ;
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-03-17 12:34:10 -07:00
await recalculateAffectedLeagues ( event . sportsSeasonId , db , { eventId : event.id , eventName : event.name ? ? undefined , matchIds : processedMatchIds } ) ;
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-03-17 12:34:10 -07:00
await autoCompleteRoundIfDone ( event . id , round , event . sportsSeasonId , db ) ;
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 ` } ;
}
// 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" ,
} ;
}
}
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 === "recalculate-floors" ) {
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 ) ;
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 ( ) ;
await db
. delete ( schema . participantResults )
. where (
eq ( schema . participantResults . sportsSeasonId , event . sportsSeasonId )
) ;
// 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 ) : [ ] ;
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 ) {
await processMatchResult (
{
round : match.round ,
winnerId : match.winnerId ? ? "" ,
loserId : match.loserId ? ? "" ,
isScoring : match.isScoring ? ? true ,
sportsSeasonId : event.sportsSeasonId ,
bracketTemplateId : event.bracketTemplateId ,
skipSideEffects : true ,
}
) ;
}
await recalculateAffectedLeagues ( event . sportsSeasonId ) ;
return { success : ` Recalculated floors from ${ sortedMatches . length } completed match(es) ` } ;
} catch ( error ) {
logger . error ( "Error recalculating floors:" , error ) ;
return {
error : error instanceof Error ? error . message : "Failed to recalculate floors" ,
} ;
}
}
2025-11-21 22:05:50 -08:00
if ( intent === "reprocess-eliminations" ) {
try {
// Get the event
const event = await getScoringEventById ( params . eventId ) ;
if ( ! event ) {
return { error : "Event not found" } ;
}
// Get all matches in the bracket
const matches = await findPlayoffMatchesByEventId ( params . eventId ) ;
if ( matches . length === 0 ) {
return { error : "No bracket exists for this event" } ;
}
// Get all participants in the sports season
const allParticipants = await findParticipantsBySportsSeasonId ( params . id ) ;
// Collect all participant IDs that appear in ANY match
const participantsInBracket = new Set < string > ( ) ;
for ( const match of matches ) {
if ( match . participant1Id ) participantsInBracket . add ( match . participant1Id ) ;
if ( match . participant2Id ) participantsInBracket . add ( match . participant2Id ) ;
}
// Mark participants NOT in the bracket as eliminated
let eliminatedCount = 0 ;
for ( const participant of allParticipants ) {
if ( ! participantsInBracket . has ( participant . id ) ) {
await setParticipantResult (
participant . id ,
event . sportsSeasonId ,
0 // 0 = didn't make playoffs, eliminated
) ;
eliminatedCount ++ ;
}
}
2026-03-21 13:41:39 -07:00
logger . log ( ` [ReprocessEliminations] Marked ${ eliminatedCount } participants as eliminated ` ) ;
2025-11-21 22:05:50 -08:00
return {
success : ` Successfully marked ${ eliminatedCount } participant(s) as eliminated ` ,
} ;
} catch ( error ) {
2026-03-21 13:41:39 -07:00
logger . error ( "Error reprocessing eliminations:" , error ) ;
2025-11-21 22:05:50 -08:00
return {
error :
error instanceof Error ? error . message : "Failed to reprocess eliminations" ,
} ;
}
}
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 ` ,
} ;
}
// 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 ) ) ;
// 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
}
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
const allParticipants = await findParticipantsBySportsSeasonId ( params . id ) ;
let eliminatedCount = 0 ;
for ( const participant of allParticipants ) {
if ( ! uniqueParticipants . has ( participant . id ) ) {
await setParticipantResult ( participant . id , params . id , 0 ) ;
eliminatedCount ++ ;
}
}
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 ) ;
// Mark eliminated group participants with finalPosition = 0
const eliminatedIds = await getEliminatedParticipantIds ( params . eventId ) ;
for ( const participantId of eliminatedIds ) {
await setParticipantResult ( participantId , event . sportsSeasonId , 0 ) ;
}
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" } ;
}