CLAUDE: Fix infinite WebSocket reconnection loop (HIGH-002)

After max reconnection attempts (10), the stuck state detector was
continuing to retry forever. Now:

- Add permanentlyFailed state flag to track when we've given up
- Set flag and stop stuck state detector when max attempts reached
- Add manualRetry() method for UI to reset state and try again
- Expose permanentlyFailed in public API for error boundary (HIGH-001)

Also marks CRIT-002 and CRIT-002-BACKEND as completed in project plan.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-01-13 20:49:04 -06:00
parent 89a63af2a8
commit 1a7e464990
2 changed files with 52 additions and 5 deletions

View File

@ -47,8 +47,8 @@
"description": "myTeamId is hardcoded to null with a TODO comment, breaking the substitution panel's ability to determine which team's lineup to display. Users cannot see or manage their own team's substitutions.",
"category": "critical",
"priority": 2,
"completed": false,
"tested": false,
"completed": true,
"tested": true,
"dependencies": ["CRIT-002-BACKEND"],
"files": [
{
@ -67,8 +67,8 @@
"description": "Backend dependency for CRIT-002. The /api/auth/me endpoint needs to return the user's team ownership information so frontend can determine if user owns home/away team.",
"category": "critical",
"priority": 2,
"completed": false,
"tested": false,
"completed": true,
"tested": true,
"dependencies": [],
"files": [
{

View File

@ -85,6 +85,7 @@ const isConnected = ref(false)
const isConnecting = ref(false)
const connectionError = ref<string | null>(null)
const lastConnectionAttempt = ref<number | null>(null)
const permanentlyFailed = ref(false) // Set when max reconnection attempts reached
// Reset reactive state on client hydration to ensure clean slate
if (import.meta.client) {
@ -93,6 +94,7 @@ if (import.meta.client) {
isConnecting.value = false
connectionError.value = null
lastConnectionAttempt.value = null
permanentlyFailed.value = false
console.log('[WebSocket] Reactive state reset on client hydration')
}
@ -133,6 +135,7 @@ export function useWebSocket() {
const state = getClientState()
return (
!isConnected.value &&
!permanentlyFailed.value &&
canConnect.value &&
state.reconnectionAttempts < MAX_RECONNECTION_ATTEMPTS
)
@ -252,6 +255,7 @@ export function useWebSocket() {
isConnected.value = false
isConnecting.value = false
connectionError.value = null
permanentlyFailed.value = false
state.reconnectionAttempts = 0
}
@ -283,6 +287,7 @@ export function useWebSocket() {
isConnected.value = false
isConnecting.value = false
connectionError.value = null
permanentlyFailed.value = false
state.reconnectionAttempts = 0
// Reconnect after a short delay
@ -291,6 +296,30 @@ export function useWebSocket() {
}, 100)
}
/**
* Manual retry after permanent failure.
* Resets all connection state and attempts fresh connection.
* Use this from UI when user clicks "Retry" after max attempts reached.
*/
function manualRetry() {
// SSR guard
if (!import.meta.client) return
const state = getClientState()
console.log('[WebSocket] Manual retry requested - resetting failed state')
// Reset all state
permanentlyFailed.value = false
connectionError.value = null
state.reconnectionAttempts = 0
// Restart stuck state detection
startStuckStateDetection()
// Attempt connection
forceReconnect()
}
/**
* Reconnect with exponential backoff
*/
@ -300,8 +329,18 @@ export function useWebSocket() {
const state = getClientState()
// Check if we've hit the max attempts
if (state.reconnectionAttempts >= MAX_RECONNECTION_ATTEMPTS) {
console.error(`[WebSocket] Max reconnection attempts (${MAX_RECONNECTION_ATTEMPTS}) reached - giving up`)
permanentlyFailed.value = true
connectionError.value = `Connection failed after ${MAX_RECONNECTION_ATTEMPTS} attempts. Click retry to try again.`
// Stop the stuck state detector since we've given up
stopStuckStateDetection()
return
}
if (!shouldReconnect.value) {
console.log('[WebSocket] Reconnection not needed or max attempts reached')
console.log('[WebSocket] Reconnection not needed')
return
}
@ -690,6 +729,12 @@ export function useWebSocket() {
if (state.stuckStateCheckInterval) return
state.stuckStateCheckInterval = setInterval(() => {
// Don't attempt if we've permanently failed - user must manually retry
if (permanentlyFailed.value) {
console.log('[WebSocket] Stuck state check skipped - permanently failed')
return
}
// Try to connect if not connected - don't check auth state
// Backend will authenticate via cookies
if (!isConnected.value && !isConnecting.value) {
@ -754,11 +799,13 @@ export function useWebSocket() {
isConnected: readonly(isConnected),
isConnecting: readonly(isConnecting),
connectionError: readonly(connectionError),
permanentlyFailed: readonly(permanentlyFailed),
canConnect,
// Actions
connect,
disconnect,
forceReconnect,
manualRetry,
}
}