тест 8

This commit is contained in:
2026-08-24 18:17:26 +03:00
parent 65a2521156
commit aac9e5b8bc
7 changed files with 170 additions and 38 deletions

View File

@@ -1483,18 +1483,19 @@ function gameCard(game) {
}
}
async function enrichOpenedGame(id, tournamentId, generation) {
async function enrichOpenedGame(id, tournamentId, generation, { opening = false } = {}) {
try {
const loaded = await request(
`/api/hockey/games/${encodeURIComponent(id)}/details/sync`
+ `?tournament_id=${encodeURIComponent(tournamentId)}`
+ `&language=${encodeURIComponent(state.language)}`,
{ method: "POST", timeoutMs: 16000 }
+ `&language=${encodeURIComponent(state.language)}`
+ `&opening=${opening ? "true" : "false"}`,
{ method: "POST", timeoutMs: opening ? 14000 : 16000 }
);
if (
generation !== state.gameOpenGeneration
|| String(state.selectedGameId || "") !== String(id)
) return;
) return false;
if (loaded?.game) state.selectedGame = loaded.game;
state.selectedMatchDetails = loaded?.details || state.selectedMatchDetails || null;
applyGameData();
@@ -1503,18 +1504,46 @@ function gameCard(game) {
if (loaded?.details_sync_failed || loaded?.degraded) {
console.warn("[Hockey] Match opened with cached/schedule data; full details are not available yet", loaded?.warnings || []);
}
return Boolean(loaded?.game);
} catch (error) {
// Full match JSON, rosters, shots and directories are enrichment data.
// Their absence must never undo a match already selected by the operator.
console.warn(`[Hockey] Optional match details sync failed for ${id}:`, error);
return false;
}
}
function clearRuntimeMatchSpecificData() {
// BUILD110: patchData() performs a deep merge. A schedule row normally has
// no home.players/away.players fields, so without an explicit reset the
// previous match roster survives until the new details request completes.
window.UIBuilderRuntime?.patchData?.({
hockey: {
selected_game: null,
home: null,
away: null,
referees: [],
match_details: null,
game_control: null,
selected_penalties: { home: null, away: null },
active_penalties: { home: null, away: null },
},
});
}
function cachedMatchHasRosters(payload) {
if (!payload?.game || !payload?.details?.loaded) return false;
const home = Array.isArray(payload.game?.home?.players) ? payload.game.home.players.length : 0;
const away = Array.isArray(payload.game?.away?.players) ? payload.game.away.players.length : 0;
return (home + away) > 0;
}
async function openGame(id, { manual = false } = {}) {
if (state.gameOpenBusy) return;
state.lastAttemptedGameId = String(id || "");
state.lastGameOpenError = "";
state.gameOpenBusy = true;
stopSelectedGamePolling();
const generation = ++state.gameOpenGeneration;
const loadingGame = matchLabelById(id) || `${t("gameNumber")} ID ${id}`;
setMatchLoading(true, { game: loadingGame, stage: t("loadingMatchDetails") });
@@ -1576,6 +1605,16 @@ function gameCard(game) {
localStorage.setItem(storage.tournament, state.selectedId);
}
// Read only the selected game from the local DB while the session is being
// created. This never contacts Stat2TV and lets a previously loaded match
// restore its own roster immediately, without exposing another game's data.
const cachedDetailsPromise = manual
? Promise.resolve(null)
: request(
`/api/hockey/games/${encodeURIComponent(id)}/details?language=${encodeURIComponent(state.language)}`,
{ timeoutMs: 2000 }
).catch(() => null);
updateMatchLoading(t("loadingMatchSession"));
const previousToken = localStorage.getItem(storage.session);
if (previousToken) {
@@ -1633,13 +1672,47 @@ function gameCard(game) {
localStorage.setItem(storage.gameDate, state.gameDate);
}
// Publish the selected match immediately. Do not wait for lineups, shots,
// standings or any remote Stat2TV dependency before the operator can work.
// Prefer cached details for this exact game. The Runtime was explicitly
// cleared above, so even a missing cache can only show an empty new match,
// never the previous match roster.
// The new session is confirmed. From this point the old match-specific
// Runtime payload must not survive even for a single render.
clearRuntimeMatchSpecificData();
const cachedDetails = await cachedDetailsPromise;
const cachedRostersReady = cachedMatchHasRosters(cachedDetails);
if (cachedDetails?.game) {
state.selectedGame = cachedDetails.game;
state.selectedMatchDetails = cachedDetails.details || null;
}
applyGameData(session);
renderSelectedChip();
startSelectedGamePolling();
setOpen(false);
// For a first-time match, keep the loading overlay until the critical
// roster card has arrived. Mapping and optional shots remain non-blocking.
// If this game's own cached roster already exists, let the operator work
// immediately and refresh it in the background.
if (!manual && !cachedRostersReady) {
updateMatchLoading(t("loadingMatchDetails"));
await enrichOpenedGame(String(id), state.selectedId, generation, { opening: true });
if (
generation === state.gameOpenGeneration
&& String(state.selectedGameId || "") === String(id)
) startSelectedGamePolling();
} else if (!manual) {
void enrichOpenedGame(String(id), state.selectedId, generation, { opening: true })
.finally(() => {
if (
generation === state.gameOpenGeneration
&& String(state.selectedGameId || "") === String(id)
) startSelectedGamePolling();
});
} else {
startSelectedGamePolling();
}
updateMatchLoading(t("loadingMatchFinishing"));
loadTournamentStandings({ sync: false }).catch(() => {});
if (selectedGameDate) loadGames({ autoSync: false }).catch(() => {});
@@ -1655,10 +1728,6 @@ function gameCard(game) {
const awayName = state.selectedGame?.away?.name || "—";
notify(`${t("gameOpened")}: ${homeName}${awayName}${loadedText}`);
if (manual) state.root?.querySelectorAll("[data-manual-game-id]").forEach((manualInput) => { manualInput.value = ""; });
// Enrich in the background. If a particular match has no full JSON card,
// the already-open schedule match remains active and usable.
void enrichOpenedGame(String(id), state.selectedId, generation);
} catch (error) {
state.lastGameOpenError = String(error?.message || error || "Неизвестная ошибка");
if (state.view === "games") renderGames();