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>
|
|
|
|
|
|
</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">
|
|
|
|
|
|
<Label className="w-20 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>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|