feat: add live replays and PostgreSQL maintenance tools
Build and Push Docker Container / build-and-push (push) Successful in 7m53s

- Stream compact live replay updates across local and clustered dashboards.
- Render responsive snake bodies as SVG paths with aligned custom icons.
- Add cache-busted assets, replay fallback routes, and live-follow playback.
- Support PostgreSQL benchmark sampling and idempotent SQLite migration.
- Add dry-run cleanup for old low-quality PostgreSQL replay payloads.
- Reward safe perimeter lanes and bump Prism to version 1.5.0.
- Add backend, migration, dashboard, and perimeter regression coverage.
This commit is contained in:
2026-08-02 00:50:46 +02:00
parent 9b99b526e4
commit f14d780f29
29 changed files with 1574 additions and 310 deletions
+9 -4
View File
@@ -4,7 +4,7 @@ from typing import Awaitable, Callable
import asyncio, inspect, json, time
class DashboardEventsService:
def __init__(self, enabled:bool, redis_url:str, channel:str, event_origin:str, shutdown_event:asyncio.Event, on_notice:Callable[[str], Awaitable[None]], logger):
def __init__(self, enabled:bool, redis_url:str, channel:str, event_origin:str, shutdown_event:asyncio.Event, on_notice:Callable[[str, str|None], Awaitable[None]], logger):
self.enabled = enabled
self.redis_url = redis_url
self.channel = channel
@@ -71,18 +71,19 @@ class DashboardEventsService:
except Exception:
pass
async def publish_notice(self, trigger:str) -> None:
async def publish_notice(self, trigger:str, game_id:str|None=None) -> None:
if not self.enabled:
return
if self.redis is None:
return
if trigger not in {'game_saved', 'stale_finalized', 'manual'}:
if trigger not in {'game_started', 'game_turn', 'game_saved', 'stale_finalized', 'manual'}:
return
message = {
'type': 'dashboard_games_update_notice',
'origin': self.event_origin,
'trigger': trigger,
'game_id': game_id,
'sent_at': int(time.time()),
}
try:
@@ -120,7 +121,11 @@ class DashboardEventsService:
continue
notice_trigger = str(payload.get('trigger') or 'game_saved')
await self.on_notice(notice_trigger)
notice_game_id_raw = payload.get('game_id')
notice_game_id = (
None if notice_game_id_raw is None else str(notice_game_id_raw)
)
await self.on_notice(notice_trigger, notice_game_id)
except asyncio.CancelledError:
pass
except Exception as error:
+47 -4
View File
@@ -12,12 +12,22 @@ class DashboardQueryService:
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
self.publish_notice:Callable[[str, str|None], Awaitable[None]] | None = None
def set_publish_notice(self, publish_notice:Callable[[str], Awaitable[None]]) -> None:
def set_publish_notice(
self, publish_notice:Callable[[str, str|None], Awaitable[None]],
) -> None:
self.publish_notice = publish_notice
async def on_dashboard_games_update_notice(self, trigger:str) -> None:
async def on_dashboard_games_update_notice(
self, trigger:str, game_id:str|None=None,
) -> None:
if trigger == 'game_turn' and game_id:
await self.push_dashboard_game_replay_update(
game_id,
publish_cluster=False,
)
return
await self.push_dashboard_games_update(
game_state=None,
publish_cluster=False,
@@ -56,6 +66,22 @@ class DashboardQueryService:
'replay': replay_payload,
}
async def build_dashboard_game_replay_update_event(self, game_id:str) -> dict:
replay_payload = await self.get_dashboard_game_replay(game_id)
if replay_payload is None:
return {
'type': 'dashboard_game_replay_update',
'game_id': game_id,
'error': 'game_not_found',
}
turns = replay_payload.get('turns', [])
return {
'type': 'dashboard_game_replay_update',
'game_id': game_id,
'game': replay_payload.get('game', {}),
'turn': turns[-1] if turns else None,
}
async def handle_dashboard_ws_request(self, payload_raw:object) -> dict|None:
if not isinstance(payload_raw, str):
return None
@@ -95,7 +121,24 @@ class DashboardQueryService:
)
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 ''))
game_id = None
if game_state is not None:
game_id = game_state.get('game', {}).get('id')
await self.publish_notice(
str(event_payload.get('trigger') or ''), game_id,
)
async def push_dashboard_game_replay_update(
self, game_id:str, publish_cluster:bool=True,
) -> None:
if self.gameplay_database is None:
return
event_payload = await self.build_dashboard_game_replay_update_event(game_id)
if event_payload.get('error'):
return
await self.ws_hub.broadcast_payload(event_payload)
if publish_cluster and self.publish_notice is not None:
await self.publish_notice('game_turn', game_id)
async def get_dashboard_summary(self) -> dict:
if self.gameplay_database is None: