The before/after QP diff (with its null = "no prior award" and null → value
"first-time score" semantics) was duplicated in processQualifyingEvent and
rescoreTennisBracketAndDetectChanges. The two entry points must stay separate
(tennis snapshots before an authoritative delete-all; CS2 preserves rows), but
the comparison itself should live in one place.
Add diffChangedQualifyingPoints(before, after) in qualifying-points.ts and call
it from both paths. Unit-tested directly; the two integration tests still cover
the end-to-end behavior.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hq3fG4Zn3unntbSQAGSqCq
processQualifyingEvent announced the full QP standings on every run. In a
multi-season shared major (e.g. Wimbledon), syncTournamentResults re-scores
each sibling window on every fan-out sync, so sibling-window leagues received
a duplicate full-standings ping each run even when nothing changed — the same
re-spam the primary tennis path already avoids.
Snapshot each participant's awarded QP before reprocessing and, after the
re-score, diff against the fresh rows to notify only participants whose QP
changed (a differing value, or a first-time score). This centralizes the
change-detection in the shared scoring path, so every sibling window and the
admin scoring/bracket routes get the quiet-on-no-change behavior.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hq3fG4Zn3unntbSQAGSqCq
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>
Make a "major" (golf/tennis/CS2) scored once on its canonical tournament
and fan out to every linked sports_season window and league.
Fan-out & completion (app/services/sync-tournament-results.ts):
- syncTournamentResults now marks each synced window's event complete
(gated by markComplete), recalculates affected leagues, and counts
recalc failures so a stale league can't hide behind a "completed" badge
- syncMajorFromPrimaryEvent promotes a primary window's derived results to
canonical tournament_results (deleting rows for dropped placements) and
fans out to siblings; fanOutMajorIfPrimary guards on the primary
- placement removals now propagate (stale rows reset to filler)
Primary-event model (scoring_events.is_primary, migration 0122):
- getPrimaryEventForTournament / isReadOnlySibling / ensurePrimaryEvent /
setPrimaryEvent; event creation auto-seeds a primary for bracket majors;
"Make primary" button on the tournament page
- per-window event/bracket/cs2 pages are read-only for non-primary linked
events (not-participating stays editable)
Tennis Grand Slam bracket (tennis_128 template + TEMPLATE_ROUND_CONFIG):
- bracket-scored qualifying major via the existing bracket pipeline
- simulator conditions in-progress EV on the real bracket (honoring
completed matches, walkover for withdrawals), QP derived from config,
round structure read from the template; CS2 + tennis share resolveStructureSource
Backfill (scripts/backfill-major-linking.ts): one-time idempotent reconcile
of existing majors (link orphans, designate primary, promote canonical, sync).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
## Summary
- Swap side-effect order so `updateProbabilitiesAfterResult` runs before `recalculateAffectedLeagues` in `processMatchResult`, `processPlayoffEvent`, and the `set-round-winners` route — projected points now reflect the new result immediately instead of waiting on the next simulation.
- Eliminate the N+1 EV lookup in `calculateTeamProjectedScore` by pre-fetching every EV the season needs in a single `inArray` query inside `recalculateStandings` and passing the map down.
- Add `skipProbabilities` option to `processPlayoffEvent`, used from `autoCompleteRoundIfDone` (callers have already refreshed EVs), avoiding a redundant pass on round-completing saves.
Fixes#31
## Test plan
- [x] `npm run typecheck`
- [x] `npx vitest run app/models/__tests__/team-projected-score.test.ts app/models/__tests__/process-match-result.test.ts app/services/__tests__/probability-updater.test.ts` — 57/57 passing
- [ ] Manual: save a round of bracket winners on a sports season with at least one fantasy league and confirm (a) the request returns faster and (b) the team standings show projected points updated to reflect the result without re-running the simulator
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #54
* Add opt-in Discord ping to standings notifications
Users who have linked their Discord account can enable a ping preference
in Settings > Notifications. When enabled, their bracket username is replaced
with a Discord @mention (<@userId>) in league standings update messages,
sending them a push notification and showing their Discord display name.
Adds discordPingEnabled column to users, a findDiscordIdsByUserIds model
helper, allowed_mentions support to the Discord webhook payload, and a
NotificationsSection settings component.
https://claude.ai/code/session_01NUv93WRrufHhpZSMyY4bjs
* Remove Display Name field from user profile settings
Username is the only user-facing name field. The display_name column
remains in the database since BetterAuth writes the OAuth provider name
there, and it serves as a programmatic fallback via getUserDisplayName.
https://claude.ai/code/session_01NUv93WRrufHhpZSMyY4bjs
* Address code review feedback on Discord ping feature
- Fix broken Account settings link in NotificationsSection: replace the
non-functional ?section=account href with an onNavigateToAccount callback
that drives the parent's useState-based section switcher
- Replace hand-rolled toggle button with the project's Switch component
(Radix UI) for consistent sizing, accessibility, and dark-mode support
- Guard update-discord-ping action: return an error if the user attempts
to enable pings without a Discord account linked
- Surface action error in NotificationsSection UI
- Cap allowed_mentions.users at 100 to avoid Discord rejecting the payload
in large leagues
- Remove the showWinner alias in scoring-calculator; inline winnerScoreChanged
- Add comment to league settings test notification explaining why discordUserId
is omitted
- Clarify database schema comment: displayName is write-only from BetterAuth
https://claude.ai/code/session_01NUv93WRrufHhpZSMyY4bjs
---------
Co-authored-by: Claude <noreply@anthropic.com>
Commissioners can add Brackt to any league. Each team drafts one manager
from their own league; at season end, that manager's final overall fantasy
ranking earns placement points. Resolution fires automatically when all
other sports finalize.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(schema): rename per-window tables to season_* prefix
Renames participants, participant_expected_values, participant_qualifying_totals,
participant_results, participant_surface_elos to season_* prefixed names.
Renames event_results.participant_id to season_participant_id.
Phase 1a of canonical tournament layer migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor: rename participant.ts model file to season-participant.ts
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(models): update model layer to use renamed schema exports
Updated all model files to use the renamed schema exports from Task 1:
- participants → seasonParticipants
- participantExpectedValues → seasonParticipantExpectedValues
- participantQualifyingTotals → seasonParticipantQualifyingTotals
- participantResults → seasonParticipantResults
- participantSurfaceElos → seasonParticipantSurfaceElos
- eventResults.participantId → eventResults.seasonParticipantId
- db.query relation accessors updated
- Relation field .participant → .seasonParticipant where applicable
- Import paths updated: ./participant → ./season-participant
Files updated (14 model files + 3 test files):
- draft-pick.ts
- draft-utils.ts
- event-result.ts
- group-stage-match.ts
- participant-result.ts
- qualifying-points.ts
- scoring-calculator.ts
- scoring-event.ts
- sports-season.ts
- surface-elo.ts
- team-score-events.ts
- cs2-major-stage.ts
- golf-skills.ts
- participant-expected-value.ts
- __tests__/sports-season.clone.test.ts
- __tests__/auto-pick.test.ts
- __tests__/executeAutoPick.timer.test.ts
Typecheck errors decreased: 779 → 499 (280 fewer)
All model file errors related to renamed schemas resolved.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(routes): update route layer to use renamed schema exports
- Update model import from ~/models/participant to ~/models/season-participant
- Rename schema.participants to schema.seasonParticipants
- Rename schema.participantResults to schema.seasonParticipantResults
- Rename db.query.participants to db.query.seasonParticipants
- Update 9 route files and 1 test file
Affected files:
- admin.sports-seasons.$id.events.$eventId.bracket.server.ts
- admin.sports-seasons.$id.participants.tsx
- api/draft.force-manual-pick.ts
- api/draft.make-pick.ts
- api/draft.replace-pick.ts
- api/seasons.$seasonId.draft.ts
- leagues/$leagueId.draft-board.$seasonId.tsx
- leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts
- admin/__tests__/sports-seasons-participants.test.ts
Error count reduced from 499 to 453 (46 errors fixed).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(routes): update route files for schema rename
Update route imports from ~/models/participant to ~/models/season-participant
and fix references to .participant/.participantId on event results to use
.seasonParticipant/.seasonParticipantId after schema rename.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(services): update simulators and services for renamed schema
Update all simulators, services, and server files to use renamed schema tables:
- participants → seasonParticipants
- participantExpectedValues → seasonParticipantExpectedValues
- participantResults → seasonParticipantResults
- eventResults.participantId → eventResults.seasonParticipantId
Files updated:
- 20 sport simulators (NBA, NHL, NFL, MLB, etc.)
- probability-updater.ts
- standings-sync/index.ts
- sports-data-sync.server.ts
- server/socket.ts
Typecheck errors reduced from 365 to 0.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* migration: rename per-window tables to season_* prefix
* fix(tests): update mock query keys after participants table rename
Change mock db.query.participants to db.query.seasonParticipants in test
files to match the schema rename from commit 66145a9. This fixes
"Cannot read properties of undefined (reading 'findFirst'/'findMany')"
errors that occurred when production code queries db.query.seasonParticipants
but test mocks only defined the old participants key.
Files updated:
- app/services/simulations/__tests__/world-cup-simulator.test.ts
- app/routes/api/__tests__/draft.force-manual-pick.test.ts
- app/routes/api/__tests__/draft.force-manual-pick.timer-mode.test.ts
- app/routes/api/__tests__/draft.make-pick.timer-mode.test.ts
- server/__tests__/timer-autodraft.test.ts
- app/models/__tests__/team-score-events.test.ts
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(tests): update remaining mock paths and keys after schema rename
* fix(tests): final two mock stragglers after schema rename
- draft-pick.test.ts: assertion on db.query.participantQualifyingTotals
- process-match-result.test.ts: mock key participants → seasonParticipants
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: add post-phase1a baseline capture (temp, for diff verification)
* chore: capture pre-migration baselines
* chore: remove post-phase1a capture helper after verification
* schema: add canonical tournament & participant tables
Adds tournaments, participants (canonical), tournament_results, and
participant_surface_elos (canonical). Adds nullable tournament_id to
scoring_events and nullable participant_id to season_participants.
Phase 1b of canonical tournament layer migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(models): add canonical tournament, participant, result, surface-elo models
Adds CRUD modules for the canonical tables created in commit 775b905.
Each module mirrors existing app/models conventions (database() from
~/database/context, schema from ~/database/schema, mock-based tests).
Key implementation notes:
- participant.ts exports use "Canonical" prefix (CanonicalParticipant,
createCanonicalParticipant, etc.) to avoid collision with existing
season-participant.ts exports
- All four models include comprehensive unit tests following the
audit-log.test.ts pattern
- Tests use mocked db responses (no real database access)
- Upsert functions use onConflictDoUpdate for appropriate unique constraints
Part of Phase 1b of canonical tournament layer migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* migration: create canonical tables, add nullable FKs
* scripts: add extractTournamentIdentity helper for backfill
Pure function that derives canonical (name, year) identity from a
scoring_events row, stripping trailing 4-digit years from the name or
falling back to eventDate. Used by the Phase 2 backfill to group
per-window events into canonical tournaments.
* scripts: add backfill orchestrator for canonical layer
Populates canonical tournaments, participants, tournament_results, and
participant_surface_elos from per-window data for qualifying-points
sports. Skips already-linked rows, is idempotent, and supports dry-run
mode.
Critical invariants enforced by the implementation:
- qualifying_points_awarded is never copied to tournament_results
- season_participant_qualifying_totals is never touched
- conflicting surface-Elo values between windows raise a loud error
(recorded in report.errors) rather than overwriting
* scripts: add backfill CLI with dry-run default
Wires backfill-canonical-layer.ts to a CLI entry point exposed as
`npm run backfill:canonical`. Defaults to --dry-run; requires --apply
to actually write. Supports --sport=<uuid> to limit to a single sport.
Exits 2 if the backfill reports errors (e.g., surface-Elo conflicts).
* fix(backfill-cli): wrap runBackfill in DatabaseContext.run
The orchestrator uses database() from ~/database/context, which requires
AsyncLocalStorage to be populated. Wrap the CLI invocation with
DatabaseContext.run(db, ...) using server/db's cached connection pool.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(backfill-cli): exit 0 on success so pg pool doesn't block
The cached postgres connection pool keeps the Node event loop open after
main() returns. Explicit process.exit(0) on success mirrors the pattern
in scripts/capture-baseline.ts.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Chris Parsons <chrisp@extrahop.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* Migrate authentication from Clerk to BetterAuth (#322)
Replaces @clerk/react-router with self-hosted better-auth to eliminate
the external Clerk dependency and keep all user/session data in our own
PostgreSQL database.
**What changed**
- New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler
- New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param
- New: UserMenu component replacing Clerk's UserButton
- Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable
- Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9)
- All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin
- root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query)
- useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check
- models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern)
- Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix
- scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts)
- scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export
- BETTERAUTH_MIGRATION.md: dev and production runbooks
- All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server
- Test fixtures: added emailVerified field
**Follow-up (post-stable)**
- Rename actor_clerk_id column → actor_user_id in commissioner_audit_log
- Drop clerk_id column from users once migration confirmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict
better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and
drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1.
The adapter works correctly at runtime with our versions — the peer dep is
only for stricter type checking. This unblocks npm ci in CI without a risky
drizzle major-version upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Redesign home page with new layout and component system
- Two-column layout (My Leagues 2/3, Upcoming Events 1/3) with mobile stack
- LeagueRow: square avatar, gradient draft highlight, rank/points display, progress bar
- MyLeaguesCard, CreateLeagueCard with shared SectionCardHeader
- UpcomingEventsCard: vertical timeline with grouped multi-league events
- Shared gradient system: BracktGradients SVG defs, GradientIcon wrapper, brand.ts constants
- Button default variant updated to green→cyan gradient
- Navbar: plain nav links with gradient hover, support/admin icon buttons
- Accessibility fixes: semantic h2 headings, aria-label on LeagueAvatar and nav elements
- Storybook stories for all new components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Responsive league row layout and mobile polish
- League rows stack avatar+name on top, stats full-width below on mobile
- Stats spread to right side on sm+ screens with border separator on mobile
- Tighter padding on mobile (px-3/py-3), full padding on sm+
- Card headers and content use px-3 sm:px-6 to reduce mobile gutters
- Two-column home layout deferred to lg breakpoint (tablet gets stacked)
- Active leagues sorted by completion percentage descending
- Default rank 1 / 0 points for active leagues with no scoring events yet
- Fix ordinal bug for 11th/12th/13th; add aria-labels to rank change indicators
- Remove dead StatDivider className prop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Improve claude file.
* Add StandingsPreview card component with podium row styling
- New StandingsPreview component with gold/silver/bronze row tints for
top 3, team avatar, and LeagueRow-style stat columns (Ranking + Points)
with rank and 7-day point change indicators
- Fix GradientIcon in Storybook by adding BracktGradients decorator to
preview.tsx (renamed from .ts to support JSX)
- Fix degenerate SVG gradient on horizontal strokes by switching
BracktGradients to gradientUnits="userSpaceOnUse" with Lucide-space
coordinates (0→24)
- Revert erroneous fill: url(#gradient) from GradientIcon; stroke-only
fix was sufficient once gradientUnits was corrected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update components on league homepage.
* Finish up league page styling.
* Work on standings page.
* Add story for RecentScoresCard
* Update Point Progression Chart.
* Sort point progression legend by ranking and add team links to standings rows
* Fix standings discrepancy on change.
* Create draft cell component.
* Update draft board page
* Draft room improvements.
* Update some draft room styling.
* Fix context menu missing.
* Move tab navigation and autodraft to header row, narrow sidebar
* Virtualize available participants list, memoize draft room props
Adds @tanstack/react-virtual to replace separate mobile/desktop lists
with a single unified virtual scroll loop. Also memoizes miniDraftGrid
and availableParticipantsSectionProps, and switches pick lookup from
Array.find to a Map for O(1) access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update draft room UI.
* More draft room fixes.
* Draft room tweaks.
* Fix Rosters page.
* Queue Section fixes.
* Mobile Draft fixes.
* Fix draft board page.
* Create bracket look.
* Bracket work.
* Finish bracket page.
* Homepage initial styling
* homepage copy
* Add privacy policy. Fixes#88.
* how to play copy
* rules copy
* Fix brackets on homepage.
* Add footer to website.
* Glow on dots.
* Landing page copy.
* Fix sidebar.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Emit standings-updated socket event after recalculate-floors so league pages refresh automatically
When the admin runs "Recalculate Floors" on the bracket page, the league
sports-season homepage was showing stale elimination data because there was
no mechanism to notify it of the change.
Fix: after recalculate-floors updates participant_results, emit a
standings-updated socket event to all fantasy-season draft rooms linked to
the sports season. The sports-season page now joins its draft room and
revalidates its loader whenever it receives that event for the matching
sports season.
https://claude.ai/code/session_01D3NbnVQdaH82Fk7Jm8y3sB
* Fix recalculate-floors incorrectly eliminating play-in losers who still advance
The playoff_matches.isScoring column defaults to true in the database. Brackets
created before this column was added (or before the migration set correct values)
have isScoring=true on play-in rounds that should be false. When recalculate-floors
replayed those matches, it took the "scoring round" path; since "Play-In Round 1"
isn't in ROUND_CONFIG, config===null, and the loser was assigned finalPosition=0
regardless of loserAdvances — permanently eliminating teams like the Suns who had
a second play-in game remaining.
Fix: build a round→isScoring lookup from the bracket template before replaying
matches and use it as the source of truth, falling back to the DB field only when
the template doesn't define the round. This ensures non-scoring play-in rounds
are always processed with isScoring=false so the loserAdvances guard fires
correctly.
Also revert unrelated socket changes from the previous (wrong) commit.
https://claude.ai/code/session_01D3NbnVQdaH82Fk7Jm8y3sB
* Fix isScoring fallback and loserAdvances gaps across all match-processing paths
Three issues found in code review around the recalculate-floors fix:
1. set-winner and set-round-winners (bracket.server.ts) used `match.isScoring ?? true`
just like recalculate-floors did. Both now derive isScoring from the bracket
template as the source of truth, falling back to the DB field only when the round
isn't defined in the template.
2. processPlayoffEvent (scoring-calculator.ts) used `matches[0]?.isScoring ?? true`
with the same DB-default problem. Now uses BRACKET_TEMPLATES[bracketTemplateId]
to look up the round's isScoring before falling back to the DB field.
3. doesLoserAdvance (playoff-match.ts) was missing the AFL afl_10 Qualifying Finals
case: both losers advance to Semi-Finals. Without this, AFL QF losers would
incorrectly receive finalPosition=0 when processed through the non-scoring path.
https://claude.ai/code/session_01D3NbnVQdaH82Fk7Jm8y3sB
* Fix no-non-null-assertion lint errors in isScoring Map lookups
Replace Map.has(key) ? Map.get(key)! : fallback pattern with
Map.get(key) ?? fallback to satisfy oxlint no-non-null-assertion rule.
https://claude.ai/code/session_01D3NbnVQdaH82Fk7Jm8y3sB
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Fix NBA Play-In 7/8 loser incorrectly marked as eliminated
processPlayoffEvent (called by autoCompleteRoundIfDone when all round matches
finish) was marking every non-scoring round loser as eliminated without
checking doesLoserAdvance. This caused the 7v8 loser, who should advance to
Play-In Round 2, to get finalPosition=0 as soon as the full round completed.
Fixes:
- processPlayoffEvent now calls doesLoserAdvance per match before writing a
0-pt elimination result, matching the guard already in processMatchResult
- recalculate-floors handler now passes loserAdvances to processMatchResult
so a full reprocess also respects the loser-advances rule
- recalculateAffectedLeagues gains a skipDiscord option; recalculate-floors
uses it so clicking the admin "Recalculate Floors" button corrects the bad
data without re-announcing results on Discord
- Add two tests confirming 7v8 loser is not eliminated and 9v10 loser is
https://claude.ai/code/session_01QmvezscLYY38gN4GbvXZbA
* Address code review feedback on Play-In loserAdvances fix
- Use outer `round` variable instead of match.round in processPlayoffEvent
(they're identical, but consistent with surrounding code)
- Add comment at recalculate-floors call site explaining skipDiscord intent
- Combine two redundant test cases into one covering all four assertions
- Add West conference matches (M3/M4) to test fixture — East-only was
insufficient given doesLoserAdvance checks matchNumber 1 & 3
- Add guard test: when bracketTemplateId is null all losers are eliminated,
catching any future refactor that drops the field from the DB query
https://claude.ai/code/session_01QmvezscLYY38gN4GbvXZbA
---------
Co-authored-by: Claude <noreply@anthropic.com>
The loser of NBA Play-In Round 1 matches 1 and 3 (East/West 7v8 games)
advances to Play-In Round 2 rather than being eliminated, but was being
assigned finalPosition=0 and appearing in Discord as knocked out.
- Add `loserAdvances` param to `processMatchResult` to skip recording an
elimination result when the loser continues to another match
- Extract `doesLoserAdvance()` to `playoff-match.ts` as the single source
of truth for which play-in matches have advancing losers (replaces
duplicated hardcoded checks at both call sites in bracket.server.ts)
- Extract `isLoserNotifiable()` as a pure exported helper so Discord
notification eligibility is testable; losers only appear when their
team's score changed OR they have a finalized (non-partial) result,
correctly suppressing advancing losers while still surfacing 0-pt
eliminations that produce no score delta
- Add 14 new unit tests covering `doesLoserAdvance`, `isLoserNotifiable`,
and the advancing-loser/eliminated-loser `processMatchResult` branches
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Placement counts (used to break points ties) previously only included
fully-finalized participants. A team whose events all resolved first would
accumulate real placement counts while tied teams with pending events had
zeros, causing the tie to be broken in favour of whichever team finished
scoring first rather than on merit.
Fix: count a participant's current position toward the tiebreaker regardless
of isPartialScore. Their position is the best available signal; if it changes,
recalculateStandings reruns and ranks update naturally.
Also removes a redundant bounds check (finalPosition > 0 was already
asserted by the outer if-condition).
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* 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>
* Show tied ranks with T prefix across standings and Discord notifications, fixes#197
- Add buildTiedRankChecker() utility to standings-display.ts; replaces
four copies of inline rank-count logic spread across components,
the league home route, and the Discord service
- Prefix shared ranks with "T" (e.g. "T3") in StandingsTable (league
panel), StandingsTable (full standings), the league home preview, and
Discord webhook notifications
- Add useMemo wrapping in StandingsTable components so tie detection
does not recompute on every render
- Fix compareTeamsForRanking to round total points to hundredths before
comparing, preventing floating-point noise from producing spurious
non-ties in the standings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix discord test to expect escaped period in rank display
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add app/lib/logger.ts: dev passes through to console; prod routes errors
to Sentry.captureException and warnings to Sentry.captureMessage, with
extra context preserved. Uses captureMessage (not captureException) for
string-only args to avoid fabricated stack traces.
- Add server/logger.ts: dev passes through; prod silences log/info but
keeps warn/error on stderr (Sentry not initialized in that process).
- Replace all console.* calls across 44 app files and 4 server files.
- Upgrade no-console from warn → error in oxlint; exempt logger files and
scripts/** via overrides.
- Add typescript/no-inferrable-types rule; fix violations in services and
simulators. Exempt test files (intentional string widening for switch/if
tests would break under literal type inference).
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* 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>
* Add oxlint and fix all lint errors
- Install oxlint, add .oxlintrc.json with rules for TypeScript/React
- Add npm run lint / lint:fix scripts
- Add Claude PostToolUse hook to run oxlint on every edited file
- Fix 101 errors: unused vars/imports, eqeqeq, prefer-const, no-new-array
- Fix no-array-index-key (use stable keys or suppress positional cases)
- Fix exhaustive-deps missing dependency in useEffect
- Promote exhaustive-deps and no-array-index-key to errors
- Fix Map.get() !== null bug in $leagueId.server.ts (should be !== undefined)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix no-explicit-any warnings and upgrade tsconfig to ES2023
- Replace all `any` types with proper types or `unknown` across ~20 files
- Add typed socket payload interfaces in draft route and useDraftSocket
- Use any[] with eslint-disable for socket.io callbacks (legitimate escape hatch)
- Bump all tsconfigs from ES2022 → ES2023 to support toSorted/toReversed
- Fix cascading type errors uncovered by removing any: Map.get narrowing,
participant relation types, ChartDataPoint, Partial<NewSeason> indexing
- Add ParticipantResultWithParticipant type to participant-result model
- Fix test fixtures to match updated interfaces (DraftCell, ParticipantResult)
- Fix duplicate getQPStandings import in sportsSeasonId.server.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Promote no-explicit-any to error
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Refine Discord webhook scoring notifications (fixes#180, #181)
- Filter scored matches to only show matchups where a manager scores
Brackt points (winner drafted) or has a team eliminated (loser drafted);
matches with no manager involvement are suppressed entirely.
- Escape Discord markdown characters (_*~`|\) in team names and usernames
to prevent formatting issues (e.g. double-underscore names causing
unintended underlines).
- Replace full standings with a "Standings Changes" section that shows
only teams whose points changed, plus any teams whose rank shifted as a
result, with ↑N / ↓N indicators for rank movement.
- Pass previousRanks map from scoring-calculator to the notification so
rank-displaced teams (who didn't score points themselves) are included.
- Update test webhook in league settings to demonstrate rank changes and
a sample scored match.
- Update all discord service tests to cover the new filtering, escaping,
and standings-change behaviour.
https://claude.ai/code/session_01FciwfdG9Sfr5ZrXUHPeB18
* fix: only set winnerUsername when winning team's score actually changed
Previously, winnerUsername was set for any drafted winner regardless of
whether points were actually scored. This caused R64 wins (where the
scoring system may not award points until later rounds) to appear in
Discord's Scored Matches section even when no Brackt points were earned.
Now winnerUsername is only set when the winner's team's totalPoints
changed after recalculation. loserUsername (eliminations) is always set
when the loser is drafted, since being knocked out is always notable.
https://claude.ai/code/session_01FciwfdG9Sfr5ZrXUHPeB18
* Refactor Discord notification logic in scoring calculator
- Compute changedTeamIds once up front; derive hasChanges from it
instead of duplicating the same iteration
- Merge usernameForParticipant and winnerUsernameForParticipant into a
single function with a requireScoreChange flag
- Replace hasDraftedParticipantMatches with hasScoredMatchesToShow,
which checks that at least one scoredMatch has a displayable username
— prevents sending a contentless Discord embed when a drafted winner
doesn't score any points and the loser isn't drafted
- Update inline comment to be sport-agnostic
https://claude.ai/code/session_01FciwfdG9Sfr5ZrXUHPeB18
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Add unique index on (team_id, season_id, snapshot_date) in team_standings_snapshots
so concurrent writes can't produce duplicate rows (migration 0051)
- Rewrite createDailySnapshot to use INSERT ... ON CONFLICT DO UPDATE inside a
transaction, replacing the SELECT-then-insert/skip pattern (N+1 queries, race-prone)
- Remove early-exit guard in server/snapshots.ts background job so it always
upserts rather than skipping seasons that already have a snapshot for today
- Call createDailySnapshot in recalculateAffectedLeagues after recalculateStandings,
so every bracket score update refreshes today's snapshot; wrapped in try/catch so
a snapshot failure can't block Discord notifications
- Also call createDailySnapshot from the admin rescore and finalize-bracket actions
- Fix UTC date bug: replace toISOString().split('T')[0] with local date parts in
both standings.ts and server/snapshots.ts (and the 7-day lookback query)
- Convert all dynamic await import() calls in bracket.server.ts to static imports
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fall back to displayName when username is null for Discord webhook
Users who sign up via OAuth (Google, GitHub, etc.) without setting a
Clerk username have a null `username` field but always have a `displayName`
(computed from firstName+lastName or email). Previously, `usernameByClerkId`
was filtered to only include users with a non-null username, causing those
owners to appear without any identifier in Discord standings messages
(e.g. "Liverpool def. Galatasaray" instead of "Liverpool def. Galatasaray (Madmike)").
https://claude.ai/code/session_01VAkeDDVZMYS1DweQnUrRnH
* Extract getUserDisplayName helper and use consistently throughout
Add a single getUserDisplayName(user) function to app/models/user.ts that
encapsulates the username → displayName fallback logic. Replace 9 scattered
inline expressions across the codebase (owner-map, scoring-calculator,
league routes, settings, invite flow, draft API, Clerk webhook) with calls
to the shared helper.
No behaviour change — all existing logic preserved, just centralised.
https://claude.ai/code/session_01VAkeDDVZMYS1DweQnUrRnH
* Fix N+1 user queries in league loader and settings loader
Add findUsersByClerkIds() batch function to the user model and replace two
separate Promise.all+findUserByClerkId loops (one for owners, one for
commissioners) with a single inArray query in both $leagueId.server.ts and
$leagueId.settings.tsx. The merged query covers both owner and commissioner
IDs in one round-trip.
https://claude.ai/code/session_01VAkeDDVZMYS1DweQnUrRnH
* Fix N+1 user queries in buildOwnerMap
Replace the Promise.all+findUserByClerkId loop with a single
findUsersByClerkIds batch query, consistent with the league loader
and settings loader fixes.
https://claude.ai/code/session_01VAkeDDVZMYS1DweQnUrRnH
---------
Co-authored-by: Claude <noreply@anthropic.com>
Participants eliminated in a non-scoring bracket round (e.g. Round of 16
in a 16-team bracket) receive finalPosition=0 and isPartialScore=false.
The previous calculateTeamScore loop only entered participantsCompleted++
when finalPosition > 0, so these participants were silently treated as
still "remaining" in the standings table — causing e.g. a team with one
eliminated participant to show "25 remaining" instead of "24 remaining".
Fix: add an else-if branch that increments participantsCompleted for any
finalized result (isPartialScore=false) regardless of finalPosition value.
The condition is intentionally broader than `=== 0` to also cover the
theoretical case of finalPosition=null with isPartialScore=false.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
**Discord webhook improvements**
- Show team owner username in parentheses in standings (e.g. "1. Alpha FC (christhrowsrocks) — 150 pts")
- Show username in parentheses in match results for drafted participants
(e.g. "Sporting (christhrowsrocks) def. Bodø/Glimt (apatel)"), omitting
the parenthesis for the undrafted side
- Username lookup only runs after the webhook URL guard to avoid wasted DB queries
- 4 new tests covering all username display combinations
**Fix calculateTeamScore partial-score counting bug**
- isPartialScore participants were incorrectly counted in participantsCompleted
and placementCounts, causing standings to show wrong remaining count and
phantom placement badges (e.g. "5th×1") for still-alive bracket participants
- Floor points still flow into totalPoints (guaranteed value for ranking)
- 3 new unit tests covering partial vs finalized behavior in calculateTeamScore
**Admin Force Re-score action**
- New "Fantasy Standings" card on every sports season admin page with a
Force Re-score button that triggers recalculateStandings on all linked
fantasy seasons — useful after data corrections without waiting for a
new scoring event
- Auth guard added to the action (was missing — layout loader only protects GET)
- Returns a meaningful message when no linked seasons exist
- Intent discriminant on action returns so success banners use actionData.intent
instead of fragile message.includes() checks
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix bracket point averaging and partial-score display in standings/breakdown
Bracket sports (UCL, NBA, NFL, etc.) use averaged points for tied
positions (e.g. QF losers all share avg of 5th-8th = 20 pts), but
both calculateTeamProjectedScore and getTeamScoreBreakdown were using
raw pointsFor5th (25) instead of the bracket-averaged value. This caused
the team breakdown page to show incorrect actual points (25 vs 20) and
the standings page to show incorrect actualPoints/projectedPoints totals.
Additionally, participants with isPartialScore=true (still alive in a
bracket with a provisional floor position) were being displayed with a
final placement badge (e.g. "5th") instead of "Pending".
Changes:
- calculateTeamProjectedScore: use calculateBracketPoints for bracket
sports; handle isPartialScore participants separately (floor in
actualPoints, incremental EV in projectedPoints, Math.max guard for
EV < floor edge case)
- getTeamScoreBreakdown: same bracket averaging fix; pass isPartialScore
through to picks; use explicit finalPosition != null && > 0 check
- TeamScoreBreakdown component: show Pending badge for isPartialScore
participants; display actual/floor + EV row for all incomplete picks;
add "actual / projected" column header subtitle
- 9 new unit tests covering all scoring branches including the EV-below-
floor clamp edge case
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix typecheck: cast DUMMY_EV_ROW to any in test fixture
ParticipantEV has additional required fields (id, participantId, etc.)
that aren't needed for the mock — cast to any to satisfy the type checker.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- NCAAM R64/R32 winners no longer receive T5 floor points; only Sweet
Sixteen winners entering the first scoring round get a floor
- AFL T5-T6 and T7-T8 score as separate tiers (avg([5,6]) and avg([7,8]))
instead of one shared avg([5-8]) pool
- Eliminated teams now always show their correct final rank (e.g. T33 for
NCAAM R64 losers) even during partial round scoring, by deriving the
rank label from the total match count per round rather than the dynamic
still-alive count
- Discord notifications now fire when a drafted participant is eliminated
with 0 points (e.g. R64 losers whose team score doesn't change)
- Discord notifications no longer include all prior matches — scoped to
the current batch via matchIds
- Discord section headers have spacing; rank medals replaced with numbers
- Rounds auto-complete when all matches are marked done; manual Complete
Round dropdown removed
- Fixed double Discord notification when the last match in a round
triggers auto-complete: processPlayoffEvent now accepts skipRecalculate
so the caller controls when the notification fires
- Fixed falsy check on finalPosition=0 in calculateTeamScore
- Undrafted 0-point participants filtered from Eliminated Teams display
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add Discord webhook notifications for standings updates
Adds a Discord webhook integration that posts standings to a configured
Discord channel whenever scores change after a scoring event. Commissioners
set the webhook URL in league settings and can send a test notification.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix Discord notification edge cases from code review
- Fix point delta sign: negative diffs now correctly show -5 pts not +-5 pts
- Replace hardcoded soccer emoji with a neutral bullet (works for all sports)
- Truncate embed description at Discord's 4096-char limit
- Thread eventId through processMatchResult and bracket batch handler so
scored matches appear in single-match notifications too
- Pass eventName to finalizeQualifyingPoints ("Final Standings") and
processSeasonStandings ("Season Complete") so those notifications have context
- Test notification now fetches current season to show "League 2025" format
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix process-match-result tests: add sportsSeasons to db mock
recalculateAffectedLeagues now queries sportsSeasons to get the sport
name for Discord notifications; the test mock db needed the stub added.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the duplicated F1Simulator (which was copy-pasted for IndyCar) with a
single AutoRacingSimulator that accepts a race points table at construction time.
Registry entries for f1_standings and indycar_standings remain separate, each
wired to the correct scoring system (F1: top-10, IndyCar: top-26 positions).
Also imports SIMULATOR_TYPES from registry into the admin route to eliminate the
duplicate hardcoded array, and updates stale comments mentioning F1/IndyCar.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
When a participant wins a bracket round, they immediately earn provisional
"floor" points (the averaged minimum they'd receive if eliminated next round).
These update as they advance and are replaced by finalized scores on elimination.
Key changes:
- Add `is_partial_score` column to `participant_results` (migration 0038)
- `processPlayoffEvent`: assign provisional position 5 to non-scoring round
winners; assign round-appropriate floors to scoring round winners via
`getGuaranteedMinimumPosition`; add catch-all for unrecognized round names
- `upsertParticipantResult`: guard against un-finalizing rows (never overwrite
isPartialScore=false with true)
- `calculateBracketPoints`: new function averaging tied bracket tiers
(5-8 → 20 pts, 3-4 → avg, 1-2 solo); used in `calculateTeamScore` for
playoff_bracket pattern (pattern-aware, doesn't affect F1/golf scoring)
- `PlayoffBracket`: "In Contention" table for still-active participants;
AFL double-chance fix (participants who won a later match excluded from
earlier round's loser list); correct `nextRank` starting position
- Server loader: batch owner DB queries (one query vs N+1); deduplicate
participantPoints; use calculateBracketPoints for bracket point display
- Clean up Phase/Q-number tracking comments throughout scoring-calculator.ts
- 3 new tests for non-scoring round provisional floor behavior
Also includes a dev admin bypass via DEV_ADMIN_CLERK_ID env var (separate
change on this branch, not part of floor scoring feature).
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- SeasonStandings: highlight rows for the logged-in user's drafted participants
with electric accent (in-points) or muted (outside points) left border + star icon
- Loader: derive userParticipantIds from current user's teams and draft picks
- EventSchedule: hide type badge for schedule_event; show "Results Pending" only
when event is past and not yet marked complete (matches admin page logic)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add FIFA_48 bracket template with 12 groups of 4, knockout stage of 32
- Add tournament groups schema, model, and GroupStageDisplay component
- Add projected/expected value scoring to standings and team breakdowns
- Add docker-compose for local PostgreSQL development
- Move DRAFT_ORDER_IMPLEMENTATION.md to plans directory
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Added loader and action functions for managing expected values in sports seasons.
- Implemented UI for recalculating probabilities based on participant results.
- Created a service to update probabilities after results are finalized, including handling finished and unfinished participants.
- Developed tests for the probability updater service to ensure correct functionality.
- Introduced a preview feature to show potential changes before applying updates.
- Added a new route for league standings: `/leagues/:leagueId/standings/:seasonId`
- Implemented the `StandingsTable` component to display team standings with ranking, points, and placement breakdown.
- Integrated tiebreaker logic for ranking teams based on total points and placements.
- Enhanced the league home page with a link to view standings.
- Updated scoring system documentation to reflect the new standings features.
- Added comprehensive tests for tiebreaker logic and `StandingsTable` component.
- Created shared types for standings to be used in both server and client code.
- Added `QualifyingPointsStandings` component to display standings based on qualifying points.
- Introduced scoring rules and projected points calculation for participants.
- Implemented tie handling for rankings and displayed appropriate UI elements based on finalization status.
- Created tests for qualifying points configuration, accumulation logic, ranking logic, and scoring workflow.
- Developed scoring calculator tests to validate fantasy points conversion from qualifying points.
- Established qualifying points management functions including initialization, retrieval, updating, and resetting of points.
- Introduced `generateStandardSeeding()` function to create proper tournament matchups (e.g., 1v16, 8v9).
- Added comprehensive tests for 4, 8, 16, and 32-team brackets to ensure correct seeding and balance.
- Updated testing instructions to reflect new features and improvements in bracket generation.
- Fixed issues with sequential seeding and ensured all matchups sum to n+1 for balance.
- Implemented batch winner selection and duplicate participant prevention for improved user experience.
- Enhanced fantasy points display on event pages for better visibility of participant results.
- 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.