from quart_common.web.logger import await_log, logging from typing import Awaitable, Callable import json from .dashboard_ws_hub import DashboardWebSocketHub from server.database import GameplayDatabase class DashboardQueryService: def __init__(self, gameplay_database:GameplayDatabase, ws_hub:DashboardWebSocketHub, logger:logging, dashboard_running_game_stale_sec:int): self.gameplay_database = gameplay_database self.ws_hub = ws_hub self.logger = logger self.dashboard_running_game_stale_sec = dashboard_running_game_stale_sec self.publish_notice:Callable[[str], Awaitable[None]] | None = None def set_publish_notice(self, publish_notice:Callable[[str], Awaitable[None]]) -> None: self.publish_notice = publish_notice async def on_dashboard_games_update_notice(self, trigger:str) -> None: await self.push_dashboard_games_update( game_state=None, publish_cluster=False, trigger=trigger, ) async def build_dashboard_games_event(self, game_state:dict|None=None, trigger_override:str|None=None) -> dict: games_payload = await self.get_dashboard_games(limit=100) summary_payload = await self.get_dashboard_summary() game_id = None if game_state is not None: game_id = game_state.get('game', {}).get('id') trigger = trigger_override or ('game_saved' if game_id else 'snapshot') return { 'type': 'dashboard_games_update', 'trigger': trigger, 'games': games_payload, 'summary': summary_payload, } async def build_dashboard_game_replay_event(self, game_id:str, request_id:str|None=None) -> dict: replay_payload = await self.get_dashboard_game_replay(game_id) if replay_payload is None: return { 'type': 'dashboard_game_replay', 'request_id': request_id, 'game_id': game_id, 'error': 'game_not_found', } return { 'type': 'dashboard_game_replay', 'request_id': request_id, 'game_id': game_id, 'replay': replay_payload, } async def handle_dashboard_ws_request(self, payload_raw:object) -> dict|None: if not isinstance(payload_raw, str): return None try: payload = json.loads(payload_raw) except json.JSONDecodeError: return None if not isinstance(payload, dict): return None if payload.get('type') != 'dashboard_game_replay_request': return None game_id = str(payload.get('game_id') or '').strip() request_id_raw = payload.get('request_id') request_id = None if request_id_raw is None else str(request_id_raw) if game_id == '': return { 'type': 'dashboard_game_replay', 'request_id': request_id, 'error': 'missing_game_id', } return await self.build_dashboard_game_replay_event( game_id=game_id, request_id=request_id, ) async def push_dashboard_games_update(self, game_state:dict|None=None, publish_cluster:bool=True, trigger:str|None=None) -> None: if self.gameplay_database is None: return event_payload = await self.build_dashboard_games_event( game_state, trigger_override=trigger, ) await self.ws_hub.broadcast_payload(event_payload) if publish_cluster and self.publish_notice is not None: await self.publish_notice(str(event_payload.get('trigger') or '')) async def get_dashboard_summary(self) -> dict: if self.gameplay_database is None: return {'enabled': False} try: await self._finalize_stale_dashboard_games() summary = await self.gameplay_database.get_summary() summary['enabled'] = True return summary except Exception as error: await await_log(self.logger.warning(f'Gameplay DB summary failed:{error}')) return {'enabled': True, 'error': ' summary_unavailable'} async def get_dashboard_games(self, limit:int=50) -> dict: if self.gameplay_database is None: return {'enabled': False, 'games': []} try: await self._finalize_stale_dashboard_games() games = await self.gameplay_database.list_games(limit=limit) return {'enabled': True, 'games': games} except Exception as error: await await_log( self.logger.warning(f'Gameplay DB game list failed:{error}') ) return {'enabled': True, 'error': 'games_unavailable', 'games': []} async def get_dashboard_game_replay(self, game_id:str) -> dict|None: if self.gameplay_database is None: return {'enabled': False, 'error': 'database_disabled', 'game_id': game_id} try: replay = await self.gameplay_database.get_game_replay(game_id) if replay is None: return None replay['enabled'] = True return replay except Exception as error: await await_log(self.logger.warning(f'Gameplay DB replay failed:{error}')) return {'enabled': True, 'error': 'replay_unavailable', 'game_id': game_id} async def _finalize_stale_dashboard_games(self) -> None: if self.gameplay_database is None: return try: await self.gameplay_database.finalize_stale_running_games(stale_after_seconds=self.dashboard_running_game_stale_sec) except Exception as error: await await_log(self.logger.warning(f'Gameplay DB stale running game finalize failed:{error}'))