${esc(item.device_id)}(() => { "use strict"; const boot = window.UI_BUILDER_BOOT || {}; if (boot.mode !== "runtime") return; const selectedDeviceStorage = "hockey.vmix.selected_device"; const state = { devices: [], activeDeviceId: "", activeDeviceIds: [], selectedDeviceId: localStorage.getItem(selectedDeviceStorage) || "", root: null, panelOpen: false, loading: false, timer: null, fastRefreshTimers: [], message: "", messageError: false, testOpenDeviceId: "", testCommand: { input: "", selectedName: "", value: "" }, }; const esc = (value) => String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); async function api(path, options = {}) { const response = await fetch(path, { cache: "no-store", credentials: "same-origin", ...options, headers: { ...(options.body ? { "Content-Type": "application/json" } : {}), ...(options.headers || {}), }, }); let payload = {}; try { payload = await response.json(); } catch (_) {} if (!response.ok) { const detail = typeof payload.detail === "string" ? payload.detail : payload.detail?.message || `HTTP ${response.status}`; throw new Error(detail); } return payload; } function runtimeData() { return window.UIBuilderRuntime?.getData?.() || {}; } function currentGameId() { const data = runtimeData(); return String(data.hockey?.selected_game?.external_id || data.hockey?.selected_game?.id || "").trim(); } function currentSessionToken() { return String(runtimeData().hockey?.operator_session?.token || "").trim(); } function activeDevices() { return state.devices.filter((item) => item.paired_to_me && item.active_for_account); } function selectedDevice() { return state.devices.find((item) => ( item.device_id === state.selectedDeviceId && item.paired_to_me && item.active_for_account )) || null; } function selectLocalDevice(deviceId) { state.selectedDeviceId = String(deviceId || "").trim(); if (state.selectedDeviceId) localStorage.setItem(selectedDeviceStorage, state.selectedDeviceId); else localStorage.removeItem(selectedDeviceStorage); window.dispatchEvent(new CustomEvent("hockey:agent-selected", { detail: { device_id: state.selectedDeviceId }, })); } function mergeDevicePayload(payload) { if (!payload || !payload.device_id) return; const index = state.devices.findIndex((item) => item.device_id === payload.device_id); const previous = index >= 0 ? state.devices[index] : {}; const merged = { ...previous, ...payload }; if (merged.paired_to_me) merged.pair_state = "mine"; else if (merged.paired) merged.pair_state = "busy"; else merged.pair_state = "free"; if (index >= 0) state.devices.splice(index, 1, merged); else state.devices.unshift(merged); } function scheduleFastRefresh() { state.fastRefreshTimers.forEach((timerId) => clearTimeout(timerId)); state.fastRefreshTimers = [0, 250, 1000].map((delay) => setTimeout(() => { load(true).catch(() => {}); }, delay)); } function refreshMappingInBackground(deviceId, assignment = null) { if (!deviceId || !assignment?.delivered) return; api(`/api/hockey/agents/devices/${encodeURIComponent(deviceId)}/apply-mapping`, { method: "POST" }) .then(() => load(true)) .catch((error) => console.warn("Could not refresh vMix Mapping after Agent selection", error)); } async function bindSelectedDevice(deviceId) { selectLocalDevice(deviceId); const token = currentSessionToken(); if (!token) return null; const result = await api(`/api/hockey/agents/devices/${encodeURIComponent(deviceId)}/select-session`, { method: "POST", // Agent selection must never wait for a full Mapping push. The session/match // assignment is committed first; Mapping is refreshed asynchronously below. body: JSON.stringify({ session_token: token, apply_mapping: false }), }); refreshMappingInBackground(deviceId, result?.assignment || null); return result; } function setMessage(text, error = false) { state.message = text || ""; state.messageError = error; render(); if (text) setTimeout(() => { if (state.message === text) { state.message = ""; render(); } }, 3500); } function ensureRoot() { if (state.root?.isConnected) return state.root; const root = document.createElement("section"); root.className = "hockey-agent-ui"; root.innerHTML = `
`; const toolbar = document.querySelector(".runtime-topbar .runtime-actions"); if (toolbar) toolbar.prepend(root); else document.body.appendChild(root); root.querySelector("[data-agent-toggle]")?.addEventListener("click", () => { state.panelOpen = !state.panelOpen; render(); }); root.addEventListener("click", onClick); root.addEventListener("input", (event) => { const field = event.target.closest("[data-agent-test-field]"); if (!field) return; const key = field.dataset.agentTestField; if (key === "input") state.testCommand.input = field.value; if (key === "selectedName") state.testCommand.selectedName = field.value; if (key === "value") state.testCommand.value = field.value; }); state.root = root; return root; } async function onClick(event) { const button = event.target.closest("button[data-agent-action]"); if (!button || state.loading) return; const action = button.dataset.agentAction; const deviceId = button.dataset.deviceId || ""; if (!deviceId) return; state.loading = true; render(); try { if (action === "pair") { const paired = await api(`/api/hockey/agents/devices/${encodeURIComponent(deviceId)}/pair`, { method: "POST", body: JSON.stringify({ make_active: true }), }); mergeDevicePayload(paired); render(); await bindSelectedDevice(deviceId); scheduleFastRefresh(); setMessage("Agent прикреплён и выбран для этой панели"); } else if (action === "activate") { const activated = await api(`/api/hockey/agents/devices/${encodeURIComponent(deviceId)}/activate`, { method: "POST" }); mergeDevicePayload(activated); render(); await bindSelectedDevice(deviceId); scheduleFastRefresh(); setMessage("Agent включён и выбран для этой панели"); } else if (action === "select") { await bindSelectedDevice(deviceId); scheduleFastRefresh(); setMessage("Эта панель теперь управляет выбранным Agent"); } else if (action === "deactivate") { await api(`/api/hockey/agents/devices/${encodeURIComponent(deviceId)}/deactivate`, { method: "POST" }); if (state.selectedDeviceId === deviceId) selectLocalDevice(""); setMessage("Получение данных для Agent остановлено"); } else if (action === "test-toggle") { state.testOpenDeviceId = state.testOpenDeviceId === deviceId ? "" : deviceId; } else if (action === "test-set-text") { const input = state.testCommand.input.trim(); const selectedName = state.testCommand.selectedName.trim(); if (!input || !selectedName) throw new Error("Заполните Input и имя текстового поля"); const result = await api(`/api/hockey/agents/devices/${encodeURIComponent(deviceId)}/test-set-text`, { method: "POST", body: JSON.stringify({ input, selected_name: selectedName, value: state.testCommand.value }), }); setMessage(`SetText выполнен · матч ${result.match_id}`); } else if (action === "unpair") { if (!window.confirm("Отвязать этот agent от вашего аккаунта?")) return; await api(`/api/hockey/agents/devices/${encodeURIComponent(deviceId)}/pair`, { method: "DELETE" }); const existing = state.devices.find((item) => item.device_id === deviceId); if (existing) mergeDevicePayload({ ...existing, paired: false, paired_to_me: false, active_for_account: false, pair_state: "free", current_match_id: "", assignment_id: "", owner: "", }); if (state.testOpenDeviceId === deviceId) state.testOpenDeviceId = ""; if (state.selectedDeviceId === deviceId) selectLocalDevice(""); scheduleFastRefresh(); setMessage("Agent отвязан"); } await load(true); } catch (error) { setMessage(error.message || "Ошибка Agent", true); } finally { state.loading = false; render(); } } async function load(force = false) { if (state.loading && !force) return; try { const payload = await api("/api/hockey/agents/devices"); state.devices = Array.isArray(payload.devices) ? payload.devices : []; state.activeDeviceId = String(payload.active_device_id || ""); state.activeDeviceIds = Array.isArray(payload.active_device_ids) ? payload.active_device_ids.map(String) : (state.activeDeviceId ? [state.activeDeviceId] : []); const selectedStillValid = state.devices.some((item) => ( item.device_id === state.selectedDeviceId && item.paired_to_me && item.active_for_account )); if (!selectedStillValid) { const mine = activeDevices(); if (mine.length === 1) selectLocalDevice(mine[0].device_id); else if (state.selectedDeviceId) selectLocalDevice(""); } const editingTest = Boolean(state.root?.querySelector("[data-agent-test-form]")?.contains(document.activeElement)); if (!editingTest) render(); } catch (error) { if (!String(error.message || "").includes("401")) console.warn("Could not load vMix agents", error); } } function deviceCard(item) { const online = Boolean(item.online); const vmix = Boolean(item.vmix_connected); const mine = Boolean(item.paired_to_me); const active = Boolean(item.active_for_account); const selectedHere = state.selectedDeviceId === item.device_id; const free = item.pair_state === "free"; const busy = item.pair_state === "busy"; const currentMatch = String(item.current_match_id || ""); let actions = ""; if (free && online) { actions = ``; } else if (mine) { const canTest = active && selectedHere && online && vmix && currentMatch; actions = ` ${active ? 'Получает данные' : ``} ${active ? (selectedHere ? 'Эта панель' : ``) : ""} ${canTest ? `` : ""} ${active ? `` : ""} `; } else if (busy) { actions = 'Занято другим оператором'; } const testForm = state.testOpenDeviceId === item.device_id ? `${esc(item.device_id)}