strat-gameplay-webapp/backend/app/services/stat_view_refresher.py
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

102 lines
3.4 KiB
Python

"""
StatViewRefresher - Refresh materialized views for statistics.
Provides methods to refresh PostgreSQL materialized views created in migration 004.
Uses REFRESH MATERIALIZED VIEW CONCURRENTLY to allow reads during refresh.
Author: Claude
Date: 2025-11-07
"""
import logging
from sqlalchemy import text
from app.database.session import AsyncSessionLocal
logger = logging.getLogger(f'{__name__}.StatViewRefresher')
class StatViewRefresher:
"""
Service for refreshing materialized views containing game statistics.
Refreshes three materialized views:
- batting_game_stats: Player batting statistics aggregated from plays
- pitching_game_stats: Player pitching statistics aggregated from plays
- game_stats: Team totals and linescore aggregated from plays
Uses CONCURRENTLY option to allow reads during refresh (requires unique indexes).
"""
# View names in order (dependencies matter for concurrent refresh)
VIEWS = [
'batting_game_stats',
'pitching_game_stats',
'game_stats'
]
async def refresh_all(self) -> None:
"""
Refresh all stat materialized views concurrently.
Refreshes views in order to handle any dependencies.
Uses REFRESH MATERIALIZED VIEW CONCURRENTLY to avoid locking.
Raises:
Exception: If any refresh fails
"""
async with AsyncSessionLocal() as session:
try:
for view_name in self.VIEWS:
logger.debug(f"Refreshing materialized view: {view_name}")
await self._refresh_view(session, view_name)
await session.commit()
logger.info(f"Successfully refreshed all {len(self.VIEWS)} materialized views")
except Exception as e:
await session.rollback()
logger.error(f"Failed to refresh materialized views: {e}", exc_info=True)
raise
async def refresh_view(self, view_name: str) -> None:
"""
Refresh a single materialized view.
Args:
view_name: Name of the materialized view to refresh
Raises:
ValueError: If view_name is not recognized
Exception: If refresh fails
"""
if view_name not in self.VIEWS:
raise ValueError(f"Unknown view: {view_name}. Must be one of {self.VIEWS}")
async with AsyncSessionLocal() as session:
try:
await self._refresh_view(session, view_name)
await session.commit()
logger.info(f"Successfully refreshed materialized view: {view_name}")
except Exception as e:
await session.rollback()
logger.error(f"Failed to refresh view {view_name}: {e}", exc_info=True)
raise
async def _refresh_view(self, session, view_name: str) -> None:
"""
Execute REFRESH MATERIALIZED VIEW CONCURRENTLY statement.
Args:
session: Active database session
view_name: Name of the view to refresh
"""
# CONCURRENTLY requires unique index (created in migration)
# Allows reads during refresh but is slightly slower
query = text(f"REFRESH MATERIALIZED VIEW CONCURRENTLY {view_name}")
await session.execute(query)
logger.debug(f"Executed: REFRESH MATERIALIZED VIEW CONCURRENTLY {view_name}")
# Singleton instance
stat_view_refresher = StatViewRefresher()