возможно теперь загружается матч через интернет, но не точно
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"server_url": "http://127.0.0.1:8000",
|
||||
"fallback_server_url": "https://khl.tvstart.ru",
|
||||
"vmix_url": "http://127.0.0.1:8088/api/",
|
||||
"device_id": "69f57eb1-805e-4d9c-a98e-970f266fdb43",
|
||||
"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
|
||||
|
||||
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")
|
||||
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,
|
||||
)
|
||||
if agent_hub is not None and result.get("game_external_id"):
|
||||
result["agent_assignment"] = await agent_hub.assign_match(
|
||||
wfl_user_id=user.id,
|
||||
game_external_id=str(result.get("game_external_id") or ""),
|
||||
tournament_external_id=str(result.get("tournament_external_id") or ""),
|
||||
device_id=vmix_device_id,
|
||||
operator_session_token=str(result.get("token") or ""),
|
||||
)
|
||||
try:
|
||||
result["agent_assignment"] = await agent_hub.assign_match(
|
||||
wfl_user_id=user.id,
|
||||
game_external_id=str(result.get("game_external_id") or ""),
|
||||
tournament_external_id=str(result.get("tournament_external_id") 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
|
||||
except ValueError as error:
|
||||
raise HTTPException(status_code=409, detail=str(error)) from error
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
gameDate: "hockey.gameDate",
|
||||
session: "hockey.operatorSessionToken",
|
||||
vmixDevice: "hockey.vmix.selected_device",
|
||||
account: "hockey.accountId",
|
||||
};
|
||||
|
||||
const state = {
|
||||
@@ -280,6 +281,32 @@
|
||||
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 = {}) {
|
||||
const { timeoutMs = 15000, ...fetchOptions } = options;
|
||||
const controller = new AbortController();
|
||||
@@ -1442,22 +1469,42 @@ function gameCard(game) {
|
||||
}
|
||||
}
|
||||
|
||||
const session = await request("/api/hockey/sessions", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
tournament_external_id: state.selectedId,
|
||||
game_external_id: id,
|
||||
display_language: state.language,
|
||||
vmix_language: state.language,
|
||||
vmix_device_id: currentAgentDeviceId(),
|
||||
replace_existing: true,
|
||||
}),
|
||||
});
|
||||
let session = null;
|
||||
const selectedDeviceId = currentAgentDeviceId();
|
||||
const sessionPayload = {
|
||||
tournament_external_id: state.selectedId,
|
||||
game_external_id: id,
|
||||
display_language: state.language,
|
||||
vmix_language: state.language,
|
||||
vmix_device_id: selectedDeviceId,
|
||||
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);
|
||||
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 scheduleDate = String(state.games?.meta?.date || state.games?.selected_date || "").trim();
|
||||
@@ -2018,6 +2065,7 @@ document.addEventListener("visibilitychange", () => {
|
||||
async function init() {
|
||||
ensureShell();
|
||||
renderStaticLabels();
|
||||
await ensureAccountScopedRuntimeState();
|
||||
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