* feat: add Sentry error monitoring (#77)
Installs and configures @sentry/react-router with server and client
instrumentation. Disabled in development to avoid noise; only active
in production.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: add VSCode Sentry MCP server config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: pass vite env to sentryReactRouter plugin
sentryReactRouter requires the ConfigEnv as a second argument to read
the vite `command` property.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements a 16-team UEFA Champions League knockout bracket simulator
using blended Elo + futures odds probabilities (70/30 split). Tracks
integer placement counts per tier to avoid fractional accumulation
drift, ensuring total EV sums to exactly 340 by construction.
- New UCLSimulator class reading live playoffMatches from DB
- Respects completed match results (eliminated teams get exact EV)
- New ucl_bracket simulator_type enum value + migration
- Registered in simulator registry; added to admin sport selector
- 18 unit tests covering math, bracket path, and integration scenarios
Fixes#113
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bracket events (playoff_game): delete all participantResults for the
sports season atomically with the event, then recalculate league standings
- Qualifying events: capture affected participant IDs before cascade
deletes eventResults, recalculate participantQualifyingTotals from
remaining events, decrement majorsCompleted if QP had been processed,
and recalculate league standings
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Every route now exports a meta function so the browser title bar reflects
the current page. Static pages use fixed titles; dynamic pages pull names
from loader data with a sensible fallback (e.g. league name, sport season
name, team name).
Titles follow the pattern "Page Name - Brackt" for user-facing routes and
"Page Name - Brackt Admin" for admin routes.
Fixes#74
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
When a participant wins a bracket round, they immediately earn provisional
"floor" points (the averaged minimum they'd receive if eliminated next round).
These update as they advance and are replaced by finalized scores on elimination.
Key changes:
- Add `is_partial_score` column to `participant_results` (migration 0038)
- `processPlayoffEvent`: assign provisional position 5 to non-scoring round
winners; assign round-appropriate floors to scoring round winners via
`getGuaranteedMinimumPosition`; add catch-all for unrecognized round names
- `upsertParticipantResult`: guard against un-finalizing rows (never overwrite
isPartialScore=false with true)
- `calculateBracketPoints`: new function averaging tied bracket tiers
(5-8 → 20 pts, 3-4 → avg, 1-2 solo); used in `calculateTeamScore` for
playoff_bracket pattern (pattern-aware, doesn't affect F1/golf scoring)
- `PlayoffBracket`: "In Contention" table for still-active participants;
AFL double-chance fix (participants who won a later match excluded from
earlier round's loser list); correct `nextRank` starting position
- Server loader: batch owner DB queries (one query vs N+1); deduplicate
participantPoints; use calculateBracketPoints for bracket point display
- Clean up Phase/Q-number tracking comments throughout scoring-calculator.ts
- 3 new tests for non-scoring round provisional floor behavior
Also includes a dev admin bypass via DEV_ADMIN_CLERK_ID env var (separate
change on this branch, not part of floor scoring feature).
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: redesign playoff bracket UI with compact layout and owner display
- Replace per-match Card wrappers with compact left-vs-right rows (stacks on mobile)
- Add TeamOwnerBadge component (colored avatar + team name + username), matching the standings "Drafted By" style
- Show owner info below each participant name in bracket matches
- Add TBD forward-reference labels ("Winner of Quarterfinals M1") computed via buildFeederMap
- Add Final Rankings table below bracket showing all participants ranked by elimination round
- Fix round ordering in loader: sort by match count descending (more matches = earlier round)
- Add loser relation to playoff matches query for elimination tracking
- Extract getAvatarColor to app/lib/color-hash.ts (shared across GroupStageDisplay, SeasonStandings, TeamOwnerBadge)
- Extract groupMatchesByRound helper; share grouped map between feeder computation and render
- Remove dead getTeamAvatar from SeasonStandings after TeamOwnerBadge adoption
- Add tests for groupMatchesByRound and buildFeederMap (7 tests)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add draft order card to league home page
Show draft order on the league home page when order is set and season
is in pre_draft or draft status. Includes team name, owner name, and
a link to the draft room.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: enhance playoff bracket with eliminated teams, points, and ownership highlights
- Show eliminated teams (including group-stage losers) in Final Rankings card
- Display computed fantasy points per participant in rankings table
- Card title switches between "Eliminated Teams" and "Final Rankings" based on bracket completion
- Highlight owned participants with electric blue name, dot indicator, and card border in bracket matches
- Highlight owned rows with electric border/background in rankings table
- Suppress EventSchedule for playoff_bracket sports seasons
- Fix title redundancy: bracket section title no longer repeats sport name
- Two-column match grid on desktop (md:grid-cols-2)
- Code review fixes: parallel DB fetches, targeted query for pre-eliminated, single-pass parseFloat, remove IIFE
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: EV simulation framework with F1 Monte Carlo simulator
- Add EV snapshot tables (participant_ev_snapshots, team_ev_snapshots) and simulation_status column on sports seasons
- Add ev-snapshot model with upsert and history query functions
- Add simulator framework: types, bracket/F1/golf simulators, registry
- F1 simulator: vig-removed ICM weighted draw (pre-season) + race-by-race Monte Carlo from current standings (in-season); per-position column normalization to prevent floating-point EV drift
- Add admin simulate route and Run Simulation button on sports season page
- Rework futures-odds admin page to save odds then run simulation in one action
- Remove recalculate-probabilities route (superseded by simulate route)
- Remove EV trend chart panel and associated DB queries
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: map simulators to sports via simulatorType field
Adds a `simulator_type` enum column to the `sports` table so each sport
can be assigned a specific simulation algorithm rather than deriving it
from the sports season's scoring pattern.
- Add `simulatorTypeEnum` (f1_standings, indycar_standings,
golf_qualifying_points, playoff_bracket) + `simulatorType` nullable
column on `sports` table; migration 0037
- Rewrite simulator registry to key off `SimulatorType` instead of
`ScoringPattern`; indycar_standings shares F1Simulator for now
- `findSportsSeasonById` now returns `SportsSeasonWithSport` so callers
have typed access to `sport.simulatorType`
- Simulate and futures-odds actions read `sport.simulatorType`; guard
fires before setting `simulationStatus: running`
- Admin sport edit page gains a Simulator Type dropdown
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract EditEventCard component to eliminate duplicated edit form
- Add edit form to non-schedule event types (bracket events)
- Move actionData feedback above debug cards for better UX
- Guard "Add Result" form from showing on playoff_game events
- Fix isPast date comparison to be safe for Date or string types
- Remove unused qpConfig/qpStandings destructuring
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- SeasonStandings: highlight rows for the logged-in user's drafted participants
with electric accent (in-points) or muted (outside points) left border + star icon
- Loader: derive userParticipantIds from current user's teams and draft picks
- EventSchedule: hide type badge for schedule_event; show "Results Pending" only
when event is past and not yet marked complete (matches admin page logic)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Sort sports seasons by status (active → upcoming → completed), with
season_standings pattern last within active, then alphabetical
- Fix rank display for teams without standings: T1 when no scores exist,
T{n+1} when some teams are scored (avoids misleading T1 for unranked teams)
- Extract rank logic to getDisplayRank() in standings-display.ts with tests
- Pre-compute standingsMap and sortedTeams to eliminate O(n²) render lookups
- Fix invite URL SSR flash by deriving origin in the loader
- Guard toast useEffect with processedParamsRef to prevent double-fire
- Hide Teams Filled and Draft Order Set post-draft; show Standings sidebar
link only when active/completed
- Show "Final standings" label when season is completed
- Remove duplicate Season Status from sidebar; add Flex Spots description
- Fix status type cast to occur at the model mapping layer, not in JSX
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
After every pick, recalculate draft eligibility for all teams and
remove any queued participants whose sport is no longer eligible
(e.g. a team queued a snooker player but just filled their last flex
slot). Previously this was only caught lazily when autodraft fired,
which could pause the draft or pick an unwanted player.
- Add getAllQueuesForSeason to draft-queue.ts — fetches all queue rows
for a season in one query (Map<teamId, QueueItem[]>) instead of N+1
per-team queries
- Add pruneIneligibleQueueItems to draft-utils.ts — uses Promise.all
for the four required data fetches, collects ineligible items in a
single loop pass, warns on orphaned participant references
- Call from both pick paths: executeAutoPick and draft.make-pick.ts
- Emit queue-eligibility-pruned socket event per affected team so the
client updates the queue UI in real time
- Add 5 tests covering: single ineligible removal, all eligible (no-op),
empty queues (no delete called, getTeamQueue never called), mixed
queue (only ineligible item removed), and unknown participant (warn)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: improve JWT expiry handling and revalidation logic in DraftRoom component
* fix: ensure authRecoveryTimerRef is initialized to undefined to prevent potential errors
- Extract groupPicksByTeamAndSport into shared picks-utils.ts to remove
duplication between TeamRosterView and DraftSummaryView
- Fix TeamRosterView: make teamPicks a proper useMemo dep on selectedTeamId,
add useEffect to reset stale selection if draftSlots changes
- Add accessible label/htmlFor to TeamRosterView team Select
- Remove dead `season` prop from DraftSummaryView (was inherited from
TeamsDraftedGrid but never used)
- Fix DraftSummaryView: sport name cells are now <th scope="row">, column
headers have scope="col", Total header uses solid bg-muted to match Sport
- Replace || null with explicit > 0 check in Total cell for clarity
- Consolidate to summaryViewProps/rosterViewProps in route for consistency
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: enhance autodraft settings with detailed option descriptions and improved state handling
* test: update AutodraftSettings tests for button interactions and API payloads
Primary fix: setTeamTimers now bails out with `return prev` when the
value hasn't changed, preventing a full DraftRoom re-render on every
1-second timer tick (was 33% of profiler samples).
Memoization: wrap AvailableParticipantsSection, TeamsDraftedGrid,
QueueSection, SidebarRecentPicks, and DraftGridSection in React.memo
so timer ticks don't cascade into heavy components that don't use
timer state.
Stable refs: wrap nine action handlers in useCallback and extract two
inline arrow functions from props objects so memo() comparisons
actually bail out. Memoize the { numFlexPicks } object passed to
TeamsDraftedGrid.
socketVersion: expose an incrementing counter from useDraftSocket so
the socket handler effect re-registers on socket recreation.
Async cleanup: add abort flag + in-flight guard to the visibilitychange
JWT refresh handler to prevent concurrent executions and stale state
updates after unmount. Add abort flag to useDraftNotifications
permissions.query() to prevent dangling onchange if unmounted
mid-promise.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move "Show drafted sports" into the sport filter dropdown (sheet/popover)
instead of a standalone checkbox in the filter bar
- When unchecked, drafted sports are hidden from the dropdown list entirely;
when checked, they appear in alphabetical order with muted text
- Auto-deselect drafted sport filters when hiding drafted sports
- Reset button also restores "Show drafted sports" to checked
- Extract SportFilterContent component to eliminate sheet/popover duplication
- Make userDraftedSportNames required; add useCallback for handlers;
memoize triggerText/triggerAriaLabel; unify all checkboxes to ShadCN Checkbox;
replace empty div divider with hr
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds GET /api/seasons/:seasonId/draft returning current pick number,
who's on the clock, and the full picks list with usernames and sport.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the single-sport native <select> with a popover (desktop) and
bottom sheet (mobile) checkbox multi-select. No sports selected shows all
participants; selecting one or more narrows the list. Trigger label adapts
to reflect current filter state and a Reset button appears when filters
are active.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Clerk's short-lived JWTs (60s) can expire when browser tabs are
backgrounded and the SDK's refresh interval is throttled. This left
users silently logged out — actions returned 401s shown as generic
error toasts with no recovery path.
Prevention: refresh the Clerk JWT on tab visibility change so the
token is always fresh when the user returns from a backgrounded tab.
Detection: replace all 12 raw fetch() calls with an authFetch wrapper
that catches 401 responses and sets an authDegraded state, triggering
a blocking recovery overlay instead of confusing toasts.
Recovery: AuthRecoveryOverlay (mirrors ConnectionOverlay pattern)
prompts the user to reload the page, which re-authenticates via
Clerk's long-lived session cookie.
Also adds missing try/catch to handleStartDraft, handleMakePick,
handleForceAutopick, handleForceManualPick, and handleReplacePick
which previously had no network error handling.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add a reference-equality check so that if HTTP revalidation fails (network
error, expired token), the sync effect does not overwrite fresh data that
draft-state-sync already applied. Also adds missing dependency array entries
(userQueue, timers, autodraftSettings) to the revalidation sync effect.
https://claude.ai/code/session_01JxJ1CYTiFWV4KTPxTnjrms (#48)
* fix: sync all draft state on mobile reconnection
After a mobile browser returns from a long background period, the draft
room had stale picks, wrong "on the clock" display, and inaccurate
available player lists. The root cause was that reconnection relied
solely on an HTTP revalidation that could fail (expired JWT, flaky
network), and the timer-update handler ignored currentPickNumber.
Changes:
- Server emits draft-state-sync on join-draft with full picks, timers,
and season state, giving the client an immediate socket-based sync
path that doesn't depend on HTTP revalidation
- timer-update handler now syncs currentPickNumber, fixing the "on the
clock" display within 1 second of reconnection
- Revalidation retry with 3s delay ensures the HTTP path succeeds even
when the network is slow to stabilize on mobile return
- Revalidation completion now also syncs teamTimers and autodraftStatus
- Added draft-state-sync client handler that applies the server snapshot
immediately (skipped when revalidation is in-flight to avoid conflicts)
Tests: 32 new tests covering reconnection sync, pick buffering/merge,
timer-update currentPickNumber sync, draft-state-sync handling,
available player filtering, on-the-clock correctness, and revalidation
retry logic.
https://claude.ai/code/session_01JxJ1CYTiFWV4KTPxTnjrms
* fix: guard against stale revalidation overwriting fresh socket data
Add a reference-equality check so that if HTTP revalidation fails (network
error, expired token), the sync effect does not overwrite fresh data that
draft-state-sync already applied. Also adds missing dependency array entries
(userQueue, timers, autodraftSettings) to the revalidation sync effect.
https://claude.ai/code/session_01JxJ1CYTiFWV4KTPxTnjrms
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: eliminate queue tab flash on mobile by using useSyncExternalStore
useMediaQuery initialized to false and updated in useEffect, causing a
paint where isMobile was false on mobile devices. This rendered the
desktop layout first, hiding the queue tab until the effect fired.
useSyncExternalStore reads matchMedia synchronously on the client so
the correct layout is used on the first render with no extra paint.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: revalidate draft room when Clerk JWT expires at load time
Clerk's short-lived JWTs (~1 min) can expire between navigations.
Server-side clerkMiddleware can't refresh them for client-side loader
fetches, so getAuth() returns userId=null — causing userTeam=undefined,
the queue button to disappear, and the tab to default to "board".
Added a useEffect that detects the mismatch: if the Clerk client SDK
has a valid userId but the loader ran without one, trigger revalidate()
so the loader re-runs with a fresh token and returns correct
userTeam/userQueue data.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: prevent infinite revalidation loop in auth guard
If the server consistently fails to populate currentUserId after JWT
refresh (e.g. misconfigured CLERK_SECRET_KEY), the previous effect
would loop indefinitely. A one-shot ref ensures we attempt auth
recovery at most once per mount.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Add text-base md:text-sm to all native <input> and <select> elements
in the draft room (including the ShadCN SelectTrigger) so iOS Safari
doesn't auto-zoom when focused.
Also extract the two near-duplicate participant selection dialogs into
a shared ParticipantSelectionDialog component (-236 lines), and replace
O(n) queue.find() scans with a memoized Map in AvailableParticipantsSection.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Show owner username as primary label in "On the Clock" section (mobile bar + desktop sidebar), with team name as a secondary label below
- Memoize currentDraftSlot to avoid O(n) array scan on every timer tick
- Derive currentDraftSlotOwnerName once instead of looking up ownerMap four times per render
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Change default desktop tab from "participants" to "board"
- Extract mobileTabs/mobileColCount before JSX to eliminate IIFE pattern
- Remove redundant h-full wrapper divs from TabsContent for consistency
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
On reconnect, revalidate() re-fetches fresh loader data but the local
useState copies (picks, currentPick, isPaused, isDraftComplete, queue)
never synced with the new values — leaving the UI stale until a manual
refresh.
- Watch revalidatorState (idle→loading→idle) to detect when a
revalidation completes and apply the fresh loader snapshot to all
affected local state
- Buffer pick-made socket events received during the revalidation window
(instead of discarding or double-applying them); merge into the DB
snapshot on completion, deduplicated by pick ID
- Keep setCurrentPick paired with setPicks in handlePickMade so the
"on the clock" indicator and the picks list never desync
- Sync queue state from fresh userQueue after revalidation so
participants drafted while the user was away disappear from their
queue even if the participant-removed-from-queues events were missed
Adds useDraftSocket reconnect test suite (9 tests) covering initial
connect, socket reconnect, visibility-change triggering, network
flapping, error exhaustion, and cleanup.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Redesign autodraft queue system with three-state control and queue-only constraint
Core Logic & Database:
- Add `queue_only` boolean column to `autodraft_settings` (migration 0031)
- Rename autodraft UI states: Off / Next Pick / All Picks (while_on mode maps to All Picks)
- `autoPickForTeam`: respects new `queueOnly` param — skips EV fallback when enabled
- `executeAutoPick`: auto-disables autodraft + emits socket event when queue empties with queueOnly ON (AC3)
- `autodraft-updated` socket event now includes `queueOnly` field
Mobile UI Overhaul:
- Rename "Lobby" tab → "Available" (AC6)
- Add new "Queue" tab to mobile bottom nav with drag-reorder, per-item Draft buttons, and autodraft controls (AC5)
- Controls tab retains commissioner tools, notifications, exit; queue controls moved to Queue tab
- Turn indicator appears on both Available and Queue tabs
Components:
- `AutodraftSettings`: replaces toggle+radio with three-state button group (Off | Next Pick | All Picks) + "Only autodraft from queue" switch (AC1, AC2)
- `QueueSection`: adds `canPick` prop + per-item Draft buttons for instant drafting when on the clock
Desktop (AC4):
- Sidebar QueueSection unchanged in position; gains same three-state controls and Draft buttons
Tests (AC7):
- `autodraft.test.ts`: updated for queueOnly field and socket event shape
- `timer-autodraft.test.ts`: new tests for queue-only constraint, auto-shutoff transitions, and all three autodraft states
https://claude.ai/code/session_01PYhJicAStoJ2u6q6dV1naB
* fix: remove erroneous ?? fallbacks in autodraft socket emissions and add autoPickForTeam tests
- Remove `?? true` default on queueOnly in queue-empty auto-disable socket emit
(line 488) — was dead code since the column is NOT NULL, but semantically wrong
and would have caused client-side UI desync if the type ever relaxed
- Remove `?? false` default on the next_pick auto-disable path for consistency
- Add app/models/__tests__/auto-pick.test.ts with 6 tests covering the queueOnly
constraint: empty queue, all items drafted, partial queue skip, and EV fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add missing queueOnly prop to AutodraftSettings test fixtures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: rewrite AutodraftSettings tests for three-state button group UI
The component was redesigned from a switch + radio buttons to Off/Next Pick/All Picks
buttons with a separate queue-only Switch toggle. Updated 17 stale tests and added 5
new tests covering the queue-only toggle and the All Picks/Off button interactions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* 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>
- Reduce ping detection window from ~45s to ~15s (pingTimeout 5s, pingInterval 10s)
- Listen to window offline/online events for instant UI feedback on network drop
- Force immediate reconnect on online event instead of waiting for backoff
- Fix stuck-reconnecting bug when network returns within ping timeout window
- Wrap on/off/emit in useCallback to prevent listener re-registration every second
- Expose reconnectCount to trigger revalidator.revalidate() after each reconnect
- Reset hasConnectedOnce ref per socket instance to avoid spurious revalidations
- Remove stale socket ref from hook return value; add emit helper instead
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add optimistic remove to handleRemoveFromQueue (was missing, causing visible lag)
- Fix stale closure on previousQueue in reorder/remove by capturing inside setQueue updater
- Replace filter(Boolean) with typed type guard filter in handleReorderQueue
- Add QueueItem type alias from schema.$inferSelect, typed useState, remove all `any` casts
- Complete temp item fields in handleAddToQueue to satisfy QueueItem type
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Display sport name as muted subtitle under participant name to distinguish
players across sports (e.g. Florida NCAAM vs Florida NCAAW)
- Remove unused eligibility prop from QueueSection
- Deduplicate double .find() per queue item into a single lookup
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- Fix settings action silently resetting timer values when draft speed
select is disabled (add null guard before overwriting draftInitialTime/
draftIncrementTime)
- Make timer decrement and pick increment atomic using SQL expressions to
prevent read-modify-write races between the timer loop and HTTP handlers
- Add UNIQUE INDEX on (season_id, pick_number) in draft_picks to prevent
duplicate picks from concurrent requests (TOCTOU guard)
- Add socket join-draft team ownership validation via DB query
- Add iteration cap to autodraft chain while loop (max = totalTeams)
- Add draftPaused re-check before firing autodraft chain in make-pick
and force-manual-pick
- Consolidate all snake draft calculations into calculatePickInfo (DRY);
remove duplicated logic from timer.ts, make-pick, force-manual-pick,
executeAutoPick, and checkAndTriggerNextAutodraft
- Fix calculatePickInfo to return snake-adjusted pickInRound matching
draftOrder values instead of raw pre-snake value
- Fix timer-update socket events to emit nextPickNumber after a pick
instead of the already-completed currentPickNumber
- Fix force-manual-pick to validate submitted teamId matches the team
whose turn it actually is at the given pick number
- Replace inline timer init in draft.start with deleteSeasonTimers +
initializeDraftTimers model functions (dead code fix)
- Fix draft loader to not crash when getTeamQueue fails (returns [])
- Fix misleading 403 message for commissioner picks
- Remove dead hidden inputs from league settings form
- Document connectedTeams single-instance limitation in socket.ts
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove 10-pick limit in SidebarRecentPicks
- Fix scroll by using max-h-[45vh] on AccordionContent instead of the
broken flex-1 approach (AccordionPrimitive.Content has overflow-hidden
hardcoded, making flex-1 and overflow-y-auto ineffective without a
bounded height)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
When a league has the "Make draft board publicly accessible" setting enabled,
unauthenticated users could view the draft-board route but were incorrectly
blocked from the draft room route with a 401. Restructure the loader to check
isPublicDraftBoard before enforcing auth/membership requirements, matching the
behaviour already implemented in the draft-board route.
https://claude.ai/code/session_016JiVAG2FrPYjAvSJi3daGD
Co-authored-by: Claude <noreply@anthropic.com>
Replace h-screen (100vh) with h-dvh (100dvh) on the root draft room
container. On mobile browsers, 100vh equals the max viewport height
when the address bar is hidden, causing the page to overflow when the
address bar is visible. 100dvh (dynamic viewport height) correctly
adjusts to the actual visible area at all times.
https://claude.ai/code/session_01GecUeTHw4Mc2ErSYJ6WXae
Co-authored-by: Claude <noreply@anthropic.com>
- Add bottom nav bar (Lobby/Board/Roster/Controls) with 56px touch targets
- Add mobile card list view in AvailableParticipantsSection with 44px Draft/Queue buttons
- Add round labels and sticky header row to DraftGridSection grid
- Add mobile Sheet for commissioner actions in DraftGridSection (replaces right-click context menu)
- Use useMediaQuery to render a single layout tree, eliminating duplicate component
instances (previously both desktop and mobile layouts were always in the DOM)
- Add compact header on mobile (text-lg vs text-3xl); hide commissioner buttons
and Exit link in header on mobile — surfaced in Controls tab instead
- Add "On Clock" bar on mobile showing current team and timer during active draft
- Add "your turn" indicator dot on Lobby bottom nav tab
- Default mobile tab to "board" for commissioner-only viewers
- Fix sticky corner cell z-index in TeamsDraftedGrid (z-10 → z-20)
- Fix round column spacer in DraftGridSection header not being sticky-left
- Extract shared prop objects to eliminate copy-paste between layouts
- Extract getParticipantState() helper to deduplicate participant render logic
- Move MobileSheetData type and MOBILE_TABS array to module scope
- Remove unnecessary non-null assertions in DraftGridSection Sheet handlers
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix force-manual-pick resetting next team's timer to initial time
When a commissioner forced a manual pick, the next team's timer was
being reset to the initial time (2 minutes) instead of carrying
forward their existing time bank balance.
This aligns force-manual-pick with the behavior of regular user picks
and force-autopick: the picking team gets their increment added, and
the next team's timer is left untouched so their bank carries forward.
https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p
* Add regression tests for draft.force-manual-pick timer behavior
18 tests across 5 describe blocks covering:
- Authorization (401/403)
- Input validation (missing fields, bad participant, ineligible sport)
- Successful pick (response shape, draft-complete detection, socket events)
- Timer behavior (increment added to picking team, new timer creation, additive not reset)
- Two regression tests confirming the next team's timer is never touched:
draftTimers.findFirst called exactly once, no timer-update emitted for
next team, db.update called exactly twice (not three times)
https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p
* Add TypeScript types and improve draft validation (#28)
* Code review fixes: type safety, security hardening, and dead code removal
- Fix Socket.IO event types: draft-paused and draft-resumed were typed as
() => void but are emitted with { seasonId, paused } data payloads
- Fix draft.force-manual-pick: add missing season.status === "draft" guard
so commissioners cannot force picks outside an active draft; add duplicate
pick-number check so a slot cannot be assigned two picks (the previous
code only checked participant uniqueness, not slot uniqueness)
- Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all
API routes and league loaders; replace (auth as any).userId casts with
proper const { userId } = await getAuth(args) destructuring
- Remove unused isSnakeDraft = true dead variable from draft.make-pick
- Replace autodraftSettings: any and draftSlots: any[] in draft-utils with
properly typed InferSelectModel / DraftSlot types
- Update force-manual-pick tests: sequence draftPicks.findFirst mock for
the two-call flow; add new tests for status-check and slot-uniqueness
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
* Fix RouterContextProvider type errors in action test files
Cast context argument to RouterContextProvider in test helpers so
ActionFunctionArgs strict typing is satisfied without weakening the
production action signatures back to any.
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
---------
Co-authored-by: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Fix dropdown and input dark mode theming in draft page
Add bg-background and text-foreground classes to native select and
input elements so they use theme CSS variables instead of browser
defaults, which rendered as white in dark mode.
https://claude.ai/code/session_01FZz7kndEqWqFScyq4R6Wxn
* Fix missing text-foreground on time bank unit select
The seconds/minutes/hours select in the time bank dialog had
bg-background but was missing text-foreground, causing text
color to fall back to browser default in dark mode.
https://claude.ai/code/session_01FZz7kndEqWqFScyq4R6Wxn
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Create timer when adding time to a team with no existing timer
When a commissioner tries to add time to a team that has no draft timer,
instead of returning a 404 error, create a new timer record with the
specified amount of time. Removing time still returns a 404 if no timer
exists (nothing to remove from).
https://claude.ai/code/session_016VpJKZZFNQQqzfmLu8pHoc
* Fix silent timer failures and missing seasonId filter in timer model
- draft.make-pick.ts: create a timer with the increment amount instead of
logging a warning and silently skipping when no timer exists for the
picking team
- draft.force-manual-pick.ts: same fix for the commissioner force-pick path;
also unconditionally emit the timer-update socket event so clients always
see the updated time
- models/draft-timer.ts: add seasonId parameter to getTeamTimer and
updateTeamTimer so they cannot match the wrong season's timer when a team
participates in multiple seasons
https://claude.ai/code/session_016VpJKZZFNQQqzfmLu8pHoc
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Fix draft timer missing after rollback causing draft to freeze
The rollback endpoint was deleting all timers but only recreating one
for the team currently on the clock. After that team made their pick,
the next team had no timer row, causing the timer system to log
"No timer found" and skip indefinitely, freezing the draft.
Fix: recreate timers for ALL teams after a rollback, each starting at
draftInitialTime.
Also add a defensive fallback in the timer system: if a timer row is
missing for the current team during an active draft, create it at
draftInitialTime instead of skipping, so the draft can never get
permanently stuck due to a missing timer.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
* 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>
* 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>
* 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>