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>
* 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 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>
* 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>