возможно теперь загружается матч через интернет, но не точно

This commit is contained in:
2026-08-19 15:53:18 +03:00
parent 915be04057
commit ed9889439a
5 changed files with 102 additions and 21 deletions

View File

@@ -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
View File

@@ -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/")

View File

@@ -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

View File

@@ -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();
}

View 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