CLAUDE: Add submitSubstitution unified wrapper for all substitution types

- Add submitSubstitution() method that routes to appropriate internal method
- Remove individual substitution methods from exports (now internal only)
- Add validation for defensive_replacement requiring position parameter
- Update tests to use unified wrapper, add test for validation error

Fixes CRIT-001: Game page called non-existent submitSubstitution method

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-01-13 20:00:22 -06:00
parent e0c12467b0
commit f2ce91a239
3 changed files with 896 additions and 9 deletions

View File

@ -0,0 +1,851 @@
{
"meta": {
"version": "1.0.0",
"created": "2025-12-26",
"lastUpdated": "2026-01-13",
"totalEstimatedHours": 52.75,
"criticalBlockersHours": 6.5,
"totalTasks": 47,
"completedTasks": 1
},
"categories": {
"critical": "Must fix before any production use",
"high": "Required for production readiness",
"medium": "Quality and maintainability improvements",
"low": "Polish and nice-to-have improvements",
"feature": "Recommended new features"
},
"tasks": [
{
"id": "CRIT-001",
"name": "Add submitSubstitution method",
"description": "Game page calls actions.submitSubstitution() but the method doesn't exist in useGameActions.ts composable. This causes runtime errors when users attempt to make any substitutions.",
"category": "critical",
"priority": 1,
"completed": true,
"tested": true,
"dependencies": [],
"files": [
{
"path": "pages/games/[id].vue",
"lines": [596, 611, 628],
"issue": "Calls non-existent submitSubstitution method"
},
{
"path": "composables/useGameActions.ts",
"lines": null,
"issue": "Missing submitSubstitution wrapper method"
}
],
"suggestedFix": "Add a submitSubstitution method in useGameActions.ts that routes to the appropriate substitution type (pinchHit, pinchRun, defensiveReplacement) based on the substitution context parameter",
"estimatedHours": 0.5,
"notes": "Quick win - straightforward wrapper method implementation"
},
{
"id": "CRIT-002",
"name": "Implement myTeamId team ownership logic",
"description": "myTeamId is hardcoded to null with a TODO comment, breaking the substitution panel's ability to determine which team's lineup to display. Users cannot see or manage their own team's substitutions.",
"category": "critical",
"priority": 2,
"completed": false,
"tested": false,
"dependencies": ["CRIT-002-BACKEND"],
"files": [
{
"path": "pages/games/[id].vue",
"lines": [406],
"issue": "const myTeamId = ref<number | null>(null) // TODO"
}
],
"suggestedFix": "1) Add team_ownership field to /api/auth/me response on backend. 2) Compare authenticated user's team(s) with game.away_team_id and game.home_team_id. 3) Set myTeamId based on ownership, or null for spectators.",
"estimatedHours": 2,
"notes": "Requires backend coordination - need to expose team ownership in auth endpoint. Consider caching team ownership in Pinia auth store."
},
{
"id": "CRIT-002-BACKEND",
"name": "Add team ownership to auth endpoint",
"description": "Backend dependency for CRIT-002. The /api/auth/me endpoint needs to return the user's team ownership information so frontend can determine if user owns home/away team.",
"category": "critical",
"priority": 2,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "../backend/app/api/auth.py",
"lines": null,
"issue": "Missing team_ids or team_ownership in response"
}
],
"suggestedFix": "Add team_ids: List[int] field to the /api/auth/me response model, populated from user's team ownership records",
"estimatedHours": 1,
"notes": "Backend task - coordinate with frontend CRIT-002"
},
{
"id": "HIGH-001",
"name": "Create error boundary for WebSocket failures",
"description": "No recovery UI exists when WebSocket connection permanently fails. Users get stuck with no way to recover except refreshing the page manually.",
"category": "high",
"priority": 3,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "components/ErrorBoundary.vue",
"lines": null,
"issue": "Component does not exist"
},
{
"path": "pages/games/[id].vue",
"lines": null,
"issue": "No error boundary wrapper"
}
],
"suggestedFix": "1) Create ErrorBoundary.vue component with slots for error state. 2) Display user-friendly message with 'Reconnect' and 'Refresh Page' buttons. 3) Wrap game page content in error boundary. 4) Expose WebSocket permanent failure state from composable.",
"estimatedHours": 3,
"notes": "Critical for production UX. Consider adding error reporting to backend for monitoring."
},
{
"id": "HIGH-002",
"name": "Fix infinite reconnection after max attempts",
"description": "After 10 WebSocket reconnection attempts fail, the stuck state detector continues retrying forever, creating an infinite loop of failed reconnections.",
"category": "high",
"priority": 4,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "composables/useWebSocket.ts",
"lines": [308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701],
"issue": "Stuck state detector ignores maxReconnectAttempts"
}
],
"suggestedFix": "1) Add permanentlyFailed state flag. 2) Set flag when reconnectAttempts >= maxReconnectAttempts. 3) Disable stuck state detector polling when permanently failed. 4) Expose flag to UI for error boundary display. 5) Add manual retry method that resets state.",
"estimatedHours": 1,
"notes": "Works in conjunction with HIGH-001 error boundary"
},
{
"id": "MED-001",
"name": "Simplify SSR client checks in WebSocket composable",
"description": "13 separate import.meta.client checks throughout useWebSocket.ts create code complexity and potential for inconsistent behavior. Pattern is error-prone and hard to maintain.",
"category": "medium",
"priority": 5,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "composables/useWebSocket.ts",
"lines": [89, 156, 203, 267, 312, 398, 445, 512, 589, 623, 667, 702, 745],
"issue": "Scattered import.meta.client checks"
}
],
"suggestedFix": "1) Create isClient constant at top of composable. 2) Use early return pattern at function entry points. 3) Consider extracting client-only logic to separate module. 4) Add SSR-safe stub implementations that no-op gracefully.",
"estimatedHours": 3,
"notes": "Reduces cognitive load and bug surface area. Test thoroughly after changes."
},
{
"id": "MED-002",
"name": "Add ARIA labels to interactive elements",
"description": "Approximately 90% of buttons, inputs, and interactive elements lack proper ARIA labels, making the app inaccessible to screen reader users.",
"category": "medium",
"priority": 6,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "components/game/ActionPanel.vue",
"lines": null,
"issue": "Buttons missing aria-label"
},
{
"path": "components/game/SubstitutionPanel.vue",
"lines": null,
"issue": "Form controls missing labels"
},
{
"path": "components/game/ScoreboardHeader.vue",
"lines": null,
"issue": "Interactive elements unlabeled"
},
{
"path": "components/game/LineupCard.vue",
"lines": null,
"issue": "Click targets missing labels"
},
{
"path": "pages/games/[id].vue",
"lines": null,
"issue": "Page-level buttons unlabeled"
}
],
"suggestedFix": "1) Audit all button, input, and clickable elements. 2) Add aria-label or aria-labelledby to each. 3) Ensure form inputs have associated labels. 4) Add aria-live regions for dynamic content updates. 5) Test with screen reader.",
"estimatedHours": 4,
"notes": "Important for accessibility compliance. Consider creating a shared button component with built-in accessibility."
},
{
"id": "MED-003",
"name": "Optimize stuck state polling interval",
"description": "Stuck state detector polls every 5 seconds regardless of connection state, wasting resources when connection is healthy.",
"category": "medium",
"priority": 7,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "composables/useWebSocket.ts",
"lines": [685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701],
"issue": "Fixed 5-second polling interval"
}
],
"suggestedFix": "1) Increase polling interval when connection is healthy (30s). 2) Decrease interval when issues detected (5s). 3) Disable polling entirely when permanently failed. 4) Use exponential backoff for reconnection attempts.",
"estimatedHours": 1,
"notes": "Minor performance improvement but good practice"
},
{
"id": "MED-004",
"name": "Standardize async error handling",
"description": "Inconsistent patterns for handling async errors across composables and pages. Some use try/catch, some use .catch(), some don't handle errors at all.",
"category": "medium",
"priority": 8,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "composables/useGameActions.ts",
"lines": null,
"issue": "Mixed error handling patterns"
},
{
"path": "composables/useWebSocket.ts",
"lines": null,
"issue": "Some async operations unhandled"
},
{
"path": "pages/games/[id].vue",
"lines": null,
"issue": "Inconsistent error handling"
}
],
"suggestedFix": "1) Define standard error handling pattern in CLAUDE.md or CONTRIBUTING.md. 2) Create useAsyncHandler composable for consistent try/catch/toast pattern. 3) Audit all async operations and apply standard pattern. 4) Ensure all user-facing operations show toast on error.",
"estimatedHours": 1,
"notes": "Consider creating ErrorHandler utility class"
},
{
"id": "MED-005",
"name": "Add null checks for optional properties",
"description": "Some TypeScript optional properties are accessed without null checks, risking runtime errors when data is incomplete.",
"category": "medium",
"priority": 9,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "components/game/DiamondView.vue",
"lines": null,
"issue": "gameState.runners accessed without check"
},
{
"path": "components/game/LineupCard.vue",
"lines": null,
"issue": "player properties accessed directly"
}
],
"suggestedFix": "1) Enable strictNullChecks in tsconfig if not already. 2) Use optional chaining (?.) for nested property access. 3) Add null coalescing (??) for default values. 4) Consider using computed properties with defaults for complex optional data.",
"estimatedHours": 1,
"notes": "Run tsc --noEmit to find all null safety issues"
},
{
"id": "MED-006",
"name": "Add keyboard navigation for modals",
"description": "Modal dialogs lack proper keyboard navigation - users can't Tab through elements or close with Escape key.",
"category": "medium",
"priority": 10,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "components/game/SubstitutionPanel.vue",
"lines": null,
"issue": "No keyboard trap or Escape handling"
},
{
"path": "components/game/ActionPanel.vue",
"lines": null,
"issue": "No focus management"
}
],
"suggestedFix": "1) Add @keydown.escape handler to close modals. 2) Implement focus trap to keep focus within modal. 3) Auto-focus first interactive element on open. 4) Return focus to trigger element on close. 5) Add role='dialog' and aria-modal='true'.",
"estimatedHours": 2,
"notes": "Consider using @vueuse/core useFocusTrap composable"
},
{
"id": "MED-007",
"name": "Make auto-decision timing configurable",
"description": "Auto-decision countdown timing is hardcoded. Should be configurable per-game or per-league for different play styles.",
"category": "medium",
"priority": 11,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "composables/useGameActions.ts",
"lines": null,
"issue": "Hardcoded auto-decision timeout"
},
{
"path": "stores/game.ts",
"lines": null,
"issue": "No configuration for timing"
}
],
"suggestedFix": "1) Add autoDecisionTimeout to game configuration. 2) Pass from backend as part of game settings. 3) Use configuration value instead of hardcoded constant. 4) Consider per-decision-type timeouts (quick for routine, longer for strategic).",
"estimatedHours": 1,
"notes": "May require backend changes to expose configuration"
},
{
"id": "MED-008",
"name": "Fix skipped environment-dependent tests",
"description": "122 tests are skipped due to environment/setup issues. While tests pass when run, the skip count masks potential issues.",
"category": "medium",
"priority": 12,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "tests/components/",
"lines": null,
"issue": "Multiple test files with skipped tests"
},
{
"path": "tests/composables/",
"lines": null,
"issue": "WebSocket tests require mocking"
}
],
"suggestedFix": "1) Audit each skipped test to understand why it's skipped. 2) Fix environment setup issues where possible. 3) Improve mocking for WebSocket and SSR-dependent tests. 4) Mark truly environment-specific tests with .skip.env annotation. 5) Reduce skip count to < 20.",
"estimatedHours": 8,
"notes": "Low ROI unless tests are failing in CI. Consider as stretch goal."
},
{
"id": "LOW-001",
"name": "Create centralized logging composable",
"description": "69 console.log statements scattered across 9 files. Need centralized logging with levels for production readiness.",
"category": "low",
"priority": 13,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "composables/useWebSocket.ts",
"lines": null,
"issue": "23 console.log statements"
},
{
"path": "composables/useGameActions.ts",
"lines": null,
"issue": "15 console.log statements"
},
{
"path": "pages/games/[id].vue",
"lines": null,
"issue": "12 console.log statements"
},
{
"path": "stores/game.ts",
"lines": null,
"issue": "8 console.log statements"
},
{
"path": "components/game/ActionPanel.vue",
"lines": null,
"issue": "5 console.log statements"
},
{
"path": "components/game/DiamondView.vue",
"lines": null,
"issue": "3 console.log statements"
},
{
"path": "components/game/LineupCard.vue",
"lines": null,
"issue": "2 console.log statements"
},
{
"path": "plugins/socket.client.ts",
"lines": null,
"issue": "1 console.log statement"
}
],
"suggestedFix": "1) Create composables/useLogger.ts with debug/info/warn/error methods. 2) Add LOG_LEVEL env variable (debug/info/warn/error/none). 3) Replace all console.log with appropriate logger.level() call. 4) Disable debug logs in production. 5) Consider adding log prefixes for filtering.",
"estimatedHours": 2,
"notes": "Improves debuggability while keeping production clean"
},
{
"id": "LOW-002",
"name": "Extract toast duration constant",
"description": "Toast duration 3000ms is hardcoded in 11 places. Should be a configurable constant.",
"category": "low",
"priority": 14,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "composables/useGameActions.ts",
"lines": null,
"issue": "Multiple hardcoded 3000 values"
},
{
"path": "pages/games/[id].vue",
"lines": null,
"issue": "Hardcoded toast durations"
}
],
"suggestedFix": "1) Create constants/ui.ts with TOAST_DURATION_MS = 3000. 2) Consider TOAST_DURATION_SHORT (2000) and TOAST_DURATION_LONG (5000). 3) Replace all hardcoded values with constants. 4) Consider adding to app configuration.",
"estimatedHours": 0.25,
"notes": "Quick win - simple find and replace"
},
{
"id": "LOW-003",
"name": "Extract WebSocket timeout constants",
"description": "WebSocket timeout values are hardcoded throughout the composable. Should be configurable constants.",
"category": "low",
"priority": 15,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "composables/useWebSocket.ts",
"lines": null,
"issue": "Hardcoded timeout values"
}
],
"suggestedFix": "1) Create constants/websocket.ts with all timeout values. 2) Export RECONNECT_DELAY_MS, MAX_RECONNECT_ATTEMPTS, STUCK_STATE_POLL_INTERVAL, etc. 3) Replace hardcoded values with constants. 4) Document each constant's purpose.",
"estimatedHours": 0.5,
"notes": "Improves maintainability and documentation"
},
{
"id": "LOW-004",
"name": "Remove or link demo pages",
"description": "4 demo pages exist but are not linked from the main UI. Either remove them or add a dev-only link.",
"category": "low",
"priority": 16,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "pages/demo/",
"lines": null,
"issue": "Orphaned demo pages"
}
],
"suggestedFix": "1) Review demo pages for usefulness. 2) If useful for development, add /dev route with links (behind auth). 3) If not useful, remove demo directory. 4) Consider moving demo content to Storybook if visual component testing needed.",
"estimatedHours": 0.5,
"notes": "Keep if useful for onboarding new developers"
},
{
"id": "LOW-005",
"name": "Remove unused imports in test files",
"description": "Several test files have unused imports that should be cleaned up.",
"category": "low",
"priority": 17,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "tests/",
"lines": null,
"issue": "Unused imports in various test files"
}
],
"suggestedFix": "1) Run ESLint with no-unused-vars rule. 2) Remove all flagged unused imports. 3) Consider adding lint rule to CI to prevent future occurrences.",
"estimatedHours": 0.25,
"notes": "Quick cleanup task"
},
{
"id": "LOW-006",
"name": "Add color-blind accessible status indicators",
"description": "Status indicators rely solely on color (red/yellow/green), making them inaccessible to color-blind users.",
"category": "low",
"priority": 18,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "components/game/ScoreboardHeader.vue",
"lines": null,
"issue": "Color-only connection status"
},
{
"path": "components/game/ActionPanel.vue",
"lines": null,
"issue": "Color-only action status"
}
],
"suggestedFix": "1) Add icons alongside colors (checkmark, warning, X). 2) Add text labels for screen readers. 3) Use patterns or shapes as secondary indicator. 4) Test with color blindness simulator.",
"estimatedHours": 1,
"notes": "Important for accessibility but lower priority than functional a11y"
},
{
"id": "LOW-007",
"name": "Optimize lineup request frequency",
"description": "Lineup data is re-requested on every game state update, even when lineup hasn't changed. Wastes bandwidth and processing.",
"category": "low",
"priority": 19,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "pages/games/[id].vue",
"lines": null,
"issue": "Lineup fetched on every state update"
},
{
"path": "stores/game.ts",
"lines": null,
"issue": "No lineup change detection"
}
],
"suggestedFix": "1) Add lineup_version or lineup_hash to game state. 2) Only fetch lineup when version changes. 3) Alternatively, include lineup in game state WebSocket updates. 4) Cache lineup in Pinia store with TTL.",
"estimatedHours": 1.5,
"notes": "May require backend changes to add lineup versioning"
},
{
"id": "LOW-008",
"name": "Convert deep watches to shallow where possible",
"description": "Some watchers use deep: true on gameState when only top-level properties are needed, causing unnecessary re-renders.",
"category": "low",
"priority": 20,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "pages/games/[id].vue",
"lines": null,
"issue": "Deep watches on gameState"
},
{
"path": "components/game/DiamondView.vue",
"lines": null,
"issue": "Unnecessary deep watching"
}
],
"suggestedFix": "1) Audit all watch() calls with deep: true. 2) Identify which actually need deep watching. 3) Convert others to watch specific computed properties. 4) Use watchEffect for simpler reactive dependencies.",
"estimatedHours": 1,
"notes": "Minor performance improvement"
},
{
"id": "LOW-009",
"name": "Centralize runner state logic",
"description": "Runner state (on base, advancing, etc.) logic is duplicated between DiamondView and game page.",
"category": "low",
"priority": 21,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "components/game/DiamondView.vue",
"lines": null,
"issue": "Runner logic duplicated"
},
{
"path": "pages/games/[id].vue",
"lines": null,
"issue": "Runner logic duplicated"
}
],
"suggestedFix": "1) Create composables/useRunnerState.ts. 2) Move all runner-related computed properties and methods there. 3) Export getRunnerOnBase, getRunnerStatus, formatRunnerDisplay, etc. 4) Update components to use composable.",
"estimatedHours": 0.5,
"notes": "DRY improvement"
},
{
"id": "LOW-010",
"name": "Remove custom toast in demo page",
"description": "Demo page has a custom toast implementation instead of using the app's standard toast system.",
"category": "low",
"priority": 22,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "pages/demo/toast.vue",
"lines": null,
"issue": "Custom toast implementation"
}
],
"suggestedFix": "1) Remove custom toast implementation. 2) Use standard useToast composable. 3) Or remove demo page entirely (see LOW-004).",
"estimatedHours": 0.25,
"notes": "Clean up if demo pages are kept"
},
{
"id": "LOW-011",
"name": "Add loading skeletons for async content",
"description": "Some components show blank space while loading. Should show skeleton loaders for better perceived performance.",
"category": "low",
"priority": 23,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "components/game/LineupCard.vue",
"lines": null,
"issue": "No loading state"
},
{
"path": "components/game/ScoreboardHeader.vue",
"lines": null,
"issue": "No loading state"
}
],
"suggestedFix": "1) Create SkeletonLoader.vue component with pulse animation. 2) Add loading prop to components. 3) Show skeleton when loading, content when ready. 4) Match skeleton shape to actual content layout.",
"estimatedHours": 2,
"notes": "Nice UX improvement but not critical"
},
{
"id": "LOW-012",
"name": "Add retry logic for failed API calls",
"description": "API calls fail immediately without retry. Should implement retry with exponential backoff for transient failures.",
"category": "low",
"priority": 24,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "composables/useApi.ts",
"lines": null,
"issue": "No retry logic"
}
],
"suggestedFix": "1) Add retry configuration to useApi. 2) Implement exponential backoff (1s, 2s, 4s). 3) Only retry on 5xx errors and network failures. 4) Don't retry on 4xx client errors. 5) Add maxRetries option (default 3).",
"estimatedHours": 1,
"notes": "Improves resilience to transient failures"
},
{
"id": "LOW-013",
"name": "Add request deduplication",
"description": "Same API request can be made multiple times simultaneously. Should deduplicate in-flight requests.",
"category": "low",
"priority": 25,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "composables/useApi.ts",
"lines": null,
"issue": "No request deduplication"
}
],
"suggestedFix": "1) Create request cache Map keyed by URL + params hash. 2) Return existing promise if request in flight. 3) Clear cache entry on completion. 4) Add option to bypass cache for POST/PUT/DELETE.",
"estimatedHours": 1,
"notes": "Prevents duplicate requests from rapid user actions"
},
{
"id": "LOW-014",
"name": "Improve WebSocket event type safety",
"description": "WebSocket event handlers use loose typing. Should use discriminated unions for better type safety.",
"category": "low",
"priority": 26,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "types/websocket.ts",
"lines": null,
"issue": "Loose event typing"
},
{
"path": "composables/useWebSocket.ts",
"lines": null,
"issue": "Event handlers not fully typed"
}
],
"suggestedFix": "1) Define discriminated union for all WebSocket events. 2) type GameEvent = { type: 'state_update'; payload: GameState } | { type: 'error'; payload: ErrorData } | ... 3) Use type guards in event handlers. 4) Generate types from backend schema if possible.",
"estimatedHours": 2,
"notes": "Improves developer experience and catches bugs at compile time"
},
{
"id": "LOW-015",
"name": "Add component prop validation",
"description": "Some components accept props without proper validation. Should add validators for complex props.",
"category": "low",
"priority": 27,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "components/game/",
"lines": null,
"issue": "Missing prop validators"
}
],
"suggestedFix": "1) Audit all component props. 2) Add validator functions for complex objects. 3) Use required: true where appropriate. 4) Add default values for optional props. 5) Consider using Zod schemas for runtime validation.",
"estimatedHours": 1,
"notes": "Catches integration bugs early"
},
{
"id": "LOW-016",
"name": "Add performance monitoring",
"description": "No visibility into frontend performance. Should add basic performance metrics collection.",
"category": "low",
"priority": 28,
"completed": false,
"tested": false,
"dependencies": [],
"files": [
{
"path": "plugins/performance.client.ts",
"lines": null,
"issue": "File does not exist"
}
],
"suggestedFix": "1) Create performance monitoring plugin. 2) Track page load times, WebSocket latency, API response times. 3) Use Performance API for accurate measurements. 4) Send metrics to backend or analytics service. 5) Add performance budget alerts.",
"estimatedHours": 3,
"notes": "Useful for production monitoring but not critical for launch"
},
{
"id": "FEAT-001",
"name": "Create centralized logging service",
"description": "New feature: Create a proper logging service with log levels, formatting, and production controls.",
"category": "feature",
"priority": 29,
"completed": false,
"tested": false,
"dependencies": ["LOW-001"],
"files": [
{
"path": "composables/useLogger.ts",
"lines": null,
"issue": "File does not exist"
},
{
"path": "utils/logger.ts",
"lines": null,
"issue": "File does not exist"
}
],
"suggestedFix": "1) Create Logger class with debug/info/warn/error methods. 2) Add timestamp and context prefixes. 3) Support LOG_LEVEL env var. 4) Add log grouping for related messages. 5) Create useLogger composable wrapper. 6) Add remote logging option for production errors.",
"estimatedHours": 2,
"notes": "Foundation for observability"
},
{
"id": "FEAT-002",
"name": "Integrate error tracking service",
"description": "New feature: Integrate Sentry or similar for production error tracking and monitoring.",
"category": "feature",
"priority": 30,
"completed": false,
"tested": false,
"dependencies": ["HIGH-001"],
"files": [
{
"path": "plugins/sentry.client.ts",
"lines": null,
"issue": "File does not exist"
},
{
"path": "nuxt.config.ts",
"lines": null,
"issue": "No Sentry configuration"
}
],
"suggestedFix": "1) Install @sentry/vue package. 2) Create Sentry client plugin. 3) Configure DSN and environment. 4) Add user context from auth store. 5) Capture WebSocket errors, API failures, unhandled exceptions. 6) Set up source maps upload for readable stack traces.",
"estimatedHours": 3,
"notes": "Critical for production debugging. Consider alternatives: LogRocket, Bugsnag"
}
],
"quickWins": [
{
"taskId": "CRIT-001",
"estimatedMinutes": 30,
"impact": "Fixes broken substitutions"
},
{
"taskId": "LOW-002",
"estimatedMinutes": 15,
"impact": "Code consistency"
},
{
"taskId": "LOW-005",
"estimatedMinutes": 15,
"impact": "Code cleanliness"
},
{
"taskId": "LOW-003",
"estimatedMinutes": 30,
"impact": "Code maintainability"
},
{
"taskId": "LOW-009",
"estimatedMinutes": 30,
"impact": "DRY compliance"
}
],
"productionBlockers": [
{
"taskId": "CRIT-001",
"reason": "Substitutions completely broken"
},
{
"taskId": "CRIT-002",
"reason": "Team ownership not working"
},
{
"taskId": "CRIT-002-BACKEND",
"reason": "Backend dependency for team ownership"
},
{
"taskId": "HIGH-001",
"reason": "No recovery from WebSocket failures"
},
{
"taskId": "HIGH-002",
"reason": "Infinite reconnection loop"
}
],
"weeklyRoadmap": {
"week1": {
"theme": "Critical Fixes",
"tasks": ["CRIT-001", "CRIT-002", "CRIT-002-BACKEND", "HIGH-001", "HIGH-002"],
"estimatedHours": 7.5
},
"week2": {
"theme": "Code Quality",
"tasks": ["LOW-001", "LOW-002", "LOW-003", "MED-004", "MED-005"],
"estimatedHours": 5.75
},
"week3": {
"theme": "Accessibility & UX",
"tasks": ["MED-002", "MED-006", "LOW-006", "LOW-011"],
"estimatedHours": 9
},
"week4": {
"theme": "Performance & Polish",
"tasks": ["MED-001", "MED-003", "LOW-007", "LOW-008", "LOW-012", "LOW-013"],
"estimatedHours": 8
}
}
}

