Commit graph

56 commits

Author SHA1 Message Date
Chris Parsons
46332ea735
feat: enhance autodraft settings with detailed option descriptions an… (#56)
* feat: enhance autodraft settings with detailed option descriptions and improved state handling

* test: update AutodraftSettings tests for button interactions and API payloads
2026-03-02 16:26:18 -08:00
Chris Parsons
ca2fd288ab
perf: fix draft room lag from excessive re-renders and listener leaks (#54)
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>
2026-03-02 13:18:47 -08:00
Chris Parsons
7b491d86d2
refactor: integrate sport filter dropdown with drafted sports toggle (#53)
- 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>
2026-03-02 10:13:51 -08:00
Chris Parsons
da0e0f2be8
feat: convert sport filter to multi-select with mobile bottom sheet (#51)
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>
2026-03-02 00:29:54 -08:00
Chris Parsons
5f23885097
fix: prevent silent logout during long-running draft sessions (#49)
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>
2026-03-01 19:33:03 -08:00
Chris Parsons
6c480d47a0
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 (#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>
2026-03-01 12:23:24 -08:00
Chris Parsons
9b6617309d
feat: add hide completed sports filter to AvailableParticipantsSection (#47) 2026-02-28 23:16:06 -08:00
Chris Parsons
0ac9ef4ff8
fix: eliminate queue tab flash on mobile by using useSyncExternalStore (#46)
* 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>
2026-02-28 09:54:14 -08:00
Chris Parsons
2df47658fd
fix: prevent iOS Safari viewport zoom on draft room form inputs (#45)
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>
2026-02-27 23:46:09 -08:00
Chris Parsons
4e447c30ab
feat: show username and team name on the clock display (#44)
- 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>
2026-02-27 23:21:08 -08:00
Chris Parsons
4547a7a4e7
refactor: default draft room to board tab and clean up tab section (#43)
- 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>
2026-02-27 23:12:21 -08:00
Chris Parsons
6dfe56e178
fix: fully sync local draft state after socket reconnection (#41)
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>
2026-02-27 22:50:46 -08:00
Chris Parsons
1ba50828f7
Claude/redesign autodraft queue c4 kp r (#40)
* 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>
2026-02-27 22:16:26 -08:00
Chris Parsons
64a60a75d8
fix: eliminate socket ghost connection and stale state on reconnect (#38)
- 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>
2026-02-24 12:24:04 -08:00
Chris Parsons
61f3fa9a4e
fix: optimistic UI and type safety for draft queue handlers (#37)
- 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>
2026-02-24 11:31:25 -08:00
Chris Parsons
b0521bb7f3
feat: show sport name in draft queue items (#36)
- 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>
2026-02-24 10:37:21 -08:00
Chris Parsons
f33e39264d
fix: harden draft timer system with race-condition safety and DRY refactor (#34)
- 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>
2026-02-23 23:23:24 -08:00
Chris Parsons
fd46e4f0b5
Show all draft picks in sidebar with scroll instead of last 10 (#33)
- 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>
2026-02-23 22:25:28 -08:00
Chris Parsons
da70bf36f1
Fix draft room to respect isPublicDraftBoard setting for unauthenticated access (#32)
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>
2026-02-23 12:00:04 -08:00
Chris Parsons
acc78db86e
fix: use h-dvh instead of h-screen on mobile draft board (#31)
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>
2026-02-23 08:22:00 -08:00
Chris Parsons
77e408cad8
Make draft room fully usable on mobile (#30)
- 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>
2026-02-22 22:36:12 -08:00
Chris Parsons
01f1480159
Claude/fix pick timer ghll n (#29)
* 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>
2026-02-22 19:29:29 -08:00
Chris Parsons
a3d5d5ea55
Fix text color visibility in form inputs and selects (#27)
* 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>
2026-02-22 18:00:21 -08:00
Chris Parsons
ab3437cd73
Display team owner names instead of team names in draft UI (#24)
* 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>
2026-02-22 16:56:07 -08:00
Chris Parsons
51cffe1762
Add commissioner time bank adjustment feature for draft (#22)
* 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>
2026-02-22 16:16:51 -08:00
Chris Parsons
7a8ea60e77
Add draft clock UI, Fischer increment timer logic, and security fixes (#19)
- 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>
2026-02-21 16:51:12 -08:00
Chris Parsons
41f3654956
Add commissioner replace pick and draft rollback features (#18)
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>
2026-02-20 22:47:29 -08:00
Chris Parsons
285981ab9d
Improve draft room UX with better error handling and UI refinements (#17)
* 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>
2026-02-20 21:50:27 -08:00
Chris Parsons
7294084c13
Refactor draft grid sports calculation and improve type safety (#16)
* 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>
2026-02-20 21:26:27 -08:00
Chris Parsons
edc4eb75e8
Optimize draft room performance with memoization and refactoring (#15)
* Fix flex spot count in draft room Teams Drafted view

The flexSpots column in the database defaults to 0 and was never
populated, causing the Teams Drafted grid to always show "0 of 0 flex".
Calculate numFlexPicks dynamically in the loader as
max(0, draftRounds - seasonSports.length) so teams see the correct
flex count (e.g. 7 rounds - 5 sports = 2 flex).

https://claude.ai/code/session_01CSzF4aWuyppGBDqM4MJmhc

* Clean up draft room flex calculation area

- Fix bug: force pick dialog now uses its own isolated search/sport
  filter state so it no longer mutates the main participants tab filters
- TeamsDraftedGrid: remove dead first pass in flexPicksByTeam memo,
  trim season prop to numFlexPicks only, drop unused type field from
  picks sport shape
- Loader: derive userAutodraftSettings from already-loaded
  autodraftSettings list, eliminating a redundant DB query
- Extract shared transformedPicks, transformedParticipants, and
  seasonSportsData into their own useMemos so both eligibility memos
  reuse them instead of duplicating the work
- Memoize draftGrid, draftedParticipantIds, and uniqueSports which
  were being recomputed on every render

https://claude.ai/code/session_01CSzF4aWuyppGBDqM4MJmhc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 21:10:26 -08:00
Chris Parsons
401efcd833
Add browser push notifications toggle to draft page (#11)
* Add push notifications implementation plan

Documents the approach for adding a browser Notification API toggle
to the draft page sidebar, including files to create/modify and
edge cases to handle.

https://claude.ai/code/session_0149MvVUYDY6pFUAV1fL4K69

* Add browser push notifications toggle to draft page

Adds a Notifications toggle in the draft sidebar (below Autodraft)
that uses the Browser Notification API to alert users when a pick is
made or when it's their turn, while the tab is not focused.

- New useDraftNotifications hook for state, permission, and localStorage
- New NotificationSettings component with Switch toggle
- Fires "It's your turn to pick!" when the next pick is the user's
- Fires "{Team} picked {Player}" for all other picks
- Gracefully hides toggle when Notification API is unavailable

https://claude.ai/code/session_0149MvVUYDY6pFUAV1fL4K69

* Fix all code review issues with push notifications

- Extract snake draft order calculation to shared getTeamForPick()
  helper in lib/draft-order.ts, removing the duplicated logic
- Fix notification firing on user's own picks (guard with team id check)
- Fix notification firing after draft is complete (early return)
- Use a ref for sendNotification to prevent full socket handler
  re-registration every time the toggle is changed
- Add permission drift listener via Permissions API so the UI reacts
  if the user revokes notification permission in browser settings
- Add SSR guard for document.hidden in sendNotification
- Remove redundant isSupported prop from NotificationSettings;
  derive unsupported state from permissionState === "unsupported"
- Move NotificationSettings out of QueueSection prop-drilling path;
  render it via a new settingsSection slot on DraftSidebar instead

https://claude.ai/code/session_0149MvVUYDY6pFUAV1fL4K69

* Fix second round of notification code review issues

- Add My Turn Only / All Picks mode granularity so users can choose
  to only be notified when it's their pick, or for every pick
- Remove double top-border by stripping leftover border-t/pt-4/mt-4
  wrapper from NotificationSettings (now provided by DraftSidebar)
- Remove unused isSupported from hook return value
- Add n.onclick = () => window.focus() so clicking a notification
  brings the draft tab back into focus
- Scope localStorage keys to userId to avoid shared-browser conflicts
  (keys now: draftNotifications-{userId}-{seasonId})
- Add notificationsModeRef alongside sendNotificationRef so mode
  changes don't trigger full socket handler re-registration

https://claude.ai/code/session_0149MvVUYDY6pFUAV1fL4K69

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 19:30:53 -08:00
Chris Parsons
1584d34b89
Redesign to dark-mode-only with navy palette and accent colors (#13)
Removes light mode entirely in favour of a permanent dark theme with a
navy-tinted background and three signature accents (electric blue,
amber/gold, coral) exposed as CSS custom properties and Tailwind
utilities (bg-electric, text-amber-accent, text-coral-accent).

- Set class="dark" on <html> and apply Clerk dark base theme
- Rewrite app.css: single :root palette (oklch navy values), custom
  --electric / --amber-accent / --coral-accent variables, remove
  duplicate .dark block and light-mode bg-white/bg-gray-950 rule
- Install @clerk/themes for Clerk dark modal support
- Replace hardcoded Tailwind colors across 30+ files:
  - Draft grid cells: blue-50/blue-950 → electric/15, green-50/950 → emerald/10
  - Timer: green-600/yellow-600/red-600 → emerald-400/amber-accent/coral-accent
  - Status badges: blue-50/green-50/gray-50 → electric/emerald/muted variants
  - Success messages: green-500/15 text-green-700 dark:text-green-400 → emerald-500/15 text-emerald-400
  - Info cards: blue-50 dark:bg-blue-950 → electric/10
  - Warning cards: yellow-500 → amber-accent variants
  - Medal/placement badges: yellow-500/orange-600 → amber-accent/coral-accent
  - Movement indicators: green-600/red-600 → emerald-400/coral-accent
  - Connection dots: green-500/red-500 → emerald-500/coral-accent
- Remove dark:hidden/dark:block logo toggle in welcome.tsx (always dark)
- Update DraftGrid test assertions to match new class names

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 19:26:11 -08:00
Chris Parsons
83994b7a74
Fix draft round showing Infinity before draft starts (#9)
When draftSlots is empty (pre-draft state), dividing by zero caused
Math.ceil to return Infinity. Guard the division to fall back to round 1.

https://claude.ai/code/session_017mLmGc5wTrESaDeRQHkSPy

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 11:16:34 -08:00
Chris Parsons
03fdba3022
Remove EV column from available participants table (#5)
* Hide EV column from draft board available participants table

https://claude.ai/code/session_016oJK1gmWZ48YWEZmVqm6f9

* Clean up dead expectedValue references after hiding EV column

- Remove expectedValue from AvailableParticipantsSection interface
- Remove EV column from Force Manual Pick commissioner dialog
- Remove expectedValue from participants loader SELECT and parsing step
- Retain ORDER BY expectedValue for default sort order

https://claude.ai/code/session_016oJK1gmWZ48YWEZmVqm6f9

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-19 17:02:55 -08:00
Chris Parsons
3210ab265f
Change participant expectedValue from integer to decimal (#1)
* feat: sync odds-based EV to participants table for draft room ranking

When admin enters futures odds or manual probabilities, the calculated EV
is now synced from participantExpectedValues to participants.expectedValue.
This closes the gap where EVs were calculated but never reflected in the
draft room ranking. Also changes expectedValue from integer to decimal(10,2)
for better precision, and displays "—" for participants with no odds data.

https://claude.ai/code/session_01JWG2zg2EMzCn6RgEufPC1T

* fix: correct expectedValue type to string in participants route

Missed instance of `expectedValue: 0` (number) that failed typecheck
after schema change from integer to decimal.

https://claude.ai/code/session_01JWG2zg2EMzCn6RgEufPC1T

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-19 11:26:40 -08:00
Chris Parsons
609c4e7b2c feat: Add hide ineligible participants functionality in AvailableParticipantsSection 2025-10-26 21:09:30 -07:00
Chris Parsons
5b10548f13 feat: Add ConnectionOverlay component for improved socket connection handling and user feedback 2025-10-26 21:01:12 -07:00
Chris Parsons
98b84095fd feat: Implement drag-and-drop functionality for queue items; add duplicate prevention for participants in queue 2025-10-25 21:02:16 -07:00
Chris Parsons
c9d3ee73cd feat: Update DraftSidebar to include recent picks section and refactor layout; enhance AvailableParticipantsSection and QueueSection styling 2025-10-25 18:25:26 -07:00
Chris Parsons
2cd4096e70 feat: Implement draft room redesign with collapsible sidebar and tabbed content
- Added AvailableParticipantsSection for participant search and filtering.
- Created DraftGridSection for displaying the draft grid with team timers and context menu.
- Developed QueueSection for managing participant queue and autodraft settings.
- Introduced RecentPicksSection to show recent draft picks.
- Implemented TeamsDraftedGrid to visualize drafted participants by team and sport.
- Added collapsible UI components for better layout management.
- Established tabbed navigation for draft board, recent picks, and teams drafted.
- Enhanced responsiveness for mobile and desktop views.
- Updated styles for a cohesive design across components.
2025-10-25 03:23:41 -07:00
Chris Parsons
190dfb92ca feat: add socket event for participant removal from queues 2025-10-24 21:46:55 -07:00
Chris Parsons
4aafd5853b feat: add connected teams tracking and emit list on connection 2025-10-24 21:31:57 -07:00
Chris Parsons
decb28dc19 Implement Omni League Draft Eligibility Rules
- 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
2025-10-24 21:12:07 -07:00
Chris Parsons
ff97e25438 feat: add autodraft status and team connection indicators to draft grid 2025-10-21 23:22:17 -07:00
Chris Parsons
6c5c9f709b feat: add public draft board and simplify draft speed settings 2025-10-20 15:03:11 -07:00
Chris Parsons
f86dfade38 feat: add draft completion banner and event handler for draft completion state 2025-10-18 23:33:13 -07:00
Chris Parsons
01b9e23eaf refactor: remove unused timeRemaining state and currentUserId prop in draft room 2025-10-18 23:23:29 -07:00
Chris Parsons
06db7fde7a feat: add draft pause/resume functionality with UI controls and socket events 2025-10-18 23:19:41 -07:00
Chris Parsons
a7f5df923f feat: add draft increment time and show team-specific timers in draft UI 2025-10-18 23:13:04 -07:00
Chris Parsons
9d41508fd0 feat: add draft API routes and implement draft room pick controls 2025-10-18 14:55:26 -07:00