идентификаторы удалений из вкладки Игра
This commit is contained in:
@@ -45,6 +45,7 @@
|
|||||||
mappingSqlPreview: null,
|
mappingSqlPreview: null,
|
||||||
mappingSqlDraft: null,
|
mappingSqlDraft: null,
|
||||||
mappingLoadErrors: {},
|
mappingLoadErrors: {},
|
||||||
|
mappingContextRefreshTimer: null,
|
||||||
countries: null,
|
countries: null,
|
||||||
players: null,
|
players: null,
|
||||||
referees: null,
|
referees: null,
|
||||||
@@ -3173,6 +3174,16 @@
|
|||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener("hockey:mapping-context-updated", () => {
|
||||||
|
if (!state.open || !isMappingSection()) return;
|
||||||
|
if (state.mappingContextRefreshTimer) clearTimeout(state.mappingContextRefreshTimer);
|
||||||
|
state.mappingContextRefreshTimer = setTimeout(async () => {
|
||||||
|
state.mappingContextRefreshTimer = null;
|
||||||
|
try { await loadMappingCatalog(); } catch (_) {}
|
||||||
|
renderMapping();
|
||||||
|
}, 80);
|
||||||
|
});
|
||||||
|
|
||||||
window.addEventListener("hockey:tournament-selected", () => {
|
window.addEventListener("hockey:tournament-selected", () => {
|
||||||
if (!state.open) return;
|
if (!state.open) return;
|
||||||
if (state.section === "khl_site" && !isKhlTournamentSelected()) state.section = "teams";
|
if (state.section === "khl_site" && !isKhlTournamentSelected()) state.section = "teams";
|
||||||
|
|||||||
46
tests/test_build78_penalty_mapping_context_auto_sync.py
Normal file
46
tests/test_build78_penalty_mapping_context_auto_sync.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
APP = (ROOT / "ui_builder/static/app.js").read_text(encoding="utf-8")
|
||||||
|
ADMIN = (ROOT / "hockey_data/static/admin-directories.js").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_side_penalty_mapping_context_is_derived_from_active_penalties():
|
||||||
|
start = APP.index("async function hockeySyncPenaltySideMappingContext")
|
||||||
|
end = APP.index("function penaltyTargetAssignmentKey", start)
|
||||||
|
snippet = APP[start:end]
|
||||||
|
assert 'sortedPenaltyEntries("home")[0]' in snippet
|
||||||
|
assert 'sortedPenaltyEntries("away")[0]' in snippet
|
||||||
|
for key in (
|
||||||
|
"selected_home_penalty_id",
|
||||||
|
"selected_home_penalty_player_id",
|
||||||
|
"selected_home_penalty_player_db_id",
|
||||||
|
"selected_home_penalty_team_penalty",
|
||||||
|
"selected_away_penalty_id",
|
||||||
|
"selected_away_penalty_player_id",
|
||||||
|
"selected_away_penalty_player_db_id",
|
||||||
|
"selected_away_penalty_team_penalty",
|
||||||
|
):
|
||||||
|
assert f"{key}:" in snippet
|
||||||
|
assert 'fetch("/api/hockey/context/batch"' in snippet
|
||||||
|
|
||||||
|
|
||||||
|
def test_penalty_context_sync_runs_when_penalty_changes_and_clears_on_finish():
|
||||||
|
assert "hockeySyncPenaltySideMappingContext().catch(() => {});" in APP
|
||||||
|
assert APP.count("hockeySyncPenaltySideMappingContext({ force: true }).catch(() => {});") >= 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_saved_penalty_player_keeps_external_and_database_ids():
|
||||||
|
start = APP.index("function hockeyCompactPlayer")
|
||||||
|
end = APP.index("function hockeyCompactInfraction", start)
|
||||||
|
snippet = APP[start:end]
|
||||||
|
assert "const externalId" in snippet
|
||||||
|
assert "const dbId" in snippet
|
||||||
|
assert "externalId," in snippet
|
||||||
|
assert "dbId," in snippet
|
||||||
|
|
||||||
|
|
||||||
|
def test_mapping_admin_refreshes_live_after_runtime_context_update():
|
||||||
|
assert 'window.addEventListener("hockey:mapping-context-updated"' in ADMIN
|
||||||
|
assert "await loadMappingCatalog()" in ADMIN
|
||||||
|
assert "renderMapping();" in ADMIN
|
||||||
@@ -83,6 +83,9 @@
|
|||||||
vmixPenaltyMirrors: new Map(),
|
vmixPenaltyMirrors: new Map(),
|
||||||
activeHockeyVmixTimerSteps: new Set(),
|
activeHockeyVmixTimerSteps: new Set(),
|
||||||
vmixPenaltyTargetAssignments: new Map(),
|
vmixPenaltyTargetAssignments: new Map(),
|
||||||
|
hockeyPenaltyMappingContextSignature: "",
|
||||||
|
hockeyPenaltyMappingContextPending: false,
|
||||||
|
hockeyPenaltyMappingContextQueued: false,
|
||||||
vmixFinishOverlayTimers: new Map(),
|
vmixFinishOverlayTimers: new Map(),
|
||||||
vmixStrengthMappingRefreshPending: false,
|
vmixStrengthMappingRefreshPending: false,
|
||||||
vmixStrengthMappingRefreshQueued: null,
|
vmixStrengthMappingRefreshQueued: null,
|
||||||
@@ -4493,6 +4496,84 @@ function startCustomTooltips() {
|
|||||||
|| Number(a.event.createdAt || 0) - Number(b.event.createdAt || 0));
|
|| Number(a.event.createdAt || 0) - Number(b.event.createdAt || 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hockeyPenaltySideMappingDetail(item, side) {
|
||||||
|
if (!item?.event) return null;
|
||||||
|
const event = item.event;
|
||||||
|
const playerIds = hockeyPenaltyPlayerIdentifiers(item.component, event);
|
||||||
|
return {
|
||||||
|
penalty_id: String(event.external_id || event.id || ""),
|
||||||
|
player_id: String(playerIds.externalId || ""),
|
||||||
|
player_db_id: String(playerIds.dbId || ""),
|
||||||
|
team_penalty: Boolean(event.teamPenalty),
|
||||||
|
side: String(side || event.player?.side || event.side || ""),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hockeySyncPenaltySideMappingContext({ force = false } = {}) {
|
||||||
|
const gameId = String(hockeyTimerSelectedGameId() || "").trim();
|
||||||
|
if (!gameId) return false;
|
||||||
|
|
||||||
|
const home = hockeyPenaltySideMappingDetail(sortedPenaltyEntries("home")[0] || null, "home");
|
||||||
|
const away = hockeyPenaltySideMappingDetail(sortedPenaltyEntries("away")[0] || null, "away");
|
||||||
|
const values = {
|
||||||
|
selected_home_penalty_id: home?.penalty_id || "",
|
||||||
|
selected_home_penalty_player_id: home?.player_id || "",
|
||||||
|
selected_home_penalty_player_db_id: home?.player_db_id || "",
|
||||||
|
selected_home_penalty_team_penalty: home ? (home.team_penalty ? "1" : "0") : "",
|
||||||
|
selected_away_penalty_id: away?.penalty_id || "",
|
||||||
|
selected_away_penalty_player_id: away?.player_id || "",
|
||||||
|
selected_away_penalty_player_db_id: away?.player_db_id || "",
|
||||||
|
selected_away_penalty_team_penalty: away ? (away.team_penalty ? "1" : "0") : "",
|
||||||
|
};
|
||||||
|
const signature = JSON.stringify([gameId, values]);
|
||||||
|
if (!force && signature === state.hockeyPenaltyMappingContextSignature) return false;
|
||||||
|
|
||||||
|
if (state.hockeyPenaltyMappingContextPending) {
|
||||||
|
state.hockeyPenaltyMappingContextQueued = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.hockeyPenaltyMappingContextPending = true;
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/hockey/context/batch", {
|
||||||
|
method: "POST", cache: "no-store", credentials: "same-origin",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
values,
|
||||||
|
context: {
|
||||||
|
game_id: gameId,
|
||||||
|
device_id: currentRuntimeVmixDeviceId(),
|
||||||
|
session_token: currentRuntimeHockeySessionToken(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
let payload = {};
|
||||||
|
try { payload = await response.json(); } catch (_) {}
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(errorDetailText(payload.detail, `HTTP ${response.status}`));
|
||||||
|
}
|
||||||
|
state.hockeyPenaltyMappingContextSignature = signature;
|
||||||
|
window.UIBuilderRuntime?.patchData?.({
|
||||||
|
hockey: {
|
||||||
|
active_penalties: { home, away },
|
||||||
|
},
|
||||||
|
}, { render: false });
|
||||||
|
window.dispatchEvent(new CustomEvent("hockey:mapping-context-updated", {
|
||||||
|
detail: { game_id: gameId, values: clone(values) },
|
||||||
|
}));
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Penalty side Mapping context error", error);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
state.hockeyPenaltyMappingContextPending = false;
|
||||||
|
if (state.hockeyPenaltyMappingContextQueued) {
|
||||||
|
state.hockeyPenaltyMappingContextQueued = false;
|
||||||
|
hockeySyncPenaltySideMappingContext({ force: true }).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function penaltyTargetAssignmentKey(step, side, target) {
|
function penaltyTargetAssignmentKey(step, side, target) {
|
||||||
return `${String(step?.id || "")}:${side}:${String(target?.id || "")}`;
|
return `${String(step?.id || "")}:${side}:${String(target?.id || "")}`;
|
||||||
}
|
}
|
||||||
@@ -6211,6 +6292,7 @@ function openTimerQuickEditor(focusActionId = "") {
|
|||||||
if (hockeyEventReady(event) && state.activeHockeyVmixTimerSteps.size) {
|
if (hockeyEventReady(event) && state.activeHockeyVmixTimerSteps.size) {
|
||||||
rebalanceVmixPenaltyTargets({ force: true, hideUnused: false }).catch((error) => console.error("Penalty target rebalance error", error));
|
rebalanceVmixPenaltyTargets({ force: true, hideUnused: false }).catch((error) => console.error("Penalty target rebalance error", error));
|
||||||
}
|
}
|
||||||
|
hockeySyncPenaltySideMappingContext().catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createHockeyPenaltyDraft(component, seed = {}, source = "manual") {
|
function createHockeyPenaltyDraft(component, seed = {}, source = "manual") {
|
||||||
@@ -6492,6 +6574,7 @@ function openTimerQuickEditor(focusActionId = "") {
|
|||||||
if (board.selectedPreviewEventIds?.[side] === event.id) board.selectedPreviewEventIds[side] = "";
|
if (board.selectedPreviewEventIds?.[side] === event.id) board.selectedPreviewEventIds[side] = "";
|
||||||
persistHockeyBoard(component, board, true);
|
persistHockeyBoard(component, board, true);
|
||||||
refreshHockeyBoardNodes(component);
|
refreshHockeyBoardNodes(component);
|
||||||
|
hockeySyncPenaltySideMappingContext({ force: true }).catch(() => {});
|
||||||
rebalanceVmixPenaltyTargets({ force: true, hideUnused: true })
|
rebalanceVmixPenaltyTargets({ force: true, hideUnused: true })
|
||||||
.catch((error) => console.error("Penalty target finish rebalance error", error))
|
.catch((error) => console.error("Penalty target finish rebalance error", error))
|
||||||
.finally(() => fireConfiguredTimerFinishActions("penalty", { side, component, event, remaining_on_side: remainingOnSide }));
|
.finally(() => fireConfiguredTimerFinishActions("penalty", { side, component, event, remaining_on_side: remainingOnSide }));
|
||||||
@@ -6526,6 +6609,7 @@ function openTimerQuickEditor(focusActionId = "") {
|
|||||||
...hockeyPenaltyContext(event)
|
...hockeyPenaltyContext(event)
|
||||||
});
|
});
|
||||||
state.vmixPenaltyMirrors.delete(penaltyMirrorKey(component, event));
|
state.vmixPenaltyMirrors.delete(penaltyMirrorKey(component, event));
|
||||||
|
hockeySyncPenaltySideMappingContext({ force: true }).catch(() => {});
|
||||||
rebalanceVmixPenaltyTargets({ force: true, hideUnused: true }).catch((error) => console.error("Penalty target remove rebalance error", error));
|
rebalanceVmixPenaltyTargets({ force: true, hideUnused: true }).catch((error) => console.error("Penalty target remove rebalance error", error));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -6620,6 +6704,7 @@ function openTimerQuickEditor(focusActionId = "") {
|
|||||||
if (completed.has(board.selectedEventId)) board.selectedEventId = null;
|
if (completed.has(board.selectedEventId)) board.selectedEventId = null;
|
||||||
persistHockeyBoard(component, board, true);
|
persistHockeyBoard(component, board, true);
|
||||||
refreshHockeyBoardNodes(component);
|
refreshHockeyBoardNodes(component);
|
||||||
|
hockeySyncPenaltySideMappingContext({ force: true }).catch(() => {});
|
||||||
const finishBySide = new Map();
|
const finishBySide = new Map();
|
||||||
completedEvents.forEach((event) => {
|
completedEvents.forEach((event) => {
|
||||||
const side = String(event.player?.side || event.side || "").toLowerCase();
|
const side = String(event.player?.side || event.side || "").toLowerCase();
|
||||||
@@ -10129,8 +10214,12 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
|
|||||||
|
|
||||||
function hockeyCompactPlayer(player) {
|
function hockeyCompactPlayer(player) {
|
||||||
if (!player || typeof player !== "object") return null;
|
if (!player || typeof player !== "object") return null;
|
||||||
|
const externalId = String(player.externalId || player.external_id || player.id || player.raw?.external_id || player.raw?.id || "");
|
||||||
|
const dbId = String(player.dbId || player.db_id || player.database_id || player.raw?.db_id || player.raw?.database_id || "");
|
||||||
return {
|
return {
|
||||||
id: String(player.id || ""),
|
id: externalId,
|
||||||
|
externalId,
|
||||||
|
dbId,
|
||||||
side: player.side === "away" ? "away" : player.side === "home" ? "home" : "",
|
side: player.side === "away" ? "away" : player.side === "home" ? "home" : "",
|
||||||
number: String(player.number || ""),
|
number: String(player.number || ""),
|
||||||
name: String(player.name || ""),
|
name: String(player.name || ""),
|
||||||
@@ -10254,6 +10343,8 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
|
|||||||
refreshHockeyBoardNodes(boardComponent);
|
refreshHockeyBoardNodes(boardComponent);
|
||||||
}
|
}
|
||||||
state.hockeyTimerGameId = String(gameId || "");
|
state.hockeyTimerGameId = String(gameId || "");
|
||||||
|
state.hockeyPenaltyMappingContextSignature = "";
|
||||||
|
hockeySyncPenaltySideMappingContext({ force: true }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function hockeyPersistGameTimers(gameId, { keepalive = false, force = false } = {}) {
|
async function hockeyPersistGameTimers(gameId, { keepalive = false, force = false } = {}) {
|
||||||
|
|||||||
Reference in New Issue
Block a user