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>
41 KiB
SBA Frontend - Long-Term Vision & Implementation Roadmap
Project: Paper Dynasty Real-Time Game Engine - SBA Frontend Framework: Vue 3 + Nuxt 3 + TypeScript Status: Planning Phase Created: 2025-01-10 Backend Status: Phase 3 Complete (730/731 tests passing, all WebSocket handlers implemented)
Executive Summary
The SBA (Strat-O-Matic Baseball Association) frontend is a mobile-first, real-time web application that connects to our production-ready FastAPI backend via WebSockets and REST APIs. With the backend fully operational (15 WebSocket event handlers, complete game engine, 99.9% test coverage), we're ready to build the user-facing interface that brings Paper Dynasty baseball to life.
Key Objectives:
- Replace legacy Google Sheets system with modern web UI
- Deliver seamless real-time multiplayer baseball gameplay
- Prioritize mobile experience (60%+ target usage)
- Support live, async, and AI game modes
- Enable spectator viewing of active games
- Achieve sub-500ms action latency
Success Metrics:
- 90% player migration within 1 month of launch
- 60%+ games played on mobile devices
- < 500ms average action latency
- System usability scale (SUS) score > 75
- Zero critical bugs requiring rollback
Current State Assessment
✅ What We Have
Backend (Production-Ready):
- Complete game engine with all strategic decisions
- 15 WebSocket event handlers (defensive, offensive, substitutions)
- Manual outcome workflow (dice rolling + card reading)
- Position ratings integration (PD league)
- Box score statistics (materialized views)
- 730/731 tests passing (99.9% coverage)
- PostgreSQL persistence with async operations
- Discord OAuth integration ready
Frontend (Basic Skeleton):
- Nuxt 3 project initialized
- Dependencies installed (Pinia, Tailwind, Socket.io, Axios)
- Basic configuration (runtime config, TypeScript strict mode)
- Tailwind CSS configured
- Development environment ready
🔲 What We Need to Build
Core Application (Phases F1-F4):
- Authentication flow (Discord OAuth)
- WebSocket connection management
- Game state synchronization
- Real-time game interface
- Decision input workflows
- Mobile-optimized UI components
- Type definitions matching backend models
Advanced Features (Phases F5-F6):
- Spectator mode
- Game history and replay
- Polish and animations
- Performance optimization
Technical Architecture
Frontend Stack
SBA Frontend (Vue 3 + Nuxt 3)
├── Runtime Environment
│ ├── Nuxt 3 (latest) - Meta framework with SSR/SPA
│ ├── Vue 3 Composition API - Reactive UI layer
│ └── TypeScript (strict mode) - Type safety
│
├── State Management
│ ├── Pinia - Application state stores
│ ├── Composables - Reusable logic hooks
│ └── WebSocket sync - Real-time state updates
│
├── Communication Layer
│ ├── Socket.io-client - WebSocket for real-time events
│ ├── Axios - REST API calls (game creation, history)
│ └── Discord OAuth - Authentication flow
│
├── UI/UX Layer
│ ├── Tailwind CSS - Utility-first styling
│ ├── Mobile-first design - Portrait optimization
│ ├── Touch gestures - Swipe navigation
│ └── Responsive breakpoints - xs/sm/md/lg/xl
│
└── Development Tools
├── Vue DevTools - Component inspection
├── Nuxt DevTools - Framework debugging
├── ESLint + Prettier - Code quality
└── TypeScript compiler - Type checking
Data Flow Architecture
┌─────────────────────────────────────────────────────────────┐
│ SBA Frontend (Nuxt 3) │
│ │
│ ┌────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ Pages │───▶│ Composables │───▶│ Pinia Stores │ │
│ │ (Routes) │ │ (Logic) │ │ (State) │ │
│ └────────────┘ └──────────────┘ └───────────────┘ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────┐ │ │
│ └─────────▶│ Components │◀───────────┘ │
│ │ (UI/UX) │ │
│ └──────────────┘ │
│ │ │
└───────────────────────────┼─────────────────────────────────┘
│
┌───────────┴───────────┐
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ WebSocket API │ │ REST API │
│ (Socket.io) │ │ (FastAPI) │
└──────────────────┘ └──────────────────┘
│ │
└───────────┬───────────┘
│
▼
┌──────────────────────────────┐
│ Game Backend (FastAPI) │
│ │
│ • Game Engine (in-memory) │
│ • PostgreSQL (persistence) │
│ • WebSocket Handlers (15) │
│ • Discord OAuth │
└──────────────────────────────┘
WebSocket Event Flow
Client → Server (Actions):
// Defensive decisions
socket.emit('set_defense', { game_id, positioning })
socket.emit('set_pitcher_focus', { game_id, focus })
// Offensive decisions
socket.emit('set_stolen_base_attempt', { game_id, runner, attempt })
socket.emit('set_offensive_approach', { game_id, approach })
// Manual workflow
socket.emit('request_manual_roll', { game_id })
socket.emit('submit_manual_outcome', { game_id, outcome_data })
// Substitutions
socket.emit('substitute_player', { game_id, ...sub_data })
socket.emit('change_pitcher', { game_id, new_pitcher_id })
Server → Client (Updates):
// State updates
socket.on('game_state_update', (state: GameState) => {})
socket.on('decision_required', (decision: DecisionPrompt) => {})
socket.on('play_completed', (play: PlayResult) => {})
// Manual workflow
socket.on('manual_roll_ready', (roll_data: RollData) => {})
socket.on('awaiting_outcome_input', (prompt: OutcomePrompt) => {})
// Game events
socket.on('inning_change', (inning: number, half: string) => {})
socket.on('game_ended', (result: GameResult) => {})
// Errors
socket.on('invalid_action', (error: ActionError) => {})
socket.on('connection_error', (error: ConnectionError) => {})
Implementation Phases
Phase F1: Core Infrastructure (Week 1)
Goal: Establish foundation for all future development
Deliverables:
- Project structure (pages, components, composables, stores)
- TypeScript type definitions (game models, API responses)
- Discord OAuth authentication flow
- WebSocket connection management composable
- REST API client utility (Axios wrapper)
- Pinia stores (auth, game, ui)
- Basic routing (home, login, game view)
- Environment configuration (.env setup)
Key Files:
types/
├── game.ts # GameState, PlayResult, etc.
├── player.ts # SbaPlayer, Lineup
├── api.ts # API request/response types
└── websocket.ts # Socket event types
composables/
├── useAuth.ts # Auth state & Discord OAuth
├── useWebSocket.ts # Socket connection management
├── useGameState.ts # Game state synchronization
└── useApi.ts # REST API calls
store/
├── auth.ts # Authentication state
├── game.ts # Active game state
└── ui.ts # UI state (modals, toasts)
pages/
├── index.vue # Landing/dashboard
├── auth/
│ ├── login.vue # Login page
│ └── callback.vue # OAuth callback handler
└── games/
└── [id].vue # Game view (placeholder)
Acceptance Criteria:
- User can log in via Discord OAuth
- WebSocket connects successfully to backend
- TypeScript compilation succeeds with no errors
- All stores properly typed and functional
- Environment variables loaded correctly
- Unit tests written and passing (20+ tests for composables and stores)
Phase F2: Game State Display (Week 2)
Goal: Display real-time game state with proper UI components
Deliverables:
- Game board component (baseball diamond)
- Scoreboard component (score, inning, count)
- Current situation display (batter, pitcher, runners)
- Play-by-play feed component
- Game state synchronization logic
- Real-time WebSocket updates
- Mobile-responsive layout
- Loading and error states
- Unit tests for all components and state sync logic
Key Components:
components/Game/
├── GameBoard.vue # Baseball diamond visualization
├── ScoreBoard.vue # Score + inning + count
├── CurrentBatter.vue # Batter card display
├── CurrentPitcher.vue # Pitcher card display
├── RunnersDisplay.vue # Base runners visual
├── PlayByPlay.vue # Play history feed
└── GameStatus.vue # Game status indicator
components/UI/
├── PlayerCard.vue # Simple SBA player card
├── LoadingSpinner.vue # Loading state
└── ErrorAlert.vue # Error display
Acceptance Criteria:
- Game state displays correctly from backend
- WebSocket updates reflected in real-time (< 200ms)
- Baseball diamond shows runners accurately
- Play-by-play updates smoothly
- Responsive on mobile (portrait mode)
- Handles connection loss gracefully
- Unit tests passing for all components (10+ tests)
Phase F3: Decision Input Workflow (Week 3)
Goal: Enable players to make strategic decisions during gameplay
Deliverables:
- Defensive positioning input
- Pitcher focus input
- Stolen base attempt inputs
- Offensive approach selection
- Decision validation logic
- Turn indicator (whose turn)
- Decision timeout warnings
- Mobile-friendly input forms
- Unit tests for decision components and validation logic
Key Components:
components/Decisions/
├── DefensiveSetup.vue # Positioning selection
├── PitcherFocusInput.vue # Pitcher focus options
├── StolenBaseInputs.vue # Per-runner SB attempts
├── OffensiveApproach.vue # Swing/bunt/hit-and-run
├── DecisionPanel.vue # Container for decision UI
└── TurnIndicator.vue # Whose turn indicator
components/UI/
├── ActionButton.vue # Primary action button
├── ButtonGroup.vue # Radio-style button group
└── DecisionTimer.vue # Countdown for decisions
WebSocket Integration:
// composables/useGameActions.ts
export const useGameActions = () => {
const { socket } = useWebSocket()
const setDefense = async (positioning: string) => {
socket.emit('set_defense', {
game_id: gameId.value,
positioning
})
}
const setOffensiveApproach = async (approach: string) => {
socket.emit('set_offensive_approach', {
game_id: gameId.value,
approach
})
}
// ... other actions
}
Acceptance Criteria:
- All decision inputs emit correct WebSocket events
- Backend validates and accepts decisions
- Mobile touch targets are 44x44px minimum
- Decision UI shows when it's player's turn
- Clear feedback when waiting for opponent
- Timeout warnings display appropriately
- Unit tests passing for decision validation and components (8+ tests)
Phase F4: Manual Outcome Workflow (Week 4)
Goal: Implement dice rolling and outcome reading interface
Deliverables:
- Dice roll button and animation
- Roll result display
- Outcome input form (hit type, bases, runs)
- Play result submission
- Dice roll history display
- Manual workflow state management
- Error handling for invalid outcomes
- Mobile-optimized outcome input
- Unit tests for manual workflow and outcome validation
Key Components:
components/Manual/
├── DiceRollButton.vue # Request roll button
├── DiceRollDisplay.vue # Roll result with animation
├── OutcomeInputForm.vue # Manual outcome entry
├── OutcomePreview.vue # Preview before submit
└── RollHistory.vue # Recent rolls display
components/UI/
├── DiceAnimation.vue # Animated dice roll
├── NumberInput.vue # Numeric input (bases, runs)
└── OutcomeSelect.vue # Outcome type dropdown
WebSocket Flow:
// 1. Request dice roll
socket.emit('request_manual_roll', { game_id })
// 2. Receive roll result
socket.on('manual_roll_ready', (data) => {
// Display: d6_1=3, d6_2a=4, d6_2b=2, d20_chaos=15, d20_resolution=8
showRollResult(data)
})
// 3. Await outcome input
socket.on('awaiting_outcome_input', (prompt) => {
// Show form: hit type, bases advanced, runs scored
showOutcomeForm(prompt)
})
// 4. Submit outcome
socket.emit('submit_manual_outcome', {
game_id,
outcome_data: {
hit_type: 'SINGLE_1',
bases_advanced: {...},
runs_scored: 1,
outs_recorded: 0
}
})
// 5. Play completed
socket.on('play_completed', (result) => {
// Update game state, show result
updateGameState(result)
})
Acceptance Criteria:
- Dice roll request triggers backend roll generation
- Roll results display with clear visual feedback
- Outcome form validates input before submission
- Play result reflects in game state immediately
- Mobile form input is intuitive (large touch targets)
- Error messages clear when invalid outcome submitted
- Unit tests passing for workflow and validation (8+ tests)
Phase F5: Substitutions & Advanced Actions (Week 5)
Goal: Enable player substitutions and pitching changes
Deliverables:
- Pinch hitter substitution UI
- Pinch runner substitution UI
- Defensive replacement UI
- Pitching change interface
- Bench/bullpen display
- Substitution validation
- Lineup position display
- Player availability indicators
- Unit tests for substitution logic and components
Key Components:
components/Substitutions/
├── SubstitutionModal.vue # Main substitution interface
├── PinchHitterSelect.vue # Select pinch hitter
├── PinchRunnerSelect.vue # Select pinch runner
├── DefensiveReplacementSelect.vue # Defensive sub
├── PitchingChangeModal.vue # Bullpen selection
├── BenchDisplay.vue # Available bench players
├── BullpenDisplay.vue # Available pitchers
└── LineupDisplay.vue # Current lineup status
components/UI/
├── PlayerList.vue # Scrollable player list
├── PlayerAvailability.vue # Availability indicator
└── Modal.vue # Reusable modal component
WebSocket Integration:
// Pinch hitter
socket.emit('substitute_player', {
game_id,
substitution_type: 'pinch_hitter',
lineup_spot_id: 123,
new_player_id: 456,
batting_order_position: 3
})
// Pitching change
socket.emit('change_pitcher', {
game_id,
new_pitcher_lineup_id: 789
})
// Defensive replacement
socket.emit('substitute_player', {
game_id,
substitution_type: 'defensive_replacement',
lineup_spot_id: 234,
new_player_id: 567,
position: '2B'
})
Acceptance Criteria:
- Substitution modals display available players
- Backend validates substitution eligibility
- Lineup updates reflect substitution immediately
- Substituted players marked as inactive
- Mobile interface allows easy player selection
- Clear indication of player availability
- Unit tests passing for substitution validation (8+ tests)
Phase F6: Game Management & History (Week 6)
Goal: Complete game lifecycle from creation to history
Deliverables:
- Game creation form
- Game lobby/pre-game interface
- Active games list
- Completed games history
- Game detail view (spectator-style)
- Play-by-play history export
- Game search and filtering
- Mobile-optimized lists
- Unit tests for game management and API integration
Key Pages:
pages/
├── games/
│ ├── create.vue # Create new game form
│ ├── lobby/[id].vue # Pre-game lobby
│ ├── active.vue # List of active games
│ ├── history.vue # Completed games list
│ └── [id]/
│ ├── index.vue # Live game view
│ ├── history.vue # Play-by-play history
│ └── stats.vue # Box score stats
└── spectate/
└── [id].vue # Spectator view
Key Components:
components/GameManagement/
├── GameCreationForm.vue # Create game form
├── GameLobby.vue # Pre-game interface
├── GamesList.vue # List of games
├── GameCard.vue # Game summary card
├── GameFilters.vue # Filter controls
└── PlayByPlayHistory.vue # Full play history
components/Stats/
├── BoxScore.vue # Full box score display
├── BattingStats.vue # Batting statistics table
├── PitchingStats.vue # Pitching statistics table
└── GameSummary.vue # Game summary card
REST API Integration:
// composables/useGameApi.ts
export const useGameApi = () => {
const api = useApi()
const createGame = async (gameData: CreateGameRequest) => {
return await api.post('/api/games', gameData)
}
const getActiveGames = async () => {
return await api.get('/api/games/active')
}
const getGameHistory = async (filters?: GameFilters) => {
return await api.get('/api/games/completed', { params: filters })
}
const getBoxScore = async (gameId: string) => {
return await api.get(`/api/games/${gameId}/box_score`)
}
}
Acceptance Criteria:
- Users can create new games with configuration
- Game lobby shows both lineups before start
- Active games list updates in real-time
- Game history loads with pagination
- Box scores display correctly after games
- Mobile lists scroll smoothly
- Search and filter work correctly
- Unit tests passing for game API and components (10+ tests)
Phase F7: Spectator Mode (Week 7)
Goal: Enable real-time spectating of active games
Deliverables:
- Spectator view (read-only game interface)
- Spectator WebSocket connection
- Spectator count display
- Join spectator mode from games list
- Spectator-specific UI elements
- Real-time updates for spectators
- Mobile-optimized spectator view
- Unit tests for spectator mode logic
Key Features:
pages/spectate/
└── [id].vue # Spectator game view
components/Spectator/
├── SpectatorGameView.vue # Read-only game display
├── SpectatorIndicator.vue # "Spectating" badge
├── SpectatorCount.vue # Number of spectators
└── SpectatorControls.vue # Leave, fullscreen, etc.
WebSocket Integration:
// Join as spectator
socket.emit('join_game', {
game_id,
role: 'spectator'
})
// Receive same updates as players
socket.on('game_state_update', (state) => {})
socket.on('play_completed', (play) => {})
socket.on('inning_change', (inning) => {})
// Spectator-specific events
socket.on('spectator_joined', (count) => {})
socket.on('spectator_left', (count) => {})
Acceptance Criteria:
- Spectators can view active games in real-time
- No decision inputs shown to spectators
- Spectator count visible to players (optional)
- Updates arrive with same latency as players
- Mobile spectator view clear and readable
- Can spectate multiple games (browser tabs)
- Unit tests passing for spectator permissions (5+ tests)
Phase F8: Polish & Animation (Week 8)
Goal: Enhance UX with animations, transitions, and polish
Deliverables:
- Dice roll animation (3D or 2D)
- Play result animations
- Page transitions
- Loading skeletons
- Toast notifications
- Error state illustrations
- Confetti for home runs (optional)
- Sound effects (optional)
- Unit tests for animation triggers and timing
Key Components:
components/Animations/
├── DiceRoll3D.vue # 3D dice animation
├── PlayResultAnimation.vue # Hit/out animations
├── RunnerMovement.vue # Animated base running
├── ScoreIncrement.vue # Score change animation
└── Confetti.vue # Home run celebration
components/UI/
├── SkeletonLoader.vue # Loading skeleton
├── Toast.vue # Toast notification
├── Transition.vue # Page transition wrapper
└── EmptyState.vue # Empty state illustration
Animation Libraries (potential):
// Consideration: Add animation libraries
// - @vueuse/motion - Vue animation utilities
// - gsap - Advanced animations (dice roll)
// - lottie-vue - Lottie animations
Acceptance Criteria:
- Dice roll feels satisfying and realistic
- Play results animate smoothly
- Transitions enhance UX without delay
- Loading states prevent confusion
- Toasts provide clear feedback
- Mobile animations perform well (60fps)
- Unit tests passing for animation logic (5+ tests)
Phase F9: Performance Optimization (Week 9)
Goal: Optimize for fast load times and smooth interactions
Deliverables:
- Code splitting optimization
- Lazy loading for heavy components
- Image optimization (player cards)
- Bundle size analysis and reduction
- Lighthouse performance audit
- WebSocket message throttling
- State update debouncing
- Memory leak prevention
- E2E tests with Playwright (full user flows)
- Performance benchmarks and automated audits
Optimization Tasks:
// 1. Lazy load heavy components
const GameBoard = defineAsyncComponent(() =>
import('~/components/Game/GameBoard.vue')
)
// 2. Throttle WebSocket updates
const throttledUpdate = useThrottle((state) => {
gameStore.setGameState(state)
}, 100)
// 3. Memoize expensive computations
const scoreDiff = computed(() => {
return gameState.value.home_score - gameState.value.away_score
})
// 4. Virtual scrolling for long lists
<VirtualList :items="playHistory" :item-height="60">
<template #default="{ item }">
<PlayItem :play="item" />
</template>
</VirtualList>
Performance Targets:
- Initial page load: < 3 seconds on 3G
- Time to interactive: < 5 seconds
- Lighthouse score: > 90
- Bundle size: < 500KB (gzipped)
- WebSocket latency: < 200ms
- Action response: < 500ms
- Memory usage: < 100MB sustained
Acceptance Criteria:
- Lighthouse audit passes all core metrics
- Bundle size reduced by 30%+ from baseline
- No memory leaks detected
- Smooth scrolling on mobile (60fps)
- WebSocket updates don't block UI
- E2E tests passing for critical user flows (10+ tests)
- Performance benchmarks meet or exceed targets
Mobile-First Design Principles
Layout Strategy
Portrait Mobile (375px - 640px):
<template>
<div class="mobile-game-view">
<!-- Sticky header: Score + inning -->
<div class="sticky top-0 z-20 bg-white shadow-lg">
<ScoreBoard compact />
</div>
<!-- Main content: Single column -->
<div class="flex flex-col gap-4 p-4">
<!-- Baseball diamond -->
<GameBoard size="compact" />
<!-- Current situation -->
<CurrentSituation mobile />
<!-- Decision panel (when active) -->
<DecisionPanel v-if="isMyTurn" mobile />
<!-- Play-by-play (collapsible) -->
<Disclosure title="Recent Plays">
<PlayByPlay :limit="5" />
</Disclosure>
</div>
<!-- Bottom navigation (optional) -->
<div class="fixed bottom-0 w-full">
<BottomNav />
</div>
</div>
</template>
Desktop (1024px+):
<template>
<div class="desktop-game-view">
<div class="grid grid-cols-2 gap-6 p-6">
<!-- Left column: Game state -->
<div class="flex flex-col gap-4">
<ScoreBoard detailed />
<GameBoard size="large" />
<CurrentSituation expanded />
</div>
<!-- Right column: Actions + history -->
<div class="flex flex-col gap-4">
<DecisionPanel v-if="isMyTurn" />
<PlayByPlay infinite-scroll />
<GameStats />
</div>
</div>
</div>
</template>
Touch Interaction Guidelines
- Minimum Touch Targets: 44x44px for all interactive elements
- Swipe Gestures:
- Swipe left/right to navigate between plays
- Pull to refresh game state
- Bottom Sheets: Use for modals on mobile (easier one-handed use)
- Haptic Feedback: Vibrate on important actions (optional)
- Avoid Hover: Don't rely on hover states for functionality
Type Safety & API Contracts
Core Type Definitions
// types/game.ts
export interface GameState {
game_id: string
league_id: string
status: 'pending' | 'active' | 'completed'
// Game situation
inning: number
half: 'top' | 'bottom'
outs: number
balls: number
strikes: number
// Score
home_score: number
away_score: number
// Runners (lineup IDs)
runners: {
first: number | null
second: number | null
third: number | null
}
// Current players
current_batter_lineup_id: number | null
current_pitcher_lineup_id: number | null
// Decisions
defensive_positioning: string | null
pitcher_focus: string | null
stolen_base_attempts: Record<string, boolean>
offensive_approach: string | null
// Teams
home_team_id: number
away_team_id: number
}
export interface Lineup {
id: number
game_id: string
team_id: number
card_id: number
position: string
batting_order: number | null
is_starter: boolean
is_active: boolean
player: SbaPlayer
}
export interface SbaPlayer {
id: number
name: string
image: string
team?: string
manager?: string
}
export interface PlayResult {
play_number: number
outcome: string
description: string
runs_scored: number
outs_recorded: number
new_state: Partial<GameState>
}
export interface DecisionPrompt {
phase: 'defense' | 'stolen_base' | 'offensive_approach' | 'manual_outcome'
role: 'home' | 'away'
timeout_seconds: number
options?: string[]
}
export interface RollData {
roll_id: string
d6_one: number
d6_two_a: number
d6_two_b: number
chaos_d20: number
resolution_d20: number
}
API Response Types
// types/api.ts
export interface CreateGameRequest {
league_id: string
home_team_id: number
away_team_id: number
game_mode: 'live' | 'async' | 'vs_ai'
visibility: 'public' | 'private'
}
export interface CreateGameResponse {
game_id: string
status: string
created_at: string
}
export interface GameListItem {
game_id: string
home_team_id: number
away_team_id: number
status: string
current_inning: number
home_score: number
away_score: number
started_at: string
}
export interface BoxScoreResponse {
game_stats: {
home_runs: number
away_runs: number
home_hits: number
away_hits: number
linescore_home: number[]
linescore_away: number[]
}
batting_stats: BattingStatLine[]
pitching_stats: PitchingStatLine[]
}
WebSocket Event Types
// types/websocket.ts
export type SocketEventMap = {
// Client → Server
'join_game': (data: { game_id: string, role: 'home' | 'away' | 'spectator' }) => void
'set_defense': (data: { game_id: string, positioning: string }) => void
'set_pitcher_focus': (data: { game_id: string, focus: string }) => void
'set_stolen_base_attempt': (data: { game_id: string, runner: string, attempt: boolean }) => void
'set_offensive_approach': (data: { game_id: string, approach: string }) => void
'request_manual_roll': (data: { game_id: string }) => void
'submit_manual_outcome': (data: { game_id: string, outcome_data: OutcomeData }) => void
// Server → Client
'game_state_update': (state: GameState) => void
'decision_required': (decision: DecisionPrompt) => void
'play_completed': (play: PlayResult) => void
'manual_roll_ready': (roll: RollData) => void
'awaiting_outcome_input': (prompt: OutcomePrompt) => void
'inning_change': (data: { inning: number, half: string }) => void
'game_ended': (result: GameResult) => void
'invalid_action': (error: ActionError) => void
'connection_error': (error: ConnectionError) => void
}
Testing Strategy
Philosophy: Unit tests are written in each phase alongside feature development, not deferred to Phase F9. This ensures immediate feedback, prevents regressions, and maintains code quality throughout the project.
Unit Testing (Phases F1-F8)
Each phase includes comprehensive unit tests for all new code:
# Run all unit tests
npm run test
# Watch mode for development
npm run test:watch
# Coverage report
npm run test:coverage
# Target: > 80% coverage
What to Test Each Phase:
- Composables: Logic, validation, error handling, state management
- Stores: State mutations, computed properties, actions, persistence
- Components: Props, emits, rendering, user interactions
- Utilities: Pure functions, formatters, validators
Testing Tools:
- Vitest: Fast unit test runner with Vue support
- @vue/test-utils: Official Vue testing library
- happy-dom: Lightweight DOM implementation
Example Component Test:
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import ScoreBoard from '~/components/Game/ScoreBoard.vue'
describe('ScoreBoard', () => {
it('displays correct score', () => {
const wrapper = mount(ScoreBoard, {
props: {
homeScore: 5,
awayScore: 3,
inning: 7,
half: 'bottom'
}
})
expect(wrapper.text()).toContain('5')
expect(wrapper.text()).toContain('3')
expect(wrapper.text()).toContain('Bottom 7')
})
})
E2E Testing (Phase F9)
End-to-end tests validate complete user flows across the entire application:
# E2E tests with Playwright
npm run test:e2e
# Headed mode (see browser)
npm run test:e2e:headed
# Specific browser
npm run test:e2e --project=chromium
# Test scenarios:
# - Full game flow (create → play → complete)
# - WebSocket reconnection and recovery
# - Mobile responsive behavior
# - Multi-user synchronization
# - Authentication flow
Example E2E Test:
import { test, expect } from '@playwright/test'
test('can create and join game', async ({ page }) => {
// Login
await page.goto('/auth/login')
await page.click('button:has-text("Login with Discord")')
// Create game
await page.goto('/games/create')
await page.selectOption('#home-team', '123')
await page.selectOption('#away-team', '456')
await page.click('button:has-text("Create Game")')
// Verify game view loaded
await expect(page.locator('.game-board')).toBeVisible()
})
Manual Testing Checklist
Mobile Testing (Real Devices):
- iPhone 12/13/14 (Safari)
- Samsung Galaxy S21/S22 (Chrome)
- iPad (Safari)
- Various screen sizes (375px - 768px)
Desktop Testing:
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
Network Conditions:
- Fast 3G
- Slow 3G
- Offline behavior
- Connection loss/recovery
Development Guidelines
Code Quality Standards
Vue/TypeScript Best Practices:
<!-- ✅ Good: Composition API with TypeScript -->
<script setup lang="ts">
interface Props {
gameId: string
isActive: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
update: [value: string]
}>()
const localState = ref('')
const displayValue = computed(() =>
props.isActive ? 'Active' : 'Inactive'
)
</script>
<!-- ❌ Bad: Options API without types -->
<script>
export default {
props: ['gameId', 'isActive'],
data() {
return { localState: '' }
}
}
</script>
Composable Best Practices:
// ✅ Good: Returns reactive refs/computeds
export const useGameState = (gameId: Ref<string>) => {
const state = ref<GameState | null>(null)
const isLoading = ref(false)
const currentInning = computed(() =>
state.value?.inning ?? 1
)
const loadState = async () => {
isLoading.value = true
// ... fetch state
isLoading.value = false
}
return {
state: readonly(state),
isLoading: readonly(isLoading),
currentInning,
loadState
}
}
// ❌ Bad: Returns non-reactive values
export const useGameState = (gameId: string) => {
let state = null // Not reactive!
return { state }
}
Store Best Practices:
// ✅ Good: Composition API store with types
export const useGameStore = defineStore('game', () => {
const state = ref<GameState | null>(null)
const currentInning = computed(() => state.value?.inning ?? 1)
const setState = (newState: GameState) => {
state.value = newState
}
return {
state: readonly(state),
currentInning,
setState
}
})
// ❌ Bad: Options API store without types
export const useGameStore = defineStore('game', {
state: () => ({ state: null }),
actions: {
setState(state) { this.state = state }
}
})
Git Workflow
Branch Strategy:
# Feature branches from main
git checkout -b frontend/auth-flow
git checkout -b frontend/game-board
git checkout -b frontend/decision-inputs
# Commit prefix: "FRONTEND:"
git commit -m "FRONTEND: Implement Discord OAuth login flow"
git commit -m "FRONTEND: Add GameBoard component with responsive layout"
Pull Request Template:
## Changes
- Added Discord OAuth authentication flow
- Created login page and callback handler
- Implemented auth store with Pinia
## Testing
- [x] Tested on mobile (iPhone 13)
- [x] Tested on desktop (Chrome)
- [x] TypeScript compilation passes
- [x] Component tests pass
## Screenshots
[Mobile login screen]
[Desktop login screen]
Deployment & DevOps
Build Process
# Production build
npm run build
# Output: .output/ directory
# Contains:
# - Server files (Nitro)
# - Static assets
# - Pre-rendered pages (if any)
Environment Variables
# .env.production
NUXT_PUBLIC_API_URL=https://api.paperdynasty.com
NUXT_PUBLIC_WS_URL=https://api.paperdynasty.com
NUXT_PUBLIC_DISCORD_CLIENT_ID=<production-client-id>
NUXT_PUBLIC_DISCORD_REDIRECT_URI=https://sba.paperdynasty.com/auth/callback
Hosting Options
Option 1: VPS with Docker
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]
Option 2: Vercel/Netlify
- Zero-config deployment
- Automatic HTTPS
- CDN edge caching
- Preview deployments for PRs
Recommendation: Start with Vercel for simplicity, migrate to VPS if needed for control.
Success Metrics & KPIs
Launch Success (First Month)
| Metric | Target | Measurement |
|---|---|---|
| Player Migration | 90% | Users active on web vs Sheets |
| Mobile Usage | 60%+ | Mobile vs desktop sessions |
| Action Latency | < 500ms | p95 WebSocket response time |
| Page Load Time | < 3s | Lighthouse audit on 3G |
| System Uptime | 99.5% | Uptime monitoring |
| Critical Bugs | 0 | Zero rollback-required bugs |
User Experience (Ongoing)
| Metric | Target | Measurement |
|---|---|---|
| SUS Score | > 75 | User survey (10 questions) |
| Net Promoter Score | > 50 | "How likely to recommend?" |
| Game Completion Rate | > 90% | Games completed vs started |
| Average Session | 30-60 min | Analytics tracking |
| Support Tickets | < 5/week | Support system |
Technical Performance (Ongoing)
| Metric | Target | Measurement |
|---|---|---|
| Lighthouse Score | > 90 | Automated audit |
| Bundle Size | < 500KB | Build analysis |
| WebSocket Latency | < 200ms | Network monitoring |
| Memory Usage | < 100MB | Browser DevTools |
| Error Rate | < 0.1% | Error tracking (Sentry) |
Risk Mitigation
Technical Risks
| Risk | Mitigation |
|---|---|
| WebSocket disconnections | Auto-reconnect with exponential backoff, state recovery from backend |
| Type mismatches with backend | Generate types from backend OpenAPI spec, integration tests |
| Mobile performance issues | Performance budget, bundle size limits, lazy loading |
| Browser compatibility | Polyfills, feature detection, graceful degradation |
| State desynchronization | Periodic state refresh, conflict resolution |
User Experience Risks
| Risk | Mitigation |
|---|---|
| Confusing UI for Sheets users | Onboarding tutorial, help documentation, familiar terminology |
| Mobile input difficulties | Large touch targets, mobile-first design, user testing |
| Slow adoption | Beta testing period, migration guide, user training sessions |
| Connection issues misunderstood | Clear connection status indicator, helpful error messages |
Next Steps
Immediate Actions (This Session)
-
Review Backend WebSocket Specifications
- Read
backend/app/websocket/handlers.py - Review event payloads and responses
- Understand decision workflow sequences
- Read
-
Create Core Type Definitions
- Start with
types/game.ts - Match backend Pydantic models exactly
- Add JSDoc comments for clarity
- Start with
-
Set Up Basic Auth Flow
- Create Discord OAuth configuration
- Implement login page
- Test callback handling
-
Initialize Pinia Stores
- Auth store with Discord user
- UI store for modals/toasts
- Game store skeleton
-
Create First Component
- Simple ScoreBoard component
- Test with mock data
- Ensure TypeScript types work
Week 1 Priorities
- Complete Phase F1 (Core Infrastructure)
- WebSocket connection working
- Type system fully defined
- Auth flow functional
- Basic routing in place
Long-Term Milestones
- Week 2: Phase F2 complete (Game state display working)
- Week 3: Phase F3 complete (Decision inputs functional)
- Week 4: Phase F4 complete (Manual workflow end-to-end)
- Week 5: Phase F5 complete (Substitutions working)
- Week 6: Phase F6 complete (Full game lifecycle)
- Week 7: Phase F7 complete (Spectator mode)
- Week 8: Phase F8 complete (Polish & animations)
- Week 9: Phase F9 complete (Performance optimized)
Target Launch: 9-10 weeks from start (Beta), +2 weeks for production hardening
Appendix
Useful Resources
Nuxt 3:
- Documentation: https://nuxt.com/docs
- Examples: https://nuxt.com/docs/examples
- Modules: https://nuxt.com/modules
Vue 3:
- Composition API: https://vuejs.org/guide/extras/composition-api-faq.html
- TypeScript: https://vuejs.org/guide/typescript/overview.html
- Best Practices: https://vuejs.org/style-guide/
Socket.io:
- Client docs: https://socket.io/docs/v4/client-api/
- TypeScript: https://socket.io/docs/v4/typescript/
Tailwind CSS:
- Documentation: https://tailwindcss.com/docs
- Customization: https://tailwindcss.com/docs/configuration
Backend API Reference
REST Endpoints:
POST /api/games # Create new game
GET /api/games/:id # Get game details
GET /api/games/active # List active games
GET /api/games/completed # List completed games
GET /api/games/:id/box_score # Get box score
WebSocket Events (Full list in backend docs):
- Connection:
join_game,leave_game - Decisions:
set_defense,set_stolen_base_attempt,set_offensive_approach - Manual:
request_manual_roll,submit_manual_outcome - Substitutions:
substitute_player,change_pitcher
Document Version: 1.0 Last Updated: 2025-01-10 Next Review: After Phase F1 completion