strat-gameplay-webapp/.claude/implementation/frontend-phase-f1-composables.md
Cal Corum eab61ad966 CLAUDE: Phases 3.5, F1-F5 Complete - Statistics & Frontend Components
This commit captures work from multiple sessions building the statistics
system and frontend component library.

Backend - Phase 3.5: Statistics System
- Box score statistics with materialized views
- Play stat calculator for real-time updates
- Stat view refresher service
- Alembic migration for materialized views
- Test coverage: 41 new tests (all passing)

Frontend - Phase F1: Foundation
- Composables: useGameState, useGameActions, useWebSocket
- Type definitions and interfaces
- Store setup with Pinia

Frontend - Phase F2: Game Display
- ScoreBoard, GameBoard, CurrentSituation, PlayByPlay components
- Demo page at /demo

Frontend - Phase F3: Decision Inputs
- DefensiveSetup, OffensiveApproach, StolenBaseInputs components
- DecisionPanel orchestration
- Demo page at /demo-decisions
- Test coverage: 213 tests passing

Frontend - Phase F4: Dice & Manual Outcome
- DiceRoller component
- ManualOutcomeEntry with validation
- PlayResult display
- GameplayPanel orchestration
- Demo page at /demo-gameplay
- Test coverage: 119 tests passing

Frontend - Phase F5: Substitutions
- PinchHitterSelector, DefensiveReplacementSelector, PitchingChangeSelector
- SubstitutionPanel with tab navigation
- Demo page at /demo-substitutions
- Test coverage: 114 tests passing

Documentation:
- PHASE_3_5_HANDOFF.md - Statistics system handoff
- PHASE_F2_COMPLETE.md - Game display completion
- Frontend phase planning docs
- NEXT_SESSION.md updated for Phase F6

Configuration:
- Package updates (Nuxt 4 fixes)
- Tailwind config enhancements
- Game store updates

Test Status:
- Backend: 731/731 passing (100%)
- Frontend: 446/446 passing (100%)
- Total: 1,177 tests passing

Next Phase: F6 - Integration (wire all components into game page)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 09:52:30 -06:00

9.6 KiB

Frontend Phase F1 - Composables Documentation

Created: 2025-01-10 Status: Complete


Overview

Two powerful composables that provide type-safe WebSocket communication and game actions. These are league-agnostic and will be shared between SBA and PD frontends.


1. useWebSocket Composable

File: composables/useWebSocket.ts (420 lines)

Purpose: Manages Socket.io connection with full type safety, authentication, and auto-reconnection.

Features

Type-Safe Communication

  • TypedSocket wrapper ensures compile-time type checking
  • All events typed with ClientToServerEvents/ServerToClientEvents
  • No runtime errors from mismatched event names

JWT Authentication

  • Automatic token injection from auth store
  • Token refresh integration
  • Auth state watching (auto-connect on login, disconnect on logout)

Auto-Reconnection

  • Exponential backoff (1s → 2s → 4s → ... → 30s max)
  • Configurable max attempts (10 attempts)
  • Graceful degradation on connection loss

Event Listener Management

  • All 15+ server events handled
  • Automatic integration with game/ui stores
  • Toast notifications for user feedback
  • Cleanup on unmount

Heartbeat

  • Periodic ping every 30 seconds
  • Keeps connection alive
  • Detects zombie connections

API

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

Usage Example

// In a page or component
const { socket, isConnected, connect } = useWebSocket()
const authStore = useAuthStore()

// Auto-connect when authenticated (handled internally)
onMounted(() => {
  if (authStore.isAuthenticated) {
    connect()
  }
})

// Type-safe event listening
watch(socket, (sock) => {
  if (sock) {
    // TypeScript knows the event signature!
    sock.on('play_completed', (play: PlayResult) => {
      console.log('Play:', play.description)
    })

    // Type-safe emit
    sock.emit('roll_dice', { game_id: 'uuid' })
  }
})

Event Handlers Implemented

Connection Events:

  • connect - Update stores, show success toast
  • disconnect - Update stores, schedule reconnection
  • connect_error - Show error, retry
  • connected - Server confirmation
  • heartbeat_ack - Keep-alive response

Game State Events:

  • game_state_update - Update game store
  • game_state_sync - Full state + play history
  • play_completed - Add to history, show toast
  • inning_change - Show notification
  • game_ended - Show final score

Decision Events:

  • decision_required - Set prompt in store
  • defensive_decision_submitted - Clear prompt, show feedback
  • offensive_decision_submitted - Clear prompt, show feedback

Manual Workflow Events:

  • dice_rolled - Store roll data, show message
  • outcome_accepted - Clear roll, show feedback

Substitution Events:

  • player_substituted - Request lineup update
  • substitution_confirmed - Show success

Data Response Events:

  • lineup_data - Update lineup in store
  • box_score_data - Available for components

Error Events:

  • error - Show error toast
  • outcome_rejected - Show validation error
  • substitution_error - Show substitution error
  • invalid_action - Show action error
  • connection_error - Store and display

Reconnection Strategy

// Exponential backoff calculation
delay = min(BASE * 2^attempts, MAX)

// Example progression:
// Attempt 1: 1s
// Attempt 2: 2s
// Attempt 3: 4s
// Attempt 4: 8s
// Attempt 5: 16s
// Attempt 6+: 30s (capped)

// Max 10 attempts before giving up

Integration with Stores

Auth Store:

  • Watches isAuthenticated state
  • Auto-connects when user logs in
  • Auto-disconnects when user logs out
  • Injects JWT token into Socket.io auth