View File

@ -246,6 +246,36 @@ export function useGameActions(gameId?: string) {
uiStore.showInfo('Requesting pitching change...', 3000) uiStore.showInfo('Requesting pitching change...', 3000)
} }
/**
* Submit substitution - unified wrapper for all substitution types
* Routes to the appropriate specific method based on type
*/
function submitSubstitution(
type: 'pinch_hitter' | 'defensive_replacement' | 'pitching_change',
playerOutLineupId: number,
playerInCardId: number,
teamId: number,
newPosition?: string
) {
switch (type) {
case 'pinch_hitter':
requestPinchHitter(playerOutLineupId, playerInCardId, teamId)
break
case 'defensive_replacement':
if (!newPosition) {
uiStore.showError('Position required for defensive replacement')
return
}
requestDefensiveReplacement(playerOutLineupId, playerInCardId, newPosition, teamId)
break
case 'pitching_change':
requestPitchingChange(playerOutLineupId, playerInCardId, teamId)
break
default:
uiStore.showError(`Unknown substitution type: ${type}`)
}
}
// ============================================================================ // ============================================================================
// Undo/Rollback Actions // Undo/Rollback Actions
// ============================================================================ // ============================================================================
@ -331,9 +361,7 @@ export function useGameActions(gameId?: string) {
submitManualOutcome, submitManualOutcome,
// Substitutions // Substitutions
requestPinchHitter, submitSubstitution,
requestDefensiveReplacement,
requestPitchingChange,
// Undo/Rollback // Undo/Rollback
undoLastPlay, undoLastPlay,

View File

@ -275,9 +275,9 @@ describe('useGameActions', () => {
}) })
describe('substitution actions', () => { describe('substitution actions', () => {
it('emits request_pinch_hitter with correct parameters', () => { it('submitSubstitution routes pinch_hitter to correct emit', () => {
const actions = useGameActions() const actions = useGameActions()
actions.requestPinchHitter(10, 20, 1) actions.submitSubstitution('pinch_hitter', 10, 20, 1)
expect(mockSocket.value.emit).toHaveBeenCalledWith('request_pinch_hitter', { expect(mockSocket.value.emit).toHaveBeenCalledWith('request_pinch_hitter', {
game_id: 'game-123', game_id: 'game-123',
@ -288,9 +288,9 @@ describe('useGameActions', () => {
expect(mockUiStore.showInfo).toHaveBeenCalledWith('Requesting pinch hitter...', 3000) expect(mockUiStore.showInfo).toHaveBeenCalledWith('Requesting pinch hitter...', 3000)
}) })
it('emits request_defensive_replacement with correct parameters', () => { it('submitSubstitution routes defensive_replacement to correct emit', () => {
const actions = useGameActions() const actions = useGameActions()
actions.requestDefensiveReplacement(10, 20, 'SS', 1) actions.submitSubstitution('defensive_replacement', 10, 20, 1, 'SS')
expect(mockSocket.value.emit).toHaveBeenCalledWith('request_defensive_replacement', { expect(mockSocket.value.emit).toHaveBeenCalledWith('request_defensive_replacement', {
game_id: 'game-123', game_id: 'game-123',
@ -302,9 +302,17 @@ describe('useGameActions', () => {
expect(mockUiStore.showInfo).toHaveBeenCalledWith('Requesting defensive replacement...', 3000) expect(mockUiStore.showInfo).toHaveBeenCalledWith('Requesting defensive replacement...', 3000)
}) })
it('emits request_pitching_change with correct parameters', () => { it('submitSubstitution shows error when defensive_replacement missing position', () => {
const actions = useGameActions() const actions = useGameActions()
actions.requestPitchingChange(10, 20, 1) actions.submitSubstitution('defensive_replacement', 10, 20, 1)
expect(mockSocket.value.emit).not.toHaveBeenCalled()
expect(mockUiStore.showError).toHaveBeenCalledWith('Position required for defensive replacement')
})
it('submitSubstitution routes pitching_change to correct emit', () => {
const actions = useGameActions()
actions.submitSubstitution('pitching_change', 10, 20, 1)
expect(mockSocket.value.emit).toHaveBeenCalledWith('request_pitching_change', { expect(mockSocket.value.emit).toHaveBeenCalledWith('request_pitching_change', {
game_id: 'game-123', game_id: 'game-123',