From 364c5149a483b36e8dc1e301c952741298099f6c Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Fri, 28 Nov 2025 22:37:53 -0600 Subject: [PATCH] CLAUDE: Fix Safari WebSocket connection issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frontend fixes for Safari/iPad WebSocket reliability: 1. Add Vite allowedHosts config (nuxt.config.ts) - Allow gameplay-demo.manticorum.com for external access - Fixes "Blocked request" error after Vite security update 2. Add connection timeout failsafe (useWebSocket.ts) - 10-second timeout resets stuck "isConnecting" state - Prevents Heisenbug where Safari connections hang indefinitely - Auto-schedules reconnection after timeout 3. Add visible debug info to connection modal (games/[id].vue) - Shows WS URL, socket status, and connection log - Helps diagnose Safari-specific issues without dev tools 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- frontend-sba/composables/useWebSocket.ts | 29 ++++++++++++++++++++++++ frontend-sba/nuxt.config.ts | 7 ++++++ frontend-sba/pages/games/[id].vue | 22 +++++++++++++++++- 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/frontend-sba/composables/useWebSocket.ts b/frontend-sba/composables/useWebSocket.ts index 3aeaa12..5786c55 100644 --- a/frontend-sba/composables/useWebSocket.ts +++ b/frontend-sba/composables/useWebSocket.ts @@ -35,6 +35,7 @@ const MAX_RECONNECTION_ATTEMPTS = 10 let socketInstance: Socket | null = null let reconnectionAttempts = 0 let reconnectionTimeout: NodeJS.Timeout | null = null +let connectionTimeoutId: NodeJS.Timeout | null = null // Timeout to prevent stuck "isConnecting" state // Singleton reactive state (shared across all useWebSocket calls) const isConnected = ref(false) @@ -84,6 +85,9 @@ export function useWebSocket() { // Connection Management // ============================================================================ + // Connection timeout constant + const CONNECTION_TIMEOUT = 10000 // 10 seconds + /** * Connect to WebSocket server with JWT authentication */ @@ -103,6 +107,21 @@ export function useWebSocket() { console.log('[WebSocket] Connecting to', wsUrl) + // Set a timeout to reset isConnecting if no event fires + // This prevents the "stuck connecting" state that can occur on Safari + if (connectionTimeoutId) { + clearTimeout(connectionTimeoutId) + } + connectionTimeoutId = setTimeout(() => { + if (isConnecting.value && !isConnected.value) { + console.warn('[WebSocket] Connection timeout - resetting state') + isConnecting.value = false + connectionError.value = 'Connection timeout - no response from server' + // Schedule a reconnection attempt + scheduleReconnection() + } + }, CONNECTION_TIMEOUT) + try { // Create or reuse socket instance if (!socketInstance) { @@ -121,6 +140,10 @@ export function useWebSocket() { console.error('[WebSocket] Connection error:', err) isConnecting.value = false connectionError.value = err instanceof Error ? err.message : 'Connection failed' + if (connectionTimeoutId) { + clearTimeout(connectionTimeoutId) + connectionTimeoutId = null + } } } @@ -225,6 +248,12 @@ export function useWebSocket() { connectionError.value = null reconnectionAttempts = 0 + // Clear connection timeout + if (connectionTimeoutId) { + clearTimeout(connectionTimeoutId) + connectionTimeoutId = null + } + // Update global flag for plugin failsafe if (typeof window !== 'undefined') { window.__ws_connected = true diff --git a/frontend-sba/nuxt.config.ts b/frontend-sba/nuxt.config.ts index a7d366f..61225fd 100644 --- a/frontend-sba/nuxt.config.ts +++ b/frontend-sba/nuxt.config.ts @@ -30,6 +30,13 @@ export default defineNuxtConfig({ port: 3000 }, + // Vite config for external hostname access + vite: { + server: { + allowedHosts: ['gameplay-demo.manticorum.com', 'localhost', '127.0.0.1'] + } + }, + typescript: { strict: true, typeCheck: false // Disable in dev - use `npm run type-check` manually diff --git a/frontend-sba/pages/games/[id].vue b/frontend-sba/pages/games/[id].vue index 2e02871..a632471 100755 --- a/frontend-sba/pages/games/[id].vue +++ b/frontend-sba/pages/games/[id].vue @@ -199,7 +199,13 @@ WebSocket: {{ isConnected ? 'Connected' : isConnecting ? 'Connecting...' : 'Disconnected' }} -

{{ connectionError }}

+

Error: {{ connectionError }}

+ +
+

WS URL: {{ wsDebugUrl }}

+

Socket exists: {{ socketExists }}

+

{{ debugLog }}

+
@@ -355,6 +361,12 @@ const gameId = computed(() => route.params.id as string) // WebSocket connection const { socket, isConnected, isConnecting, connectionError, connect, forceReconnect } = useWebSocket() +// Debug info for troubleshooting Safari WebSocket issues +const config = useRuntimeConfig() +const wsDebugUrl = computed(() => config.public.wsUrl || 'not set') +const socketExists = computed(() => socket.value ? 'yes' : 'no') +const debugLog = ref('Loading...') + // Pass the raw string value from route params, not computed value // useGameActions will create its own computed internally if needed const actions = useGameActions(route.params.id as string) @@ -657,10 +669,18 @@ const updateScoreBoardHeight = () => { // Lifecycle onMounted(async () => { + // Debug logging for Safari troubleshooting + debugLog.value = `Mounted at ${new Date().toLocaleTimeString()}\n` + debugLog.value += `isConnected: ${isConnected.value}, isConnecting: ${isConnecting.value}\n` + // Try to connect WebSocket immediately - cookies will be sent automatically // Backend will authenticate via cookies and reject if invalid if (!isConnected.value && !isConnecting.value) { + debugLog.value += 'Calling connect()...\n' connect() + debugLog.value += 'connect() called\n' + } else { + debugLog.value += 'Skipped connect (already connected/connecting)\n' } // Also check auth store (for display purposes)