* 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>
Implements a new "standard" timer mode alongside the existing chess clock
mode. In standard mode the per-pick timer resets to a fixed value after
every pick (no carry-over), and the speed selector shows plain time values
instead of named chess-clock presets.
Key changes:
- Add `draft_timer_mode` enum column to `seasons` table (migration 0053)
- `draft.start`: standard mode seeds timers at `draftIncrementTime` (the
per-pick value) rather than `draftInitialTime`
- `draft.make-pick`: three-way branch — standard resets, chess clock
owner earns increment, commissioner/admin pick leaves bank frozen
- `draft.force-manual-pick`: commissioner picks never earn bank time;
chess clock path uses a pre-pick snapshot to avoid a race window with
the 1-second timer loop
- `executeAutoPick` in draft-utils: auto picks never earn bank time;
chess clock path skips the DB update (timer already at 0)
- League creation and settings pages: mode-aware speed selector (raw
seconds for standard, named presets for chess clock); shared
`parseDraftSpeed` utility extracted to `app/lib/draft-timer.ts`
- Tests added for draft.start timer init and make-pick timer mode
behavior; force-manual-pick tests updated for new timer semantics
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fall back to displayName when username is null for Discord webhook
Users who sign up via OAuth (Google, GitHub, etc.) without setting a
Clerk username have a null `username` field but always have a `displayName`
(computed from firstName+lastName or email). Previously, `usernameByClerkId`
was filtered to only include users with a non-null username, causing those
owners to appear without any identifier in Discord standings messages
(e.g. "Liverpool def. Galatasaray" instead of "Liverpool def. Galatasaray (Madmike)").
https://claude.ai/code/session_01VAkeDDVZMYS1DweQnUrRnH
* Extract getUserDisplayName helper and use consistently throughout
Add a single getUserDisplayName(user) function to app/models/user.ts that
encapsulates the username → displayName fallback logic. Replace 9 scattered
inline expressions across the codebase (owner-map, scoring-calculator,
league routes, settings, invite flow, draft API, Clerk webhook) with calls
to the shared helper.
No behaviour change — all existing logic preserved, just centralised.
https://claude.ai/code/session_01VAkeDDVZMYS1DweQnUrRnH
* Fix N+1 user queries in league loader and settings loader
Add findUsersByClerkIds() batch function to the user model and replace two
separate Promise.all+findUserByClerkId loops (one for owners, one for
commissioners) with a single inArray query in both $leagueId.server.ts and
$leagueId.settings.tsx. The merged query covers both owner and commissioner
IDs in one round-trip.
https://claude.ai/code/session_01VAkeDDVZMYS1DweQnUrRnH
* Fix N+1 user queries in buildOwnerMap
Replace the Promise.all+findUserByClerkId loop with a single
findUsersByClerkIds batch query, consistent with the league loader
and settings loader fixes.
https://claude.ai/code/session_01VAkeDDVZMYS1DweQnUrRnH
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Grant sitewide admins commissioner-level access in leagues
- `isCommissioner()` now returns true for site admins, covering all
commissioner-gated loaders (league home, settings, sport season detail)
and draft API routes (start, pause, resume, rollback, replace-pick,
force-autopick, force-manual-pick, adjust-time-bank, make-pick)
- Added `hasCommissionerRecord()` (DB-only, no admin bypass) for the
"already a commissioner" duplicate-entry check in the settings action,
preventing a false positive when adding a site admin as commissioner
- `isCommissioner()` now runs the admin check and DB query in parallel
via Promise.all to avoid a serial roundtrip on every check
- Added "admin" to the `picked_by_type` enum (migration 0049) so picks
forced by a site admin are recorded accurately in the audit log rather
than as "commissioner"
- 8 unit tests covering both isCommissioner and hasCommissionerRecord
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix draft.force-manual-pick tests broken by isUserAdminByClerkId
The route now calls isUserAdminByClerkId which hits database().query.users,
but the test's mock DB had no query.users entry. Add a vi.mock for
~/models/user and default isUserAdminByClerkId to false in beforeEach.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
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>
Adds GET /api/seasons/:seasonId/draft returning current pick number,
who's on the clock, and the full picks list with usernames and sport.
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>
- 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>
* 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>
* Create timer when adding time to a team with no existing timer
When a commissioner tries to add time to a team that has no draft timer,
instead of returning a 404 error, create a new timer record with the
specified amount of time. Removing time still returns a 404 if no timer
exists (nothing to remove from).
https://claude.ai/code/session_016VpJKZZFNQQqzfmLu8pHoc
* Fix silent timer failures and missing seasonId filter in timer model
- draft.make-pick.ts: create a timer with the increment amount instead of
logging a warning and silently skipping when no timer exists for the
picking team
- draft.force-manual-pick.ts: same fix for the commissioner force-pick path;
also unconditionally emit the timer-update socket event so clients always
see the updated time
- models/draft-timer.ts: add seasonId parameter to getTeamTimer and
updateTeamTimer so they cannot match the wrong season's timer when a team
participates in multiple seasons
https://claude.ai/code/session_016VpJKZZFNQQqzfmLu8pHoc
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Fix draft timer missing after rollback causing draft to freeze
The rollback endpoint was deleting all timers but only recreating one
for the team currently on the clock. After that team made their pick,
the next team had no timer row, causing the timer system to log
"No timer found" and skip indefinitely, freezing the draft.
Fix: recreate timers for ALL teams after a rollback, each starting at
draftInitialTime.
Also add a defensive fallback in the timer system: if a timer row is
missing for the current team during an active draft, create it at
draftInitialTime instead of skipping, so the draft can never get
permanently stuck due to a missing timer.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
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>
- Created draft eligibility calculation logic in `app/lib/draft-eligibility.ts`
- Added interfaces for sport availability and draft eligibility
- Implemented `calculateDraftEligibility` function to determine eligible sports based on team picks and global availability
- Developed `getEligibilitySummary` function for human-readable eligibility status
- Updated API endpoints to validate eligibility before making picks
- Enhanced frontend draft room UI to reflect eligibility status and provide visual indicators for ineligible participants
- Comprehensive implementation summary and testing documentation added
- All changes type-checked and unit tests passed successfully