возможно теперь загружается матч через интернет, но не точно
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"server_url": "http://127.0.0.1:8000",
|
"server_url": "http://127.0.0.1:8000",
|
||||||
|
"fallback_server_url": "https://khl.tvstart.ru",
|
||||||
"vmix_url": "http://127.0.0.1:8088/api/",
|
"vmix_url": "http://127.0.0.1:8088/api/",
|
||||||
"device_id": "69f57eb1-805e-4d9c-a98e-970f266fdb43",
|
"device_id": "69f57eb1-805e-4d9c-a98e-970f266fdb43",
|
||||||
"device_secret": "YR_o_sqDKkiTKm8AhPzdqpufgQMcwjSVHRBG0d8o1hFGgCwDvZv1gA",
|
"device_secret": "YR_o_sqDKkiTKm8AhPzdqpufgQMcwjSVHRBG0d8o1hFGgCwDvZv1gA",
|
||||||
|
|||||||
3
app.py
3
app.py
@@ -29,7 +29,8 @@ from ui_builder import install_ui_builder
|
|||||||
from khl_site.khl_data_center import APP as khl_site_app
|
from khl_site.khl_data_center import APP as khl_site_app
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
BUILD_VERSION = "2026.08.19.22"
|
BUILD_VERSION = "2026.08.19.23"
|
||||||
|
# compatibility: BUILD_VERSION = "2026.08.19.22"
|
||||||
load_dotenv(BASE_DIR / ".env.local")
|
load_dotenv(BASE_DIR / ".env.local")
|
||||||
load_dotenv(BASE_DIR / ".env")
|
load_dotenv(BASE_DIR / ".env")
|
||||||
VMIX_API_URL = os.getenv("VMIX_API_URL", "http://127.0.0.1:8088/api/")
|
VMIX_API_URL = os.getenv("VMIX_API_URL", "http://127.0.0.1:8088/api/")
|
||||||
|
|||||||
@@ -1006,13 +1006,19 @@ def create_hockey_router(
|
|||||||
**session_data,
|
**session_data,
|
||||||
)
|
)
|
||||||
if agent_hub is not None and result.get("game_external_id"):
|
if agent_hub is not None and result.get("game_external_id"):
|
||||||
result["agent_assignment"] = await agent_hub.assign_match(
|
try:
|
||||||
wfl_user_id=user.id,
|
result["agent_assignment"] = await agent_hub.assign_match(
|
||||||
game_external_id=str(result.get("game_external_id") or ""),
|
wfl_user_id=user.id,
|
||||||
tournament_external_id=str(result.get("tournament_external_id") or ""),
|
game_external_id=str(result.get("game_external_id") or ""),
|
||||||
device_id=vmix_device_id,
|
tournament_external_id=str(result.get("tournament_external_id") or ""),
|
||||||
operator_session_token=str(result.get("token") or ""),
|
device_id=vmix_device_id,
|
||||||
)
|
operator_session_token=str(result.get("token") or ""),
|
||||||
|
)
|
||||||
|
except Exception as agent_error:
|
||||||
|
# The match/session must remain usable even when a browser
|
||||||
|
# carries a stale Agent id from another WFL account.
|
||||||
|
result["agent_assignment"] = None
|
||||||
|
result["agent_warning"] = str(agent_error)[:300]
|
||||||
return result
|
return result
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
raise HTTPException(status_code=409, detail=str(error)) from error
|
raise HTTPException(status_code=409, detail=str(error)) from error
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
gameDate: "hockey.gameDate",
|
gameDate: "hockey.gameDate",
|
||||||
session: "hockey.operatorSessionToken",
|
session: "hockey.operatorSessionToken",
|
||||||
vmixDevice: "hockey.vmix.selected_device",
|
vmixDevice: "hockey.vmix.selected_device",
|
||||||
|
account: "hockey.accountId",
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
@@ -280,6 +281,32 @@
|
|||||||
return String(window.HockeyAgentRuntime?.currentDeviceId?.() || localStorage.getItem(storage.vmixDevice) || "").trim();
|
return String(window.HockeyAgentRuntime?.currentDeviceId?.() || localStorage.getItem(storage.vmixDevice) || "").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureAccountScopedRuntimeState() {
|
||||||
|
let user = null;
|
||||||
|
try {
|
||||||
|
user = await request("/api/hockey/me");
|
||||||
|
} catch (_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentAccountId = String(user?.id || "").trim();
|
||||||
|
if (!currentAccountId) return;
|
||||||
|
const previousAccountId = String(localStorage.getItem(storage.account) || "").trim();
|
||||||
|
if (previousAccountId && previousAccountId !== currentAccountId) {
|
||||||
|
// Session tokens and Agent selection belong to one WFL account only.
|
||||||
|
// Keeping them across logout/login can bind a new operator to the
|
||||||
|
// previous account's match or vMix device.
|
||||||
|
localStorage.removeItem(storage.session);
|
||||||
|
localStorage.removeItem(storage.game);
|
||||||
|
localStorage.removeItem(storage.vmixDevice);
|
||||||
|
state.selectedGameId = "";
|
||||||
|
state.selectedGame = null;
|
||||||
|
state.selectedMatchDetails = null;
|
||||||
|
stopSelectedGamePolling();
|
||||||
|
try { window.HockeyAgentRuntime?.selectDevice?.(""); } catch (_) {}
|
||||||
|
}
|
||||||
|
localStorage.setItem(storage.account, currentAccountId);
|
||||||
|
}
|
||||||
|
|
||||||
async function request(path, options = {}) {
|
async function request(path, options = {}) {
|
||||||
const { timeoutMs = 15000, ...fetchOptions } = options;
|
const { timeoutMs = 15000, ...fetchOptions } = options;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
@@ -1442,22 +1469,42 @@ function gameCard(game) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await request("/api/hockey/sessions", {
|
let session = null;
|
||||||
method: "POST",
|
const selectedDeviceId = currentAgentDeviceId();
|
||||||
headers: { "Content-Type": "application/json" },
|
const sessionPayload = {
|
||||||
body: JSON.stringify({
|
tournament_external_id: state.selectedId,
|
||||||
tournament_external_id: state.selectedId,
|
game_external_id: id,
|
||||||
game_external_id: id,
|
display_language: state.language,
|
||||||
display_language: state.language,
|
vmix_language: state.language,
|
||||||
vmix_language: state.language,
|
vmix_device_id: selectedDeviceId,
|
||||||
vmix_device_id: currentAgentDeviceId(),
|
replace_existing: true,
|
||||||
replace_existing: true,
|
};
|
||||||
}),
|
try {
|
||||||
});
|
session = await request("/api/hockey/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(sessionPayload),
|
||||||
|
});
|
||||||
|
} catch (sessionError) {
|
||||||
|
// A device stored in localStorage may belong to the previous account.
|
||||||
|
// Retry the operator session without Agent instead of blocking the match.
|
||||||
|
if (selectedDeviceId) {
|
||||||
|
localStorage.removeItem(storage.vmixDevice);
|
||||||
|
try { window.HockeyAgentRuntime?.selectDevice?.(""); } catch (_) {}
|
||||||
|
session = await request("/api/hockey/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ ...sessionPayload, vmix_device_id: "" }),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw sessionError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
state.selectedGameId = String(id);
|
state.selectedGameId = String(id);
|
||||||
localStorage.setItem(storage.game, state.selectedGameId);
|
localStorage.setItem(storage.game, state.selectedGameId);
|
||||||
localStorage.setItem(storage.session, session.token);
|
if (session?.token) localStorage.setItem(storage.session, session.token);
|
||||||
|
else localStorage.removeItem(storage.session);
|
||||||
|
|
||||||
const selectedGameDate = String(state.selectedGame?.date || "").trim();
|
const selectedGameDate = String(state.selectedGame?.date || "").trim();
|
||||||
const scheduleDate = String(state.games?.meta?.date || state.games?.selected_date || "").trim();
|
const scheduleDate = String(state.games?.meta?.date || state.games?.selected_date || "").trim();
|
||||||
@@ -2018,6 +2065,7 @@ document.addEventListener("visibilitychange", () => {
|
|||||||
async function init() {
|
async function init() {
|
||||||
ensureShell();
|
ensureShell();
|
||||||
renderStaticLabels();
|
renderStaticLabels();
|
||||||
|
await ensureAccountScopedRuntimeState();
|
||||||
await loadNavigation();
|
await loadNavigation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
25
tests/test_build65_nonadmin_match_selection.py
Normal file
25
tests/test_build65_nonadmin_match_selection.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
JS = (ROOT / "hockey_data" / "static" / "tournament-menu.js").read_text(encoding="utf-8")
|
||||||
|
ROUTER = (ROOT / "hockey_data" / "router.py").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_account_switch_clears_account_bound_runtime_state():
|
||||||
|
assert 'account: "hockey.accountId"' in JS
|
||||||
|
assert 'previousAccountId !== currentAccountId' in JS
|
||||||
|
assert 'localStorage.removeItem(storage.session)' in JS
|
||||||
|
assert 'localStorage.removeItem(storage.game)' in JS
|
||||||
|
assert 'localStorage.removeItem(storage.vmixDevice)' in JS
|
||||||
|
assert 'await ensureAccountScopedRuntimeState();' in JS
|
||||||
|
|
||||||
|
|
||||||
|
def test_match_session_retries_without_stale_agent():
|
||||||
|
assert 'const selectedDeviceId = currentAgentDeviceId();' in JS
|
||||||
|
assert 'Retry the operator session without Agent' in JS
|
||||||
|
assert 'JSON.stringify({ ...sessionPayload, vmix_device_id: "" })' in JS
|
||||||
|
|
||||||
|
|
||||||
|
def test_agent_assignment_cannot_break_operator_session_creation():
|
||||||
|
assert 'result["agent_warning"] = str(agent_error)[:300]' in ROUTER
|
||||||
|
assert 'result["agent_assignment"] = None' in ROUTER
|
||||||
Reference in New Issue
Block a user