* Show toast instead of redirecting after league settings save
Commishes now stay on the settings page when saving, with a Sonner
toast confirming the save rather than being navigated to the league
homepage.
https://claude.ai/code/session_01PewdKxVfAbqasJbmk2UGw4
* Fix toast firing for unrelated intents after settings save
Tag update responses with intent: "update" so the success useEffect
only fires for the main settings form, not for owner/commissioner/
reset-draft actions which also return { success: true }. Also replaces
the remaining ?updated=true redirect (no-season edge case) with the
same response shape for consistency.
https://claude.ai/code/session_01PewdKxVfAbqasJbmk2UGw4
---------
Co-authored-by: Claude <noreply@anthropic.com>
useBlocker was receiving a boolean, which caused it to block the settings
form's own POST submission before the onSubmit state-clear could propagate.
Switching to a BlockerFunction that checks nextLocation.pathname lets
same-URL form posts through, so the "leave without saving?" dialog only
fires on genuine navigation away from the settings page.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add Discord notification when draft order is set or randomized
Posts the full draft order to the league's Discord webhook after each
set-draft-order or randomize-draft-order action, with distinct titles
(📋 vs 🎲) so members can tell how it was determined.
https://claude.ai/code/session_01XrBxu358RK6q8iDgRQbMFx
* Refine Discord draft order notification: league name/link, usernames, polish
- Replace seasonName with leagueName + leagueUrl; title links to the league
- Distinguish manual vs randomized in title ("Manually Set" vs "Randomized")
- Show owner username next to each team name in the draft order list
- Remove footer in favour of the linked embed title
- Add 4096-char description clamp consistent with standings notification
- Use a teamMap in both intent branches to avoid O(n²) find-in-loop
- Add test for all-unowned-teams rendering no trailing parentheses
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Remove emoji from draft order notification titles
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: resolve all 48 WCAG 2.2 AA accessibility issues
Critical fixes:
- Add aria-label to all unlabeled inputs/selects in draft dialogs (ParticipantSelectionDialog, TimeBankAdjustmentDialog, AvailableParticipantsSection)
- Add role="dialog" + aria-modal + focus trap to ConnectionOverlay and AuthRecoveryOverlay
- Add aria-live region and connection status announcement to ConnectionOverlay
Serious fixes:
- Add skip-to-content link in root.tsx with id="main-content" on <main>
- Add aria-label to UserMenu trigger button
- Add aria-describedby + role="alert" to all auth form error messages (login, register, onboarding, forgot-password, reset-password)
- Replace emoji column headers in StandingsTable with aria-label + aria-hidden spans
- Add aria-live="assertive" to "It's your turn" desktop and mobile on-clock indicators
- Add aria-live="polite" to draft room countdown timer
- Add pause button to SportTicker (WCAG 2.2.2); add aria-hidden to ticker content
- Fix Footer text contrast (changed from 28% to text-muted-foreground)
- Fix OvernightPauseSettings: add htmlFor/id pairs and role="radiogroup"+aria-checked to mode buttons
- Fix DraftSetupSection: replace broken htmlFor with aria-label on date picker button
- Add aria-label to PeopleSection owner and commissioner selects
- Add labels to ScoringPresetPicker score inputs; add role="radiogroup"+aria-checked to preset buttons
- Add role="radiogroup"+aria-checked to AutodraftSettings option buttons
- Add accessible names, aria-current="step", and <ol> list semantics to WizardStepper
Moderate fixes:
- Add aria-controls to RecentPicksFeed toggle button; wrap picks list in aria-live region
- Add role="tab"+aria-selected+aria-controls to mobile board sub-tabs + role="tabpanel"
- Add role="radiogroup"+aria-checked to TimerModeSelector
- Add aria-current="page" + aria-label to SettingsDesktopNav
- Add aria-label="Admin navigation" to admin sidebar nav
- Add scope="col" + <caption> to StandingsTable and ScoringTables
- Add ARIA table roles (role="table/rowgroup/row/columnheader/rowheader/cell") to DraftSummaryView CSS grid
Minor fixes:
- Add aria-hidden="true" to decorative trend icons in StandingsTable
- Add aria-hidden="true" to desktop column header labels row in AvailableParticipantsSection
- Replace title with aria-label on all icon-only buttons (watchlist, queue) in AvailableParticipantsSection
- Add aria-label to NotificationSettings switchOnly Switch
- Add prefers-reduced-motion check to SlotMachineHeadline JS animation
- Bump --muted-foreground from 55% to 62% opacity for improved contrast margin
https://claude.ai/code/session_01JXajpFxhqLf8aPCncP81k3
* Fix code review findings from WCAG compliance pass
- Add Arrow key navigation + roving tabindex to all role=radiogroup
components (AutodraftSettings x2, TimerModeSelector,
OvernightPauseSettings, ScoringPresetPicker) per ARIA radio pattern
- Extract shared focus-trap logic into useFocusTrap hook; update
ConnectionOverlay and AuthRecoveryOverlay to use it
- Add tabIndex={-1} to ConnectionOverlay Card so focus can land in
spinner-only state (no interactive children)
- Replace aria-live on loading dots container with sr-only span so
status changes are announced by text content, not aria-label
- Remove contradictory aria-hidden+role=columnheader from
AvailableParticipantsSection visual-only header row
- Remove invalid scope="col" from div[role=columnheader] in
DraftSummaryView (scope is only valid on <th>)
- Remove redundant aria-label from ParticipantSelectionDialog sport
select (htmlFor label is sufficient)
- Change WizardStepper connector <li> to role=presentation
- Revert muted-foreground from 62% to 55% (original already passes
contrast; footer was fixed separately via text-muted-foreground)
https://claude.ai/code/session_01JXajpFxhqLf8aPCncP81k3
* Fix lint error and update tests for WCAG role changes
- Replace el! non-null assertion with optional chaining in useFocusTrap
- Update AutodraftSettings tests to query role="radio" instead of
role="button" (buttons have an explicit radio role since the WCAG pass)
- Update AvailableParticipantsSection watchlist tests to use
getByRole/getAllByRole instead of getByTitle/getAllByTitle (watchlist
buttons now use aria-label instead of title)
https://claude.ai/code/session_01JXajpFxhqLf8aPCncP81k3
---------
Co-authored-by: Claude <noreply@anthropic.com>
Commissioners can now opt in to having a draft start automatically at
its scheduled draftDateTime rather than requiring a manual "Start Draft"
click. The 1-second timer loop checks for eligible seasons each tick and
calls the new shared startDraft() service, which is also used by the
existing commissioner HTTP route.
- Add autoStartDraft boolean to seasons table (migration 0108)
- Extract core start logic into app/services/draft-autostart.ts so both
the API route and the timer can call it without AsyncLocalStorage
- Add checkAndAutoStartDrafts() to the timer tick; disables autoStartDraft
and stops retrying if no draft order is set at fire time
- Guard against null draftDateTime in both the timer query (isNotNull)
and server actions (autoStartDraft forced false when datetime is null)
- Add "Auto-start at scheduled time" checkbox to league creation wizard
(step 3) and league settings, gated on date+time being set
- Show countdown + "Start Now" button in draft room when auto-start is
scheduled; reuses the existing nowForPauseCheck 1-second ticker
- Show orange warning banner on league homepage to commissioners when
auto-start is within 1 hour and no draft order has been configured
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Rename queue page to Set Pre-Draft Queue, remove VORP, fix re-add race condition
- Rename page title, h1, button, error message, and tests from
"Pre-Draft Rankings" to "Set Pre-Draft Queue"
- Remove VORP values and "Sorted by VORP" labels from the all-players list
- Fix bug where removing a player then immediately re-adding would fail
with "already in queue": track in-flight removes in a ref and guard
handleAdd against firing while a remove is still in-flight
https://claude.ai/code/session_01TGAV9A2c72PNkL1ffY7Xeq
* Address code review findings on Set Pre-Draft Queue page
- Rename component function from PreDraftRankings to SetPreDraftQueue
- Guard handleRemove against temp IDs — items added optimistically but
not yet written to the DB have no server-side record to delete; skip
the API call and just drop them from local state
- Surface a toast when handleAdd is blocked by a pending remove instead
of silently no-oping
- Group both refs before their shared useEffect for readability
- Drop justify-between from the All Players desktop heading (sole child)
- Update test describe blocks to "Set Pre-Draft Queue Access"
https://claude.ai/code/session_01TGAV9A2c72PNkL1ffY7Xeq
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add pre-draft queue builder so users can rank players before draft order is set
Creates a new /leagues/:leagueId/draft-queue/:seasonId page that lets team
owners browse participants by VORP and build their autopick queue during the
pre_draft phase. The queue uses the existing draftQueue table so it carries
seamlessly into the live draft room.
Adds a "Build Your Queue" button to DraftInfoCard that shows only when draft
order has not been set yet (replaced by "Enter Draft Room" once it is).
https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG
* Improve pre-draft rankings UX: mobile tabs, add/remove toggle, rename
On mobile, show tabs ("All Players" / "My Rankings") instead of a stacked
layout that buries the queue below a long participant list. On the All Players
list, the button now toggles between Add and Remove so users never need to
switch tabs just to drop someone. Desktop keeps the side-by-side panel layout.
Also renames the feature throughout from "queue builder" to "pre-draft rankings"
and the DraftInfoCard button to "Set Pre-Draft Rankings".
https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG
* Fix all code review issues in pre-draft rankings feature
- Replace useFetcher with direct fetch() for add/remove/reorder so rapid
clicks no longer cancel in-flight requests
- Add toast.error() on all failure paths (matching live draft room pattern)
and revert optimistic state when operations fail
- Add useRef to give handleReorder a stable reference without a localQueue
closure dependency, preventing unnecessary QueueSection re-renders
- Convert allPlayersPanel and rankingsPanel to useMemo
- Remove leagueId from loader return; read from useParams() instead
- Extract queueBuilderHref to a variable in the league home component
- Add emptyMessage prop to QueueSection with a correct message for the
pre-draft context ("Add players from All Players...")
- Add draft-queue-access.test.ts covering all loader access control paths:
401/403 errors, status-based redirects, and redirect URL correctness
https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG
* Fix lint: use !== null check instead of != null
oxlint enforces eqeqeq; replace != null with !== null && !== undefined.
https://claude.ai/code/session_01Gu2DkTWL3nv74EMuGPpxhG
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Show season name instead of year/bracket format; add local timezone hint to draft time
Replace the raw year number and scoringType string in the league creation
sport picker, review step, and league settings sports section with the
human-readable season name (e.g. "2025 NBA Season"), matching how the
league homepage sports list already displays seasons.
Add a client-side local timezone label below the draft date & time fields
on both the league creation wizard and the league settings page, so users
know that draft times are in their local timezone.
https://claude.ai/code/session_01GtJowGruAc1A4jURkSDVjX
* Address code review: shared tz hook, short abbrev, sort fix, dead type cleanup, test update
- Extract timezone detection into useLocalTimezone hook to avoid duplication
across DraftSetupSection and Step3DraftSettings
- Use Intl short timezone abbreviation (e.g. "EST") instead of raw IANA name,
matching DraftInfoCard's existing pattern
- Sort review step sport list by season name instead of sport.name so the
visible order matches the displayed text
- Remove unused scoringType and year fields from SportsSection's local type
- Update SportsSection tests to use realistic season names and the new
display format
https://claude.ai/code/session_01GtJowGruAc1A4jURkSDVjX
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Fix custom chess-clock timer detection and save settings blocker
- Extract getInitialDraftSpeed() helper so both useState init and
resetSettingsFormState use the same logic; unrecognized presets
now produce a "custom:{bank}:{incr}" string instead of falling
back to "standard", so the custom section auto-expands correctly
(e.g. 8 hr bank + 30 min increment is no longer shown as Standard)
- resetSettingsFormState was not resetting draftSpeed at all; fixed
- Clear hasUnsavedSettingsChanges in the Form's onSubmit so the
useBlocker check is false at the moment the save navigation starts,
preventing the "Leave without saving?" dialog from firing on save
- Replace the cleared-on-success useEffect with one that re-marks
dirty when the action returns an error, so the warning reappears
after a failed save
https://claude.ai/code/session_01DgHNkvJXGizx41CE2tkVS1
* Address code review: single source of truth for presets, useEffect guard, tests
- Extract CHESS_CLOCK_PRESETS (pure data) into draft-timer.ts as the
canonical source of truth; DraftSpeedPicker.tsx now spreads those
values into CHESS_PRESETS rather than duplicating the numbers
- getInitialDraftSpeed moved to draft-timer.ts and rewritten to use
CHESS_CLOCK_PRESETS.find() — no more hardcoded bankSec/incrSec values
in three separate places
- parseDraftSpeed switch replaced with CHESS_CLOCK_PRESETS.find() and
an explicit fallback comment; eliminates the silent default-handles-
"standard" fragility called out in review
- useEffect that re-marks settings dirty now guards against non-settings
errors (draft-order, commissioner actions) which carry a `section`
field — previously any error actionData would spuriously set
hasUnsavedSettingsChanges=true
- Add parseDraftSpeed and getInitialDraftSpeed test suites to
draft-timer.test.ts, including parametrised preset coverage via
it.each(CHESS_CLOCK_PRESETS)
https://claude.ai/code/session_01DgHNkvJXGizx41CE2tkVS1
* Fix lint: use !== null/undefined instead of != null
oxlint enforces eqeqeq; the null check in getInitialDraftSpeed used
!= which triggered two errors.
https://claude.ai/code/session_01DgHNkvJXGizx41CE2tkVS1
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add admin users management page with pagination, search, and inline username editing
https://claude.ai/code/session_01E4UUQqQedhFP2F5YcBRArs
* Fix five issues from admin users page code review
- admin.users: use useEffect to close inline edit on success so validation
errors are not silently discarded when the save button fires onClick
- root: add /admin to ONBOARDING_EXEMPT so admin users without a username
are not redirect-looped away from admin pages
- settings: skip username validation when the username field is empty so
updating timezone alone does not break for accounts without a username
- models/user: exclude soft-deleted users from findUsersPaginated
- admin.users: remove unused hasMore from loader return; simplify header div
https://claude.ai/code/session_01E4UUQqQedhFP2F5YcBRArs
* Fix lint: rename shadowed variables in user model
- findUserByUsername: use imported sql directly instead of destructuring
it from the callback (shadowed the top-level import)
- findUsersPaginated: rename orderBy callback param from users to t
(shadowed the outer users result variable)
https://claude.ai/code/session_01E4UUQqQedhFP2F5YcBRArs
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add username onboarding prompt for new signups
New users (especially via OAuth/Google) are redirected to /onboarding
to choose a display username before accessing the app. Adds unique
constraint on users.username and case-insensitive uniqueness validation.
https://claude.ai/code/session_01UinBMAy9d6dQzzbcUAdq8J
* Address all code review issues on username onboarding
- Replace case-sensitive unique constraint with functional unique index on
lower(username) so DB-level uniqueness is case-insensitive (fix#1)
- Share USERNAME_RE from models/user.ts; add same format + uniqueness
validation to the settings update-profile action (fix#2)
- Wrap updateUser calls in try/catch to handle race-condition DB errors
gracefully in both onboarding and settings (fix#3)
- Add unit tests for suggestUsername, safeRedirectTo, USERNAME_RE, and
isOnboardingExempt (fix#4)
- Guard suggestUsername against returning strings shorter than 3 chars (fix#5)
- Preserve the user's intended destination via redirectTo query param
through the onboarding flow (fix#6)
- Treat empty username in settings as a validation error instead of a
silent no-op (fix#7)
- Use exact/sub-path matching in isOnboardingExempt to prevent
false-positive prefix matches like /loginpage (fix#8)
https://claude.ai/code/session_01UinBMAy9d6dQzzbcUAdq8J
---------
Co-authored-by: Claude <noreply@anthropic.com>
* 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>
* Add MLS standings sync and fix simulator conference resolution
- New MlsStandingsAdapter using ESPN's free soccer API (no key required),
returning Eastern/Western conference data, W/D/L/GF/GA/GD/PTS, conference
rank, and home/away records; registered under mls_bracket
- Display route now shows soccer table columns and a 9-team playoff cutoff
line for mls_bracket (top 9 per conference qualify for MLS Cup Playoffs)
- MLS simulator gains a third conference fallback: reads externalId="Eastern"
or "Western" on the participant, mirroring the LLWS pool-assignment pattern
so admins can bootstrap conference data before the first standings sync
- 9 unit tests covering stat mapping, conference normalization, rank ordering,
and error paths
https://claude.ai/code/session_01WhzXHpv6taXdHzhgvnv83u
* Address code review feedback on MLS standings + simulator
1. Update mls-simulator.ts module comment to document the new step-3
externalId fallback in the conference resolution order
2. Tighten normalizeConference to exact-match "Eastern"/"Western
Conference" instead of broad substring, preventing false matches
on names like "Northeast"
3. Pre-parse statsMap for each entry before the sort so it isn't
rebuilt O(n log n) times during comparison
4. Document the externalId name-matching tradeoff on
parseConferenceFromExternalId
5. Try ESPN's gamesPlayed stat before falling back to wins+losses+ties
sum; export parseConferenceFromExternalId for direct testing
6. Add tests: normalizeConference passthrough, winPct=0 at preseason,
and parseConferenceFromExternalId (case-insensitivity, null/undefined,
numeric ESPN IDs, unrecognized strings)
https://claude.ai/code/session_01WhzXHpv6taXdHzhgvnv83u
* Fix lint: replace != null with !== null && !== undefined
oxlint enforces eqeqeq; the five != null checks in mls.ts were flagged.
https://claude.ai/code/session_01WhzXHpv6taXdHzhgvnv83u
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add MLS sport simulator (mls_bracket)
Adds Major League Soccer as a draftable sport with a full-season Monte Carlo
simulator. Models both preseason regular-season projection (34 games across
Eastern and Western conferences) and the MLS Cup Playoffs bracket, including
the Wild Card (single game + PKs), Round 1 best-of-3 series, Conference
Semis/Finals (single game), and MLS Cup.
P1–P8 mapping: MLS Cup winner, finalist, Conference Finals losers, Conference
Semifinals losers. Conference assignment reads from regularSeasonStandings.conference
or falls back to the region simulator input ("Eastern"/"Western").
Admin inputs: projectedTablePoints (primary, max 102 for 34×3), with derivation
chain to sourceElo via existing input-policy; sourceOdds as alternative.
No hardcoded team data — all inputs are admin-managed per season.
- database/schema.ts: add mls_bracket to simulatorTypeEnum
- drizzle/0104_chief_boom_boom.sql: migration for the new enum value
- mls-simulator.ts: MLSSimulator + exported pure helpers for testability
- registry.ts / manifest.ts / simulator-config.ts: register mls_bracket
- mls-simulator.test.ts: 38 unit tests covering all helpers and sync checks
https://claude.ai/code/session_015wkBJ3SYGcMGjsddKKGkwa
* Fix MLS simulator: config loading, sourceOdds fallback, normalization
Three issues found in code review comparing against EPL and NFL simulators:
1. Load simulator config from DB via getSportsSeasonSimulatorConfig so
admins can override iterations, seasonGames, parityFactor, drawRates
per season without code changes. Previously all constants were hardcoded.
2. Add sourceOdds → convertFuturesToElo fallback when no sourceElo is
present. EPL and NFL both do this; MLS was throwing immediately instead
of attempting the odds conversion that the manifest declares as optional.
3. Call normalizeSimulationResultColumns before returning results, matching
EPL's local normalization pattern for consistency (runner also normalizes
globally, but EPL calls it locally too).
https://claude.ai/code/session_015wkBJ3SYGcMGjsddKKGkwa
* Polish MLS simulator: configNumber zero, logger warning, Map lookup
Three small fixes from secondary code review:
1. configNumber: allow value >= 0 (not just > 0) so admins can
legitimately set baseDrawRate or drawDecay to 0 without the value
being silently discarded and replaced with the default.
2. Add logger.warn when a participant is excluded from simulation due
to a missing Elo rating, matching the NLL bracket-aware pattern.
Gives admins a visible signal instead of a silent exclusion.
3. getBySeeds: build a Map once instead of calling Array.find() per
seed. Eliminates 4M linear scans across 50k iterations for a 9-
element array — trivially fast either way, but Map is the right tool.
https://claude.ai/code/session_015wkBJ3SYGcMGjsddKKGkwa
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Revert ncaaw-simulator to Barthag win probability formula; set
realistic rating bounds (ratingMin: 0.70, ratingMax: 0.97) so derived
ratings stay in the range where the formula behaves well
- Add batchSaveFuturesOddsForSimulator which clears all ratings (manual
and generated) before upserting sourceOdds, so futures odds always
drive the simulation rather than being silently overridden by existing
Barthag ratings from Simulator Setup
- Add clearSourceOddsForParticipants to zero out both tables for
participants excluded from a bulk import
- Add "Clear existing odds" checkbox to the bulk import card; applies
client-side on match and server-side on submit
- Fix missing sportsSeasonId filter in batchSaveFuturesOddsForSimulator
pre-clear UPDATE (could have wiped ratings across other seasons)
- Fix race condition: run batchSaveSourceOdds then
batchSaveFuturesOddsForSimulator sequentially so the simulator inputs
table always ends in the correct cleared state
- Fix Math.round in convertFuturesToElo collapsing Barthag-scale ratings
to 0 or 1; Elo callers already round after clamping
- Handle all-identical-odds edge case in convertFuturesToElo (assign
midpoint instead of throwing)
- Add missingRatingStrategy: worstKnownMinus to ncaaw_bracket manifest
so fallbackRatingDelta is live config, not dead
- Log warning in resolveRatings when only 1 participant has odds
- Reset clearExisting checkbox after applyMatches
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: plan NLL preseason simulator
* Add NLL Season + Playoffs Monte Carlo simulator
14-team regular season (18 games) projects top-8 via Elo + decaying
projectedWins prior (adjusts for games already played). Playoff bracket:
QF single-game (1v8, 2v7, 3v6, 4v5), SF and Finals best-of-3. Three
runtime modes: bracket-aware → known-seed → regular-season projection.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix standings table bugs and polish across all three scoring patterns
- Fix isLastBeforePointsLine/PlayoffLine: was incorrectly suppressing the
bottom border on the last row of a section when no divider followed
(nextStanding === undefined case). Now correctly requires nextStanding
to exist and exceed the cutoff rank.
- Fix totalCols colSpan overcounting: hidden sm:table-cell columns
(Drafted By / Mgr) don't occupy column slots on mobile, so counting
them caused the divider rows to span one too many. Replaced with
colSpan={100} (browser caps to actual column count).
- Move pointsLinePushed mutation out of render in SeasonStandings and
QualifyingPointsStandings: replaced let+mutation+array-push pattern
with pre-computed firstOver8Idx and React.Fragment per row.
- Replace array-returning .map() with keyed React.Fragment in both files.
- Remove unused description prop from SeasonStandingsProps.
- Use useId() for Switch id props in all three components to prevent
id collisions when mounted multiple times.
- Fix formatQP NaN fallback from "0" to "—".
- Add comment noting canFinalize guards the admin-only finalize UI.
- Drop dead description prop pass-through in SportSeasonDisplay.
Fixes#408
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix RegularSeasonStandings tests broken by showStats toggle
- STK/streak tests: click the Details switch before asserting on stats
columns, which are now hidden behind the toggle by default
- Ownership badge test: use getAllByText since the badge renders in both
the mobile inline slot and the hidden-sm desktop Mgr cell
- Division label test: remove expectation for inline per-row division
labels, which were intentionally removed to fix mobile scroll
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix MLB streak doubling bug, hide empty standings columns, improve mobile layout
- mlb.ts: use streakCode alone (the API already returns the full string like
"W3"); appending streakNumber was doubling the digit, causing "L33" display
- Update mlb.test.ts mocks to match real API format (streakCode "W3" not "W")
- RegularSeasonStandings: conditionally hide GB/L10/STK columns when no rows
have data, so pre-season or stats-free sports don't show blank columns
- Mobile two-row layout: GP/PCT/GB/L10 move to a secondary sub-row (sm:hidden)
so all data stays visible without horizontal scroll on small screens; STK and
W/L remain on the primary row; reduce min-w from 740px to 360px
https://claude.ai/code/session_01RADi3LhYMPbRDm5no1ZpdF
* Address code review: cleanup mlb streak type, fix soccer GP on mobile, hoist showSubRow
- mlb.ts: drop redundant `?? undefined` after optional chain; document that
streakCode is the full string (e.g. "W3") and streakNumber is unused
- RegularSeasonStandings: hoist hasSecondaryStats → showSubRow to component
level (it only depends on a prop, not on individual row data)
- Remove non-functional `truncate`/`min-w-0` from team name cell — truncation
requires table-layout:fixed which we don't use; team names size naturally
- Soccer GP was hidden on mobile with no sub-row to surface it; GP now shows
inline for soccer on all viewports, hidden only for non-soccer (which has
the secondary sub-row)
- Add comment explaining totalCols counts hidden-on-mobile columns for colSpan
- Add missing test for GB column hiding when no rows have gamesBack data
https://claude.ai/code/session_01RADi3LhYMPbRDm5no1ZpdF
---------
Co-authored-by: Claude <noreply@anthropic.com>
Leagues where the user is a commissioner but has no team are now shown
in a "Leagues I Commish" subsection below their member leagues, rather
than silently appearing with no stats. A live draft always takes
priority over the commish-only row so commissioners still see the draft
banner.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix circular dependency and client-bundle server-only code in simulator routes
NCAAM and NCAAW simulators imported getParticipantSimulatorInputs from
models/simulator, which imports manifest.ts, which imports registry.ts —
creating a cycle back through registry.ts → simulator files. This caused
a TDZ ReferenceError in the client bundle.
Fix: inline a direct seasonParticipantSimulatorInputs query in both
simulators (they already run direct DB queries) instead of going through
the model layer, breaking the cycle.
Also moves getSimulatorInputPolicy from the component body to the loader
on the simulator setup page. The component calling it pulled input-policy.ts
→ manifest.ts → registry.ts → all simulator implementations into the client
bundle, which contain Node.js-only code and caused "Error loading route
module" in production.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Move registry calls from component to loader in admin.sports.$id
SIMULATOR_TYPES and getSimulatorInfo were used directly in the component
body to build the simulator type dropdown. This dragged registry.ts —
which imports every simulator implementation — into the client bundle,
causing a TDZ ReferenceError that broke any route co-loaded with it
(observed as "Error loading route module" on admin events pages).
Fix: pre-build simulatorOptions in the loader and pass them as plain
serializable data. The registry imports remain for the action's
validator but are now only reachable from server-only code.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
NCAAM and NCAAW simulators imported getParticipantSimulatorInputs from
models/simulator, which imports manifest.ts, which imports registry.ts —
creating a cycle back through registry.ts → simulator files. This caused
a TDZ ReferenceError in the client bundle.
Fix: inline a direct seasonParticipantSimulatorInputs query in both
simulators (they already run direct DB queries) instead of going through
the model layer, breaking the cycle.
Also moves getSimulatorInputPolicy from the component body to the loader
on the simulator setup page. The component calling it pulled input-policy.ts
→ manifest.ts → registry.ts → all simulator implementations into the client
bundle, which contain Node.js-only code and caused "Error loading route
module" in production.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces three new schema tables (simulator_profiles,
sports_season_simulator_configs, season_participant_simulator_inputs),
a central model layer (app/models/simulator.ts), and a single runner
entry point so every simulator run follows the same prepare → simulate
→ persist → snapshot → recalculate flow.
Key additions:
- manifest.ts: per-simulator display names, default configs, required/
optional inputs, derivable-input declarations, and setup sections
- input-policy.ts: resolves sourceElo from projectedWins,
projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds;
supports block / fallbackElo / averageKnown / worstKnownMinus strategies
- runner.ts: single entry point for admin simulation runs; materialises
derived inputs, normalises result columns, zeroes omitted participants,
snapshots EVs, and recalculates linked fantasy standings
- /admin/simulators: inventory page with per-season readiness and bulk run
- /admin/sports-seasons/:id/simulator: per-season setup page with readiness
summary, input-policy editor, raw JSON config override, and CSV bulk input
- NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs,
falling back to the hardcoded name-keyed maps while DB data is being populated
- Clone flow copies simulator config by default; volatile inputs (odds, Elo)
only copied when explicitly requested
Code-review fixes included in this commit:
- source field in compatibility bridge checked with !== null instead of !== undefined
- sourceEloRequirementLabel no longer appends "configured fallback" when the
participant is already excluded from all resolved sources
- Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel
- save-config preserves existing inputPolicy when the submitted JSON omits it
- Input table truncation label added (Showing 20 of N)
- CSV description notes values must not contain commas
- N+1 comment added to listSportsSeasonSimulatorSummaries
- assertRegistrySchemaDriftFree called in manifest tests
- Runner test suite added covering happy path, already-running guard,
readiness failure, empty results, and error recovery with status reset
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
When a pick is made the picked player's row flashes teal (--electric)
then fades out over 750ms. Works correctly with the TanStack Virtual
list by keeping the row in filteredParticipants during the animation
via animatingOutParticipantIds, then removing it after 650ms.
Key design decisions:
- Animation state lives in AvailableParticipantsSection (internal
useState + timers); the draft room passes a pickAnimationSignal
{id, seq} rather than the Set, so memo only breaks on each pick,
not on the 650ms cleanup
- displayDrafted freezes row content (badges, buttons) during the
animation to prevent height shifts
- Per-ID independent timers via Map ref support rapid autodraft picks
running concurrently without cancellation
- Timers are cleared on unmount in both the draft room and component
- prefers-reduced-motion: skips the color flash, plain opacity fade only
- Removes "Draft Grid" heading from DraftGridSection (visual cleanup)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Reduce double-stacked padding on draft board page from 64px to 32px on
mobile by using responsive p-2/px-2 md:p-4/md:px-4 classes.
Replace dashboard CSS grid with flex layout so My Leagues and Create
League stack naturally at the top without being stretched apart by the
row-span-2 events column. Mobile order preserved: My Leagues, Events,
Create League.
* Add account deletion and multi-section settings page
- Rename /user-profile → /settings with 301 redirect from old URL
- Add multi-section settings nav (Profile, Account, API placeholder, Data & Privacy)
reusing existing SettingsDesktopNav/SettingsMobileGridNav components
- Implement account deletion via anonymization: wipes all PII from users row,
releases team ownerships, removes commissioner records, deletes sessions/accounts
- Add data export request form that emails privacy@brackt.com via Resend
- Add deletedAt timestamp column to users table (migration 0100)
- Add anonymizeUserAccount() to user model
- Add removeAllCommissionersByUserId() to commissioner model
- Tests for both new model functions
- Update UserMenu "Profile" link → "Settings" at /settings
https://claude.ai/code/session_017Hvmof82Xr3UwKFc3pnC4X
* Address code review feedback on settings/account deletion
1. Wrap anonymizeUserAccount in a DB transaction so partial failures
(e.g. session delete succeeds but user update fails) can't leave
accounts in an inconsistent state
2. Escape user email and notes with escapeHtml() before interpolating
into the data request email body
3. Reset AlertDialog confirmed state when dialog is dismissed via
backdrop click or Escape key (onOpenChange handler)
4. Extract accounts DB query to app/models/account.ts (findLinkedAccountsByUserId)
to comply with the "always query through app/models/" convention
5. Replace dynamic imports() in action handlers with static top-level imports
6. Remove the unused hard-delete deleteUser() function
7. Add lastDataRequestAt timestamp to users (migration 0101) and enforce
a 30-day server-side cooldown on data export requests
8. Replace fragile actionData type casts with a proper ActionData
discriminated union; narrowing now works without `as` assertions
9. Strengthen tests: verify which schema tables are passed to delete()
and that operations run inside the transaction
https://claude.ai/code/session_017Hvmof82Xr3UwKFc3pnC4X
* Fix no-non-null-assertion lint error in PrivacySection
Replace non-null assertion with optional chaining on dataRequestCooldownUntil
to satisfy oxlint no-non-null-assertion rule.
https://claude.ai/code/session_017Hvmof82Xr3UwKFc3pnC4X
---------
Co-authored-by: Claude <noreply@anthropic.com>
Introduces a shared email module that wraps all transactional emails in a
cohesive dark-themed template matching the site's visual identity (Barlow
font, #14171e background, #adf661 CTA buttons, logomark PNG header).
- app/lib/email.server.ts: new module with sendEmail, wrapInEmailTemplate,
emailButton, emailParagraph, and escapeHtml helpers; Resend instance is
lazily initialized as a singleton
- public/email-logo.png: 96×124px PNG of the logomark for email use (SVG
gradients are not supported in Outlook)
- auth.server.ts: password reset and email verification now use the branded
template; errors are thrown so Better Auth can surface failures
- support.tsx: internal notification uses sendEmail; adds a user-facing
branded confirmation email (fire-and-forget so failures don't surface to
the user); plain text fallback included
- scripts/sync-prod-db.sh: remove first_name/last_name from sanitize query
(columns were dropped in #399)
- 13 unit tests covering all template helpers
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
New email/password accounts must verify before signing in. Social login
users (Google, Discord) are unaffected. Existing users are grandfathered
via an idempotent migration. Adds a /check-email interstitial with a
resend button, and a --success CSS token for consistent styling.
Closes#343
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix mobile league settings: prevent sports scroll and remove completion checkmarks
- Add overflow-hidden to sport label text span so flex truncation is properly contained
- Add min-w-0 to SettingsSection header inner flex item to prevent title/description overflow
- Remove completion check marks from mobile grid nav buttons (league settings has no completion concept)
- Remove "X of Y set" counter from mobile nav header
- Remove isComplete field from SettingsGridSection type and all data
https://claude.ai/code/session_01T7iTb9YZLuWJFtKV753tdB
* Fix race window in user creation and restore FlagSvg safety fallback
auth.server.ts: switch generateId to application-generated UUIDs so the
ID is known before the insert, then move flagConfig assignment from
create.after (a separate UPDATE) to create.before so it lands in the
initial INSERT with no race window.
FlagSvg.tsx: validate the config with parseFlagConfig before rendering;
corrupt or missing data falls back to a neutral gray triband flag rather
than silently rendering blank SVG shapes.
https://claude.ai/code/session_01T7iTb9YZLuWJFtKV753tdB
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Remove backfill-flag-configs script
One-time migration has been run; script is no longer needed.
https://claude.ai/code/session_014RUwPfm1qc539xLxW4m9Uy
* Clean up flag config code
- FlagSvg: remove redundant getDisplayFlagConfig call; config is already a
valid FlagConfig by type, so the validation/fallback was dead code. Also
replace the unreachable `?? "#adf661"` fallback with a non-null assertion.
- team/user models: pre-generate ID before insert so flagConfig is persisted
immediately on creation rather than relying on display-time generation.
- auth.server.ts: add create.after hook so BetterAuth-created users also get
a flagConfig written to the DB on signup.
https://claude.ai/code/session_014RUwPfm1qc539xLxW4m9Uy
* Fix lint: replace non-null assertion in FlagSvg with destructure defaults
oxlint forbids the ! operator; destructuring with empty-string defaults
is equivalent since FlagConfig always has 3 colors.
https://claude.ai/code/session_014RUwPfm1qc539xLxW4m9Uy
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Fix standings page team icons to use selected team avatar
The full standings page was not passing avatar data (logoUrl, flagConfig,
avatarType, ownerAvatarData) to StandingsPreview entries, so all teams
showed the default initials fallback instead of their chosen avatar.
Fetches the avatar columns from the teams table in the loader, resolves
owner avatar data for teams using the owner avatar type, and threads
all avatar fields through formattedStandings into previewEntries.
https://claude.ai/code/session_01CVt5Vo2PDGY4G3wSWbsmRG
* Add StandingsPreview component tests covering avatar rendering
Tests were missing for the StandingsPreview component, which is the
component that actually renders team avatars on the full standings page.
Covers uploaded image, flag SVG, owner avatar inheritance, and the
generated-flag fallback, plus basic content rendering (team names,
owner names, points, description, full standings link).
https://claude.ai/code/session_01CVt5Vo2PDGY4G3wSWbsmRG
* Fix StandingsPreview test ambiguous text query for team names
FlagSvg renders a <title> element with the team name for accessibility,
so getByText("Team Alpha") matched two elements. Scope the query to the
<p> element to target only the visible team name display.
https://claude.ai/code/session_01CVt5Vo2PDGY4G3wSWbsmRG
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Remove rounded clipping on navbar user avatar button (was showing dark circular background)
- Fix "My Avatar" preview in team settings showing circular instead of square shape
- Pass owner avatar data through to TeamAvatar in standings, draft room, and draft board so teams with avatarType="owner" correctly show the user's avatar instead of the generated flag
- Consolidate user lookups in draft board loader to avoid duplicate DB queries
- Fix DraftSlotWithTeam interface to use RawFlagConfig type and include logoUrl/flagConfig/avatarType fields
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Refactor league settings into per-section components (#347)
Extract all settings sections from the monolithic 1009-line route file into
individual components under app/components/league/settings/. Route file drops
to ~300 lines. Separates draft-order dirty state from general settings dirty
state, deduplicates section-change handling, and fixes several bugs found
during review (typo in pointsFor5th, wrong mock in tests, lint violations).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix Stop hook loop: suppress output on typecheck success
The Stop hook was producing stdout on every run, causing Claude Code to
feed it back as context and rewake the model each turn. Now emits a
systemMessage JSON only on failure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Typescript fix
* Remove type asserting
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes#72
Upgrades ineligibleReasons from Record<string, string> to a structured
{code, message} type so the UI can render the human-readable reason
inline below the Ineligible badge in both AvailableParticipantsSection
and ParticipantSelectionDialog. API routes surface the message in 400
responses unchanged.
Co-authored-by: Claude Sonnet 4.6 <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>