Game Store:

  • Updates on all game state events
  • Adds plays to history
  • Manages decision prompts
  • Stores dice rolls
  • Updates lineups

UI Store:

  • Shows connection toasts
  • Displays error messages
  • Provides user feedback on actions
  • Shows play descriptions

2. useGameActions Composable

File: composables/useGameActions.ts (280 lines)

Purpose: Type-safe wrapper for emitting game actions to the server with validation and error handling.

Features

Validation

  • Checks socket connection before emit
  • Validates game ID exists
  • User-friendly error messages

Type Safety

  • All parameters typed
  • Matches backend event expectations
  • Compile-time checks

User Feedback

  • Toast notifications for actions
  • Loading states
  • Error handling

API

const actions = useGameActions(gameId?) // Optional gameId, uses store if not provided

// Connection
actions.joinGame('player' | 'spectator')
actions.leaveGame()

// Strategic decisions
actions.submitDefensiveDecision(decision: DefensiveDecision)
actions.submitOffensiveDecision(decision: OffensiveDecision)

// Manual workflow
actions.rollDice()
actions.submitManualOutcome(outcome: PlayOutcome, hitLocation?: string)

// Substitutions
actions.requestPinchHitter(playerOutId, playerInId, teamId)
actions.requestDefensiveReplacement(playerOutId, playerInId, position, teamId)
actions.requestPitchingChange(playerOutId, playerInId, teamId)

// Data requests
actions.getLineup(teamId)
actions.getBoxScore()
actions.requestGameState()  // For reconnection recovery

Usage Example

// In a game page/component
const gameStore = useGameStore()
const actions = useGameActions()

// Join game on mount
onMounted(() => {
  actions.joinGame('player')
})

// Submit defensive decision
const submitDefense = () => {
  actions.submitDefensiveDecision({
    alignment: 'normal',
    infield_depth: 'double_play',
    outfield_depth: 'normal',
    hold_runners: [3],
  })
}

// Roll dice
const onRollDice = () => {
  if (gameStore.canRollDice) {
    actions.rollDice()
  }
}

// Submit outcome after reading card
const submitOutcome = (outcome: PlayOutcome) => {
  actions.submitManualOutcome(outcome, 'CF')
}

// Leave game on unmount
onUnmounted(() => {
  actions.leaveGame()
})

Validation Pattern

Every action method follows this pattern:

function someAction(...params) {
  // 1. Validate connection
  if (!validateConnection()) return

  // 2. Additional validation (e.g., canRollDice)
  if (!someCondition) {
    uiStore.showWarning('Cannot perform action')
    return
  }

  // 3. Log action
  console.log('[GameActions] Performing action')

  // 4. Emit with type safety
  socket.value!.emit('event_name', {
    game_id: currentGameId.value!,
    ...params
  })

  // 5. User feedback
  uiStore.showInfo('Action submitted...', 3000)
}

Integration Example

Complete example showing how composables work together:

<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import { useWebSocket } from '~/composables/useWebSocket'
import { useGameActions } from '~/composables/useGameActions'
import { useGameStore } from '~/store/game'
import { useAuthStore } from '~/store/auth'

const route = useRoute()
const gameId = route.params.id as string

const { isConnected, connect } = useWebSocket()
const actions = useGameActions(gameId)
const gameStore = useGameStore()
const authStore = useAuthStore()

// Connect and join game on mount
onMounted(() => {
  if (authStore.isAuthenticated) {
    connect()
  }

  // Wait for connection, then join
  watch(isConnected, (connected) => {
    if (connected) {
      actions.joinGame('player')
    }
  }, { immediate: true })
})

// Leave game on unmount
onUnmounted(() => {
  actions.leaveGame()
})

// Game actions
const rollDice = () => actions.rollDice()
const submitDefense = (decision) => actions.submitDefensiveDecision(decision)
const pinchHit = () => actions.requestPinchHitter(10, 25, 1)
</script>

<template>
  <div>
    <div v-if="!isConnected">
      Connecting to game server...
    </div>

    <div v-else-if="gameStore.needsDefensiveDecision">
      <!-- Defensive decision UI -->
      <button @click="submitDefense(...)">Submit Defense</button>
    </div>

    <div v-else-if="gameStore.canRollDice">
      <button @click="rollDice">Roll Dice</button>
    </div>

    <div v-else-if="gameStore.canSubmitOutcome">
      <!-- Outcome selection UI -->
    </div>
  </div>
</template>

Why These Composables Are Shared

Both useWebSocket and useGameActions are 100% league-agnostic:

No SBA-specific logic

  • Use generic GameState type
  • Work with any PlayOutcome
  • Handle any substitution type

No PD-specific logic

  • No scouting data assumptions
  • No advanced ratings logic
  • Pure communication layer

Configuration-driven

  • WS URL from runtime config
  • Game ID from parameter or store
  • League ID in game state

Future PD Frontend: Will use identical composables with zero changes.


Testing Checklist

  • Connection established with valid JWT
  • Auto-reconnection on disconnect (test with network throttle)
  • Exponential backoff working (check console logs)
  • All 15+ events handled correctly
  • Store updates on state events
  • Toast notifications shown
  • Heartbeat sent every 30s
  • Type errors caught at compile time
  • Actions emit correct events
  • Validation prevents invalid emits
  • Error messages user-friendly
  • Cleanup on unmount
  • Works in both SBA and PD contexts

Status: Complete and production-ready Lines of Code: 700 lines (420 + 280) Test Coverage: Pending (Phase F9) Shared Between Leagues: Yes - 100% reusable