- Add watchlist feature: eye icon per participant, "Watched Only" filter, DB
table + migration, toggle API route, socket sync on reconnect
- Make RecentPicksFeed collapsible on mobile participants tab (chevron toggle,
defaults expanded)
- Add AutodraftSettings to mobile controls tab (was desktop-only)
- Pin Pause/Resume Draft and Exit Draft Room buttons to the bottom of the
mobile controls tab
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add overnight pause feature for draft timers
Protects players from having their pick timer expire while asleep.
Admins configure a nightly window (league-wide or per-user timezone);
the timer freezes during that window while autodraft can still fire.
Fixes#66
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix TS error: guard getUserDisplayName against undefined user
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add commish right-click context menus to MiniDraftGrid team headers
Adds onAdjustTimeBankOpen and onSetAutodraftOpen props to MiniDraftGrid
so the last-two-rounds display on the Participants tab shows the same
"Adjust Time Bank…" / "Set Autodraft…" context menu on right-click that
the full Draft Board already exposes. Also passes those callbacks from
the route's miniDraftGrid useMemo so they are wired up for commissioners.
https://claude.ai/code/session_017JCShLVs9xZ6FZmyrUFaE1
* Address all code review issues for draft room context menus
Layout bug: flex-1 min-w-20 was nested inside headerInner instead of the
direct flex-child wrapper in MiniDraftGrid, breaking equal column sizing.
Fixed by moving the classes to the outermost element in both the menu and
non-menu paths (matching DraftGridSection's pattern).
MiniDraftGrid improvements:
- Hoist hasHeaderMenu constant above the draftSlots.map() loop
- Add optional connectedTeams prop; disconnected teams render italic +
muted-foreground, consistent with DraftGridSection
- connectedTeams now wired through the route's miniDraftGrid useMemo
DraftGridSection interface:
- Make onForceAutopick and onForceManualPickOpen optional (?) to match
every other commissioner callback; add null checks at all three call
sites (context menu, mobile MoreVertical button, mobile Sheet)
- Gate those two callbacks behind isCommissioner at the route level
Tests (new files):
- app/components/__tests__/MiniDraftGrid.test.tsx — covers layout classes,
connected/disconnected styling, and all four context menu interactions
- app/components/__tests__/DraftGridSection.test.tsx — covers all three
context menu surfaces for both commissioner and non-commissioner roles
Storybook: add CommissionerView and DisconnectedTeams stories to
MiniDraftGrid.stories.tsx
https://claude.ai/code/session_017JCShLVs9xZ6FZmyrUFaE1
* Move hasHeaderMenu out of IIFE into component body
Computing it before the return statement is the right place since it only
depends on props, not loop variables. Removes the IIFE entirely and fixes
the indentation of the map callback body.
https://claude.ai/code/session_017JCShLVs9xZ6FZmyrUFaE1
* Mock HTMLElement.scrollTo in test setup
jsdom does not implement scrollTo on elements, causing any component that
calls element.scrollTo() (e.g. MiniDraftGrid's scroll-to-current-cell
effect) to throw in unit tests.
https://claude.ai/code/session_017JCShLVs9xZ6FZmyrUFaE1
---------
Co-authored-by: Claude <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>
Show visual dividers in the available participants list indicating where
your future draft picks fall relative to the current pick position.
Dividers are suppressed when any filter is active (search, sport filter,
hide drafted, etc.) since positional references are meaningless in a
filtered view.
- Add getProjectedPicks() to draft-order.ts for computing future pick
positions in a snake draft
- Interleave divider items into the virtualized list in
AvailableParticipantsSection
- Extract ListItem type to module scope
- Add tests for getProjectedPicks, divider rendering, and filter
suppression
Logo SVGs referenced by string path (/logo.svg) were cached by CDNs and
browsers after deployments. Import them with Vite's ?url suffix so the
build emits content-hashed filenames that force a refresh on change.
LeagueRow and StandingsPreview had duplicated StatColumn, StatDivider,
and rank-change indicator helpers. Extract them into a shared
StatHelpers module so both pages render rank and points identically
(#3 style with consistent rank-change arrows).
* 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>
* fix: address VORP code review issues
- Switch admin EV form to batchUpsertParticipantEVs to avoid N×N
concurrent UPDATE storm (was calling upsertParticipantEV per
participant, each triggering a full syncVorpForSeason)
- deleteParticipantEV now resets vorpValue to "0" and calls
syncVorpForSeason so remaining participants' ranks stay correct
- syncVorpForSeason issues a single bulk CASE UPDATE instead of
N individual UPDATE statements
- Add doc warning on recalculateEV that callers must sync VORP manually
- Extract REPLACEMENT_LEVEL_START/END_IDX constants; clarify comment
that 12-14 is a fixed product decision, not derived from league size
- Include vorpValue in draft room participant select projection
- Update drizzle-orm mock to support sql.join; update test assertions
to reflect single bulk-update call
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: resolve lint errors (toSorted, unused var)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Update draft participant sorting to use vorpValue column instead of expectedValue
in draft room initialization and auto-pick selection for both single-sport and
multi-sport seasons.
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>
Every route now exports a meta function so the browser title bar reflects
the current page. Static pages use fixed titles; dynamic pages pull names
from loader data with a sensible fallback (e.g. league name, sport season
name, team name).
Titles follow the pattern "Page Name - Brackt" for user-facing routes and
"Page Name - Brackt Admin" for admin routes.
Fixes#74
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
After every pick, recalculate draft eligibility for all teams and
remove any queued participants whose sport is no longer eligible
(e.g. a team queued a snooker player but just filled their last flex
slot). Previously this was only caught lazily when autodraft fired,
which could pause the draft or pick an unwanted player.
- Add getAllQueuesForSeason to draft-queue.ts — fetches all queue rows
for a season in one query (Map<teamId, QueueItem[]>) instead of N+1
per-team queries
- Add pruneIneligibleQueueItems to draft-utils.ts — uses Promise.all
for the four required data fetches, collects ineligible items in a
single loop pass, warns on orphaned participant references
- Call from both pick paths: executeAutoPick and draft.make-pick.ts
- Emit queue-eligibility-pruned socket event per affected team so the
client updates the queue UI in real time
- Add 5 tests covering: single ineligible removal, all eligible (no-op),
empty queues (no delete called, getTeamQueue never called), mixed
queue (only ineligible item removed), and unknown participant (warn)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: improve JWT expiry handling and revalidation logic in DraftRoom component
* fix: ensure authRecoveryTimerRef is initialized to undefined to prevent potential errors
- Extract groupPicksByTeamAndSport into shared picks-utils.ts to remove
duplication between TeamRosterView and DraftSummaryView
- Fix TeamRosterView: make teamPicks a proper useMemo dep on selectedTeamId,
add useEffect to reset stale selection if draftSlots changes
- Add accessible label/htmlFor to TeamRosterView team Select
- Remove dead `season` prop from DraftSummaryView (was inherited from
TeamsDraftedGrid but never used)
- Fix DraftSummaryView: sport name cells are now <th scope="row">, column
headers have scope="col", Total header uses solid bg-muted to match Sport
- Replace || null with explicit > 0 check in Total cell for clarity
- Consolidate to summaryViewProps/rosterViewProps in route for consistency
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: enhance autodraft settings with detailed option descriptions and improved state handling
* test: update AutodraftSettings tests for button interactions and API payloads
Primary fix: setTeamTimers now bails out with `return prev` when the
value hasn't changed, preventing a full DraftRoom re-render on every
1-second timer tick (was 33% of profiler samples).
Memoization: wrap AvailableParticipantsSection, TeamsDraftedGrid,
QueueSection, SidebarRecentPicks, and DraftGridSection in React.memo
so timer ticks don't cascade into heavy components that don't use
timer state.
Stable refs: wrap nine action handlers in useCallback and extract two
inline arrow functions from props objects so memo() comparisons
actually bail out. Memoize the { numFlexPicks } object passed to
TeamsDraftedGrid.
socketVersion: expose an incrementing counter from useDraftSocket so
the socket handler effect re-registers on socket recreation.
Async cleanup: add abort flag + in-flight guard to the visibilitychange
JWT refresh handler to prevent concurrent executions and stale state
updates after unmount. Add abort flag to useDraftNotifications
permissions.query() to prevent dangling onchange if unmounted
mid-promise.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move "Show drafted sports" into the sport filter dropdown (sheet/popover)
instead of a standalone checkbox in the filter bar
- When unchecked, drafted sports are hidden from the dropdown list entirely;
when checked, they appear in alphabetical order with muted text
- Auto-deselect drafted sport filters when hiding drafted sports
- Reset button also restores "Show drafted sports" to checked
- Extract SportFilterContent component to eliminate sheet/popover duplication
- Make userDraftedSportNames required; add useCallback for handlers;
memoize triggerText/triggerAriaLabel; unify all checkboxes to ShadCN Checkbox;
replace empty div divider with hr
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the single-sport native <select> with a popover (desktop) and
bottom sheet (mobile) checkbox multi-select. No sports selected shows all
participants; selecting one or more narrows the list. Trigger label adapts
to reflect current filter state and a Reset button appears when filters
are active.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Clerk's short-lived JWTs (60s) can expire when browser tabs are
backgrounded and the SDK's refresh interval is throttled. This left
users silently logged out — actions returned 401s shown as generic
error toasts with no recovery path.
Prevention: refresh the Clerk JWT on tab visibility change so the
token is always fresh when the user returns from a backgrounded tab.
Detection: replace all 12 raw fetch() calls with an authFetch wrapper
that catches 401 responses and sets an authDegraded state, triggering
a blocking recovery overlay instead of confusing toasts.
Recovery: AuthRecoveryOverlay (mirrors ConnectionOverlay pattern)
prompts the user to reload the page, which re-authenticates via
Clerk's long-lived session cookie.
Also adds missing try/catch to handleStartDraft, handleMakePick,
handleForceAutopick, handleForceManualPick, and handleReplacePick
which previously had no network error handling.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add a reference-equality check so that if HTTP revalidation fails (network
error, expired token), the sync effect does not overwrite fresh data that
draft-state-sync already applied. Also adds missing dependency array entries
(userQueue, timers, autodraftSettings) to the revalidation sync effect.
https://claude.ai/code/session_01JxJ1CYTiFWV4KTPxTnjrms (#48)
* fix: sync all draft state on mobile reconnection
After a mobile browser returns from a long background period, the draft
room had stale picks, wrong "on the clock" display, and inaccurate
available player lists. The root cause was that reconnection relied
solely on an HTTP revalidation that could fail (expired JWT, flaky
network), and the timer-update handler ignored currentPickNumber.
Changes:
- Server emits draft-state-sync on join-draft with full picks, timers,
and season state, giving the client an immediate socket-based sync
path that doesn't depend on HTTP revalidation
- timer-update handler now syncs currentPickNumber, fixing the "on the
clock" display within 1 second of reconnection
- Revalidation retry with 3s delay ensures the HTTP path succeeds even
when the network is slow to stabilize on mobile return
- Revalidation completion now also syncs teamTimers and autodraftStatus
- Added draft-state-sync client handler that applies the server snapshot
immediately (skipped when revalidation is in-flight to avoid conflicts)
Tests: 32 new tests covering reconnection sync, pick buffering/merge,
timer-update currentPickNumber sync, draft-state-sync handling,
available player filtering, on-the-clock correctness, and revalidation
retry logic.
https://claude.ai/code/session_01JxJ1CYTiFWV4KTPxTnjrms
* fix: guard against stale revalidation overwriting fresh socket data
Add a reference-equality check so that if HTTP revalidation fails (network
error, expired token), the sync effect does not overwrite fresh data that
draft-state-sync already applied. Also adds missing dependency array entries
(userQueue, timers, autodraftSettings) to the revalidation sync effect.
https://claude.ai/code/session_01JxJ1CYTiFWV4KTPxTnjrms
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: eliminate queue tab flash on mobile by using useSyncExternalStore
useMediaQuery initialized to false and updated in useEffect, causing a
paint where isMobile was false on mobile devices. This rendered the
desktop layout first, hiding the queue tab until the effect fired.
useSyncExternalStore reads matchMedia synchronously on the client so
the correct layout is used on the first render with no extra paint.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: revalidate draft room when Clerk JWT expires at load time
Clerk's short-lived JWTs (~1 min) can expire between navigations.
Server-side clerkMiddleware can't refresh them for client-side loader
fetches, so getAuth() returns userId=null — causing userTeam=undefined,
the queue button to disappear, and the tab to default to "board".
Added a useEffect that detects the mismatch: if the Clerk client SDK
has a valid userId but the loader ran without one, trigger revalidate()
so the loader re-runs with a fresh token and returns correct
userTeam/userQueue data.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: prevent infinite revalidation loop in auth guard
If the server consistently fails to populate currentUserId after JWT
refresh (e.g. misconfigured CLERK_SECRET_KEY), the previous effect
would loop indefinitely. A one-shot ref ensures we attempt auth
recovery at most once per mount.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Add text-base md:text-sm to all native <input> and <select> elements
in the draft room (including the ShadCN SelectTrigger) so iOS Safari
doesn't auto-zoom when focused.
Also extract the two near-duplicate participant selection dialogs into
a shared ParticipantSelectionDialog component (-236 lines), and replace
O(n) queue.find() scans with a memoized Map in AvailableParticipantsSection.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Show owner username as primary label in "On the Clock" section (mobile bar + desktop sidebar), with team name as a secondary label below
- Memoize currentDraftSlot to avoid O(n) array scan on every timer tick
- Derive currentDraftSlotOwnerName once instead of looking up ownerMap four times per render
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Change default desktop tab from "participants" to "board"
- Extract mobileTabs/mobileColCount before JSX to eliminate IIFE pattern
- Remove redundant h-full wrapper divs from TabsContent for consistency
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
On reconnect, revalidate() re-fetches fresh loader data but the local
useState copies (picks, currentPick, isPaused, isDraftComplete, queue)
never synced with the new values — leaving the UI stale until a manual
refresh.
- Watch revalidatorState (idle→loading→idle) to detect when a
revalidation completes and apply the fresh loader snapshot to all
affected local state
- Buffer pick-made socket events received during the revalidation window
(instead of discarding or double-applying them); merge into the DB
snapshot on completion, deduplicated by pick ID
- Keep setCurrentPick paired with setPicks in handlePickMade so the
"on the clock" indicator and the picks list never desync
- Sync queue state from fresh userQueue after revalidation so
participants drafted while the user was away disappear from their
queue even if the participant-removed-from-queues events were missed
Adds useDraftSocket reconnect test suite (9 tests) covering initial
connect, socket reconnect, visibility-change triggering, network
flapping, error exhaustion, and cleanup.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Redesign autodraft queue system with three-state control and queue-only constraint
Core Logic & Database:
- Add `queue_only` boolean column to `autodraft_settings` (migration 0031)
- Rename autodraft UI states: Off / Next Pick / All Picks (while_on mode maps to All Picks)
- `autoPickForTeam`: respects new `queueOnly` param — skips EV fallback when enabled
- `executeAutoPick`: auto-disables autodraft + emits socket event when queue empties with queueOnly ON (AC3)
- `autodraft-updated` socket event now includes `queueOnly` field
Mobile UI Overhaul:
- Rename "Lobby" tab → "Available" (AC6)
- Add new "Queue" tab to mobile bottom nav with drag-reorder, per-item Draft buttons, and autodraft controls (AC5)
- Controls tab retains commissioner tools, notifications, exit; queue controls moved to Queue tab
- Turn indicator appears on both Available and Queue tabs
Components:
- `AutodraftSettings`: replaces toggle+radio with three-state button group (Off | Next Pick | All Picks) + "Only autodraft from queue" switch (AC1, AC2)
- `QueueSection`: adds `canPick` prop + per-item Draft buttons for instant drafting when on the clock
Desktop (AC4):
- Sidebar QueueSection unchanged in position; gains same three-state controls and Draft buttons
Tests (AC7):
- `autodraft.test.ts`: updated for queueOnly field and socket event shape
- `timer-autodraft.test.ts`: new tests for queue-only constraint, auto-shutoff transitions, and all three autodraft states
https://claude.ai/code/session_01PYhJicAStoJ2u6q6dV1naB
* fix: remove erroneous ?? fallbacks in autodraft socket emissions and add autoPickForTeam tests
- Remove `?? true` default on queueOnly in queue-empty auto-disable socket emit
(line 488) — was dead code since the column is NOT NULL, but semantically wrong
and would have caused client-side UI desync if the type ever relaxed
- Remove `?? false` default on the next_pick auto-disable path for consistency
- Add app/models/__tests__/auto-pick.test.ts with 6 tests covering the queueOnly
constraint: empty queue, all items drafted, partial queue skip, and EV fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add missing queueOnly prop to AutodraftSettings test fixtures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: rewrite AutodraftSettings tests for three-state button group UI
The component was redesigned from a switch + radio buttons to Off/Next Pick/All Picks
buttons with a separate queue-only Switch toggle. Updated 17 stale tests and added 5
new tests covering the queue-only toggle and the All Picks/Off button interactions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Reduce ping detection window from ~45s to ~15s (pingTimeout 5s, pingInterval 10s)
- Listen to window offline/online events for instant UI feedback on network drop
- Force immediate reconnect on online event instead of waiting for backoff
- Fix stuck-reconnecting bug when network returns within ping timeout window
- Wrap on/off/emit in useCallback to prevent listener re-registration every second
- Expose reconnectCount to trigger revalidator.revalidate() after each reconnect
- Reset hasConnectedOnce ref per socket instance to avoid spurious revalidations
- Remove stale socket ref from hook return value; add emit helper instead
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add optimistic remove to handleRemoveFromQueue (was missing, causing visible lag)
- Fix stale closure on previousQueue in reorder/remove by capturing inside setQueue updater
- Replace filter(Boolean) with typed type guard filter in handleReorderQueue
- Add QueueItem type alias from schema.$inferSelect, typed useState, remove all `any` casts
- Complete temp item fields in handleAddToQueue to satisfy QueueItem type
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Display sport name as muted subtitle under participant name to distinguish
players across sports (e.g. Florida NCAAM vs Florida NCAAW)
- Remove unused eligibility prop from QueueSection
- Deduplicate double .find() per queue item into a single lookup
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix settings action silently resetting timer values when draft speed
select is disabled (add null guard before overwriting draftInitialTime/
draftIncrementTime)
- Make timer decrement and pick increment atomic using SQL expressions to
prevent read-modify-write races between the timer loop and HTTP handlers
- Add UNIQUE INDEX on (season_id, pick_number) in draft_picks to prevent
duplicate picks from concurrent requests (TOCTOU guard)
- Add socket join-draft team ownership validation via DB query
- Add iteration cap to autodraft chain while loop (max = totalTeams)
- Add draftPaused re-check before firing autodraft chain in make-pick
and force-manual-pick
- Consolidate all snake draft calculations into calculatePickInfo (DRY);
remove duplicated logic from timer.ts, make-pick, force-manual-pick,
executeAutoPick, and checkAndTriggerNextAutodraft
- Fix calculatePickInfo to return snake-adjusted pickInRound matching
draftOrder values instead of raw pre-snake value
- Fix timer-update socket events to emit nextPickNumber after a pick
instead of the already-completed currentPickNumber
- Fix force-manual-pick to validate submitted teamId matches the team
whose turn it actually is at the given pick number
- Replace inline timer init in draft.start with deleteSeasonTimers +
initializeDraftTimers model functions (dead code fix)
- Fix draft loader to not crash when getTeamQueue fails (returns [])
- Fix misleading 403 message for commissioner picks
- Remove dead hidden inputs from league settings form
- Document connectedTeams single-instance limitation in socket.ts
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove 10-pick limit in SidebarRecentPicks
- Fix scroll by using max-h-[45vh] on AccordionContent instead of the
broken flex-1 approach (AccordionPrimitive.Content has overflow-hidden
hardcoded, making flex-1 and overflow-y-auto ineffective without a
bounded height)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
When a league has the "Make draft board publicly accessible" setting enabled,
unauthenticated users could view the draft-board route but were incorrectly
blocked from the draft room route with a 401. Restructure the loader to check
isPublicDraftBoard before enforcing auth/membership requirements, matching the
behaviour already implemented in the draft-board route.
https://claude.ai/code/session_016JiVAG2FrPYjAvSJi3daGD
Co-authored-by: Claude <noreply@anthropic.com>
Replace h-screen (100vh) with h-dvh (100dvh) on the root draft room
container. On mobile browsers, 100vh equals the max viewport height
when the address bar is hidden, causing the page to overflow when the
address bar is visible. 100dvh (dynamic viewport height) correctly
adjusts to the actual visible area at all times.
https://claude.ai/code/session_01GecUeTHw4Mc2ErSYJ6WXae
Co-authored-by: Claude <noreply@anthropic.com>
- Add bottom nav bar (Lobby/Board/Roster/Controls) with 56px touch targets
- Add mobile card list view in AvailableParticipantsSection with 44px Draft/Queue buttons
- Add round labels and sticky header row to DraftGridSection grid
- Add mobile Sheet for commissioner actions in DraftGridSection (replaces right-click context menu)
- Use useMediaQuery to render a single layout tree, eliminating duplicate component
instances (previously both desktop and mobile layouts were always in the DOM)
- Add compact header on mobile (text-lg vs text-3xl); hide commissioner buttons
and Exit link in header on mobile — surfaced in Controls tab instead
- Add "On Clock" bar on mobile showing current team and timer during active draft
- Add "your turn" indicator dot on Lobby bottom nav tab
- Default mobile tab to "board" for commissioner-only viewers
- Fix sticky corner cell z-index in TeamsDraftedGrid (z-10 → z-20)
- Fix round column spacer in DraftGridSection header not being sticky-left
- Extract shared prop objects to eliminate copy-paste between layouts
- Extract getParticipantState() helper to deduplicate participant render logic
- Move MobileSheetData type and MOBILE_TABS array to module scope
- Remove unnecessary non-null assertions in DraftGridSection Sheet handlers
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix force-manual-pick resetting next team's timer to initial time
When a commissioner forced a manual pick, the next team's timer was
being reset to the initial time (2 minutes) instead of carrying
forward their existing time bank balance.
This aligns force-manual-pick with the behavior of regular user picks
and force-autopick: the picking team gets their increment added, and
the next team's timer is left untouched so their bank carries forward.
https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p
* Add regression tests for draft.force-manual-pick timer behavior
18 tests across 5 describe blocks covering:
- Authorization (401/403)
- Input validation (missing fields, bad participant, ineligible sport)
- Successful pick (response shape, draft-complete detection, socket events)
- Timer behavior (increment added to picking team, new timer creation, additive not reset)
- Two regression tests confirming the next team's timer is never touched:
draftTimers.findFirst called exactly once, no timer-update emitted for
next team, db.update called exactly twice (not three times)
https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p
* Add TypeScript types and improve draft validation (#28)
* Code review fixes: type safety, security hardening, and dead code removal
- Fix Socket.IO event types: draft-paused and draft-resumed were typed as
() => void but are emitted with { seasonId, paused } data payloads
- Fix draft.force-manual-pick: add missing season.status === "draft" guard
so commissioners cannot force picks outside an active draft; add duplicate
pick-number check so a slot cannot be assigned two picks (the previous
code only checked participant uniqueness, not slot uniqueness)
- Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all
API routes and league loaders; replace (auth as any).userId casts with
proper const { userId } = await getAuth(args) destructuring
- Remove unused isSnakeDraft = true dead variable from draft.make-pick
- Replace autodraftSettings: any and draftSlots: any[] in draft-utils with
properly typed InferSelectModel / DraftSlot types
- Update force-manual-pick tests: sequence draftPicks.findFirst mock for
the two-call flow; add new tests for status-check and slot-uniqueness
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
* Fix RouterContextProvider type errors in action test files
Cast context argument to RouterContextProvider in test helpers so
ActionFunctionArgs strict typing is satisfied without weakening the
production action signatures back to any.
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
---------
Co-authored-by: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Fix dropdown and input dark mode theming in draft page
Add bg-background and text-foreground classes to native select and
input elements so they use theme CSS variables instead of browser
defaults, which rendered as white in dark mode.
https://claude.ai/code/session_01FZz7kndEqWqFScyq4R6Wxn
* Fix missing text-foreground on time bank unit select
The seconds/minutes/hours select in the time bank dialog had
bg-background but was missing text-foreground, causing text
color to fall back to browser default in dark mode.
https://claude.ai/code/session_01FZz7kndEqWqFScyq4R6Wxn
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Show manager username instead of team name on draft board
Both the read-only draft board and the active draft room now display
the manager's username (falling back to displayName if no username is
set, and to team name if the team is unassigned) in the column headers
of the draft grid.
https://claude.ai/code/session_01C97GauJaAB83NVWxdpNVh1
* Address code review: extract ownerMap helper, fix TeamsDraftedGrid
- Extract ownerMap building into app/lib/owner-map.ts to eliminate
duplication across the two loaders
- Guard against null username AND displayName so the map only contains
real string values
- Apply username display to TeamsDraftedGrid (the "Teams" tab in the
active draft room), which was missed in the initial change
https://claude.ai/code/session_01C97GauJaAB83NVWxdpNVh1
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add commissioner time bank adjustment via right-click on draft board team headers
Commissioners can right-click any team header on the Draft Board tab to open
an "Adjust Time Bank..." dialog. The dialog supports adding or removing an
arbitrary amount of time in seconds, minutes, or hours. The change is applied
immediately to the database and broadcast to all clients via the existing
timer-update Socket.IO event so every participant sees the updated clock
in real time.
- New API route: POST /api/draft/adjust-time-bank (commissioner-only)
- DraftGridSection: wraps team headers in a ContextMenu for commissioners
- Draft room: dialog state, handler, and updated DraftGridSection props
https://claude.ai/code/session_013wxPKzLUCx3nC3LpxgjvQL
* Fix code review issues in commissioner time bank adjustment
- Wrap fetch in try/finally so isAdjustingTimeBank is always reset,
even on network errors that cause fetch to throw
- Guard against totalSeconds rounding to 0 for tiny fractional inputs
- Reject API requests when the draft is not in 'draft' status (409)
- Set input min to 0.001 so the browser rejects zero in native validation
https://claude.ai/code/session_013wxPKzLUCx3nC3LpxgjvQL
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Add prominent clock badge to tab bar (lights up on your turn) with
correct Fischer increment chess-clock model: bank starts at initialTime,
+= incrementTime after each pick, other teams' banks untouched
- Extract pure timer helpers to app/lib/draft-timer.ts and add 29 unit
tests covering formatClockTime, calculateTimeAfterPick, getTimerColorClass,
and full snake-draft lifecycle regression scenarios
- Fix make-pick.ts: add status !== 'draft' and draftPaused server guards
- Fix draft-utils.ts: replace (global as any).__socketIO with getSocketIO(),
fix timeUsed to store actual timeRemaining at pick moment (not always 120),
add null timer warning, move timer fetch before pick insert
- Fix draft.start.ts: batch timer inserts, guard against empty draftSlots
- Fix DraftGridSection: memoize currentTeamId, widen teamTimers type to
Record<string, number | undefined>
- Fix duplicate animate-pulse (getTimerColorClass already includes it)
- Clamp negative seconds in formatClockTime to guard against timer drift
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Commissioners can now right-click any picked cell in the draft grid to
replace the drafted participant (available during and after draft) or
roll back the draft to that pick number (available during draft only).
Replace pick enforces sport eligibility with the replaced slot treated
as free, updates the pick in-place, and broadcasts a pick-replaced
socket event so all clients update in real time. Rollback deletes all
picks from the selected pick onward, resets the season pick number,
clears all timers, and creates a fresh timer for the team now on the
clock.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Remove pause/resume controls when draft is complete, rename Live to Connected
- Hide Pause/Resume Draft buttons when isDraftComplete is true
- Change 'Live' status indicator to 'Connected' in both draft room and draft board views
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
* Fix code review issues in draft room: security, bugs, and quality
Security:
- Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick,
and make-pick all now query the commissioners table instead of league.createdBy,
so co-commissioners have consistent access to all draft controls
Bugs:
- canPick now includes !isPaused so the UI correctly blocks picks during a pause
- isDraftComplete initial state now covers 'completed' season status, not just 'active'
- Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500
Code quality:
- Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft,
handleRemoveFromQueue, and handleReorderQueue
- Replace alert() with toast.error() in handleMakePick, handleForceAutopick,
handleForceManualPick for consistent UX
- Memoize filteredParticipants with useMemo to avoid recomputing on every render
- Replace custom force-pick dialog div with ShadCN Dialog component for proper
keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx
- Remove console.log debug statements from socket event handlers and API routes
- Replace (global as any).__socketIO with getSocketIO() across all API routes
- Replace window.location.reload() in handleStartDraft with useRevalidator
https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Fix flex pick count using unique sports drafted, not total season sports
The flex count was calculated as `totalPicks - totalSportsInSeason`, which
always returned 0 when a team drafted multiple participants from the same
sport (e.g. 2 picks from UEFA Champions League with 2 sports in season =
2 - 2 = 0). Now uses the number of unique sports the team has actually
drafted from, so 2 picks in 1 sport correctly shows 1 flex used.
https://claude.ai/code/session_01DZ6jqgZTDEmJucanBtRCtM
* Code review fixes for TeamsDraftedGrid and draft route
- Fix misleading early-return message: "No picks have been made yet" only
fires when there are no sports configured, so update to say "No sports
have been configured for this season."
- Remove unused draftOrder and team.seasonId from TeamsDraftedGrid props
- Replace availableParticipants prop with sports prop to eliminate
duplicate sports derivation; route already computes seasonSportsData
- Add alphabetical sort to seasonSportsData so ordering is consistent
- Fix availableParticipants: any[] in loader — restructure to ternary so
TypeScript infers the Drizzle select type directly
- Remove any annotation from seasonSportsData forEach callback (now typed)
- Fix teamSportPicks.indexOf(pick) O(n) re-scan — use map callback index i
https://claude.ai/code/session_01DZ6jqgZTDEmJucanBtRCtM
---------
Co-authored-by: Claude <noreply@anthropic.com>