Files
hockey_new/hockey_data/static/agent-devices.js
2026-08-19 15:08:39 +03:00

351 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(() => {
"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("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
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 = `
<button class="hockey-agent-chip" type="button" data-agent-toggle aria-expanded="false" aria-label="vMix Agent" title="vMix Agent">
<span class="hockey-agent-dot"></span>
</button>
<div class="hockey-agent-panel" data-agent-panel hidden></div>
`;
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 = `<button type="button" data-agent-action="pair" data-device-id="${esc(item.device_id)}">Прикрепить</button>`;
} else if (mine) {
const canTest = active && selectedHere && online && vmix && currentMatch;
actions = `
${active
? '<span class="hockey-agent-badge active">Получает данные</span>'
: `<button type="button" data-agent-action="activate" data-device-id="${esc(item.device_id)}">Включить</button>`}
${active
? (selectedHere
? '<span class="hockey-agent-badge active">Эта панель</span>'
: `<button type="button" data-agent-action="select" data-device-id="${esc(item.device_id)}">Использовать здесь</button>`)
: ""}
${canTest ? `<button class="secondary" type="button" data-agent-action="test-toggle" data-device-id="${esc(item.device_id)}">${state.testOpenDeviceId === item.device_id ? "Скрыть тест" : "Тест SetText"}</button>` : ""}
${active ? `<button class="secondary" type="button" data-agent-action="deactivate" data-device-id="${esc(item.device_id)}">Не получать</button>` : ""}
<button class="danger" type="button" data-agent-action="unpair" data-device-id="${esc(item.device_id)}">Отвязать</button>
`;
} else if (busy) {
actions = '<span class="hockey-agent-badge busy">Занято другим оператором</span>';
}
const testForm = state.testOpenDeviceId === item.device_id ? `
<div class="hockey-agent-test" data-agent-test-form>
<div class="hockey-agent-test-title"><strong>Тест Web → Agent → vMix</strong><small>SetText</small></div>
<label>Input <input data-agent-test-field="input" value="${esc(state.testCommand.input)}" placeholder="например Scorebug"></label>
<label>Поле <input data-agent-test-field="selectedName" value="${esc(state.testCommand.selectedName)}" placeholder="например HomeTeam.Text"></label>
<label>Текст <input data-agent-test-field="value" value="${esc(state.testCommand.value)}" placeholder="TEST FROM WEB"></label>
<button type="button" data-agent-action="test-set-text" data-device-id="${esc(item.device_id)}">Отправить SetText</button>
<small>Тест выполняется только на этом Agent и его текущем матче.</small>
</div>` : "";
return `
<article class="hockey-agent-device ${online ? "is-online" : "is-offline"} ${active ? "is-active" : ""} ${selectedHere ? "is-selected-here" : ""}">
<div class="hockey-agent-device-title">
<span class="hockey-agent-status-light ${online ? "online" : "offline"}"></span>
<div><strong>${esc(item.name || item.hostname || item.device_id)}</strong><code>${esc(item.device_id)}</code></div>
</div>
<div class="hockey-agent-device-grid">
<span>Agent <b>${online ? "ONLINE" : "OFFLINE"}</b></span>
<span>vMix <b class="${vmix ? "ok" : "warn"}">${vmix ? "CONNECTED" : "NO CONNECTION"}</b></span>
${currentMatch ? `<span>Матч <b>${esc(currentMatch)}</b></span>` : ""}
${item.mapping ? `<span>Mapping <b class="ok">${esc(item.mapping.name)} · v${esc(item.mapping.version)}</b></span>` : (item.project_fingerprint ? `<span>Mapping <b class="warn">НЕ НАСТРОЕН</b></span>` : "")}
${item.project_input_count ? `<span>vMix структура <b>${esc(item.project_input_count)} Inputs · ${esc(item.project_field_count || 0)} полей</b></span>` : ""}
${item.agent_version ? `<span>Версия <b>${esc(item.agent_version)}</b></span>` : ""}
</div>
<div class="hockey-agent-actions">${actions}</div>
${testForm}
</article>
`;
}
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("")
: `<div class="hockey-agent-empty">Активных agent.exe пока нет.<br>Запустите agent.exe на компьютере с vMix.</div>`;
panel.innerHTML = `
<header>
<div><small>VMIX BRIDGE</small><strong>Устройства</strong></div>
<button type="button" class="icon" data-agent-close aria-label="Закрыть">×</button>
</header>
<div class="hockey-agent-current-match">Web матч: <b>${esc(gameId || "не выбран")}</b></div>
${state.message ? `<div class="hockey-agent-notice ${state.messageError ? "error" : ""}">${esc(state.message)}</div>` : ""}
<div class="hockey-agent-list">${cards}</div>
<footer>Активных Agent может быть несколько. F1, Space, таймеры и другие команды идут только в Agent с меткой «Эта панель».</footer>
`;
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);
})();