Commit graph

175 commits

Author SHA1 Message Date
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
9dc7e860cb
feat: show live draft alert banner on league page (#42)
Adds a pulsing yellow alert banner between the league header and content
sections when the season status is "draft", with a link to the draft room.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 23:00:42 -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
364b05cd96
Fix draft state not updating when returning from backgrounded mobile app (#39)
* Fix draft state not updating when returning from backgrounded mobile app

Mobile browsers suspend JavaScript and silently drop WebSocket connections
when the user switches to another app, without firing "offline"/"online"
events. Add a visibilitychange handler so that when the user returns:
- If the socket is disconnected, reconnect immediately (skipping backoff)
- If the socket appears connected but JS was suspended, rejoin the draft
  room and trigger a loader revalidation to catch any missed picks/state

https://claude.ai/code/session_016tCZVFjSeHdQsdKktbDHEt

* Simplify reconnect handlers and fix connect_error overlay flicker

- Merge handleOnline and the shared branch of handleVisibilityChange into a
  single handleReturn function. visibilitychange is now a thin guard that calls
  it only on show. Both events share the same logic: reconnect if socket dropped,
  or restore UI state + rejoin room + revalidate if the socket survived.

- Remove setIsReconnecting(false) from connect_error: reconnect_attempt fires
  immediately after and resets it to true anyway, causing the "Reconnecting"
  overlay to flicker off and back on during every retry cycle.

https://claude.ai/code/session_016tCZVFjSeHdQsdKktbDHEt

* Fix four issues from useDraftSocket code review

- Manager listener leak: add socket.io.off() for reconnect_attempt and
  reconnect_failed in cleanup — socket.disconnect() only tears down the
  socket, not the Manager listeners, causing them to accumulate on re-mounts.

- reconnect_failed dead code: add reconnectionAttempts: 10 to io() config
  so the handler is actually reachable after exhausting retries.

- connectionError flicker: remove setConnectionError from connect_error —
  reconnect_attempt fires immediately after and clears it anyway, causing the
  error overlay to flash on every retry cycle. Error now only appears via
  reconnect_failed once all attempts are exhausted. connect_error instead
  ensures setIsReconnecting(true) so the reconnecting overlay shows instead
  of the initial "Connecting to Draft" spinner.

- Add comment to on/off/emit noting they are no-ops if called before the
  effect runs (socketRef.current === null).

https://claude.ai/code/session_016tCZVFjSeHdQsdKktbDHEt

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-25 09:36:52 -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
3343e7a405
fix: address code review issues from draft board scrolling and team creation changes (#35)
- DraftGrid: wrap header and rows in a single min-w-max container so
  column widths share a sizing context and can't desync
- Settings: wrap team + draft slot creation in a DB transaction so a
  slot insert failure can't leave orphaned teams with no slots
- Settings: replace Math.max(...spread) with reduce to avoid the JS
  argument limit (defensive, given the 6-16 team cap)
- Settings: remove now-unused createManyTeams import

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 10:01:27 -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
34da0594d1
Fix draft timer creation for teams without existing timers (#26)
* 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>
2026-02-22 17:27:16 -08:00
Chris Parsons
a3ec556ecc
Refactor autodraft chain logic and improve timer handling (#25)
* 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>
2026-02-22 17:02:35 -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
b35791e76a
Fix public draft board access not working when setting is enabled (#23)
* Fix public draft board access not working when setting is enabled

Two bugs were causing the isPublicDraftBoard setting to fail:

1. Draft board loader called getAuth() before checking isPublicDraftBoard.
   Restructured to skip auth entirely for public boards - only call getAuth
   when the board is private and we need to verify member/commissioner access.

2. Settings action saved the league (name + isPublicDraftBoard) AFTER fetching
   the current season. If the season fetch had any issue, updateLeague was never
   reached. Moved the league-level save to happen unconditionally before the
   season fetch, so isPublicDraftBoard is always persisted on form submit.

https://claude.ai/code/session_01Jp2tE3YXhx2jdb6CgCnvZy

* Fix bugs in draft board access and settings update action

- Wrap updateLeague call in try-catch so DB errors return a user-friendly
  message instead of an unhandled 500
- Fix season-update catch block to say "season settings" not "league" since
  the league was already saved successfully at that point
- Validate leagueId URL param against season.leagueId in draft board loader
  to prevent accessing a season via the wrong league's URL
- Change 403 message for authenticated non-members from "not public" to
  "you don't have access" to accurately reflect their logged-in state
- Add settings-update.test.ts covering name validation, league save,
  error handling, draft speed mapping, and season-specific validation
- Add draft-board-access.test.ts covering public/private access rules,
  leagueId mismatch detection, commissioner/owner/unauthenticated cases,
  and the new per-role 403 message distinction

https://claude.ai/code/session_01Jp2tE3YXhx2jdb6CgCnvZy

* Fix TypeScript errors in settings-update tests

TS was narrowing const draftSpeed = 'fast' to the literal type 'fast',
making comparisons with 'slow'/'very-slow' etc. flagged as impossible.
Widen all draftSpeed locals and the season status locals to string so the
if-chains compile cleanly under strict mode.

https://claude.ai/code/session_01Jp2tE3YXhx2jdb6CgCnvZy

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 16:31:24 -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
8a444a51a1
Improve error boundary UI with status-specific error pages (#21)
* Improve error pages with styled, status-aware UI

Replace the bare-bones error boundary with a proper error page that:
- Renders a minimal Brackt header so users can navigate home
- Shows distinct messages and icons for 401, 403, 404, 500+ errors
- Displays a "Sign in from the home page" hint on 401 so logged-out
  users know how to recover
- Hides internal details in production (stack traces dev-only)
- Uses existing dark theme Tailwind classes for visual consistency

https://claude.ai/code/session_011jv8desa5vhSkjZHSjWZiV

* Address code review feedback on error pages

- Move statusIcon out of ErrorBoundary render as a plain module-level
  function to avoid React re-creating a component type on every render
- Collapse 401 action into a single descriptive button label
  ("Go to Home Page to Sign In") instead of a fragmented button + hint
- Fix details/summary border radius collision by using overflow-hidden
  on the container and removing rounded-md from the summary element;
  separate the pre block with a border-t instead

https://claude.ai/code/session_011jv8desa5vhSkjZHSjWZiV

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-21 23:47:42 -08:00
Chris Parsons
e9798263b5
Add rules page, rewrite how-to-play, and fix scoring display (#20)
- Add new /rules route with official league rules covering rosters,
  scoring, major-based QP scoring, tiebreakers, draft, and season rules
- Rewrite how-to-play page with a tutorial/marketing tone, highlighting
  the Fischer increment draft clock as a novel feature
- Add Rules link to navbar (desktop and mobile)
- Align QP values in both pages with DEFAULT_QP_VALUES in code
- Fix tiebreaker description to match placement-count logic in code
- Update all fantasy point displays from toFixed(1) to toFixed(2) across
  StandingsTable, TeamScoreBreakdown, and PointProgressionChart
- Update tests to match new two-decimal-place point display format

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 21:58: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
9ae00a8385
Add Zod validation and expand export/import for EV and results data (#14)
* Include participant EV/futures data in data sync export/import

The export now includes the full participantExpectedValues records
(placement probabilities, source, sourceOdds) alongside participants.
On import, EV records are upserted in merge mode and created fresh in
replace mode (cascade handles cleanup). Backward-compatible with older
exports that lack the participantExpectedValues field.

https://claude.ai/code/session_01TVzuuDR7jPtmihaXTrsX7B

* Address all data-sync code review findings

- Wrap importSportsDataFromJSON in a db.transaction() so any mid-import
  failure rolls back cleanly instead of leaving partial data
- Add zod validation of imported JSON before any DB writes, giving a
  clear error on malformed/incompatible files
- Fix N+1 queries: build participantIdMap during participant import so
  the EV and results sections resolve IDs from memory, not extra queries
- Fix merge mode silently skipping existing participants — now updates
  shortName, externalId, and expectedValue
- Add participantResults to export/import (was deleted in replace mode
  but never exported, so results were permanently lost)
- Restore calculatedAt timestamp on EV import instead of defaulting to
  now(); stored as ISO string in the export for portability
- Document that onDelete:cascade covers participantExpectedValues and
  participantResults when participants is deleted; remove the now-
  redundant explicit participantResults delete from replace mode
- Bump export version to 1.1; old v1.0 files still import cleanly since
  participantExpectedValues, participantResults, and calculatedAt are
  all optional in the zod schema
- Collapse all six DB fetches in exportSportsDataToJSON into one
  Promise.all for parallel execution
- Add countAllParticipants() and countAllParticipantEVs() model funcs
- Show Participants and EV Records counts on the dashboard stat cards
- Replace raw DOM form creation/submit in handleImport with useFetcher,
  giving proper loading state, no full-page reload, and type-safe data
- Remove dead export intent handler from the action function

https://claude.ai/code/session_01TVzuuDR7jPtmihaXTrsX7B

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 20:25:25 -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
8b480a0b9a
Fix ~/database/context resolving to browser stub during dev SSR (#12)
`resolve.alias` with `isSsrBuild` only works at build time — during
`npm run dev`, `isSsrBuild` is always false, so `ssrLoadModule` was
loading the browser stub (DatabaseContext = null) instead of the real
AsyncLocalStorage implementation, causing a TypeError on every request.

Replace the alias with an `enforce: 'pre'` plugin that checks
`options.ssr` at resolve time, which Vite correctly sets for both dev
SSR module loading and production SSR builds.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 16:10:22 -08:00
Chris Parsons
3323522782
Fix sports season page 404 when sports season is linked to multiple leagues (#10)
The loader was grabbing seasonSports[0] blindly instead of filtering by
the current league, causing a 404 when the first linked fantasy season
belonged to a different league.

https://claude.ai/code/session_01Mq6BqFhiYFuJyDcc8ueVJc

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 11:35:35 -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
47b69ce9cf
Include commissioner leagues in user's active leagues (#8)
* Show leagues where user is a member regardless of team assignment

The homepage now uses a union query to show leagues where the user
either has a team in the current season OR is a commissioner. Previously,
commissioners without a team could not see their leagues on the homepage.

https://claude.ai/code/session_01E3ugKTfatkEc5TcDGXvHiW

* Fix stale empty state card title in home route

Update 'No Active Leagues' to 'No Leagues' to match the updated
description which is now about membership rather than active seasons.

https://claude.ai/code/session_01E3ugKTfatkEc5TcDGXvHiW

* Fix union import: use two queries with JS deduplication

drizzle-orm 0.36.x does not export a standalone union() function.
Replace with two awaited queries merged via a Map (dedup by id),
then sorted by createdAt descending in JS.

https://claude.ai/code/session_01E3ugKTfatkEc5TcDGXvHiW

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 10:28:37 -08:00
Chris Parsons
8c9b131c00
Update scoring system and add Counter-Strike to special scoring (#7)
* Update how-to-play scoring rules to match standard scoring

- Update placement points: 1st 100, 2nd 70, 3rd-4th 45, 5th-8th 20
- Add Counter-Strike to the special majors scoring section alongside Golf & Tennis
- Add qualifying points breakdown (8/5/3/2/1) per major tournament

https://claude.ai/code/session_01JUAqVZgo7M7XRBLzpeaWpH

* Fix scoring to show all 8 individual placements correctly

Display each placement (1st-8th) with its own point value rather than
grouping/averaging: 100, 70, 50, 40, 25, 25, 15, 15

https://claude.ai/code/session_01JUAqVZgo7M7XRBLzpeaWpH

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 10:00:35 -08:00
Chris Parsons
eb7aa42cef
Add implementation plan for commissioner-without-team feature (#6)
* Add implementation plan for commissioner-without-team feature

https://claude.ai/code/session_01NSRMSYtb7jSFbngDS8okn3

* Allow commissioners to exist without owning a team

- Add opt-out checkbox on league creation ("I want to play in this league")
  so the creator can be commissioner-only without claiming a team
- Add Commissioner Management card to league settings with add/remove
  commissioner UI; guards against removing the last commissioner
- Add countCommissionersByLeagueId model helper for the last-commissioner guard
- Show "No team" indicator on the league homepage next to commissioners
  who don't own a team in the current season

https://claude.ai/code/session_01NSRMSYtb7jSFbngDS8okn3

* Fix code review issues in commissioner management

- Add success/error feedback banners to commissioner add/remove actions
- Gate allUsers query to admin-only (prevents exposing all users to non-admin commissioners)
- Prevent commissioner self-removal in both action (server) and UI (client)
- Add "(you)" label next to current user in commissioner list
- Remove all unnecessary `any` type casts in favor of inferred types

https://claude.ai/code/session_01NSRMSYtb7jSFbngDS8okn3

* Allow commissioners to add league members as co-commissioners

Non-admin commissioners can now add users who already own a team in
the league as co-commissioners. Admins still see the full user list.
Previously the add-commissioner form was admin-only.

https://claude.ai/code/session_01NSRMSYtb7jSFbngDS8okn3

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 08:45:09 -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
2d6dd6ec9f
Refactor expected values form to batch save all participants (#4)
* fix: remove probability sum validation and add batch save with total EV

- Remove the requirement that manual probabilities sum to 1.0, allowing
  partial distributions (e.g. when a participant has <100% chance of top 8)
- Replace per-row save buttons with a single "Save All" button that submits
  all participants at once
- Add a Total EV row at the bottom of the table summing all participant EVs
- Update action to batch process all participants from a single form submission

https://claude.ai/code/session_01WHRynBCcugSK7HHEmN6Yuy

* fix: validate participantIds server-side instead of trusting form input

Fetch participants from the DB in the action using the season ID from
the URL params, eliminating the untrusted participantIds hidden field.
Remove the now-unused hidden input from the frontend form.

https://claude.ai/code/session_01WHRynBCcugSK7HHEmN6Yuy

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-19 15:57:05 -08:00
Chris Parsons
eb260649b1
Add bulk import feature for futures odds with fuzzy matching (#3)
* Add bulk import to futures odds page

Adds a textarea where admins can paste odds from FanDuel, DraftKings,
OddsChecker, or any site. A client-side parser extracts American odds
from each line and fuzzy-matches team names to participants (exact,
substring, and word-overlap matching). Shows matched/unmatched results
before applying to the individual input fields.

https://claude.ai/code/session_01JvgcqKSJZ36bEztfg6CkWP

* Code review fixes for futures-odds page

- Remove unused upsertParticipantEVWithNormalization import
- Remove debug console.log statements left in save action
- Move normalizeName to module level (pure function, no component deps)
- Pre-compute normalized participant names once in findParticipantMatch
  instead of re-normalizing across three separate passes
- Fix array index key on unmatched list to use composite string key
- Drop unused result variable from upsertParticipantEV call

https://claude.ai/code/session_01JvgcqKSJZ36bEztfg6CkWP

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-19 14:34:45 -08:00
Chris Parsons
740ffbb411
fix: exclude database/context from browser bundle to fix Docker build (#2)
database/context.ts uses AsyncLocalStorage from node:async_hooks which
is a Node.js-only API. Vite externalizes it for browser builds but the
resulting error "AsyncLocalStorage is not exported by __vite-browser-external"
caused npm run build to fail.

Fix: add a Vite resolve.alias for the browser (non-SSR) build that maps
~/database/context to a browser-safe stub. The stub is never called at
runtime since database() is only invoked in server-side loaders, but it
allows the client bundle to build without errors.

https://claude.ai/code/session_01Fjf9WFqNnuHmmC5yTedL7G

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-19 11:54:32 -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
9211cad7d1 fix: correct test expectations for ICM edge cases and duplicate text queries
- ICM tests: fix single participant, zero probabilities, and 2-participant
  tests to account for simulation only filling min(participants, 8) positions
- TeamScoreBreakdown tests: use getAllByText for "150.0" which now appears
  in both the header and the Actual Points summary card

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 14:24:07 -08:00
Chris Parsons
96b4c5a350 fix: align ev-calculator tests with decimal (0-1) probability format
The implementation and rest of the system (ICM calculator, probability
updater) use decimals (0-1) for probabilities, but the tests were using
percentages (0-100). Updated tests and docs to match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 23:26:32 -08:00