CLAUDE: Fix Safari WebSocket connection issues

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 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2025-11-28 22:37:53 -06:00
parent acd080b437
commit 364c5149a4
3 changed files with 57 additions and 1 deletions

View File

@ -35,6 +35,7 @@ const MAX_RECONNECTION_ATTEMPTS = 10
let socketInstance: Socket<ServerToClientEvents, ClientToServerEvents> | 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

View File

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

View File

@ -199,7 +199,13 @@
<span :class="isConnected ? 'text-green-600' : isConnecting ? 'text-yellow-600' : 'text-red-600'"></span>
<span class="text-gray-700 dark:text-gray-300">WebSocket: {{ isConnected ? 'Connected' : isConnecting ? 'Connecting...' : 'Disconnected' }}</span>
</div>
<p v-if="connectionError" class="text-xs text-red-600 mt-2">{{ connectionError }}</p>
<p v-if="connectionError" class="text-xs text-red-600 mt-2">Error: {{ connectionError }}</p>
<!-- Debug info -->
<div class="mt-2 text-xs text-gray-500 border-t pt-2">
<p>WS URL: {{ wsDebugUrl }}</p>
<p>Socket exists: {{ socketExists }}</p>
<p class="mt-1 font-mono text-[10px] max-h-24 overflow-y-auto">{{ debugLog }}</p>
</div>
</div>
<!-- Action buttons -->
@ -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)