Auto-populate & auto-score tennis Grand Slam brackets from Wikipedia
Add a per-event "Sync Draw" that pulls a tennis major's full 128-player
draw from its Wikipedia article, auto-creates/links participants (and
propagates them to every linked sibling season), builds the bracket, and
runs the qualifying-points scorer. Re-running advances the bracket and
scoring as matches complete.
Core
- match-sync: WikipediaTennisAdapter + wikitext bracket parser, DrawSync
DTOs, syncTennisDraw orchestrator, pure draw->rows mapping
- playoff-match: populateBracketFromDraw (idempotent upsert on externalMatchId)
- scoring_events.externalSourceKey column (Wikipedia article; migration 0123)
- admin bracket "Sync Draw" card (accepts URL or title) + cron pass
Scoring fix
- deriveBracketQualifyingStates only floors players who have reached the
scoring stage; for deep brackets (tennis_128) early-round losers earn 0 QP
instead of a phantom 9th-place floor. CS2/simple_8 behavior preserved
(gated on rounds[0].isScoring). Re-sync reconciles stale QP rows in a
transaction.
Matching & parsing
- accent-folding in normalizeTeamName; strip Wikipedia "(tennis)"
disambiguators; treat Bye/TBD/Qualifier as TBD; extract wikilink before
template-stripping so {{nowrap}}-wrapped players parse
Admin UX
- dry-run preview (matched / will-create / possible duplicates / unfilled
slots) with inline "rename existing" / "create as new" resolution via
fetcher (no full reload)
Tests: parser vs real 2025 Wimbledon fixture, draw mapping, tennis_128
scoring, accent/disambiguator/nowrap parsing, URL parsing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 00:32:35 -07:00
|
|
|
|
import { Form, Link, useFetcher } from "react-router";
|
2026-06-18 02:33:05 +00:00
|
|
|
|
import { Fragment, useState, useEffect, useMemo, useRef } from "react";
|
|
|
|
|
|
import { localDateTimeToUtcIso, utcIsoToLocalDateTime } from "~/lib/date-utils";
|
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-10 12:10:52 -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 { loader, action } from "./admin.sports-seasons.$id.events.$eventId.bracket.server";
|
|
|
|
|
|
import { Button } from "~/components/ui/button";
|
|
|
|
|
|
import { Input } from "~/components/ui/input";
|
|
|
|
|
|
import { Label } from "~/components/ui/label";
|
|
|
|
|
|
import {
|
|
|
|
|
|
Card,
|
|
|
|
|
|
CardContent,
|
|
|
|
|
|
CardDescription,
|
|
|
|
|
|
CardHeader,
|
|
|
|
|
|
CardTitle,
|
|
|
|
|
|
} from "~/components/ui/card";
|
|
|
|
|
|
import {
|
|
|
|
|
|
Select,
|
|
|
|
|
|
SelectContent,
|
|
|
|
|
|
SelectItem,
|
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
|
SelectValue,
|
|
|
|
|
|
} from "~/components/ui/select";
|
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 { ArrowLeft, Plus, Trophy, X, Check, ChevronDown, ChevronRight, Calendar, TrendingUp, Trash2 } from "lucide-react";
|
2026-03-16 12:27:43 -07:00
|
|
|
|
import { getAllBracketTemplates, getBracketTemplate, getOrderedRoundsFromMatches, buildNCAA68SlotMap, ALL_16_SEEDS, type BracketRegion } from "~/lib/bracket-templates";
|
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 { computeGroupStandings } from "~/models/group-stage-match";
|
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 {
|
|
|
|
|
|
Table,
|
|
|
|
|
|
TableBody,
|
|
|
|
|
|
TableCell,
|
|
|
|
|
|
TableHead,
|
|
|
|
|
|
TableHeader,
|
|
|
|
|
|
TableRow,
|
|
|
|
|
|
} from "~/components/ui/table";
|
2026-02-14 22:30:12 -08:00
|
|
|
|
import { Badge } from "~/components/ui/badge";
|
2025-11-08 21:36:29 -08:00
|
|
|
|
import { ParticipantSelector } from "~/components/ParticipantSelector";
|
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-10 12:10:52 -07:00
|
|
|
|
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
|
|
|
|
|
|
return [{ title: `Bracket — ${data?.event?.name ?? "Event"} - Brackt Admin` }];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-18 02:33:05 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Schedule editor for a single group-stage match.
|
|
|
|
|
|
*
|
|
|
|
|
|
* The admin enters a local wall-clock time; on submit it is converted to a UTC
|
|
|
|
|
|
* ISO string (in the hidden `scheduledAt` field) so the DB stores a true UTC
|
|
|
|
|
|
* instant. The stored UTC value is rendered back into the input in the
|
|
|
|
|
|
* browser's local timezone via a client-only effect (avoids SSR hydration
|
|
|
|
|
|
* mismatch, since the server runs in UTC).
|
|
|
|
|
|
*/
|
|
|
|
|
|
function GroupMatchScheduleForm({
|
|
|
|
|
|
match,
|
|
|
|
|
|
localTzAbbr,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
match: { id: string; scheduledAt: string | Date | null | undefined };
|
|
|
|
|
|
localTzAbbr: string;
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (inputRef.current) {
|
|
|
|
|
|
inputRef.current.value = utcIsoToLocalDateTime(match.scheduledAt);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [match.scheduledAt]);
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Form
|
|
|
|
|
|
method="post"
|
|
|
|
|
|
className="flex items-center gap-1"
|
|
|
|
|
|
onSubmit={(e) => {
|
|
|
|
|
|
const form = e.currentTarget;
|
|
|
|
|
|
const local = form.elements.namedItem("scheduledAtLocal") as HTMLInputElement;
|
|
|
|
|
|
const hidden = form.elements.namedItem("scheduledAt") as HTMLInputElement;
|
|
|
|
|
|
if (hidden) hidden.value = localDateTimeToUtcIso(local?.value) ?? "";
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<input type="hidden" name="intent" value="update-group-match-schedule" />
|
|
|
|
|
|
<input type="hidden" name="matchId" value={match.id} />
|
|
|
|
|
|
{/* Hidden field holds the UTC ISO string written by onSubmit */}
|
|
|
|
|
|
<input type="hidden" name="scheduledAt" defaultValue="" />
|
|
|
|
|
|
<Calendar className="h-3 w-3 text-muted-foreground shrink-0" />
|
|
|
|
|
|
<Input
|
|
|
|
|
|
ref={inputRef}
|
|
|
|
|
|
type="datetime-local"
|
|
|
|
|
|
name="scheduledAtLocal"
|
|
|
|
|
|
title={`Time zone: ${localTzAbbr}`}
|
|
|
|
|
|
className="h-6 text-xs px-1 py-0"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Button type="submit" size="sm" variant="ghost" className="h-6 px-1.5 text-xs">
|
|
|
|
|
|
Save
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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 { loader, action };
|
|
|
|
|
|
|
Auto-populate & auto-score tennis Grand Slam brackets from Wikipedia
Add a per-event "Sync Draw" that pulls a tennis major's full 128-player
draw from its Wikipedia article, auto-creates/links participants (and
propagates them to every linked sibling season), builds the bracket, and
runs the qualifying-points scorer. Re-running advances the bracket and
scoring as matches complete.
Core
- match-sync: WikipediaTennisAdapter + wikitext bracket parser, DrawSync
DTOs, syncTennisDraw orchestrator, pure draw->rows mapping
- playoff-match: populateBracketFromDraw (idempotent upsert on externalMatchId)
- scoring_events.externalSourceKey column (Wikipedia article; migration 0123)
- admin bracket "Sync Draw" card (accepts URL or title) + cron pass
Scoring fix
- deriveBracketQualifyingStates only floors players who have reached the
scoring stage; for deep brackets (tennis_128) early-round losers earn 0 QP
instead of a phantom 9th-place floor. CS2/simple_8 behavior preserved
(gated on rounds[0].isScoring). Re-sync reconciles stale QP rows in a
transaction.
Matching & parsing
- accent-folding in normalizeTeamName; strip Wikipedia "(tennis)"
disambiguators; treat Bye/TBD/Qualifier as TBD; extract wikilink before
template-stripping so {{nowrap}}-wrapped players parse
Admin UX
- dry-run preview (matched / will-create / possible duplicates / unfilled
slots) with inline "rename existing" / "create as new" resolution via
fetcher (no full reload)
Tests: parser vs real 2025 Wimbledon fixture, draw mapping, tennis_128
scoring, accent/disambiguator/nowrap parsing, URL parsing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 00:32:35 -07:00
|
|
|
|
/**
|
|
|
|
|
|
* One "possible duplicate" row in the draw preview. Uses a fetcher so resolving
|
|
|
|
|
|
* it (rename existing / create as new) submits in the background — the preview
|
|
|
|
|
|
* stays on screen instead of reloading the page and forcing a fresh dry run.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function DuplicateRow({
|
|
|
|
|
|
p,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
p: { name: string; externalId: string | null; suggestion: string; suggestionId: string };
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const fetcher = useFetcher<{ success?: string; error?: string }>();
|
|
|
|
|
|
const busy = fetcher.state !== "idle";
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<li className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
|
|
|
|
|
<span>
|
|
|
|
|
|
<span className="font-medium">{p.name}</span>
|
|
|
|
|
|
<span className="text-muted-foreground"> → looks like </span>
|
|
|
|
|
|
<span className="font-medium">{p.suggestion}</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
{fetcher.data?.success ? (
|
|
|
|
|
|
<span className="text-xs font-medium text-emerald-500">✓ {fetcher.data.success}</span>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<fetcher.Form method="post" className="flex items-center gap-2">
|
|
|
|
|
|
<input type="hidden" name="name" value={p.name} />
|
|
|
|
|
|
<input type="hidden" name="externalId" value={p.externalId ?? ""} />
|
|
|
|
|
|
<input type="hidden" name="participantId" value={p.suggestionId} />
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="submit"
|
|
|
|
|
|
name="intent"
|
|
|
|
|
|
value="relink-draw-participant"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
disabled={busy}
|
|
|
|
|
|
>
|
|
|
|
|
|
Rename “{p.suggestion}” → “{p.name}”
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="submit"
|
|
|
|
|
|
name="intent"
|
|
|
|
|
|
value="create-draw-participant"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
disabled={busy}
|
|
|
|
|
|
>
|
|
|
|
|
|
Create as new
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</fetcher.Form>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{fetcher.data?.error && (
|
|
|
|
|
|
<span className="text-xs text-destructive">{fetcher.data.error}</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</li>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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 default function EventBracket({
|
|
|
|
|
|
loaderData,
|
|
|
|
|
|
actionData,
|
|
|
|
|
|
}: Route.ComponentProps) {
|
2026-02-14 22:30:12 -08:00
|
|
|
|
const { sportsSeason, event, participants, matches, tournamentGroups } = loaderData;
|
2025-11-04 22:09:44 -08:00
|
|
|
|
const [selectedTemplate, setSelectedTemplate] = useState("simple_8");
|
|
|
|
|
|
const [selectedParticipants, setSelectedParticipants] = useState<Record<number, string>>({});
|
|
|
|
|
|
const [selectedWinners, setSelectedWinners] = useState<Record<string, string>>({});
|
2026-02-14 22:30:12 -08:00
|
|
|
|
const [knockoutAssignments, setKnockoutAssignments] = useState<Record<string, string>>({});
|
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
|
|
|
|
const [expandedMatches, setExpandedMatches] = useState<Set<string>>(new Set());
|
2026-03-11 15:13:50 -07:00
|
|
|
|
const localTzAbbr = new Intl.DateTimeFormat("en-US", { timeZoneName: "short" })
|
|
|
|
|
|
.formatToParts(new Date())
|
|
|
|
|
|
.find(p => p.type === "timeZoneName")?.value ?? "local";
|
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
|
|
|
|
|
|
|
|
|
|
const toggleMatchExpanded = (matchId: string) => {
|
|
|
|
|
|
setExpandedMatches(prev => {
|
|
|
|
|
|
const next = new Set(prev);
|
|
|
|
|
|
if (next.has(matchId)) next.delete(matchId);
|
|
|
|
|
|
else next.add(matchId);
|
|
|
|
|
|
return next;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
2025-11-04 22:09:44 -08:00
|
|
|
|
const templates = getAllBracketTemplates();
|
|
|
|
|
|
const template = templates.find((t) => t.id === selectedTemplate);
|
|
|
|
|
|
|
2026-02-14 22:30:12 -08:00
|
|
|
|
// Determine if this is a group-stage event
|
2026-04-13 01:25:32 -04:00
|
|
|
|
const hasGroupStage = template?.groupStage !== null && template?.groupStage !== undefined;
|
2026-03-15 21:52:47 -07:00
|
|
|
|
|
|
|
|
|
|
// Determine if this template uses named regions (e.g. NCAA 68)
|
|
|
|
|
|
const hasRegions = (template?.regions?.length ?? 0) > 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Per-region configuration state: name + which seed slots are play-ins
|
|
|
|
|
|
// Initialized from: stored event config → template defaults → empty
|
|
|
|
|
|
const buildDefaultRegionConfig = (tmpl: typeof template) => {
|
|
|
|
|
|
const source =
|
|
|
|
|
|
(event.bracketRegionConfig as unknown as BracketRegion[] | null) ?? tmpl?.regions ?? [];
|
|
|
|
|
|
return source.map((r) => ({
|
|
|
|
|
|
name: r.name,
|
|
|
|
|
|
playInSeeds: r.playIns.map((pi) => pi.seedSlot),
|
|
|
|
|
|
}));
|
|
|
|
|
|
};
|
|
|
|
|
|
const [regionConfig, setRegionConfig] = useState(() => buildDefaultRegionConfig(template));
|
|
|
|
|
|
|
|
|
|
|
|
// Derive effective BracketRegion[] from regionConfig state
|
|
|
|
|
|
const effectiveRegions: BracketRegion[] = useMemo(() => {
|
|
|
|
|
|
return regionConfig.map((cfg) => {
|
|
|
|
|
|
const playInSet = new Set(cfg.playInSeeds);
|
|
|
|
|
|
return {
|
|
|
|
|
|
name: cfg.name,
|
|
|
|
|
|
directSeeds: ALL_16_SEEDS.filter((s) => !playInSet.has(s)),
|
|
|
|
|
|
playIns: cfg.playInSeeds.map((s) => ({ seedSlot: s, teams: 2 as const })),
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
}, [regionConfig]);
|
|
|
|
|
|
|
|
|
|
|
|
const regionSlotMap = useMemo(
|
|
|
|
|
|
() => (effectiveRegions.length > 0 ? buildNCAA68SlotMap(effectiveRegions) : null),
|
|
|
|
|
|
[effectiveRegions]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Region config handlers
|
|
|
|
|
|
const updateRegionName = (regionIdx: number, name: string) => {
|
|
|
|
|
|
setRegionConfig((prev) =>
|
|
|
|
|
|
prev.map((r, i) => (i === regionIdx ? { ...r, name } : r))
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
const addPlayIn = (regionIdx: number) => {
|
|
|
|
|
|
setRegionConfig((prev) =>
|
|
|
|
|
|
prev.map((r, i) => {
|
|
|
|
|
|
if (i !== regionIdx || r.playInSeeds.length >= 2) return r;
|
|
|
|
|
|
// Default to seed 16 if no play-ins yet, otherwise seed 11
|
|
|
|
|
|
const defaultSeed = r.playInSeeds.length === 0 ? 16 : 11;
|
|
|
|
|
|
const seed = r.playInSeeds.includes(defaultSeed)
|
|
|
|
|
|
? ([11, 16].find((s) => !r.playInSeeds.includes(s)) ?? 11)
|
|
|
|
|
|
: defaultSeed;
|
|
|
|
|
|
return { ...r, playInSeeds: [...r.playInSeeds, seed] };
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
// Reset participant selections since slot indices change
|
|
|
|
|
|
setSelectedParticipants({});
|
|
|
|
|
|
};
|
|
|
|
|
|
const removePlayIn = (regionIdx: number, piIdx: number) => {
|
|
|
|
|
|
setRegionConfig((prev) =>
|
|
|
|
|
|
prev.map((r, i) =>
|
|
|
|
|
|
i === regionIdx
|
|
|
|
|
|
? { ...r, playInSeeds: r.playInSeeds.filter((_, j) => j !== piIdx) }
|
|
|
|
|
|
: r
|
|
|
|
|
|
)
|
|
|
|
|
|
);
|
|
|
|
|
|
setSelectedParticipants({});
|
|
|
|
|
|
};
|
|
|
|
|
|
const updatePlayInSeed = (regionIdx: number, piIdx: number, seed: number) => {
|
|
|
|
|
|
setRegionConfig((prev) =>
|
|
|
|
|
|
prev.map((r, i) =>
|
|
|
|
|
|
i === regionIdx
|
|
|
|
|
|
? {
|
|
|
|
|
|
...r,
|
|
|
|
|
|
playInSeeds: r.playInSeeds.map((s, j) => (j === piIdx ? seed : s)),
|
|
|
|
|
|
}
|
|
|
|
|
|
: r
|
|
|
|
|
|
)
|
|
|
|
|
|
);
|
|
|
|
|
|
setSelectedParticipants({});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-14 22:30:12 -08:00
|
|
|
|
const isGroupStageEvent = tournamentGroups.length > 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Determine the phase for group-stage events
|
|
|
|
|
|
const knockoutPopulated = useMemo(() => {
|
|
|
|
|
|
if (!isGroupStageEvent) return false;
|
|
|
|
|
|
// Check if any Round of 32 match has participants assigned
|
2026-03-21 09:44:05 -07:00
|
|
|
|
const r32Matches = matches.filter((m) => m.round === "Round of 32");
|
|
|
|
|
|
return r32Matches.some((m) => m.participant1Id || m.participant2Id);
|
2026-02-14 22:30:12 -08:00
|
|
|
|
}, [isGroupStageEvent, matches]);
|
|
|
|
|
|
|
|
|
|
|
|
// Group stage stats
|
|
|
|
|
|
const groupStageStats = useMemo(() => {
|
|
|
|
|
|
if (!isGroupStageEvent) return null;
|
|
|
|
|
|
let eliminated = 0;
|
|
|
|
|
|
let total = 0;
|
|
|
|
|
|
for (const group of tournamentGroups) {
|
2026-03-21 09:44:05 -07:00
|
|
|
|
for (const member of group.members || []) {
|
2026-02-14 22:30:12 -08:00
|
|
|
|
total++;
|
|
|
|
|
|
if (member.eliminated) eliminated++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return { eliminated, advancing: total - eliminated, total };
|
|
|
|
|
|
}, [isGroupStageEvent, tournamentGroups]);
|
|
|
|
|
|
|
|
|
|
|
|
// Advancing participants (non-eliminated) for knockout assignment
|
|
|
|
|
|
const advancingParticipants = useMemo(() => {
|
|
|
|
|
|
if (!isGroupStageEvent) return [];
|
|
|
|
|
|
const advancing: Array<{ id: string; name: string }> = [];
|
|
|
|
|
|
for (const group of tournamentGroups) {
|
2026-03-21 09:44:05 -07:00
|
|
|
|
for (const member of group.members || []) {
|
2026-02-14 22:30:12 -08:00
|
|
|
|
if (!member.eliminated && member.participant) {
|
|
|
|
|
|
advancing.push({
|
|
|
|
|
|
id: member.participant.id,
|
|
|
|
|
|
name: member.participant.name,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return advancing;
|
|
|
|
|
|
}, [isGroupStageEvent, tournamentGroups]);
|
|
|
|
|
|
|
|
|
|
|
|
// Available advancing participants for knockout assignment (exclude already assigned)
|
|
|
|
|
|
const availableAdvancingMap = useMemo(() => {
|
|
|
|
|
|
const assignedIds = new Set(Object.values(knockoutAssignments).filter(Boolean));
|
|
|
|
|
|
const map: Record<string, typeof advancingParticipants> = {};
|
|
|
|
|
|
|
|
|
|
|
|
// Create 32 slots (16 matches × 2 slots)
|
|
|
|
|
|
for (let m = 1; m <= 16; m++) {
|
|
|
|
|
|
for (const slot of ["participant1Id", "participant2Id"]) {
|
|
|
|
|
|
const key = `match-${m}-${slot}`;
|
|
|
|
|
|
const currentSelection = knockoutAssignments[key];
|
|
|
|
|
|
map[key] = advancingParticipants.filter(
|
|
|
|
|
|
(p) => !assignedIds.has(p.id) || p.id === currentSelection
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return map;
|
|
|
|
|
|
}, [knockoutAssignments, advancingParticipants]);
|
|
|
|
|
|
|
2025-11-04 22:09:44 -08:00
|
|
|
|
// Clear selected winners after successful batch submission
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (actionData?.success) {
|
|
|
|
|
|
setSelectedWinners({});
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [actionData?.success]);
|
|
|
|
|
|
|
2026-03-15 21:52:47 -07:00
|
|
|
|
// Reset selected participants and region config when template changes
|
2025-11-04 22:09:44 -08:00
|
|
|
|
const handleTemplateChange = (newTemplateId: string) => {
|
|
|
|
|
|
setSelectedTemplate(newTemplateId);
|
|
|
|
|
|
setSelectedParticipants({});
|
2026-03-15 21:52:47 -07:00
|
|
|
|
const newTemplate = templates.find((t) => t.id === newTemplateId);
|
|
|
|
|
|
setRegionConfig(buildDefaultRegionConfig(newTemplate));
|
2025-11-04 22:09:44 -08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Update selected participant for a seed
|
|
|
|
|
|
const handleParticipantChange = (seedIndex: number, participantId: string) => {
|
|
|
|
|
|
setSelectedParticipants(prev => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[seedIndex]: participantId
|
|
|
|
|
|
}));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-14 22:30:12 -08:00
|
|
|
|
// Memoized available participants calculation
|
2025-11-08 21:36:29 -08:00
|
|
|
|
const availableParticipantsMap = useMemo(() => {
|
|
|
|
|
|
const selectedIdsSet = new Set(Object.values(selectedParticipants));
|
|
|
|
|
|
const map: Record<number, typeof participants> = {};
|
|
|
|
|
|
const totalSeeds = template?.totalTeams || 8;
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < totalSeeds; i++) {
|
|
|
|
|
|
const currentSelection = selectedParticipants[i];
|
|
|
|
|
|
map[i] = participants.filter((p: { id: string; name: string }) => {
|
|
|
|
|
|
return !selectedIdsSet.has(p.id) || p.id === currentSelection;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
return map;
|
|
|
|
|
|
}, [selectedParticipants, participants, template?.totalTeams]);
|
|
|
|
|
|
|
|
|
|
|
|
const getAvailableParticipants = (seedIndex: number) => {
|
|
|
|
|
|
return availableParticipantsMap[seedIndex] || participants;
|
2025-11-04 22:09:44 -08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Update selected winner for a match
|
|
|
|
|
|
const handleWinnerChange = (matchId: string, winnerId: string) => {
|
|
|
|
|
|
setSelectedWinners(prev => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[matchId]: winnerId
|
|
|
|
|
|
}));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Get matches with pending winner selections in a round
|
|
|
|
|
|
const getPendingWinnersInRound = (round: string) => {
|
|
|
|
|
|
return Object.entries(selectedWinners)
|
|
|
|
|
|
.filter(([matchId]) => {
|
2026-03-21 09:44:05 -07:00
|
|
|
|
const match = matches.find((m) => m.id === matchId);
|
2025-11-04 22:09:44 -08:00
|
|
|
|
return match && match.round === round && !match.isComplete;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
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-08 21:36:29 -08:00
|
|
|
|
// Memoized: Group matches by round
|
|
|
|
|
|
const matchesByRound = useMemo(() => {
|
|
|
|
|
|
const grouped: Record<string, typeof matches> = {};
|
|
|
|
|
|
for (const match of matches) {
|
|
|
|
|
|
if (!grouped[match.round]) {
|
|
|
|
|
|
grouped[match.round] = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
grouped[match.round].push(match);
|
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-08 21:36:29 -08:00
|
|
|
|
return grouped;
|
|
|
|
|
|
}, [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
|
|
|
|
|
2026-02-14 22:30:12 -08:00
|
|
|
|
// Memoized: Get available rounds in chronological order
|
2025-11-08 21:36:29 -08:00
|
|
|
|
const availableRounds = useMemo(() => {
|
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196)
* Fix no-shadow and consistent-function-scoping lint violations
Resolves all 11 no-shadow and 16 consistent-function-scoping oxlint
warnings and promotes both rules to errors in .oxlintrc.json.
no-shadow: renamed Drizzle callback params (sports→s, matches→m,
seasons→s) to avoid shadowing outer imports; removed shadowed
destructures (eq, inArray) from where callbacks; renamed inner
template→bracketTemplate, prev→currentTimers, season→ss, name→teamName
(with name: teamName fix to preserve semantics).
consistent-function-scoping: moved formatDate, getRankBadge,
getMovementIndicator, getPositionBadge, getStatusBadge, toDateStr,
elo (×2), weightedPick, sortByMatchNumber (×2) to module scope;
moved formatTime (×2), isValidLeagueName, getDraftTimes,
makeSeasonQueues to file scope in test files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix no-non-null-assertion lint violations and promote to error
Eliminates all 208 no-non-null-assertion warnings across 38 files.
Promotes typescript/no-non-null-assertion from warn to error in
.oxlintrc.json.
Fix patterns applied:
- Map.get(key)! after .has() check → extract with get() + null guard
- Map.get(key)! on pre-populated count maps → ?? 0 default
- .set(id, map.get(id)! + 1) increment → ?? 0 before adding
- participant1Id!/participant2Id! on DB matches → ?? "" fallback
- array.find()! in tests → guard + throw or expect().toBeDefined()
- bracketTemplateCache.get(id)! → null guard extract
- Various nullable field accesses → optional chain or ?? default
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix prefer-add-event-listener, no-unassigned-import, require-module-specifiers
Resolves all 9 remaining non-console lint warnings and promotes all
three rules to errors in .oxlintrc.json.
- prefer-add-event-listener: converted onchange/onclick/onload
assignments to addEventListener in useDraftNotifications.ts and
admin.data-sync.tsx; stored changeHandler ref for proper cleanup
with removeEventListener
- no-unassigned-import: configured rule with allow list for legitimate
side-effect imports (*.css, @testing-library/jest-dom,
@testing-library/cypress/add-commands)
- require-module-specifiers: removed redundant `export {}` from
cypress/support/e2e.ts (file already has an import)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix TypeScript errors from no-non-null-assertion fixes
Two fixes introduced by the non-null assertion cleanup produced type
errors:
- scoring-event.ts: `?? ""` was wrong type for a participant object map;
restructured to explicit null guards so TypeScript can narrow correctly
- standings-sync/index.ts: `?? null` after name-match lookup lost the
truthy guarantee, causing TS18047 on the write-back block; added
`participant &&` guard before accessing its properties
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add npm run typecheck as Stop hook in Claude settings
Runs a full project typecheck at the end of each Claude turn so type
errors surface as feedback before the next message.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 10:59:51 -07:00
|
|
|
|
const bracketTemplate = event.bracketTemplateId ? getBracketTemplate(event.bracketTemplateId) : undefined;
|
|
|
|
|
|
return getOrderedRoundsFromMatches(matches, bracketTemplate);
|
2025-11-08 21:36:29 -08:00
|
|
|
|
}, [matches, event.bracketTemplateId]);
|
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-14 20:01:21 -08:00
|
|
|
|
// Check if all matches are complete
|
|
|
|
|
|
const allMatchesComplete = useMemo(() => {
|
2026-03-21 09:44:05 -07:00
|
|
|
|
return matches.length > 0 && matches.every((m) => m.isComplete);
|
2025-11-14 20:01:21 -08:00
|
|
|
|
}, [matches]);
|
|
|
|
|
|
|
2026-02-14 22:30:12 -08:00
|
|
|
|
// No bracket and no groups yet = setup phase
|
|
|
|
|
|
const showSetup = matches.length === 0 && !isGroupStageEvent;
|
|
|
|
|
|
|
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 (
|
|
|
|
|
|
<div className="p-8">
|
|
|
|
|
|
<div className="max-w-6xl mx-auto">
|
|
|
|
|
|
<div className="mb-6">
|
|
|
|
|
|
<Button variant="ghost" size="sm" asChild className="mb-2">
|
|
|
|
|
|
<Link to={`/admin/sports-seasons/${sportsSeason.id}/events/${event.id}`}>
|
|
|
|
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
|
|
|
|
Back to Event
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h1 className="text-3xl font-bold">Playoff Bracket</h1>
|
|
|
|
|
|
<p className="text-muted-foreground mt-1">
|
|
|
|
|
|
{event.name} - {sportsSeason.sport.name} {sportsSeason.name}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
|
{actionData?.error && (
|
|
|
|
|
|
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
|
|
|
|
|
|
{actionData.error}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{actionData?.success && (
|
2026-02-20 19:26:11 -08:00
|
|
|
|
<div className="bg-emerald-500/15 text-emerald-400 px-4 py-3 rounded-md text-sm">
|
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
|
|
|
|
{actionData.success}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
Auto-populate & auto-score tennis Grand Slam brackets from Wikipedia
Add a per-event "Sync Draw" that pulls a tennis major's full 128-player
draw from its Wikipedia article, auto-creates/links participants (and
propagates them to every linked sibling season), builds the bracket, and
runs the qualifying-points scorer. Re-running advances the bracket and
scoring as matches complete.
Core
- match-sync: WikipediaTennisAdapter + wikitext bracket parser, DrawSync
DTOs, syncTennisDraw orchestrator, pure draw->rows mapping
- playoff-match: populateBracketFromDraw (idempotent upsert on externalMatchId)
- scoring_events.externalSourceKey column (Wikipedia article; migration 0123)
- admin bracket "Sync Draw" card (accepts URL or title) + cron pass
Scoring fix
- deriveBracketQualifyingStates only floors players who have reached the
scoring stage; for deep brackets (tennis_128) early-round losers earn 0 QP
instead of a phantom 9th-place floor. CS2/simple_8 behavior preserved
(gated on rounds[0].isScoring). Re-sync reconciles stale QP rows in a
transaction.
Matching & parsing
- accent-folding in normalizeTeamName; strip Wikipedia "(tennis)"
disambiguators; treat Bye/TBD/Qualifier as TBD; extract wikilink before
template-stripping so {{nowrap}}-wrapped players parse
Admin UX
- dry-run preview (matched / will-create / possible duplicates / unfilled
slots) with inline "rename existing" / "create as new" resolution via
fetcher (no full reload)
Tests: parser vs real 2025 Wimbledon fixture, draw mapping, tennis_128
scoring, accent/disambiguator/nowrap parsing, URL parsing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 00:32:35 -07:00
|
|
|
|
{/* Possible-duplicate review after a draw sync */}
|
|
|
|
|
|
{actionData?.drawSyncResult && actionData.drawSyncResult.unmatched.length > 0 && (
|
|
|
|
|
|
<Card className="border-amber-500/40">
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="text-amber-500">
|
|
|
|
|
|
Review {actionData.drawSyncResult.unmatched.length} possible duplicate
|
|
|
|
|
|
{actionData.drawSyncResult.unmatched.length === 1 ? "" : "s"}
|
|
|
|
|
|
</CardTitle>
|
|
|
|
|
|
<CardDescription>
|
|
|
|
|
|
These players were auto-created but closely resemble an existing participant —
|
|
|
|
|
|
if a duplicate, results won't reach the drafted copy. To fix: on the{" "}
|
|
|
|
|
|
<Link
|
|
|
|
|
|
to={`/admin/sports-seasons/${sportsSeason.id}/participants`}
|
|
|
|
|
|
className="underline font-medium"
|
|
|
|
|
|
>
|
|
|
|
|
|
participants page
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
, set the correct participant's external ID to the Wikipedia name, delete the
|
|
|
|
|
|
duplicate, then click <span className="font-medium">Sync Draw</span> again.
|
|
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<ul className="space-y-1 text-sm">
|
|
|
|
|
|
{actionData.drawSyncResult.unmatched.map((p) => (
|
|
|
|
|
|
<li key={p.externalId ?? p.name} className="flex items-center gap-2">
|
|
|
|
|
|
<span className="font-medium">{p.name}</span>
|
|
|
|
|
|
{p.externalId && p.externalId !== p.name && (
|
|
|
|
|
|
<span className="text-muted-foreground text-xs">({p.externalId})</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Dry-run preview of a draw sync */}
|
|
|
|
|
|
{actionData?.drawPreview && (
|
|
|
|
|
|
<Card className="border-sky-500/40">
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="text-sky-500">Draw preview (no changes made)</CardTitle>
|
|
|
|
|
|
<CardDescription>{actionData.drawPreview.article}</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="space-y-3 text-sm">
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-x-6 gap-y-1 sm:grid-cols-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
Players: <span className="font-medium">{actionData.drawPreview.totalPlayers}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
Matched:{" "}
|
|
|
|
|
|
<span className="font-medium text-emerald-500">
|
|
|
|
|
|
{actionData.drawPreview.matched}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
Will create:{" "}
|
|
|
|
|
|
<span className="font-medium">{actionData.drawPreview.willCreate.length}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
Matches:{" "}
|
|
|
|
|
|
<span className="font-medium">
|
|
|
|
|
|
{actionData.drawPreview.completedMatches}/{actionData.drawPreview.totalMatches}{" "}
|
|
|
|
|
|
done
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
Unfilled R1 slots:{" "}
|
|
|
|
|
|
<span className="font-medium">{actionData.drawPreview.tbdFirstRound}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{actionData.drawPreview.possibleDuplicates.length > 0 && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p className="font-medium text-amber-500">
|
|
|
|
|
|
Possible duplicates ({actionData.drawPreview.possibleDuplicates.length}) — would
|
|
|
|
|
|
be created but resemble an existing participant:
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<ul className="mt-2 space-y-2">
|
|
|
|
|
|
{actionData.drawPreview.possibleDuplicates.map((p) => (
|
|
|
|
|
|
<DuplicateRow key={p.externalId ?? p.name} p={p} />
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
<p className="mt-2 text-xs text-muted-foreground">
|
|
|
|
|
|
<span className="font-medium">Rename</span> if it's the same player (links
|
|
|
|
|
|
the existing participant so it matches);{" "}
|
|
|
|
|
|
<span className="font-medium">Create as new</span> if they're different
|
|
|
|
|
|
people. Then re-run Preview or Sync Draw.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{actionData.drawPreview.willCreate.length > 0 && (
|
|
|
|
|
|
<details>
|
|
|
|
|
|
<summary className="cursor-pointer text-muted-foreground">
|
|
|
|
|
|
Show all {actionData.drawPreview.willCreate.length} players that would be created
|
|
|
|
|
|
</summary>
|
|
|
|
|
|
<ul className="mt-1 columns-2 sm:columns-3">
|
|
|
|
|
|
{actionData.drawPreview.willCreate.map((p) => (
|
|
|
|
|
|
<li key={p.externalId ?? p.name}>{p.name}</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</details>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
|
Fix any duplicates on the{" "}
|
|
|
|
|
|
<Link
|
|
|
|
|
|
to={`/admin/sports-seasons/${sportsSeason.id}/participants`}
|
|
|
|
|
|
className="underline font-medium"
|
|
|
|
|
|
>
|
|
|
|
|
|
participants page
|
|
|
|
|
|
</Link>{" "}
|
|
|
|
|
|
first, then click <span className="font-medium">Sync Draw</span>.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Sync Draw from Wikipedia — tennis Grand Slam auto-populate + auto-score */}
|
|
|
|
|
|
{event.isQualifyingEvent &&
|
|
|
|
|
|
sportsSeason.sport.simulatorType === "tennis_qualifying_points" && (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle>Sync Draw from Wikipedia</CardTitle>
|
|
|
|
|
|
<CardDescription>
|
|
|
|
|
|
Auto-populate and score this Grand Slam bracket from its Wikipedia draw
|
|
|
|
|
|
article. Re-run during the tournament to pull in completed matches.
|
|
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<Form method="post" className="space-y-3">
|
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
|
<Label htmlFor="externalSourceKey">Wikipedia draw page (URL or title)</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="externalSourceKey"
|
|
|
|
|
|
name="externalSourceKey"
|
|
|
|
|
|
placeholder="https://en.wikipedia.org/wiki/2025_Wimbledon_Championships_–_Men's_singles"
|
|
|
|
|
|
defaultValue={event.externalSourceKey ?? ""}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
|
Paste the article URL or type the title — both work. Use{" "}
|
|
|
|
|
|
<span className="font-medium">Preview</span> first to see matches and any
|
|
|
|
|
|
new/duplicate players before writing anything.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
|
<Button type="submit" name="intent" value="preview-draw" variant="outline">
|
|
|
|
|
|
Preview (dry run)
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button type="submit" name="intent" value="sync-draw">
|
|
|
|
|
|
Sync Draw
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-04-15 14:56:33 -07:00
|
|
|
|
{/* Reprocess Bracket - Full rebuild of participant results.
|
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
|
|
|
|
Hidden once every match is complete — at that point all placements are final
|
|
|
|
|
|
and "finalize bracket" should be used instead. */}
|
|
|
|
|
|
{matches.length > 0 && !matches.every((m) => m.isComplete) && (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
2026-04-15 14:56:33 -07:00
|
|
|
|
<CardTitle>Reprocess Bracket</CardTitle>
|
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
|
|
|
|
<CardDescription>
|
2026-04-15 14:56:33 -07:00
|
|
|
|
Replay all completed matches from scratch and re-mark non-bracket
|
|
|
|
|
|
participants as eliminated. Use this to fix scoring data after rule
|
|
|
|
|
|
changes or when results look incorrect.
|
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
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<Form method="post">
|
2026-04-15 14:56:33 -07:00
|
|
|
|
<input type="hidden" name="intent" value="reprocess-bracket" />
|
Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator (#242)
* Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator, fixes #127
- New `groupStageMatches` table for recording group play results (W/D/L, scores, matchday, schedule)
- `computeGroupStandings()` model function: pts → GD → GF → name tiebreaker ordering
- `GroupStageStandings` component showing all 12 groups with standings table and manager column
- Admin bracket UI: group match score entry, per-group standings, "Recalculate Floors" action
- `WorldCupSimulator`: 50k Monte Carlo covering group stage + best-8 3rd-place + knockout + 3rd place game
- Fuzzy name matching for national team Elo lookup (exact → substring → word-overlap), warns on miss
- Partial group completion: completed matches replayed with real scores, remaining matches simulated
- Elo priority: admin-entered sourceElo > futures odds converted to Elo > hardcoded national team ratings
- `fifa_48` bracket template: added Third Place Game round with `loserFeedsInto` on Semifinals
- Scoring rules: distinct 3rd/4th place for `fifa_48` (not averaged), QF losers share 5th–8th equally
- Floor scoring: SF participants guaranteed 4th (provisional), finalized after 3rd place game
- `recalculate-floors` admin action deletes and replays all results from scratch (fixes stale guard bug)
- Unique index on `(tournamentGroupId, participant1Id, participant2Id)` to prevent duplicate pairings
- Batch `findMatchesByGroupIds()` replacing N sequential queries in the sport season loader
- League home mini-standings now shows `actualPoints` (includes floor) instead of `totalPoints` only
- Elo ratings admin page supports World Cup (same bulk-import flow as snooker)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix TypeScript errors: update GroupStandingData type to use findMatchesByGroupIds
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Increase Node heap to 4GB for unit tests in CI to prevent OOM
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix OOM in CI: make WorldCupSimulator simulation count configurable for tests
Tests now pass numSimulations=500 instead of the production default of 50,000.
Six simulator tests × 50k iterations each was exhausting the 4GB heap on GitHub
Actions runners. Also reduce simGroupMatch stat tests from 50k to 5k iterations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 10:27:47 -07:00
|
|
|
|
<Button type="submit" variant="outline">
|
2026-04-15 14:56:33 -07:00
|
|
|
|
Reprocess Bracket
|
Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator (#242)
* Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator, fixes #127
- New `groupStageMatches` table for recording group play results (W/D/L, scores, matchday, schedule)
- `computeGroupStandings()` model function: pts → GD → GF → name tiebreaker ordering
- `GroupStageStandings` component showing all 12 groups with standings table and manager column
- Admin bracket UI: group match score entry, per-group standings, "Recalculate Floors" action
- `WorldCupSimulator`: 50k Monte Carlo covering group stage + best-8 3rd-place + knockout + 3rd place game
- Fuzzy name matching for national team Elo lookup (exact → substring → word-overlap), warns on miss
- Partial group completion: completed matches replayed with real scores, remaining matches simulated
- Elo priority: admin-entered sourceElo > futures odds converted to Elo > hardcoded national team ratings
- `fifa_48` bracket template: added Third Place Game round with `loserFeedsInto` on Semifinals
- Scoring rules: distinct 3rd/4th place for `fifa_48` (not averaged), QF losers share 5th–8th equally
- Floor scoring: SF participants guaranteed 4th (provisional), finalized after 3rd place game
- `recalculate-floors` admin action deletes and replays all results from scratch (fixes stale guard bug)
- Unique index on `(tournamentGroupId, participant1Id, participant2Id)` to prevent duplicate pairings
- Batch `findMatchesByGroupIds()` replacing N sequential queries in the sport season loader
- League home mini-standings now shows `actualPoints` (includes floor) instead of `totalPoints` only
- Elo ratings admin page supports World Cup (same bulk-import flow as snooker)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix TypeScript errors: update GroupStandingData type to use findMatchesByGroupIds
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Increase Node heap to 4GB for unit tests in CI to prevent OOM
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix OOM in CI: make WorldCupSimulator simulation count configurable for tests
Tests now pass numSimulations=500 instead of the production default of 50,000.
Six simulator tests × 50k iterations each was exhausting the 4GB heap on GitHub
Actions runners. Also reduce simGroupMatch stat tests from 50k to 5k iterations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 10:27:47 -07:00
|
|
|
|
</Button>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</CardContent>
|
2026-09-04 21:40:08 +00:00
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Re-seed AFL Wildcard winners. Advancement pairs them by ladder position on
|
|
|
|
|
|
every Wildcard result, so this is only for a bracket advanced before that
|
|
|
|
|
|
rule existed — a completed match cannot be re-submitted to re-run it. */}
|
|
|
|
|
|
{event.bracketTemplateId === "afl_10" && matches.length > 0 && (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle>Re-seed Wildcard Winners</CardTitle>
|
|
|
|
|
|
<CardDescription>
|
|
|
|
|
|
Pair the Elimination Finals by ladder position: 5th hosts the
|
|
|
|
|
|
lower-ranked Wildcard winner and 6th the higher-ranked one. Only moves
|
|
|
|
|
|
the qualifier slots — no results, scores or placements change, and
|
|
|
|
|
|
nothing is announced. Does nothing if the pairings are already right.
|
|
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<Form method="post">
|
|
|
|
|
|
<input type="hidden" name="intent" value="reseed-afl-wildcard" />
|
|
|
|
|
|
<Button type="submit" variant="outline">
|
|
|
|
|
|
Re-seed Wildcard Winners
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</CardContent>
|
2026-09-11 18:10:30 +00:00
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Fix the Semi-Final pairings. Elimination Final n feeds Semi-Final n, but
|
|
|
|
|
|
brackets advanced before that was fixed crossed the two winners, and no
|
|
|
|
|
|
admin action re-runs advancement. */}
|
|
|
|
|
|
{event.bracketTemplateId === "afl_10" && matches.length > 0 && (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle>Fix Semi-Final Pairings</CardTitle>
|
|
|
|
|
|
<CardDescription>
|
|
|
|
|
|
Feed each Elimination Final into the Semi-Final it belongs to: EF1
|
|
|
|
|
|
winner into SF1 and EF2 winner into SF2. Only moves the qualifier slots
|
|
|
|
|
|
— no results, scores or placements change, and nothing is announced.
|
|
|
|
|
|
Does nothing if the pairings are already right.
|
|
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<Form method="post">
|
|
|
|
|
|
<input type="hidden" name="intent" value="reseed-afl-semifinals" />
|
|
|
|
|
|
<Button type="submit" variant="outline">
|
|
|
|
|
|
Fix Semi-Final Pairings
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</CardContent>
|
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
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
Lay out brackets from the feeder graph
The LLWS bracket didn't read as a bracket: cards sat above games that
don't feed them, connectors joined the wrong pairs, and several games had
no line at all.
The stored data was correct — LLWS_ADVANCEMENT already matches the
official 2026 LLBWS bracket game for game. The renderer was the problem.
TreeColumns placed cards at `index * (height / roundSize)` and
ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds
only for an exact halving. The LLWS winners bracket is not one: two of
the four Opening Round games skip Winners Round 2 and go straight to the
semifinals, so those two got stranded in column one with nothing beside
them, and the halving branch drew confident, wrong connectors for the
rest.
Lay out from the graph instead. app/lib/bracket-layout.ts inverts a
template's advancement into "what fills each slot", then assigns columns
by depth from the group's final, orders each column by the parent's slot
order, and centres each card on its feeders. Counting back from the final
is what makes a printed bracket line up: a team entering late is drawn in
the column where it actually plays. This reproduces the official
International bracket exactly, and fixes Elimination Round 3, where the
official bracket prints the later game on top but match-number sort put
it below.
Because column is depth, every in-group edge spans exactly one gutter, so
connectors now draw for unplayed games too. Cards also take a fixed
height rather than stretching to fill their column, which is what made a
lone final tower over the rest.
Empty slots name their source — "Loser of Winners SF 1" rather than
"TBD". That is the only way to show the feeds crossing between the
winners and elimination brackets, which render as separate trees.
Also:
- Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer
can import it without pulling the database context into the browser
bundle; models/playoff-match re-exports it.
- Page the mobile view one group at a time, matching desktop. A whole
double-elimination phase is a DAG, not a tree, so its columns would be
arbitrary.
- Add a clear-bracket admin action. Nothing else could rewrite a match's
participants, so a mis-seeded bracket had no repair path at all.
- Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the
routing and layout tests check against one copy of the official bracket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
|
|
|
|
{/* Clear Bracket - the only escape hatch for a mis-seeded bracket. Nothing else
|
|
|
|
|
|
can rewrite a match's participants, so a wrong seeding has to be torn down
|
|
|
|
|
|
and rebuilt via the setup form below, which reappears once this runs. */}
|
|
|
|
|
|
{matches.length > 0 && (
|
|
|
|
|
|
<Card className="border-destructive/40">
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle>Clear Bracket</CardTitle>
|
|
|
|
|
|
<CardDescription>
|
|
|
|
|
|
Delete every match in this bracket so it can be set up again from
|
Only derive feeders where the halving rule actually holds
Review caught that the generic feeder rule was being applied to templates
that route by their own logic. It was harmless as dead code; driving the
renderer with it made several brackets worse than before.
The rule pairs rounds by array order and assumes match n is fed by 2n-1
and 2n. That describes advanceWinnerTemplate, not every bracket:
- afl_10's Wildcard Round feeds the Elimination Finals, skipping the round
listed next to it, so array order fabricated the entire chain and drew
ten wrong connectors contradicting advanceAFLWinner.
- fifa_48's Third Place Game sits between the Semifinals and the Finals,
so the Finals came out fed by the third place game. Once BracketTreeView
filtered the consolation round out, the group had three roots and the
whole World Cup bracket rendered with no connectors at all.
- ncaa_68 labelled Round of 64 #1/#2 with First Four feeds that
advanceFirstFourWinner doesn't use.
- nba_20's play-in halves in size but pairs the 7v8 loser with the 9v10
winner.
Follow each round's declared feedsInto, and derive edges only where the
round halves exactly — the condition under which the generic ceil(n/2)
mapping is true. Bespoke transitions that happen to halve are named
explicitly. Slots left without a feeder read TBD, which is honest.
Dropping those edges sends the group to the fallback, so the fallback now
has to keep drawing what those brackets already drew: halving U-shapes by
round size, and winner tracing through irregular shapes. Previously it
drew nothing, which also silently removed every connector from brackets
with no bracketTemplateId.
Also from review:
- clear-bracket deleted seasonParticipantResults for the entire sports
season with no rebuild. That table is keyed by season, not event, so it
wiped placements for every other event in the season — permanently
zeroing standings on a finalized qualifying season. Delete only the
matches and point the admin at Reprocess Bracket, which rebuilds
placements correctly.
- The clear-bracket form sent confirm=true from a hidden field, making the
server's completed-match guard unreachable. It's a checkbox now, so the
guard is real, including without JS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 18:18:05 +00:00
|
|
|
|
scratch. Use this when the wrong participants were seeded. Placements
|
|
|
|
|
|
are left alone — run Reprocess Bracket after rebuilding to clear any
|
|
|
|
|
|
that the discarded results produced.
|
Lay out brackets from the feeder graph
The LLWS bracket didn't read as a bracket: cards sat above games that
don't feed them, connectors joined the wrong pairs, and several games had
no line at all.
The stored data was correct — LLWS_ADVANCEMENT already matches the
official 2026 LLBWS bracket game for game. The renderer was the problem.
TreeColumns placed cards at `index * (height / roundSize)` and
ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds
only for an exact halving. The LLWS winners bracket is not one: two of
the four Opening Round games skip Winners Round 2 and go straight to the
semifinals, so those two got stranded in column one with nothing beside
them, and the halving branch drew confident, wrong connectors for the
rest.
Lay out from the graph instead. app/lib/bracket-layout.ts inverts a
template's advancement into "what fills each slot", then assigns columns
by depth from the group's final, orders each column by the parent's slot
order, and centres each card on its feeders. Counting back from the final
is what makes a printed bracket line up: a team entering late is drawn in
the column where it actually plays. This reproduces the official
International bracket exactly, and fixes Elimination Round 3, where the
official bracket prints the later game on top but match-number sort put
it below.
Because column is depth, every in-group edge spans exactly one gutter, so
connectors now draw for unplayed games too. Cards also take a fixed
height rather than stretching to fill their column, which is what made a
lone final tower over the rest.
Empty slots name their source — "Loser of Winners SF 1" rather than
"TBD". That is the only way to show the feeds crossing between the
winners and elimination brackets, which render as separate trees.
Also:
- Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer
can import it without pulling the database context into the browser
bundle; models/playoff-match re-exports it.
- Page the mobile view one group at a time, matching desktop. A whole
double-elimination phase is a DAG, not a tree, so its columns would be
arbitrary.
- Add a clear-bracket admin action. Nothing else could rewrite a match's
participants, so a mis-seeded bracket had no repair path at all.
- Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the
routing and layout tests check against one copy of the official bracket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<Form
|
|
|
|
|
|
method="post"
|
Only derive feeders where the halving rule actually holds
Review caught that the generic feeder rule was being applied to templates
that route by their own logic. It was harmless as dead code; driving the
renderer with it made several brackets worse than before.
The rule pairs rounds by array order and assumes match n is fed by 2n-1
and 2n. That describes advanceWinnerTemplate, not every bracket:
- afl_10's Wildcard Round feeds the Elimination Finals, skipping the round
listed next to it, so array order fabricated the entire chain and drew
ten wrong connectors contradicting advanceAFLWinner.
- fifa_48's Third Place Game sits between the Semifinals and the Finals,
so the Finals came out fed by the third place game. Once BracketTreeView
filtered the consolation round out, the group had three roots and the
whole World Cup bracket rendered with no connectors at all.
- ncaa_68 labelled Round of 64 #1/#2 with First Four feeds that
advanceFirstFourWinner doesn't use.
- nba_20's play-in halves in size but pairs the 7v8 loser with the 9v10
winner.
Follow each round's declared feedsInto, and derive edges only where the
round halves exactly — the condition under which the generic ceil(n/2)
mapping is true. Bespoke transitions that happen to halve are named
explicitly. Slots left without a feeder read TBD, which is honest.
Dropping those edges sends the group to the fallback, so the fallback now
has to keep drawing what those brackets already drew: halving U-shapes by
round size, and winner tracing through irregular shapes. Previously it
drew nothing, which also silently removed every connector from brackets
with no bracketTemplateId.
Also from review:
- clear-bracket deleted seasonParticipantResults for the entire sports
season with no rebuild. That table is keyed by season, not event, so it
wiped placements for every other event in the season — permanently
zeroing standings on a finalized qualifying season. Delete only the
matches and point the admin at Reprocess Bracket, which rebuilds
placements correctly.
- The clear-bracket form sent confirm=true from a hidden field, making the
server's completed-match guard unreachable. It's a checkbox now, so the
guard is real, including without JS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 18:18:05 +00:00
|
|
|
|
className="space-y-3"
|
Lay out brackets from the feeder graph
The LLWS bracket didn't read as a bracket: cards sat above games that
don't feed them, connectors joined the wrong pairs, and several games had
no line at all.
The stored data was correct — LLWS_ADVANCEMENT already matches the
official 2026 LLBWS bracket game for game. The renderer was the problem.
TreeColumns placed cards at `index * (height / roundSize)` and
ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds
only for an exact halving. The LLWS winners bracket is not one: two of
the four Opening Round games skip Winners Round 2 and go straight to the
semifinals, so those two got stranded in column one with nothing beside
them, and the halving branch drew confident, wrong connectors for the
rest.
Lay out from the graph instead. app/lib/bracket-layout.ts inverts a
template's advancement into "what fills each slot", then assigns columns
by depth from the group's final, orders each column by the parent's slot
order, and centres each card on its feeders. Counting back from the final
is what makes a printed bracket line up: a team entering late is drawn in
the column where it actually plays. This reproduces the official
International bracket exactly, and fixes Elimination Round 3, where the
official bracket prints the later game on top but match-number sort put
it below.
Because column is depth, every in-group edge spans exactly one gutter, so
connectors now draw for unplayed games too. Cards also take a fixed
height rather than stretching to fill their column, which is what made a
lone final tower over the rest.
Empty slots name their source — "Loser of Winners SF 1" rather than
"TBD". That is the only way to show the feeds crossing between the
winners and elimination brackets, which render as separate trees.
Also:
- Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer
can import it without pulling the database context into the browser
bundle; models/playoff-match re-exports it.
- Page the mobile view one group at a time, matching desktop. A whole
double-elimination phase is a DAG, not a tree, so its columns would be
arbitrary.
- Add a clear-bracket admin action. Nothing else could rewrite a match's
participants, so a mis-seeded bracket had no repair path at all.
- Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the
routing and layout tests check against one copy of the official bracket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
|
|
|
|
onSubmit={(e) => {
|
|
|
|
|
|
if (
|
|
|
|
|
|
!confirm(
|
Only derive feeders where the halving rule actually holds
Review caught that the generic feeder rule was being applied to templates
that route by their own logic. It was harmless as dead code; driving the
renderer with it made several brackets worse than before.
The rule pairs rounds by array order and assumes match n is fed by 2n-1
and 2n. That describes advanceWinnerTemplate, not every bracket:
- afl_10's Wildcard Round feeds the Elimination Finals, skipping the round
listed next to it, so array order fabricated the entire chain and drew
ten wrong connectors contradicting advanceAFLWinner.
- fifa_48's Third Place Game sits between the Semifinals and the Finals,
so the Finals came out fed by the third place game. Once BracketTreeView
filtered the consolation round out, the group had three roots and the
whole World Cup bracket rendered with no connectors at all.
- ncaa_68 labelled Round of 64 #1/#2 with First Four feeds that
advanceFirstFourWinner doesn't use.
- nba_20's play-in halves in size but pairs the 7v8 loser with the 9v10
winner.
Follow each round's declared feedsInto, and derive edges only where the
round halves exactly — the condition under which the generic ceil(n/2)
mapping is true. Bespoke transitions that happen to halve are named
explicitly. Slots left without a feeder read TBD, which is honest.
Dropping those edges sends the group to the fallback, so the fallback now
has to keep drawing what those brackets already drew: halving U-shapes by
round size, and winner tracing through irregular shapes. Previously it
drew nothing, which also silently removed every connector from brackets
with no bracketTemplateId.
Also from review:
- clear-bracket deleted seasonParticipantResults for the entire sports
season with no rebuild. That table is keyed by season, not event, so it
wiped placements for every other event in the season — permanently
zeroing standings on a finalized qualifying season. Delete only the
matches and point the admin at Reprocess Bracket, which rebuilds
placements correctly.
- The clear-bracket form sent confirm=true from a hidden field, making the
server's completed-match guard unreachable. It's a checkbox now, so the
guard is real, including without JS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 18:18:05 +00:00
|
|
|
|
`Delete all ${matches.length} match(es) in this bracket? Recorded results will be lost.`
|
Lay out brackets from the feeder graph
The LLWS bracket didn't read as a bracket: cards sat above games that
don't feed them, connectors joined the wrong pairs, and several games had
no line at all.
The stored data was correct — LLWS_ADVANCEMENT already matches the
official 2026 LLBWS bracket game for game. The renderer was the problem.
TreeColumns placed cards at `index * (height / roundSize)` and
ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds
only for an exact halving. The LLWS winners bracket is not one: two of
the four Opening Round games skip Winners Round 2 and go straight to the
semifinals, so those two got stranded in column one with nothing beside
them, and the halving branch drew confident, wrong connectors for the
rest.
Lay out from the graph instead. app/lib/bracket-layout.ts inverts a
template's advancement into "what fills each slot", then assigns columns
by depth from the group's final, orders each column by the parent's slot
order, and centres each card on its feeders. Counting back from the final
is what makes a printed bracket line up: a team entering late is drawn in
the column where it actually plays. This reproduces the official
International bracket exactly, and fixes Elimination Round 3, where the
official bracket prints the later game on top but match-number sort put
it below.
Because column is depth, every in-group edge spans exactly one gutter, so
connectors now draw for unplayed games too. Cards also take a fixed
height rather than stretching to fill their column, which is what made a
lone final tower over the rest.
Empty slots name their source — "Loser of Winners SF 1" rather than
"TBD". That is the only way to show the feeds crossing between the
winners and elimination brackets, which render as separate trees.
Also:
- Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer
can import it without pulling the database context into the browser
bundle; models/playoff-match re-exports it.
- Page the mobile view one group at a time, matching desktop. A whole
double-elimination phase is a DAG, not a tree, so its columns would be
arbitrary.
- Add a clear-bracket admin action. Nothing else could rewrite a match's
participants, so a mis-seeded bracket had no repair path at all.
- Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the
routing and layout tests check against one copy of the official bracket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
|
|
|
|
)
|
|
|
|
|
|
) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<input type="hidden" name="intent" value="clear-bracket" />
|
Only derive feeders where the halving rule actually holds
Review caught that the generic feeder rule was being applied to templates
that route by their own logic. It was harmless as dead code; driving the
renderer with it made several brackets worse than before.
The rule pairs rounds by array order and assumes match n is fed by 2n-1
and 2n. That describes advanceWinnerTemplate, not every bracket:
- afl_10's Wildcard Round feeds the Elimination Finals, skipping the round
listed next to it, so array order fabricated the entire chain and drew
ten wrong connectors contradicting advanceAFLWinner.
- fifa_48's Third Place Game sits between the Semifinals and the Finals,
so the Finals came out fed by the third place game. Once BracketTreeView
filtered the consolation round out, the group had three roots and the
whole World Cup bracket rendered with no connectors at all.
- ncaa_68 labelled Round of 64 #1/#2 with First Four feeds that
advanceFirstFourWinner doesn't use.
- nba_20's play-in halves in size but pairs the 7v8 loser with the 9v10
winner.
Follow each round's declared feedsInto, and derive edges only where the
round halves exactly — the condition under which the generic ceil(n/2)
mapping is true. Bespoke transitions that happen to halve are named
explicitly. Slots left without a feeder read TBD, which is honest.
Dropping those edges sends the group to the fallback, so the fallback now
has to keep drawing what those brackets already drew: halving U-shapes by
round size, and winner tracing through irregular shapes. Previously it
drew nothing, which also silently removed every connector from brackets
with no bracketTemplateId.
Also from review:
- clear-bracket deleted seasonParticipantResults for the entire sports
season with no rebuild. That table is keyed by season, not event, so it
wiped placements for every other event in the season — permanently
zeroing standings on a finalized qualifying season. Delete only the
matches and point the admin at Reprocess Bracket, which rebuilds
placements correctly.
- The clear-bracket form sent confirm=true from a hidden field, making the
server's completed-match guard unreachable. It's a checkbox now, so the
guard is real, including without JS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 18:18:05 +00:00
|
|
|
|
{/* The server refuses to discard completed matches unless this is
|
|
|
|
|
|
checked. Sending it unconditionally from a hidden field would make
|
|
|
|
|
|
that guard unreachable, including for a submit without JS. */}
|
|
|
|
|
|
{matches.some((m: { isComplete: boolean }) => m.isComplete) && (
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
id="confirm-clear-bracket"
|
|
|
|
|
|
name="confirm"
|
|
|
|
|
|
value="true"
|
|
|
|
|
|
className="h-4 w-4"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor="confirm-clear-bracket" className="font-normal">
|
|
|
|
|
|
Yes, discard the results already recorded in this bracket
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
Lay out brackets from the feeder graph
The LLWS bracket didn't read as a bracket: cards sat above games that
don't feed them, connectors joined the wrong pairs, and several games had
no line at all.
The stored data was correct — LLWS_ADVANCEMENT already matches the
official 2026 LLBWS bracket game for game. The renderer was the problem.
TreeColumns placed cards at `index * (height / roundSize)` and
ConnectorColumn assumed matches 2k and 2k+1 feed match k, which holds
only for an exact halving. The LLWS winners bracket is not one: two of
the four Opening Round games skip Winners Round 2 and go straight to the
semifinals, so those two got stranded in column one with nothing beside
them, and the halving branch drew confident, wrong connectors for the
rest.
Lay out from the graph instead. app/lib/bracket-layout.ts inverts a
template's advancement into "what fills each slot", then assigns columns
by depth from the group's final, orders each column by the parent's slot
order, and centres each card on its feeders. Counting back from the final
is what makes a printed bracket line up: a team entering late is drawn in
the column where it actually plays. This reproduces the official
International bracket exactly, and fixes Elimination Round 3, where the
official bracket prints the later game on top but match-number sort put
it below.
Because column is depth, every in-group edge spans exactly one gutter, so
connectors now draw for unplayed games too. Cards also take a fixed
height rather than stretching to fill their column, which is what made a
lone final tower over the rest.
Empty slots name their source — "Loser of Winners SF 1" rather than
"TBD". That is the only way to show the feeds crossing between the
winners and elimination brackets, which render as separate trees.
Also:
- Move the LLWS routing table to app/lib/llws-bracket.ts so the renderer
can import it without pulling the database context into the browser
bundle; models/playoff-match re-exports it.
- Page the mobile view one group at a time, matching desktop. A whole
double-elimination phase is a DAG, not a tree, so its columns would be
arbitrary.
- Add a clear-bracket admin action. Nothing else could rewrite a match's
participants, so a mis-seeded bracket had no repair path at all.
- Lift the PDF transcription into app/test/fixtures/llws-bracket.ts so the
routing and layout tests check against one copy of the official bracket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnzbrCHoM8ESbtbDamaqFb
2026-08-21 17:50:59 +00:00
|
|
|
|
<Button type="submit" variant="destructive">
|
|
|
|
|
|
Clear Bracket
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-02-14 22:30:12 -08:00
|
|
|
|
{/* ====== SETUP PHASE ====== */}
|
|
|
|
|
|
{showSetup && (
|
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
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle>Generate Bracket</CardTitle>
|
|
|
|
|
|
<CardDescription>
|
|
|
|
|
|
Create the bracket structure for this playoff event
|
|
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<Form method="post" className="space-y-4">
|
2026-02-14 22:30:12 -08:00
|
|
|
|
{/* Intent depends on whether this is a group-stage template */}
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="hidden"
|
|
|
|
|
|
name="intent"
|
|
|
|
|
|
value={hasGroupStage ? "generate-groups" : "generate-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
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
2025-11-04 22:09:44 -08:00
|
|
|
|
<Label htmlFor="templateId">Bracket Template</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
name="templateId"
|
|
|
|
|
|
value={selectedTemplate}
|
|
|
|
|
|
onValueChange={handleTemplateChange}
|
|
|
|
|
|
required
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger id="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
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
2025-11-04 22:09:44 -08:00
|
|
|
|
{templates.map((t) => (
|
|
|
|
|
|
<SelectItem key={t.id} value={t.id}>
|
|
|
|
|
|
{t.name} - {t.totalTeams} teams
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
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
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
2025-11-04 22:09:44 -08:00
|
|
|
|
{template && (
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
2026-02-14 22:30:12 -08:00
|
|
|
|
{template.groupStage ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
Groups: {template.groupStage.groupCount} groups of {template.groupStage.teamsPerGroup}
|
|
|
|
|
|
{" | "}Knockout: {template.rounds.map(r => r.name).join(" \u2192 ")}
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>Rounds: {template.rounds.map(r => r.name).join(" \u2192 ")}</>
|
|
|
|
|
|
)}
|
2025-11-04 22:09:44 -08:00
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
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
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-14 22:30:12 -08:00
|
|
|
|
{/* Group-stage template: show group assignments */}
|
|
|
|
|
|
{hasGroupStage && template?.groupStage && (
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
<Label>Assign Participants to Groups</Label>
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
Assign {template.totalTeams} participants across {template.groupStage.groupCount} groups
|
|
|
|
|
|
({template.groupStage.teamsPerGroup} per group).
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
|
|
|
|
{template.groupStage.groupLabels.map((label, groupIndex) => (
|
|
|
|
|
|
<Card key={label} className="border-dashed">
|
|
|
|
|
|
<CardHeader className="py-3 px-4">
|
|
|
|
|
|
<CardTitle className="text-base">Group {label}</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="px-4 pb-4 space-y-2">
|
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196)
* Fix no-shadow and consistent-function-scoping lint violations
Resolves all 11 no-shadow and 16 consistent-function-scoping oxlint
warnings and promotes both rules to errors in .oxlintrc.json.
no-shadow: renamed Drizzle callback params (sports→s, matches→m,
seasons→s) to avoid shadowing outer imports; removed shadowed
destructures (eq, inArray) from where callbacks; renamed inner
template→bracketTemplate, prev→currentTimers, season→ss, name→teamName
(with name: teamName fix to preserve semantics).
consistent-function-scoping: moved formatDate, getRankBadge,
getMovementIndicator, getPositionBadge, getStatusBadge, toDateStr,
elo (×2), weightedPick, sortByMatchNumber (×2) to module scope;
moved formatTime (×2), isValidLeagueName, getDraftTimes,
makeSeasonQueues to file scope in test files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix no-non-null-assertion lint violations and promote to error
Eliminates all 208 no-non-null-assertion warnings across 38 files.
Promotes typescript/no-non-null-assertion from warn to error in
.oxlintrc.json.
Fix patterns applied:
- Map.get(key)! after .has() check → extract with get() + null guard
- Map.get(key)! on pre-populated count maps → ?? 0 default
- .set(id, map.get(id)! + 1) increment → ?? 0 before adding
- participant1Id!/participant2Id! on DB matches → ?? "" fallback
- array.find()! in tests → guard + throw or expect().toBeDefined()
- bracketTemplateCache.get(id)! → null guard extract
- Various nullable field accesses → optional chain or ?? default
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix prefer-add-event-listener, no-unassigned-import, require-module-specifiers
Resolves all 9 remaining non-console lint warnings and promotes all
three rules to errors in .oxlintrc.json.
- prefer-add-event-listener: converted onchange/onclick/onload
assignments to addEventListener in useDraftNotifications.ts and
admin.data-sync.tsx; stored changeHandler ref for proper cleanup
with removeEventListener
- no-unassigned-import: configured rule with allow list for legitimate
side-effect imports (*.css, @testing-library/jest-dom,
@testing-library/cypress/add-commands)
- require-module-specifiers: removed redundant `export {}` from
cypress/support/e2e.ts (file already has an import)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix TypeScript errors from no-non-null-assertion fixes
Two fixes introduced by the non-null assertion cleanup produced type
errors:
- scoring-event.ts: `?? ""` was wrong type for a participant object map;
restructured to explicit null guards so TypeScript can narrow correctly
- standings-sync/index.ts: `?? null` after name-match lookup lost the
truthy guarantee, causing TS18047 on the write-back block; added
`participant &&` guard before accessing its properties
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add npm run typecheck as Stop hook in Claude settings
Runs a full project typecheck at the end of each Claude turn so type
errors surface as feedback before the next message.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 10:59:51 -07:00
|
|
|
|
{[...Array(template.groupStage?.teamsPerGroup ?? 0)].map((_, teamIndex) => {
|
|
|
|
|
|
const seedIndex = groupIndex * (template.groupStage?.teamsPerGroup ?? 0) + teamIndex;
|
2026-02-14 22:30:12 -08:00
|
|
|
|
const availableParticipants = getAvailableParticipants(seedIndex);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={seedIndex}>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="hidden"
|
|
|
|
|
|
name={`participant${seedIndex}`}
|
|
|
|
|
|
value={selectedParticipants[seedIndex] || ""}
|
|
|
|
|
|
required
|
|
|
|
|
|
/>
|
|
|
|
|
|
<ParticipantSelector
|
|
|
|
|
|
participants={availableParticipants}
|
|
|
|
|
|
value={selectedParticipants[seedIndex] || ""}
|
|
|
|
|
|
onValueChange={(value) => handleParticipantChange(seedIndex, value)}
|
|
|
|
|
|
placeholder={`Team ${teamIndex + 1}...`}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-03-15 21:52:47 -07:00
|
|
|
|
{/* Non-group-stage template: region-grouped or flat seeded list */}
|
|
|
|
|
|
{!hasGroupStage && hasRegions && regionSlotMap && (
|
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
|
|
|
|
|
|
|
{/* ── Region configuration ─────────────────────────────── */}
|
|
|
|
|
|
<div className="border rounded-lg p-4 space-y-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h4 className="font-semibold text-sm">Configure Regions</h4>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
|
|
|
|
Set each region's name and which seed slots are play-in games.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{regionConfig.map((region, r) => (
|
2026-03-21 09:44:05 -07:00
|
|
|
|
// eslint-disable-next-line react/no-array-index-key
|
2026-03-15 21:52:47 -07:00
|
|
|
|
<div key={r} className="flex flex-wrap items-center gap-2">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
className="w-36 h-8 text-sm"
|
|
|
|
|
|
value={region.name}
|
|
|
|
|
|
onChange={(e) => updateRegionName(r, e.target.value)}
|
|
|
|
|
|
placeholder={`Region ${r + 1}`}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{region.playInSeeds.map((seed, pi) => (
|
2026-03-21 09:44:05 -07:00
|
|
|
|
// eslint-disable-next-line react/no-array-index-key
|
2026-03-15 21:52:47 -07:00
|
|
|
|
<div key={pi} className="flex items-center gap-1">
|
|
|
|
|
|
<span className="text-xs text-muted-foreground">play-in seed:</span>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={String(seed)}
|
|
|
|
|
|
onValueChange={(val) => updatePlayInSeed(r, pi, parseInt(val, 10))}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-8 w-16 text-sm">
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{ALL_16_SEEDS.map((s) => (
|
|
|
|
|
|
<SelectItem key={s} value={String(s)}>{s}</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
className="h-7 w-7 p-0 text-muted-foreground"
|
|
|
|
|
|
onClick={() => removePlayIn(r, pi)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<X className="h-3 w-3" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{region.playInSeeds.length < 2 && (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
|
onClick={() => addPlayIn(r)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Plus className="h-3 w-3 mr-1" />
|
|
|
|
|
|
Add play-in
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{/* Hidden fields so server receives the region config */}
|
|
|
|
|
|
{regionConfig.map((region, r) => (
|
2026-03-21 09:44:05 -07:00
|
|
|
|
// eslint-disable-next-line react/no-array-index-key
|
2026-03-15 21:52:47 -07:00
|
|
|
|
<Fragment key={r}>
|
|
|
|
|
|
<input type="hidden" name={`regionName${r}`} value={region.name} />
|
|
|
|
|
|
<input type="hidden" name={`regionPlayInSeeds${r}`} value={region.playInSeeds.join(",")} />
|
|
|
|
|
|
</Fragment>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* ── Seed entry per region ─────────────────────────────── */}
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
Select participants by region. Use search to find teams quickly.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
|
{effectiveRegions.map((region, r) => {
|
|
|
|
|
|
const directOffset = regionSlotMap.directOffsets[r];
|
|
|
|
|
|
return (
|
2026-03-21 09:44:05 -07:00
|
|
|
|
// eslint-disable-next-line react/no-array-index-key
|
2026-03-15 21:52:47 -07:00
|
|
|
|
<div key={r} className="border rounded-lg p-4 space-y-2">
|
|
|
|
|
|
<h4 className="font-semibold text-sm">{region.name || `Region ${r + 1}`}</h4>
|
|
|
|
|
|
{region.directSeeds.map((seedNum, pos) => {
|
|
|
|
|
|
const globalIdx = directOffset + pos;
|
|
|
|
|
|
const opponentSeed = 17 - seedNum;
|
|
|
|
|
|
const opponentIsPlayIn = region.playIns.some((pi) => pi.seedSlot === opponentSeed);
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={seedNum} className="flex items-center gap-2">
|
|
|
|
|
|
<Label className="w-16 text-xs text-muted-foreground shrink-0">
|
|
|
|
|
|
Seed {seedNum}
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="hidden"
|
|
|
|
|
|
name={`participant${globalIdx}`}
|
|
|
|
|
|
value={selectedParticipants[globalIdx] || ""}
|
|
|
|
|
|
required
|
|
|
|
|
|
/>
|
|
|
|
|
|
<ParticipantSelector
|
|
|
|
|
|
participants={getAvailableParticipants(globalIdx)}
|
|
|
|
|
|
value={selectedParticipants[globalIdx] || ""}
|
|
|
|
|
|
onValueChange={(value) => handleParticipantChange(globalIdx, value)}
|
|
|
|
|
|
placeholder={`Seed ${seedNum}…`}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{opponentIsPlayIn && (
|
|
|
|
|
|
<span className="text-xs text-muted-foreground shrink-0">← faces play-in</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
{region.playIns.length > 0 && (
|
|
|
|
|
|
<p className="text-xs text-muted-foreground pt-1 border-t">
|
|
|
|
|
|
{region.playIns.map((pi) => `Seed ${pi.seedSlot}`).join(" & ")} via play-in ↓
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* ── Play-in team entry ─────────────────────────────────── */}
|
|
|
|
|
|
{regionSlotMap.playInOffsets.length > 0 && (
|
|
|
|
|
|
<div className="border rounded-lg p-4 space-y-4">
|
|
|
|
|
|
<h4 className="font-semibold text-sm">Play-In Teams</h4>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
|
Enter both teams for each play-in game. The winner advances to the main bracket.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{regionSlotMap.playInOffsets.map((pi, ffIdx) => {
|
|
|
|
|
|
const regionName = effectiveRegions[pi.regionIndex]?.name || `Region ${pi.regionIndex + 1}`;
|
|
|
|
|
|
const idx1 = pi.startIndex;
|
|
|
|
|
|
const idx2 = pi.startIndex + 1;
|
|
|
|
|
|
return (
|
2026-03-21 09:44:05 -07:00
|
|
|
|
<div key={`${pi.regionIndex}-${pi.seedSlot}`} className="space-y-2">
|
2026-03-15 21:52:47 -07:00
|
|
|
|
<p className="text-xs font-medium text-muted-foreground">
|
|
|
|
|
|
Play-In {ffIdx + 1} — {regionName} {pi.seedSlot}-seed
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{[idx1, idx2].map((globalIdx, slot) => (
|
|
|
|
|
|
<div key={globalIdx} className="flex items-center gap-2">
|
|
|
|
|
|
<Label className="w-16 text-xs text-muted-foreground shrink-0">
|
|
|
|
|
|
Team {slot + 1}
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="hidden"
|
|
|
|
|
|
name={`participant${globalIdx}`}
|
|
|
|
|
|
value={selectedParticipants[globalIdx] || ""}
|
|
|
|
|
|
required
|
|
|
|
|
|
/>
|
|
|
|
|
|
<ParticipantSelector
|
|
|
|
|
|
participants={getAvailableParticipants(globalIdx)}
|
|
|
|
|
|
value={selectedParticipants[globalIdx] || ""}
|
|
|
|
|
|
onValueChange={(value) => handleParticipantChange(globalIdx, value)}
|
|
|
|
|
|
placeholder={`${regionName} ${pi.seedSlot}-seed play-in team ${slot + 1}…`}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Non-group-stage, non-region template: flat seeded list */}
|
|
|
|
|
|
{!hasGroupStage && !hasRegions && (
|
2026-02-14 22:30:12 -08:00
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label>Select Participants (in order)</Label>
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
Select {template?.totalTeams || 8} participants for the bracket. Use search to quickly find participants.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{[...Array(template?.totalTeams || 8)].map((_, i) => {
|
|
|
|
|
|
const availableParticipants = getAvailableParticipants(i);
|
2026-04-13 00:51:53 -04:00
|
|
|
|
const slotLabel = template?.participantLabels?.[i] ?? `Seed ${i + 1}`;
|
2026-02-14 22:30:12 -08:00
|
|
|
|
|
|
|
|
|
|
return (
|
2026-03-21 09:44:05 -07:00
|
|
|
|
// eslint-disable-next-line react/no-array-index-key
|
2026-02-14 22:30:12 -08:00
|
|
|
|
<div key={i} className="flex items-center gap-2">
|
Fix final rankings for double-elimination brackets
Review of the llws_20 bracket turned up two display bugs in the shared
ranking code, both triggered by the template setting loserFeedsInto on
every winners-bracket round.
findConsolationRound took the FIRST round with loserFeedsInto and
treated its target as a third-place game. That is sound for fifa_48,
whose only such round is the Semifinals, but llws_20 uses loserFeedsInto
to route losers into the elimination bracket — so "Elimination Round 1"
was being read as the consolation game. Its four matches were then
placed as exact positions inside the Opening Round tier, corrupting the
final rankings list. A consolation round now has to be terminal: its
winner plays no further game, which is precisely what lets its result
split two exact positions.
The rank walk also advanced by a round's match count on the assumption
that every match places its loser. That holds in single elimination and
must be kept for undecided rounds — four semifinalists occupy positions
1-4 whether or not the games have been played — but a winners-bracket
loss places nobody, since the loser drops into the elimination bracket
and is ranked by whatever knocks them out later. Those rounds consumed
positions they never filled, so a 20-team bracket ranked its last teams
T23. Rounds whose losers are placed later now consume nothing, leaving
LLWS at 1, T2, 3, 4, T5, T7, T9, T13, T17 for the full 20-team field.
Single-elimination templates are unaffected; afl_10's sub-8th ranks were
inflated the same way and are now correct too.
Also from review: the llws_20 participant labels were 21-24 characters
in an 80px fixed-width admin column and would have wrapped to three
lines across all 20 rows. Shortened them and widened the column.
Tests play a full 20-team tournament through the real advancement map
and assert the resulting rank labels.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAxqBVzfmKJe6WQ6VF9guj
2026-08-03 18:33:22 +00:00
|
|
|
|
<Label className="w-28 text-sm text-muted-foreground shrink-0">
|
2026-04-13 00:51:53 -04:00
|
|
|
|
{slotLabel}
|
2026-02-14 22:30:12 -08:00
|
|
|
|
</Label>
|
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="hidden"
|
|
|
|
|
|
name={`participant${i}`}
|
|
|
|
|
|
value={selectedParticipants[i] || ""}
|
|
|
|
|
|
required
|
|
|
|
|
|
/>
|
|
|
|
|
|
<ParticipantSelector
|
|
|
|
|
|
participants={availableParticipants}
|
|
|
|
|
|
value={selectedParticipants[i] || ""}
|
|
|
|
|
|
onValueChange={(value) => handleParticipantChange(i, value)}
|
2026-04-13 00:51:53 -04:00
|
|
|
|
placeholder={`Select ${slotLabel.toLowerCase()}...`}
|
2026-02-14 22:30:12 -08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-11-08 21:36:29 -08:00
|
|
|
|
</div>
|
2026-02-14 22:30:12 -08:00
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
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
|
|
|
|
|
|
|
|
|
|
<Button type="submit" className="w-full">
|
|
|
|
|
|
<Plus className="mr-2 h-4 w-4" />
|
2026-02-14 22:30:12 -08:00
|
|
|
|
{hasGroupStage ? "Generate Groups & Bracket" : "Generate 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
|
|
|
|
</Button>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-02-14 22:30:12 -08:00
|
|
|
|
{/* ====== GROUP STAGE MANAGEMENT (Phase 2) ====== */}
|
|
|
|
|
|
{isGroupStageEvent && !knockoutPopulated && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{/* Group Stage Summary */}
|
|
|
|
|
|
{groupStageStats && (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle>Group Stage</CardTitle>
|
|
|
|
|
|
<CardDescription>
|
|
|
|
|
|
Manage group stage eliminations. Mark teams as eliminated, then assign advancing teams to the knockout bracket.
|
|
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<div className="flex gap-4 text-sm">
|
|
|
|
|
|
<Badge variant="secondary">
|
|
|
|
|
|
{groupStageStats.total} total teams
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
<Badge variant="destructive">
|
|
|
|
|
|
{groupStageStats.eliminated} eliminated
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
<Badge variant="default">
|
|
|
|
|
|
{groupStageStats.advancing} advancing
|
|
|
|
|
|
</Badge>
|
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
|
|
|
|
</div>
|
2026-02-14 22:30:12 -08:00
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Group Cards */}
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
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
|
|
|
|
{tournamentGroups.map((group) => {
|
|
|
|
|
|
const members = group.members || [];
|
|
|
|
|
|
const groupMatches = group.groupMatches || [];
|
|
|
|
|
|
const standings = computeGroupStandings(
|
|
|
|
|
|
members
|
|
|
|
|
|
.filter((m) => m.participant !== null && m.participant !== undefined)
|
|
|
|
|
|
.map((m) => ({
|
|
|
|
|
|
participantId: m.participant?.id ?? "",
|
|
|
|
|
|
participantName: m.participant?.name ?? "",
|
|
|
|
|
|
})),
|
|
|
|
|
|
groupMatches.map((m) => ({
|
|
|
|
|
|
participant1Id: m.participant1Id,
|
|
|
|
|
|
participant2Id: m.participant2Id,
|
|
|
|
|
|
participant1Score: m.participant1Score,
|
|
|
|
|
|
participant2Score: m.participant2Score,
|
|
|
|
|
|
isComplete: m.isComplete,
|
|
|
|
|
|
}))
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Card key={group.id}>
|
|
|
|
|
|
<CardHeader className="py-3 px-4">
|
|
|
|
|
|
<CardTitle className="text-base">Group {group.groupName}</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="px-4 pb-4 space-y-3">
|
|
|
|
|
|
{/* Standings table */}
|
|
|
|
|
|
{standings.length > 0 && (
|
|
|
|
|
|
<div className="overflow-x-auto">
|
|
|
|
|
|
<table className="w-full text-xs">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr className="border-b text-muted-foreground">
|
|
|
|
|
|
<th className="text-left pb-1 font-medium">Team</th>
|
|
|
|
|
|
<th className="text-center pb-1 font-medium w-6">P</th>
|
|
|
|
|
|
<th className="text-center pb-1 font-medium w-6">W</th>
|
|
|
|
|
|
<th className="text-center pb-1 font-medium w-6">D</th>
|
|
|
|
|
|
<th className="text-center pb-1 font-medium w-6">L</th>
|
|
|
|
|
|
<th className="text-center pb-1 font-medium w-8">GD</th>
|
|
|
|
|
|
<th className="text-center pb-1 font-medium w-7">Pts</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
{standings.map((row, idx) => {
|
|
|
|
|
|
const member = members.find((m) => m.participant?.id === row.participantId);
|
|
|
|
|
|
const isTop2 = idx < 2;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<tr
|
|
|
|
|
|
key={row.participantId}
|
|
|
|
|
|
className={`border-b last:border-0 ${
|
|
|
|
|
|
member?.eliminated
|
|
|
|
|
|
? "text-muted-foreground line-through"
|
|
|
|
|
|
: isTop2
|
|
|
|
|
|
? "text-green-600 dark:text-green-400 font-medium"
|
|
|
|
|
|
: ""
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<td className="py-0.5 truncate max-w-[80px]">{row.participantName}</td>
|
|
|
|
|
|
<td className="text-center">{row.played}</td>
|
|
|
|
|
|
<td className="text-center">{row.wins}</td>
|
|
|
|
|
|
<td className="text-center">{row.draws}</td>
|
|
|
|
|
|
<td className="text-center">{row.losses}</td>
|
|
|
|
|
|
<td className="text-center">{row.gd > 0 ? `+${row.gd}` : row.gd}</td>
|
|
|
|
|
|
<td className="text-center font-semibold">{row.points}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Elimination toggles */}
|
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
|
{members.map((member) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={member.id}
|
|
|
|
|
|
className={`flex items-center justify-between gap-2 p-2 rounded-md border ${
|
|
|
|
|
|
member.eliminated
|
|
|
|
|
|
? "bg-destructive/10 border-destructive/20"
|
|
|
|
|
|
: "bg-background border-border"
|
|
|
|
|
|
}`}
|
2026-02-14 22:30:12 -08:00
|
|
|
|
>
|
Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator (#242)
* Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator, fixes #127
- New `groupStageMatches` table for recording group play results (W/D/L, scores, matchday, schedule)
- `computeGroupStandings()` model function: pts → GD → GF → name tiebreaker ordering
- `GroupStageStandings` component showing all 12 groups with standings table and manager column
- Admin bracket UI: group match score entry, per-group standings, "Recalculate Floors" action
- `WorldCupSimulator`: 50k Monte Carlo covering group stage + best-8 3rd-place + knockout + 3rd place game
- Fuzzy name matching for national team Elo lookup (exact → substring → word-overlap), warns on miss
- Partial group completion: completed matches replayed with real scores, remaining matches simulated
- Elo priority: admin-entered sourceElo > futures odds converted to Elo > hardcoded national team ratings
- `fifa_48` bracket template: added Third Place Game round with `loserFeedsInto` on Semifinals
- Scoring rules: distinct 3rd/4th place for `fifa_48` (not averaged), QF losers share 5th–8th equally
- Floor scoring: SF participants guaranteed 4th (provisional), finalized after 3rd place game
- `recalculate-floors` admin action deletes and replays all results from scratch (fixes stale guard bug)
- Unique index on `(tournamentGroupId, participant1Id, participant2Id)` to prevent duplicate pairings
- Batch `findMatchesByGroupIds()` replacing N sequential queries in the sport season loader
- League home mini-standings now shows `actualPoints` (includes floor) instead of `totalPoints` only
- Elo ratings admin page supports World Cup (same bulk-import flow as snooker)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix TypeScript errors: update GroupStandingData type to use findMatchesByGroupIds
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Increase Node heap to 4GB for unit tests in CI to prevent OOM
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix OOM in CI: make WorldCupSimulator simulation count configurable for tests
Tests now pass numSimulations=500 instead of the production default of 50,000.
Six simulator tests × 50k iterations each was exhausting the 4GB heap on GitHub
Actions runners. Also reduce simGroupMatch stat tests from 50k to 5k iterations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 10:27:47 -07:00
|
|
|
|
<span
|
|
|
|
|
|
className={`text-sm truncate ${
|
|
|
|
|
|
member.eliminated ? "line-through text-muted-foreground" : ""
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{member.participant?.name || "Unknown"}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<Form method="post" className="shrink-0">
|
|
|
|
|
|
<input type="hidden" name="intent" value="toggle-elimination" />
|
|
|
|
|
|
<input type="hidden" name="memberId" value={member.id} />
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="submit"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant={member.eliminated ? "outline" : "destructive"}
|
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
|
>
|
|
|
|
|
|
{member.eliminated ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Check className="h-3 w-3 mr-1" />
|
|
|
|
|
|
Reinstate
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<X className="h-3 w-3 mr-1" />
|
|
|
|
|
|
Eliminate
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
2026-02-14 22:30:12 -08:00
|
|
|
|
</div>
|
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
|
|
|
|
|
|
|
|
|
|
{/* Match score entry */}
|
|
|
|
|
|
{groupMatches.length > 0 && (
|
|
|
|
|
|
<div className="space-y-1.5 pt-1 border-t">
|
|
|
|
|
|
<p className="text-xs font-medium text-muted-foreground">Matches</p>
|
|
|
|
|
|
{groupMatches.map((match) => (
|
|
|
|
|
|
<div key={match.id} className="text-xs space-y-1">
|
|
|
|
|
|
{/* Schedule row */}
|
2026-06-18 02:33:05 +00:00
|
|
|
|
<GroupMatchScheduleForm match={match} localTzAbbr={localTzAbbr} />
|
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
|
|
|
|
{/* Score row */}
|
|
|
|
|
|
<Form method="post" className="flex items-center gap-1">
|
|
|
|
|
|
<input type="hidden" name="intent" value="update-group-match" />
|
|
|
|
|
|
<input type="hidden" name="matchId" value={match.id} />
|
|
|
|
|
|
<span className="truncate max-w-[70px]">
|
|
|
|
|
|
{match.participant1?.name ?? "?"}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
name="participant1Score"
|
|
|
|
|
|
min={0}
|
|
|
|
|
|
defaultValue={match.participant1Score ?? ""}
|
|
|
|
|
|
className={`h-6 w-10 text-center text-xs px-1 py-0 ${match.isComplete ? "bg-muted" : ""}`}
|
|
|
|
|
|
disabled={match.isComplete}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="text-muted-foreground">–</span>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
name="participant2Score"
|
|
|
|
|
|
min={0}
|
|
|
|
|
|
defaultValue={match.participant2Score ?? ""}
|
|
|
|
|
|
className={`h-6 w-10 text-center text-xs px-1 py-0 ${match.isComplete ? "bg-muted" : ""}`}
|
|
|
|
|
|
disabled={match.isComplete}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="truncate max-w-[70px] text-right">
|
|
|
|
|
|
{match.participant2?.name ?? "?"}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
{!match.isComplete && (
|
|
|
|
|
|
<Button type="submit" size="sm" variant="default" className="h-6 px-1.5 text-xs shrink-0">
|
|
|
|
|
|
Save
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{match.isComplete && (
|
|
|
|
|
|
<Badge variant="secondary" className="h-5 text-xs px-1 shrink-0">FT</Badge>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
2026-02-14 22:30:12 -08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Knockout Assignment Section */}
|
|
|
|
|
|
{groupStageStats && groupStageStats.advancing === 32 && (
|
|
|
|
|
|
<Card className="border-blue-500">
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle>Assign Knockout Bracket</CardTitle>
|
|
|
|
|
|
<CardDescription>
|
|
|
|
|
|
32 teams are advancing. Assign them to the Round of 32 knockout bracket slots.
|
|
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<Form method="post" className="space-y-4">
|
|
|
|
|
|
<input type="hidden" name="intent" value="populate-knockout" />
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
|
{[...Array(16)].map((_, matchIndex) => {
|
|
|
|
|
|
const matchNum = matchIndex + 1;
|
|
|
|
|
|
const key1 = `match-${matchNum}-participant1Id`;
|
|
|
|
|
|
const key2 = `match-${matchNum}-participant2Id`;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Card key={matchNum} className="border-dashed">
|
|
|
|
|
|
<CardContent className="p-4 space-y-2">
|
|
|
|
|
|
<div className="text-sm font-semibold text-muted-foreground">
|
|
|
|
|
|
Match {matchNum}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="hidden"
|
|
|
|
|
|
name={key1}
|
|
|
|
|
|
value={knockoutAssignments[key1] || ""}
|
|
|
|
|
|
required
|
|
|
|
|
|
/>
|
|
|
|
|
|
<ParticipantSelector
|
|
|
|
|
|
participants={availableAdvancingMap[key1] || []}
|
|
|
|
|
|
value={knockoutAssignments[key1] || ""}
|
|
|
|
|
|
onValueChange={(value) =>
|
|
|
|
|
|
setKnockoutAssignments((prev) => ({ ...prev, [key1]: value }))
|
|
|
|
|
|
}
|
|
|
|
|
|
placeholder="Team 1..."
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div className="text-center text-xs text-muted-foreground">vs</div>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="hidden"
|
|
|
|
|
|
name={key2}
|
|
|
|
|
|
value={knockoutAssignments[key2] || ""}
|
|
|
|
|
|
required
|
|
|
|
|
|
/>
|
|
|
|
|
|
<ParticipantSelector
|
|
|
|
|
|
participants={availableAdvancingMap[key2] || []}
|
|
|
|
|
|
value={knockoutAssignments[key2] || ""}
|
|
|
|
|
|
onValueChange={(value) =>
|
|
|
|
|
|
setKnockoutAssignments((prev) => ({ ...prev, [key2]: value }))
|
|
|
|
|
|
}
|
|
|
|
|
|
placeholder="Team 2..."
|
|
|
|
|
|
/>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Button type="submit" className="w-full">
|
|
|
|
|
|
Populate Knockout Bracket
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{groupStageStats && groupStageStats.advancing !== 32 && groupStageStats.eliminated > 0 && (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardContent className="pt-6">
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
{groupStageStats.advancing} teams advancing (need exactly 32 to populate knockout bracket).
|
|
|
|
|
|
{groupStageStats.advancing > 32 && ` Eliminate ${groupStageStats.advancing - 32} more.`}
|
|
|
|
|
|
{groupStageStats.advancing < 32 && ` Reinstate ${32 - groupStageStats.advancing} teams or adjust eliminations.`}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
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-14 20:01:21 -08:00
|
|
|
|
|
2026-02-14 22:30:12 -08:00
|
|
|
|
{/* ====== KNOCKOUT BRACKET DISPLAY (Phase 3) ====== */}
|
|
|
|
|
|
{/* Show bracket rounds when: non-group event with matches, OR group event with knockout populated */}
|
|
|
|
|
|
{((matches.length > 0 && !isGroupStageEvent) || knockoutPopulated) && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{availableRounds.map((round: string) => (
|
|
|
|
|
|
<Card key={round}>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle>{round}</CardTitle>
|
|
|
|
|
|
<CardDescription>
|
|
|
|
|
|
{matchesByRound[round].length} {matchesByRound[round].length === 1 ? "match" : "matches"}
|
|
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<Table>
|
|
|
|
|
|
<TableHeader>
|
|
|
|
|
|
<TableRow>
|
2026-03-21 09:44:05 -07:00
|
|
|
|
<TableHead className="w-8" />
|
2026-02-14 22:30:12 -08:00
|
|
|
|
<TableHead className="w-16">Match</TableHead>
|
|
|
|
|
|
<TableHead>Participant 1</TableHead>
|
|
|
|
|
|
<TableHead>Score</TableHead>
|
|
|
|
|
|
<TableHead className="w-24 text-center">vs</TableHead>
|
|
|
|
|
|
<TableHead>Score</TableHead>
|
|
|
|
|
|
<TableHead>Participant 2</TableHead>
|
|
|
|
|
|
<TableHead>Winner</TableHead>
|
|
|
|
|
|
<TableHead className="w-32">Actions</TableHead>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
</TableHeader>
|
|
|
|
|
|
<TableBody>
|
|
|
|
|
|
{matchesByRound[round].map((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
|
|
|
|
<Fragment key={match.id}>
|
|
|
|
|
|
<TableRow>
|
|
|
|
|
|
<TableCell className="w-8 p-1">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => toggleMatchExpanded(match.id)}
|
|
|
|
|
|
className="text-muted-foreground hover:text-foreground"
|
|
|
|
|
|
aria-label="Toggle games and odds"
|
|
|
|
|
|
>
|
|
|
|
|
|
{expandedMatches.has(match.id)
|
|
|
|
|
|
? <ChevronDown className="h-4 w-4" />
|
|
|
|
|
|
: <ChevronRight className="h-4 w-4" />}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</TableCell>
|
2026-02-14 22:30:12 -08:00
|
|
|
|
<TableCell className="font-semibold">
|
|
|
|
|
|
{match.matchNumber}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell>
|
|
|
|
|
|
{match.participant1?.name || "TBD"}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell>
|
|
|
|
|
|
{match.participant1Score ? parseFloat(match.participant1Score) : "-"}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell className="text-center text-muted-foreground">
|
|
|
|
|
|
vs
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell>
|
|
|
|
|
|
{match.participant2Score ? parseFloat(match.participant2Score) : "-"}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell>
|
|
|
|
|
|
{match.participant2?.name || "TBD"}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell>
|
|
|
|
|
|
{match.winner?.name ? (
|
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
|
<Trophy className="h-4 w-4 text-yellow-500" />
|
|
|
|
|
|
{match.winner.name}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
"-"
|
|
|
|
|
|
)}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell>
|
|
|
|
|
|
{!match.isComplete && match.participant1Id && match.participant2Id ? (
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={selectedWinners[match.id] || ""}
|
|
|
|
|
|
onValueChange={(value) => handleWinnerChange(match.id, value)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-8 w-full">
|
|
|
|
|
|
<SelectValue placeholder="Select winner" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value={match.participant1Id}>
|
|
|
|
|
|
{match.participant1?.name}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value={match.participant2Id}>
|
|
|
|
|
|
{match.participant2?.name}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
) : match.isComplete ? (
|
|
|
|
|
|
<span className="text-sm text-muted-foreground">Complete</span>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span className="text-sm text-muted-foreground">Waiting</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
</TableRow>
|
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
|
|
|
|
|
|
|
|
|
|
{/* Expanded: Games & Odds panel */}
|
|
|
|
|
|
{expandedMatches.has(match.id) && (
|
|
|
|
|
|
<TableRow className="bg-muted/30">
|
|
|
|
|
|
<TableCell colSpan={9} className="p-4">
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
|
|
|
|
|
|
|
|
|
|
{/* Games section */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="flex items-center gap-2 mb-3">
|
|
|
|
|
|
<Calendar className="h-4 w-4 text-muted-foreground" />
|
|
|
|
|
|
<span className="text-sm font-semibold">Game Schedule</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Existing games */}
|
2026-03-21 09:44:05 -07:00
|
|
|
|
{match.games?.length > 0 ? (
|
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
|
|
|
|
<div className="space-y-2 mb-3">
|
2026-03-21 09:44:05 -07:00
|
|
|
|
{match.games.map((game) => (
|
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
|
|
|
|
<div key={game.id} className="flex items-center gap-2 text-sm bg-background rounded p-2 border">
|
|
|
|
|
|
<span className="font-medium w-12 shrink-0">Game {game.gameNumber}</span>
|
|
|
|
|
|
<span className="text-muted-foreground flex-1">
|
|
|
|
|
|
{game.scheduledAt
|
|
|
|
|
|
? new Date(game.scheduledAt).toLocaleString()
|
|
|
|
|
|
: "No date set"}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<Badge variant={
|
|
|
|
|
|
game.status === "complete" ? "default"
|
|
|
|
|
|
: game.status === "postponed" ? "destructive"
|
|
|
|
|
|
: "secondary"
|
|
|
|
|
|
} className="text-xs">
|
|
|
|
|
|
{game.status}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
{game.status === "complete" && game.winner && (
|
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
|
W: {game.winner.name}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<Form method="post" className="shrink-0">
|
|
|
|
|
|
<input type="hidden" name="intent" value="delete-game" />
|
|
|
|
|
|
<input type="hidden" name="gameId" value={game.id} />
|
|
|
|
|
|
<button type="submit" className="text-muted-foreground hover:text-destructive">
|
|
|
|
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<p className="text-xs text-muted-foreground mb-3">No games scheduled yet.</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Add game form */}
|
2026-03-11 15:13:50 -07:00
|
|
|
|
<Form method="post" className="flex gap-2 items-end" onSubmit={(e) => {
|
|
|
|
|
|
const form = e.currentTarget;
|
2026-03-11 16:22:29 -07:00
|
|
|
|
// Read from the datetime-local display input (not submitted directly)
|
|
|
|
|
|
const scheduledAtLocal = form.elements.namedItem("scheduledAtLocal") as HTMLInputElement;
|
|
|
|
|
|
// Write the UTC ISO string into the hidden input that IS submitted
|
|
|
|
|
|
const scheduledAtHidden = form.elements.namedItem("scheduledAt") as HTMLInputElement;
|
|
|
|
|
|
if (scheduledAtHidden) {
|
|
|
|
|
|
scheduledAtHidden.value = localDateTimeToUtcIso(scheduledAtLocal?.value) ?? "";
|
2026-03-11 15:13:50 -07:00
|
|
|
|
}
|
|
|
|
|
|
}}>
|
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
|
|
|
|
<input type="hidden" name="intent" value="add-game" />
|
|
|
|
|
|
<input type="hidden" name="matchId" value={match.id} />
|
2026-03-11 16:22:29 -07:00
|
|
|
|
{/* Hidden field holds the UTC ISO string written by onSubmit */}
|
|
|
|
|
|
<input type="hidden" name="scheduledAt" defaultValue="" />
|
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
|
|
|
|
<div className="flex-1">
|
|
|
|
|
|
<Label className="text-xs">Game #</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
name="gameNumber"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min={1}
|
|
|
|
|
|
max={9}
|
2026-03-21 09:44:05 -07:00
|
|
|
|
defaultValue={(match.games?.length ?? 0) + 1}
|
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
|
|
|
|
className="h-8 w-20"
|
|
|
|
|
|
required
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex-1">
|
2026-03-11 15:13:50 -07:00
|
|
|
|
<Label className="text-xs">Date & Time ({localTzAbbr})</Label>
|
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
|
|
|
|
<Input
|
2026-03-11 16:22:29 -07:00
|
|
|
|
name="scheduledAtLocal"
|
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
|
|
|
|
type="datetime-local"
|
|
|
|
|
|
className="h-8"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button type="submit" size="sm" variant="outline" className="h-8">
|
|
|
|
|
|
<Plus className="h-3.5 w-3.5 mr-1" />
|
|
|
|
|
|
Add
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Odds section */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="flex items-center gap-2 mb-3">
|
|
|
|
|
|
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
|
|
|
|
|
<span className="text-sm font-semibold">Moneyline Odds</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-21 09:44:05 -07:00
|
|
|
|
{match.odds?.length > 0 ? (
|
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
|
|
|
|
<div className="space-y-2 mb-3">
|
2026-03-21 09:44:05 -07:00
|
|
|
|
{match.odds.map((odd) => (
|
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
|
|
|
|
<div key={odd.id} className="flex items-center gap-2 text-sm bg-background rounded p-2 border">
|
|
|
|
|
|
<span className="flex-1 font-medium">{odd.participant?.name}</span>
|
|
|
|
|
|
<span className={`font-mono ${odd.moneylineOdds > 0 ? "text-emerald-500" : "text-muted-foreground"}`}>
|
|
|
|
|
|
{odd.moneylineOdds > 0 ? "+" : ""}{odd.moneylineOdds}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
|
({(parseFloat(odd.impliedProbability) * 100).toFixed(1)}%)
|
|
|
|
|
|
</span>
|
|
|
|
|
|
{odd.oddsSource && (
|
|
|
|
|
|
<span className="text-xs text-muted-foreground">{odd.oddsSource}</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<Form method="post" className="shrink-0">
|
|
|
|
|
|
<input type="hidden" name="intent" value="delete-odds" />
|
|
|
|
|
|
<input type="hidden" name="matchId" value={match.id} />
|
|
|
|
|
|
<input type="hidden" name="participantId" value={odd.participantId} />
|
|
|
|
|
|
<button type="submit" className="text-muted-foreground hover:text-destructive">
|
|
|
|
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<p className="text-xs text-muted-foreground mb-3">No odds set.</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Set odds forms for each participant */}
|
|
|
|
|
|
{[
|
|
|
|
|
|
{ id: match.participant1Id, name: match.participant1?.name },
|
|
|
|
|
|
{ id: match.participant2Id, name: match.participant2?.name },
|
|
|
|
|
|
].filter(p => p.id).map(participant => (
|
|
|
|
|
|
<Form key={participant.id} method="post" className="flex gap-2 items-end mb-2">
|
|
|
|
|
|
<input type="hidden" name="intent" value="upsert-odds" />
|
|
|
|
|
|
<input type="hidden" name="matchId" value={match.id} />
|
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196)
* Fix no-shadow and consistent-function-scoping lint violations
Resolves all 11 no-shadow and 16 consistent-function-scoping oxlint
warnings and promotes both rules to errors in .oxlintrc.json.
no-shadow: renamed Drizzle callback params (sports→s, matches→m,
seasons→s) to avoid shadowing outer imports; removed shadowed
destructures (eq, inArray) from where callbacks; renamed inner
template→bracketTemplate, prev→currentTimers, season→ss, name→teamName
(with name: teamName fix to preserve semantics).
consistent-function-scoping: moved formatDate, getRankBadge,
getMovementIndicator, getPositionBadge, getStatusBadge, toDateStr,
elo (×2), weightedPick, sortByMatchNumber (×2) to module scope;
moved formatTime (×2), isValidLeagueName, getDraftTimes,
makeSeasonQueues to file scope in test files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix no-non-null-assertion lint violations and promote to error
Eliminates all 208 no-non-null-assertion warnings across 38 files.
Promotes typescript/no-non-null-assertion from warn to error in
.oxlintrc.json.
Fix patterns applied:
- Map.get(key)! after .has() check → extract with get() + null guard
- Map.get(key)! on pre-populated count maps → ?? 0 default
- .set(id, map.get(id)! + 1) increment → ?? 0 before adding
- participant1Id!/participant2Id! on DB matches → ?? "" fallback
- array.find()! in tests → guard + throw or expect().toBeDefined()
- bracketTemplateCache.get(id)! → null guard extract
- Various nullable field accesses → optional chain or ?? default
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix prefer-add-event-listener, no-unassigned-import, require-module-specifiers
Resolves all 9 remaining non-console lint warnings and promotes all
three rules to errors in .oxlintrc.json.
- prefer-add-event-listener: converted onchange/onclick/onload
assignments to addEventListener in useDraftNotifications.ts and
admin.data-sync.tsx; stored changeHandler ref for proper cleanup
with removeEventListener
- no-unassigned-import: configured rule with allow list for legitimate
side-effect imports (*.css, @testing-library/jest-dom,
@testing-library/cypress/add-commands)
- require-module-specifiers: removed redundant `export {}` from
cypress/support/e2e.ts (file already has an import)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix TypeScript errors from no-non-null-assertion fixes
Two fixes introduced by the non-null assertion cleanup produced type
errors:
- scoring-event.ts: `?? ""` was wrong type for a participant object map;
restructured to explicit null guards so TypeScript can narrow correctly
- standings-sync/index.ts: `?? null` after name-match lookup lost the
truthy guarantee, causing TS18047 on the write-back block; added
`participant &&` guard before accessing its properties
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add npm run typecheck as Stop hook in Claude settings
Runs a full project typecheck at the end of each Claude turn so type
errors surface as feedback before the next message.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 10:59:51 -07:00
|
|
|
|
<input type="hidden" name="participantId" value={participant.id ?? ""} />
|
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
|
|
|
|
<div className="flex-1">
|
|
|
|
|
|
<Label className="text-xs">{participant.name ?? "TBD"}</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
name="moneylineOdds"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
placeholder="-110"
|
|
|
|
|
|
className="h-8"
|
|
|
|
|
|
required
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
|
<Label className="text-xs">Source</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
name="oddsSource"
|
|
|
|
|
|
placeholder="e.g. DraftKings"
|
|
|
|
|
|
className="h-8"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button type="submit" size="sm" variant="outline" className="h-8">
|
|
|
|
|
|
Set
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Fragment>
|
2026-02-14 22:30:12 -08:00
|
|
|
|
))}
|
|
|
|
|
|
</TableBody>
|
|
|
|
|
|
</Table>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Batch Submit Winners */}
|
|
|
|
|
|
{getPendingWinnersInRound(round).length > 0 && (
|
|
|
|
|
|
<Form method="post" className="mt-4">
|
|
|
|
|
|
<input type="hidden" name="intent" value="set-round-winners" />
|
|
|
|
|
|
<input type="hidden" name="round" value={round} />
|
|
|
|
|
|
{getPendingWinnersInRound(round).map(([matchId, winnerId]) => (
|
|
|
|
|
|
<input key={matchId} type="hidden" name={`winner-${matchId}`} value={winnerId} />
|
|
|
|
|
|
))}
|
|
|
|
|
|
<Button type="submit" className="w-full">
|
|
|
|
|
|
Save {getPendingWinnersInRound(round).length} Winner{getPendingWinnersInRound(round).length > 1 ? 's' : ''} for {round}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* Finalize Bracket Button */}
|
|
|
|
|
|
{allMatchesComplete && !event.isComplete && (
|
2026-02-20 19:26:11 -08:00
|
|
|
|
<Card className="border-emerald-500/30 bg-emerald-500/10">
|
2026-02-14 22:30:12 -08:00
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="flex items-center gap-2">
|
2026-02-20 19:26:11 -08:00
|
|
|
|
<Trophy className="h-5 w-5 text-emerald-400" />
|
2026-02-14 22:30:12 -08:00
|
|
|
|
Finalize Bracket
|
|
|
|
|
|
</CardTitle>
|
|
|
|
|
|
<CardDescription>
|
|
|
|
|
|
All matches are complete! Process all rounds and assign final placements.
|
|
|
|
|
|
</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<Form method="post">
|
|
|
|
|
|
<input type="hidden" name="intent" value="finalize-bracket" />
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
<div className="text-sm space-y-2">
|
|
|
|
|
|
<p className="font-medium">This will:</p>
|
|
|
|
|
|
<ul className="list-disc list-inside space-y-1 text-muted-foreground">
|
|
|
|
|
|
<li>Process all playoff rounds in order</li>
|
|
|
|
|
|
<li>Assign fantasy placements (1st-8th) based on bracket results</li>
|
|
|
|
|
|
<li>Assign 0 points to participants not in the bracket</li>
|
|
|
|
|
|
<li>Mark the event as complete</li>
|
|
|
|
|
|
<li>Recalculate all team standings</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
2026-02-20 19:26:11 -08:00
|
|
|
|
<Button type="submit" className="w-full">
|
2026-02-14 22:30:12 -08:00
|
|
|
|
Finalize Bracket & Update Standings
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
2025-11-14 20:01:21 -08:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Event Complete Badge */}
|
|
|
|
|
|
{event.isComplete && (
|
2026-02-20 19:26:11 -08:00
|
|
|
|
<Card className="border-electric/30 bg-electric/10">
|
2025-11-14 20:01:21 -08:00
|
|
|
|
<CardContent className="pt-6">
|
2026-02-20 19:26:11 -08:00
|
|
|
|
<div className="flex items-center gap-2 text-electric">
|
2025-11-14 20:01:21 -08:00
|
|
|
|
<Trophy className="h-5 w-5" />
|
|
|
|
|
|
<span className="font-semibold">Event Complete</span>
|
|
|
|
|
|
<span className="text-sm text-muted-foreground ml-auto">
|
|
|
|
|
|
{event.completedAt && new Date(event.completedAt).toLocaleDateString()}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
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
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|