(() => { "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, 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 }, })); } async function bindSelectedDevice(deviceId) { selectLocalDevice(deviceId); const token = currentSessionToken(); if (!token) return null; return api(`/api/hockey/agents/devices/${encodeURIComponent(deviceId)}/select-session`, { method: "POST", body: JSON.stringify({ session_token: token }), }); } 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") { await api(`/api/hockey/agents/devices/${encodeURIComponent(deviceId)}/pair`, { method: "POST", body: JSON.stringify({ make_active: true }), }); await bindSelectedDevice(deviceId); setMessage("Agent прикреплён и выбран для этой панели"); } else if (action === "activate") { await api(`/api/hockey/agents/devices/${encodeURIComponent(deviceId)}/activate`, { method: "POST" }); await bindSelectedDevice(deviceId); setMessage("Agent включён и выбран для этой панели"); } else if (action === "select") { await bindSelectedDevice(deviceId); 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" }); if (state.testOpenDeviceId === deviceId) state.testOpenDeviceId = ""; if (state.selectedDeviceId === deviceId) selectLocalDevice(""); 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 ? `
Тест Web → Agent → vMixSetText
Тест выполняется только на этом Agent и его текущем матче.
` : ""; return `
${esc(item.name || item.hostname || item.device_id)}${esc(item.device_id)}
Agent ${online ? "ONLINE" : "OFFLINE"} vMix ${vmix ? "CONNECTED" : "NO CONNECTION"} ${currentMatch ? `Матч ${esc(currentMatch)}` : ""} ${item.mapping ? `Mapping ${esc(item.mapping.name)} · v${esc(item.mapping.version)}` : (item.project_fingerprint ? `Mapping НЕ НАСТРОЕН` : "")} ${item.project_input_count ? `vMix структура ${esc(item.project_input_count)} Inputs · ${esc(item.project_field_count || 0)} полей` : ""} ${item.agent_version ? `Версия ${esc(item.agent_version)}` : ""}
${actions}
${testForm}
`; } function render() { const root = ensureRoot(); const chip = root.querySelector("[data-agent-toggle]"); const panel = root.querySelector("[data-agent-panel]"); if (!chip || !panel) return; const active = activeDevices(); const selected = selectedDevice(); const freeOnline = state.devices.filter((item) => item.online && item.pair_state === "free").length; root.classList.toggle("has-active", Boolean(selected?.online)); root.classList.toggle("has-vmix", Boolean(selected?.online && selected?.vmix_connected)); root.classList.toggle("is-offline", Boolean(selected && !selected.online)); let chipTitle = "vMix Agent · не подключён"; if (selected) { const name = selected.name || selected.hostname || "Agent"; const vmixState = selected.vmix_connected ? "vMix подключён" : "vMix не подключён"; chipTitle = `${name} · ${vmixState}${selected.current_match_id ? ` · матч ${selected.current_match_id}` : ""}`; } else if (active.length > 1) { chipTitle = `vMix Agent · ${active.length} активных · выберите устройство`; } else if (freeOnline) { chipTitle = `vMix Agent · ${freeOnline} свободных устройств`; } chip.title = chipTitle; chip.setAttribute("aria-label", chipTitle); chip.setAttribute("aria-expanded", state.panelOpen ? "true" : "false"); panel.hidden = !state.panelOpen; if (!state.panelOpen) return; const gameId = currentGameId(); const cards = state.devices.length ? state.devices.map(deviceCard).join("") : `
Активных agent.exe пока нет.
Запустите agent.exe на компьютере с vMix.
`; panel.innerHTML = `
VMIX BRIDGEУстройства
Web матч: ${esc(gameId || "не выбран")}
${state.message ? `
${esc(state.message)}
` : ""}
${cards}
`; panel.querySelector("[data-agent-close]")?.addEventListener("click", () => { state.panelOpen = false; render(); }); } window.HockeyAgentRuntime = { currentDeviceId: () => String(state.selectedDeviceId || ""), selectDevice: (deviceId) => { selectLocalDevice(deviceId); render(); }, getDevices: () => state.devices.slice(), }; window.addEventListener("hockey:game-selected", () => { setTimeout(() => load(true), 300); setTimeout(() => load(true), 1300); }); window.addEventListener("focus", () => load(true)); document.addEventListener("visibilitychange", () => { if (!document.hidden) load(true); }); ensureRoot(); load(true); state.timer = window.setInterval(() => { if (!document.hidden) load(); }, 3000); })();