strat-gameplay-webapp/frontend-sba/composables/CLAUDE.md
Cal Corum 9f88317b79 CLAUDE: Fix cookie security and SSR data fetching for iPad/Safari
Resolved WebSocket connection issues and games list loading on iPad:

- cookies.py: Added is_secure_context() to set Secure flag when accessed
  via HTTPS even in development mode (Safari requires this)
- useWebSocket.ts: Changed auto-connect from immediate watcher to
  onMounted hook for safer SSR hydration
- games/index.vue: Replaced onMounted + fetchGames() with useAsyncData
  for SSR data fetching with proper cookie forwarding
- Updated COOKIE_AUTH_IMPLEMENTATION.md with new issues and solutions
- Updated composables/CLAUDE.md with auto-connect pattern documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 21:06:42 -06:00

3.2 KiB

Composables Directory

Vue 3 composables for shared logic. These handle WebSocket communication and game actions.

Available Composables

useWebSocket.ts

Purpose: Manages Socket.io connection with authentication and auto-reconnection.

Key Exports:

const {
  socket,           // Computed<TypedSocket | null>
  isConnected,      // Readonly<Ref<boolean>>
  isConnecting,     // Readonly<Ref<boolean>>
  connectionError,  // Readonly<Ref<string | null>>
  connect,          // () => void
  disconnect,       // () => void
} = useWebSocket()

Event Flow (Backend → Store):

socketInstance.on('game_state_update') → gameStore.setGameState()
socketInstance.on('lineup_data')       → gameStore.updateLineup()
socketInstance.on('decision_required') → gameStore.setDecisionPrompt()
socketInstance.on('play_completed')    → gameStore.addPlayToHistory()
socketInstance.on('dice_rolled')       → gameStore.setPendingRoll()

Singleton Pattern: Socket instance is module-level, shared across all useWebSocket() calls.

Auto-Connect Pattern: Uses onMounted hook (not immediate: true watcher) to connect when already authenticated. This is safer for SSR hydration:

// Watch for auth changes (not immediate)
watch(() => authStore.isAuthenticated, (authenticated) => {
  if (authenticated && !isConnected.value) {
    connect()
  } else if (!authenticated && isConnected.value) {
    disconnect()
  }
})

// Initial connection on client-side mount
onMounted(() => {
  if (authStore.isAuthenticated && !isConnected.value) {
    connect()
  }
})

Why not immediate: true? Using immediate: true with import.meta.client guard can cause SSR hydration issues. The onMounted hook only runs on client-side, making it safer.

useGameActions.ts

Purpose: Wraps WebSocket emits with type safety and validation.

Key Exports:

const {
  joinGame,           // (gameId: string) => void
  requestGameState,   // (gameId: string) => void
  setDefense,         // (gameId: string, decision: DefensiveDecision) => void
  setOffense,         // (gameId: string, decision: OffensiveDecision) => void
  rollDice,           // (gameId: string) => void
  submitOutcome,      // (gameId: string, outcome: PlayOutcome) => void
  // ... substitution methods
} = useGameActions()

Usage Pattern:

// In component
const { setDefense } = useGameActions()

const handleSubmit = (decision: DefensiveDecision) => {
  setDefense(gameId, decision)
}

Data Flow Architecture

User Action → useGameActions → socket.emit() → Backend
                                                  ↓
Component ← gameStore ← useWebSocket.on() ← socket event

Why this separation?

  • useWebSocket: Low-level connection management, event routing
  • useGameActions: High-level game operations, business logic validation
  • gameStore: Centralized state, computed getters

Common Issues

Issue Cause Solution
"Not connected" error Socket disconnected Check isConnected before actions
Events not firing Listeners not set up Ensure useWebSocket() called in component
Stale data Missed reconnection Call requestGameState() after reconnect