update dashboards over websocket and remove database path from GameplayDatabase output
Build and Push Docker Container / build-and-push (push) Failing after 13m18s

This commit is contained in:
2026-04-06 01:49:26 +02:00
parent 4626a491f5
commit 097a7f295a
3 changed files with 323 additions and 22 deletions
+150 -12
View File
@@ -750,6 +750,10 @@
let activeGameId = String(initialGameId || "");
let gamesWebSocket = null;
let gamesWebSocketReconnectTimer = null;
let gamesWebSocketServerShuttingDown = false;
let hasLoadedReplayOnce = false;
let replayRequestSeq = 0;
const pendingReplayRequests = new Map();
let selectedSnakeId = null;
const svgCache = new Map();
@@ -1373,6 +1377,7 @@
}
function scheduleDashboardGamesWebSocketReconnect() {
if (gamesWebSocketServerShuttingDown) return;
if (gamesWebSocketReconnectTimer) return;
gamesWebSocketReconnectTimer = window.setTimeout(() => {
gamesWebSocketReconnectTimer = null;
@@ -1380,6 +1385,14 @@
}, 1500);
}
function rejectPendingReplayRequests(message) {
for (const pending of pendingReplayRequests.values()) {
window.clearTimeout(pending.timeoutId);
pending.reject(new Error(message));
}
pendingReplayRequests.clear();
}
function applyDashboardGamesUpdate(payload) {
const nextSummary = payload && payload.summary && typeof payload.summary === "object"
? payload.summary
@@ -1407,9 +1420,8 @@
clearActiveGame();
}
const payloadGameId = payload && payload.game_id ? String(payload.game_id) : "";
if (payloadGameId && payloadGameId === activeGameId) {
loadReplay(payloadGameId);
if (activeGameId && gameIds.has(activeGameId) && payload && payload.trigger === "game_saved") {
loadReplay(activeGameId);
return;
}
@@ -1421,6 +1433,10 @@
}
function connectDashboardGamesWebSocket() {
if (gamesWebSocketServerShuttingDown) return;
if (gamesWebSocket && (gamesWebSocket.readyState === WebSocket.OPEN || gamesWebSocket.readyState === WebSocket.CONNECTING)) {
return;
}
const wsUrl = dashboardGamesWebSocketUrl();
try {
gamesWebSocket = new WebSocket(wsUrl);
@@ -1436,13 +1452,47 @@
} catch {
return;
}
if (!payload || payload.type !== "dashboard_games_update") return;
applyDashboardGamesUpdate(payload);
if (!payload || !payload.type) return;
if (payload.type === "dashboard_ws_shutdown") {
gamesWebSocketServerShuttingDown = true;
if (gamesWebSocketReconnectTimer) {
clearTimeout(gamesWebSocketReconnectTimer);
gamesWebSocketReconnectTimer = null;
}
rejectPendingReplayRequests("Server shutting down");
if (gamesWebSocket) {
gamesWebSocket.close();
}
return;
}
if (payload.type === "dashboard_game_replay") {
const requestId = String(payload.request_id || "");
if (!requestId) return;
const pending = pendingReplayRequests.get(requestId);
if (!pending) return;
pendingReplayRequests.delete(requestId);
window.clearTimeout(pending.timeoutId);
if (payload.error) {
pending.reject(new Error(String(payload.error)));
return;
}
pending.resolve(payload.replay || null);
return;
}
if (payload.type === "dashboard_games_update") {
applyDashboardGamesUpdate(payload);
}
});
gamesWebSocket.addEventListener("close", () => {
gamesWebSocket = null;
scheduleDashboardGamesWebSocketReconnect();
rejectPendingReplayRequests("Dashboard websocket disconnected");
if (!gamesWebSocketServerShuttingDown) {
scheduleDashboardGamesWebSocketReconnect();
}
});
gamesWebSocket.addEventListener("error", () => {
@@ -1452,6 +1502,79 @@
});
}
function waitForDashboardGamesWebSocketOpen(timeoutMs = 4000) {
if (gamesWebSocketServerShuttingDown) {
return Promise.resolve(false);
}
if (!gamesWebSocket || gamesWebSocket.readyState === WebSocket.CLOSED) {
connectDashboardGamesWebSocket();
}
if (gamesWebSocket && gamesWebSocket.readyState === WebSocket.OPEN) {
return Promise.resolve(true);
}
return new Promise((resolve) => {
if (!gamesWebSocket) {
resolve(false);
return;
}
const socketRef = gamesWebSocket;
let settled = false;
const cleanup = () => {
if (!socketRef) return;
socketRef.removeEventListener("open", onOpen);
socketRef.removeEventListener("close", onClose);
socketRef.removeEventListener("error", onError);
window.clearTimeout(timeoutId);
};
const finish = (value) => {
if (settled) return;
settled = true;
cleanup();
resolve(value);
};
const onOpen = () => finish(true);
const onClose = () => finish(false);
const onError = () => finish(false);
const timeoutId = window.setTimeout(() => finish(false), timeoutMs);
socketRef.addEventListener("open", onOpen);
socketRef.addEventListener("close", onClose);
socketRef.addEventListener("error", onError);
});
}
async function requestReplayOverWebSocket(gameId) {
const isOpen = await waitForDashboardGamesWebSocketOpen();
if (!isOpen || !gamesWebSocket || gamesWebSocket.readyState !== WebSocket.OPEN) {
throw new Error("Dashboard websocket unavailable");
}
const requestId = `replay-${Date.now()}-${replayRequestSeq++}`;
return await new Promise((resolve, reject) => {
const timeoutId = window.setTimeout(() => {
pendingReplayRequests.delete(requestId);
reject(new Error(`Replay websocket timeout for ${gameId}`));
}, 4000);
pendingReplayRequests.set(requestId, { resolve, reject, timeoutId });
try {
gamesWebSocket.send(JSON.stringify({
type: "dashboard_game_replay_request",
request_id: requestId,
game_id: gameId,
}));
} catch (error) {
window.clearTimeout(timeoutId);
pendingReplayRequests.delete(requestId);
reject(error);
}
});
}
function clearActiveGame() {
for (const row of gamesBodyEl.querySelectorAll("tr")) {
row.classList.remove("active");
@@ -1609,12 +1732,24 @@
}
async function loadReplay(gameId) {
const response = await fetch(`/dashboard/game/${gameId}`);
if (!response.ok) {
renderThinking({ my_move: "-", my_thinking: { error: `Replay load failed for ${gameId}` } });
return;
let nextReplay = null;
try {
nextReplay = await requestReplayOverWebSocket(gameId);
} catch {
if (!hasLoadedReplayOnce) {
renderThinking({ my_move: "-", my_thinking: { error: `Replay websocket unavailable for ${gameId}` } });
return;
}
const response = await fetch(`/dashboard/game/${gameId}`);
if (!response.ok) {
renderThinking({ my_move: "-", my_thinking: { error: `Replay load failed for ${gameId}` } });
return;
}
nextReplay = await response.json();
}
replay = await response.json();
replay = nextReplay;
hasLoadedReplayOnce = true;
activeGameId = String(gameId || "");
await preloadReplaySvgs();
turnIndex = 0;
@@ -1749,8 +1884,9 @@
async function boot() {
renderThinking(null);
loadSummary();
await loadGames();
connectDashboardGamesWebSocket();
await waitForDashboardGamesWebSocketOpen(2500);
await loadGames();
syncMonoOffset();
}
@@ -1780,10 +1916,12 @@
});
window.addEventListener("beforeunload", () => {
gamesWebSocketServerShuttingDown = true;
if (gamesWebSocketReconnectTimer) {
clearTimeout(gamesWebSocketReconnectTimer);
gamesWebSocketReconnectTimer = null;
}
rejectPendingReplayRequests("Dashboard unloading");
if (gamesWebSocket) {
gamesWebSocket.close();
gamesWebSocket = null;