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
+32 -12
View File
@@ -4,8 +4,8 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Snake Dashboard</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/root.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<link rel="stylesheet" href="{{ static_url('css/root.css') }}">
<link rel="stylesheet" href="{{ static_url('css/styles.css') }}">
</head>
<body>
<div class="page">
@@ -51,6 +51,7 @@
<option value="400">1.5x</option>
<option value="250">2x</option>
<option value="160">2.5x</option>
<option value="120">3x</option>
</select>
</label>
<input type="range" min="0" max="0" step="1" id="turn-slider" value="0">
@@ -68,16 +69,16 @@
</main>
</div>
<script src="{{ url_for('static', filename='js/Utils.js') }}"></script>
<script src="{{ url_for('static', filename='js/Snake.js') }}"></script>
<script src="{{ url_for('static', filename='js/MoveTable.js') }}"></script>
<script src="{{ url_for('static', filename='js/SnakeTable.js') }}"></script>
<script src="{{ url_for('static', filename='js/GameBoard.js') }}"></script>
<script src="{{ url_for('static', filename='js/OverallStats.js') }}"></script>
<script src="{{ url_for('static', filename='js/GamesTable.js') }}"></script>
<script src="{{ url_for('static', filename='js/Thinking.js') }}"></script>
<script src="{{ url_for('static', filename='js/DashboardWebSocket.js') }}"></script>
<script src="{{ url_for('static', filename='js/GameState.js') }}"></script>
<script src="{{ static_url('js/Utils.js') }}"></script>
<script src="{{ static_url('js/Snake.js') }}"></script>
<script src="{{ static_url('js/MoveTable.js') }}"></script>
<script src="{{ static_url('js/SnakeTable.js') }}"></script>
<script src="{{ static_url('js/GameBoard.js') }}"></script>
<script src="{{ static_url('js/OverallStats.js') }}"></script>
<script src="{{ static_url('js/GamesTable.js') }}"></script>
<script src="{{ static_url('js/Thinking.js') }}"></script>
<script src="{{ static_url('js/DashboardWebSocket.js') }}"></script>
<script src="{{ static_url('js/GameState.js') }}"></script>
<script>
const initialGameId = {{ initial_game_id|tojson }};
const initialSummary = {{ initial_summary|tojson }};
@@ -102,6 +103,20 @@
gameState._gamesTable = gamesTable;
const dashboardWS = new DashboardWebSocket({
onReplayUpdate: (payload) => {
const games = Array.isArray(dashboardGamesPayload.games)
? dashboardGamesPayload.games
: [];
const listedGame = games.find(
(game) => String(game.game_id) === String(payload.game_id),
);
if (listedGame && payload.game) {
listedGame.final_turn = payload.game.final_turn;
listedGame.status = payload.game.status;
gamesTable.render(games, gameState.activeGameId);
}
gameState.applyLiveTurn(payload.game_id, payload.game, payload.turn);
},
onGamesUpdate: (payload) => {
if (payload.summary) {
dashboardSummary = payload.summary;
@@ -111,6 +126,11 @@
dashboardGamesPayload = payload.games;
const games = Array.isArray(dashboardGamesPayload.games) ? dashboardGamesPayload.games : [];
gamesTable.render(games, gameState.activeGameId);
const activeGame = games.find((game) => String(game.game_id) === gameState.activeGameId);
if (activeGame && gameState.replay && gameState.replay.game) {
gameState.replay.game.status = activeGame.status;
gameState.replay.game.final_turn = activeGame.final_turn;
}
}
},
});