595 lines
23 KiB
JavaScript
595 lines
23 KiB
JavaScript
"use strict";
|
|
|
|
const state = {
|
|
token: "",
|
|
siteId: "",
|
|
config: { page_size: 16, max_active_previews: 4, webrtc_base_url: "" },
|
|
items: [],
|
|
quota: null,
|
|
cursors: [null],
|
|
pageIndex: 0,
|
|
nextCursor: null,
|
|
selected: new Map(),
|
|
previewing: new Set(),
|
|
loading: false,
|
|
};
|
|
|
|
const $ = (selector) => document.querySelector(selector);
|
|
const $$ = (selector) => Array.from(document.querySelectorAll(selector));
|
|
|
|
const labels = {
|
|
modality: { video: "视频", radar: "雷达", contact: "门磁", button: "按钮", wearable: "可穿戴", other: "其他" },
|
|
desired: { enabled: "启用", disabled: "停用" },
|
|
actual: { online: "在线", offline: "离线", failed: "失败", pending: "收敛中" },
|
|
adapter: { ready: "适配器就绪", pending: "适配器处理中", adapter_not_ready: "适配器未交付", authentication_failed: "认证失败", unavailable: "适配器不可用" },
|
|
capability: { video_capture: "成像", audio_capture: "音频", spatial_rule: "空间配置", telemetry: "遥测" },
|
|
};
|
|
|
|
function showToast(message) {
|
|
const toast = $("#toast");
|
|
toast.textContent = message;
|
|
toast.hidden = false;
|
|
window.clearTimeout(showToast.timer);
|
|
showToast.timer = window.setTimeout(() => { toast.hidden = true; }, 4200);
|
|
}
|
|
|
|
function setText(selector, value) {
|
|
const target = $(selector);
|
|
if (target) target.textContent = String(value);
|
|
}
|
|
|
|
function formatTime(value) {
|
|
if (!value) return "—";
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) return "—";
|
|
return new Intl.DateTimeFormat("zh-CN", {
|
|
month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit",
|
|
hour12: false,
|
|
}).format(date);
|
|
}
|
|
|
|
function isPreviewEligible(device) {
|
|
return Array.isArray(device.capabilities) && device.capabilities.includes("video_capture") &&
|
|
device.desired_state === "enabled" && device.actual_state === "online" && device.converged === true;
|
|
}
|
|
|
|
function attentionNeeded(device) {
|
|
return device.converged !== true || device.actual_state === "offline" || device.actual_state === "failed" ||
|
|
device.adapter_status === "authentication_failed" || device.adapter_status === "unavailable";
|
|
}
|
|
|
|
function statusFor(device) {
|
|
if (device.actual_state === "online" && device.converged) return { text: "在线 · 已收敛", tone: "success" };
|
|
if (device.actual_state === "failed" || device.adapter_status === "authentication_failed") return { text: labels.adapter[device.adapter_status] || "失败", tone: "danger" };
|
|
if (device.actual_state === "offline") return { text: "离线", tone: "danger" };
|
|
return { text: device.converged ? (labels.actual[device.actual_state] || "未知") : "等待收敛", tone: "warning" };
|
|
}
|
|
|
|
function createStatusBadge(device) {
|
|
const value = statusFor(device);
|
|
const badge = document.createElement("span");
|
|
badge.className = `status-badge ${value.tone}`;
|
|
badge.textContent = value.text;
|
|
return badge;
|
|
}
|
|
|
|
function setConnected(connected) {
|
|
const pill = $("#connectionPill");
|
|
const foot = $(".sidebar-foot");
|
|
pill.classList.toggle("connected", connected);
|
|
foot.classList.toggle("connected", connected);
|
|
setText("#connectionText", connected ? "已连接" : "未连接");
|
|
setText("#sessionState", connected ? "会话仅驻留当前页面" : "尚未建立会话");
|
|
}
|
|
|
|
function setLoading(loading) {
|
|
state.loading = loading;
|
|
$("#refreshDevices").disabled = loading || !state.token;
|
|
$("#refreshOperations").disabled = loading || !state.token;
|
|
$("#retryLoad").disabled = loading || !state.token;
|
|
$("#prevPage").disabled = loading || state.pageIndex === 0;
|
|
$("#nextPage").disabled = loading || !state.nextCursor;
|
|
setText("#deviceTableStatus", loading ? "正在读取最新状态…" : state.token ? "已读取当前页真实设备状态。" : "请先建立会话。");
|
|
}
|
|
|
|
function showDegraded(title, message) {
|
|
setText("#degradedTitle", title);
|
|
setText("#degradedMessage", message);
|
|
$("#degradedBanner").hidden = false;
|
|
}
|
|
|
|
function clearDegraded() {
|
|
$("#degradedBanner").hidden = true;
|
|
}
|
|
|
|
async function apiFetch(path) {
|
|
const response = await fetch(path, {
|
|
headers: { Authorization: `Bearer ${state.token}`, Accept: "application/json" },
|
|
cache: "no-store",
|
|
});
|
|
let payload = null;
|
|
try { payload = await response.json(); } catch (_) { payload = null; }
|
|
if (!response.ok) {
|
|
const error = new Error(payload && payload.code ? payload.code : `http_${response.status}`);
|
|
error.status = response.status;
|
|
error.payload = payload;
|
|
throw error;
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
function buildDevicePath(cursor) {
|
|
const params = new URLSearchParams();
|
|
params.set("limit", String(state.config.page_size));
|
|
const modality = $("#modalityFilter").value;
|
|
const desired = $("#desiredFilter").value;
|
|
const actual = $("#actualFilter").value;
|
|
if (modality) params.set("modality", modality);
|
|
if (desired) params.set("desired_state", desired);
|
|
if (actual) params.set("actual_state", actual);
|
|
if (cursor) params.set("cursor", cursor);
|
|
return `/api/v1/sites/${encodeURIComponent(state.siteId)}/devices?${params.toString()}`;
|
|
}
|
|
|
|
async function loadDevices(options = {}) {
|
|
if (!state.token || state.loading) return;
|
|
const cursor = state.cursors[state.pageIndex] || null;
|
|
setLoading(true);
|
|
clearDegraded();
|
|
try {
|
|
const payload = await apiFetch(buildDevicePath(cursor));
|
|
state.items = Array.isArray(payload.items) ? payload.items : [];
|
|
state.quota = payload.quota || null;
|
|
state.nextCursor = payload.page && payload.page.has_more ? payload.page.next_cursor : null;
|
|
for (const device of state.items) {
|
|
if (state.selected.has(device.id)) {
|
|
if (isPreviewEligible(device)) state.selected.set(device.id, device);
|
|
else state.selected.delete(device.id);
|
|
}
|
|
}
|
|
setConnected(true);
|
|
renderAll();
|
|
if (options.announce !== false) showToast(`已刷新第 ${state.pageIndex + 1} 页,共 ${state.items.length} 台设备`);
|
|
return true;
|
|
} catch (error) {
|
|
handleLoadError(error);
|
|
throw error;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
function handleLoadError(error) {
|
|
let title = "无法读取最新状态";
|
|
let message = "未知不会显示为在线。请检查 Sense 进程、网络和依赖后重试。";
|
|
if (error.status === 401) {
|
|
state.token = "";
|
|
setConnected(false);
|
|
title = "会话无效或已过期";
|
|
message = "Bearer token 已从页面内存清除,请重新建立会话。";
|
|
} else if (error.status === 403) {
|
|
title = "当前角色没有设备读取权限";
|
|
message = "页面不会尝试绕过权限;请使用具备 devices:read 的会话。";
|
|
} else if (error.status === 404) {
|
|
title = "无法访问该站点";
|
|
message = "目标不存在或当前会话无权访问,页面不会区分这两种情况。";
|
|
} else if (error.payload && ["quota_projection_unavailable", "area_policy_unavailable"].includes(error.payload.code)) {
|
|
title = "策略投影暂时不可用";
|
|
message = "已有链路不会因此被静默停用;相关新写入应保持禁用。";
|
|
}
|
|
showDegraded(title, message);
|
|
state.items = [];
|
|
state.quota = null;
|
|
renderAll();
|
|
}
|
|
|
|
function visibleItems() {
|
|
const query = $("#deviceSearch").value.trim().toLocaleLowerCase("zh-CN");
|
|
if (!query) return state.items;
|
|
return state.items.filter((device) => `${device.name} ${device.serial_number}`.toLocaleLowerCase("zh-CN").includes(query));
|
|
}
|
|
|
|
function renderAll() {
|
|
renderDevices();
|
|
renderMetrics();
|
|
renderHealth();
|
|
renderOperations();
|
|
renderSelection();
|
|
}
|
|
|
|
function renderMetrics() {
|
|
const used = state.quota && Number.isInteger(state.quota.used_video_channels) ? state.quota.used_video_channels : null;
|
|
const max = state.quota && Number.isInteger(state.quota.max_video_channels) ? state.quota.max_video_channels : null;
|
|
setText("#quotaMetric", used === null || max === null ? "—" : `${used} / ${max}`);
|
|
setText("#quotaHint", state.quota ? `投影状态:${state.quota.status || "未知"}` : "连接后读取 Bell 投影");
|
|
setText("#pageMetric", state.items.length);
|
|
setText("#convergedMetric", state.items.filter((device) => device.converged === true).length);
|
|
const attention = state.items.filter(attentionNeeded).length;
|
|
setText("#attentionMetric", attention);
|
|
setText("#deviceCount", state.items.length);
|
|
setText("#issueCount", attention);
|
|
}
|
|
|
|
function addTextCell(row, primary, secondary) {
|
|
const cell = document.createElement("td");
|
|
const strong = document.createElement("strong");
|
|
strong.textContent = primary || "—";
|
|
cell.appendChild(strong);
|
|
if (secondary) {
|
|
const small = document.createElement("small");
|
|
small.textContent = secondary;
|
|
cell.appendChild(small);
|
|
}
|
|
row.appendChild(cell);
|
|
}
|
|
|
|
function createPreviewCheckbox(device) {
|
|
const label = document.createElement("label");
|
|
label.className = "select-preview";
|
|
const checkbox = document.createElement("input");
|
|
checkbox.type = "checkbox";
|
|
checkbox.checked = state.selected.has(device.id);
|
|
checkbox.disabled = !isPreviewEligible(device);
|
|
checkbox.setAttribute("aria-label", checkbox.disabled ? `${device.name} 当前不可预览` : `选择 ${device.name} 进行预览`);
|
|
checkbox.addEventListener("change", () => toggleSelection(device, checkbox));
|
|
label.appendChild(checkbox);
|
|
return label;
|
|
}
|
|
|
|
function renderDevices() {
|
|
const rows = $("#deviceRows");
|
|
const cards = $("#deviceCards");
|
|
rows.replaceChildren();
|
|
cards.replaceChildren();
|
|
const items = visibleItems();
|
|
for (const device of items) {
|
|
const row = document.createElement("tr");
|
|
const selectCell = document.createElement("td");
|
|
selectCell.appendChild(createPreviewCheckbox(device));
|
|
row.appendChild(selectCell);
|
|
addTextCell(row, device.name, device.serial_number);
|
|
addTextCell(row, labels.modality[device.modality] || device.modality, (device.capabilities || []).map((value) => labels.capability[value] || value).join(" / "));
|
|
const statusCell = document.createElement("td");
|
|
statusCell.appendChild(createStatusBadge(device));
|
|
row.appendChild(statusCell);
|
|
addTextCell(row, labels.desired[device.desired_state] || device.desired_state, labels.actual[device.actual_state] || device.actual_state);
|
|
addTextCell(row, device.area_id || "—", device.projection_versions && device.projection_versions.area_policy_source_version ? `策略 v${device.projection_versions.area_policy_source_version}` : "策略版本未知");
|
|
addTextCell(row, formatTime(device.updated_at), device.next_attempt_at ? `重试 ${formatTime(device.next_attempt_at)}` : "");
|
|
rows.appendChild(row);
|
|
|
|
const card = document.createElement("article");
|
|
card.className = "device-card";
|
|
const cardHead = document.createElement("div");
|
|
cardHead.className = "device-card-head";
|
|
const identity = document.createElement("div");
|
|
const name = document.createElement("strong");
|
|
const serial = document.createElement("small");
|
|
name.textContent = device.name;
|
|
serial.textContent = device.serial_number;
|
|
identity.append(name, serial);
|
|
cardHead.append(identity, createPreviewCheckbox(device));
|
|
const status = createStatusBadge(device);
|
|
const details = document.createElement("dl");
|
|
const pairs = [
|
|
["状态", "", status],
|
|
["模态", labels.modality[device.modality] || device.modality],
|
|
["期望 / 实际", `${labels.desired[device.desired_state] || device.desired_state} / ${labels.actual[device.actual_state] || device.actual_state}`],
|
|
["Area", device.area_id || "—"],
|
|
];
|
|
for (const [term, value, node] of pairs) {
|
|
const wrapper = document.createElement("div");
|
|
const dt = document.createElement("dt");
|
|
const dd = document.createElement("dd");
|
|
dt.textContent = term;
|
|
if (node) dd.appendChild(node); else dd.textContent = value;
|
|
wrapper.append(dt, dd);
|
|
details.appendChild(wrapper);
|
|
}
|
|
card.append(cardHead, details);
|
|
cards.appendChild(card);
|
|
}
|
|
if (items.length === 0) {
|
|
const empty = document.createElement("div");
|
|
empty.className = "empty-compact";
|
|
empty.textContent = state.token ? "当前页没有符合条件的设备。" : "请先建立会话。";
|
|
cards.appendChild(empty.cloneNode(true));
|
|
const cell = document.createElement("td");
|
|
cell.colSpan = 7;
|
|
cell.appendChild(empty);
|
|
const row = document.createElement("tr");
|
|
row.appendChild(cell);
|
|
rows.appendChild(row);
|
|
}
|
|
setText("#pageLabel", `第 ${state.pageIndex + 1} 页 · 每页 ${state.config.page_size} · 当前显示 ${items.length} 项`);
|
|
$("#prevPage").disabled = state.loading || state.pageIndex === 0;
|
|
$("#nextPage").disabled = state.loading || !state.nextCursor;
|
|
}
|
|
|
|
function toggleSelection(device, checkbox) {
|
|
if (checkbox.checked) {
|
|
if (!isPreviewEligible(device)) {
|
|
checkbox.checked = false;
|
|
showToast("只有在线、已启用且已收敛的视频设备可以预览");
|
|
return;
|
|
}
|
|
if (!state.selected.has(device.id) && state.selected.size >= state.config.max_active_previews) {
|
|
checkbox.checked = false;
|
|
showToast(`最多同时选择 ${state.config.max_active_previews} 路预览`);
|
|
return;
|
|
}
|
|
state.selected.set(device.id, device);
|
|
} else {
|
|
state.selected.delete(device.id);
|
|
if (state.previewing.has(device.id)) stopPreview(device.id);
|
|
}
|
|
renderSelection();
|
|
renderDevices();
|
|
}
|
|
|
|
function renderSelection() {
|
|
setText("#selectionCount", state.selected.size);
|
|
setText("#previewCount", `${state.previewing.size}/${state.config.max_active_previews}`);
|
|
$("#startPreview").disabled = state.selected.size === 0;
|
|
$("#stopAll").disabled = state.previewing.size === 0;
|
|
}
|
|
|
|
function previewURL(deviceID) {
|
|
const url = new URL(`${state.config.webrtc_base_url}/devices/${encodeURIComponent(deviceID)}`);
|
|
url.searchParams.set("controls", "true");
|
|
url.searchParams.set("muted", "true");
|
|
url.searchParams.set("autoplay", "true");
|
|
url.searchParams.set("playsInline", "true");
|
|
return url.toString();
|
|
}
|
|
|
|
function startSelectedPreviews() {
|
|
if (state.selected.size === 0) return;
|
|
state.previewing = new Set(state.selected.keys());
|
|
const grid = $("#previewGrid");
|
|
grid.replaceChildren();
|
|
for (const device of state.selected.values()) {
|
|
const card = document.createElement("article");
|
|
card.className = "preview-card";
|
|
card.dataset.deviceId = device.id;
|
|
const frame = document.createElement("div");
|
|
frame.className = "preview-frame";
|
|
const iframe = document.createElement("iframe");
|
|
iframe.title = `${device.name} 实时预览`;
|
|
iframe.loading = "eager";
|
|
iframe.allow = "autoplay; fullscreen";
|
|
iframe.sandbox = "allow-scripts allow-same-origin";
|
|
iframe.referrerPolicy = "no-referrer";
|
|
iframe.src = previewURL(device.id);
|
|
frame.appendChild(iframe);
|
|
const meta = document.createElement("div");
|
|
meta.className = "preview-meta";
|
|
const identity = document.createElement("div");
|
|
const name = document.createElement("strong");
|
|
const note = document.createElement("small");
|
|
name.textContent = device.name;
|
|
note.textContent = "MediaMTX WebRTC · 按需读取";
|
|
identity.append(name, note);
|
|
const stop = document.createElement("button");
|
|
stop.type = "button";
|
|
stop.className = "button secondary";
|
|
stop.textContent = "停止";
|
|
stop.addEventListener("click", () => stopPreview(device.id));
|
|
meta.append(identity, stop);
|
|
card.append(frame, meta);
|
|
grid.appendChild(card);
|
|
}
|
|
switchView("monitor");
|
|
renderSelection();
|
|
showToast(`已按需启动 ${state.previewing.size} 路预览`);
|
|
}
|
|
|
|
function stopPreview(deviceID) {
|
|
state.previewing.delete(deviceID);
|
|
const card = $(`[data-device-id="${CSS.escape(deviceID)}"]`);
|
|
if (card) {
|
|
const iframe = card.querySelector("iframe");
|
|
if (iframe) iframe.removeAttribute("src");
|
|
card.remove();
|
|
}
|
|
if (state.previewing.size === 0) renderPreviewEmpty();
|
|
renderSelection();
|
|
}
|
|
|
|
function stopAllPreviews() {
|
|
for (const iframe of $$("#previewGrid iframe")) iframe.removeAttribute("src");
|
|
state.previewing.clear();
|
|
renderPreviewEmpty();
|
|
renderSelection();
|
|
showToast("全部预览已停止");
|
|
}
|
|
|
|
function renderPreviewEmpty() {
|
|
const grid = $("#previewGrid");
|
|
grid.replaceChildren();
|
|
const empty = document.createElement("div");
|
|
empty.className = "preview-empty";
|
|
const mark = document.createElement("span");
|
|
mark.setAttribute("aria-hidden", "true");
|
|
mark.textContent = "▷";
|
|
const title = document.createElement("strong");
|
|
title.textContent = "尚未启动预览";
|
|
const note = document.createElement("p");
|
|
note.textContent = "前往设备页选择最多 4 路运行中的视频设备。";
|
|
empty.append(mark, title, note);
|
|
grid.appendChild(empty);
|
|
}
|
|
|
|
function renderHealth() {
|
|
const container = $("#overviewHealth");
|
|
container.replaceChildren();
|
|
if (!state.token || state.items.length === 0) {
|
|
const empty = document.createElement("div");
|
|
empty.className = "empty-compact";
|
|
empty.textContent = state.token ? "当前页没有设备事实。" : "建立会话后显示真实状态。";
|
|
container.appendChild(empty);
|
|
return;
|
|
}
|
|
const groups = [
|
|
["在线且已收敛", state.items.filter((device) => device.actual_state === "online" && device.converged).length, "设备可用于按需预览"],
|
|
["等待收敛", state.items.filter((device) => !device.converged).length, "期望 generation 尚未被完整观察"],
|
|
["离线或失败", state.items.filter((device) => ["offline", "failed"].includes(device.actual_state)).length, "查看重试时间和稳定错误码"],
|
|
];
|
|
for (const [title, count, note] of groups) container.appendChild(createHealthItem(title, count, note));
|
|
}
|
|
|
|
function createHealthItem(title, count, note) {
|
|
const item = document.createElement("div");
|
|
item.className = "health-item";
|
|
const text = document.createElement("div");
|
|
const strong = document.createElement("strong");
|
|
const small = document.createElement("small");
|
|
strong.textContent = title;
|
|
small.textContent = note;
|
|
text.append(strong, small);
|
|
const value = document.createElement("span");
|
|
value.className = "value";
|
|
value.textContent = String(count);
|
|
item.append(text, value);
|
|
return item;
|
|
}
|
|
|
|
function renderOperations() {
|
|
const container = $("#operationIssues");
|
|
container.replaceChildren();
|
|
const issues = state.items.filter(attentionNeeded);
|
|
if (issues.length === 0) {
|
|
const empty = document.createElement("div");
|
|
empty.className = "empty-compact";
|
|
empty.textContent = state.token ? "当前页没有已知对账差异;这不代表未加载设备或其他基础设施正常。" : "建立会话后显示。";
|
|
container.appendChild(empty);
|
|
return;
|
|
}
|
|
for (const device of issues) {
|
|
const message = device.last_error_code ? `错误 ${device.last_error_code}` : `generation ${device.observed_generation || 0} / ${device.generation || 0}`;
|
|
container.appendChild(createHealthItem(device.name, device.failure_count || 0, device.next_attempt_at ? `${message} · 下次 ${formatTime(device.next_attempt_at)}` : message));
|
|
}
|
|
}
|
|
|
|
function switchView(name) {
|
|
$$(".view").forEach((view) => {
|
|
const active = view.dataset.page === name;
|
|
view.hidden = !active;
|
|
view.classList.toggle("active", active);
|
|
});
|
|
$$('[data-view]').forEach((button) => {
|
|
const active = button.dataset.view === name;
|
|
button.toggleAttribute("aria-current", active);
|
|
});
|
|
$("#main-content").focus({ preventScroll: true });
|
|
window.scrollTo({ top: 0, behavior: "auto" });
|
|
}
|
|
|
|
function resetPaging() {
|
|
state.cursors = [null];
|
|
state.pageIndex = 0;
|
|
state.nextCursor = null;
|
|
}
|
|
|
|
async function connect(event) {
|
|
event.preventDefault();
|
|
const siteInput = $("#siteInput");
|
|
const tokenInput = $("#tokenInput");
|
|
const site = siteInput.value.trim();
|
|
const token = tokenInput.value;
|
|
const errorBox = $("#contextError");
|
|
errorBox.hidden = true;
|
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/.test(site)) {
|
|
errorBox.textContent = "Site ID 格式无效:只能使用 1~64 位字母、数字、点、下划线、冒号或连字符。";
|
|
errorBox.hidden = false;
|
|
siteInput.focus();
|
|
return;
|
|
}
|
|
if (!token || token.length > 4096) {
|
|
errorBox.textContent = "请输入有效的 Bearer token。";
|
|
errorBox.hidden = false;
|
|
tokenInput.focus();
|
|
return;
|
|
}
|
|
state.siteId = site;
|
|
state.token = token;
|
|
tokenInput.value = "";
|
|
setText("#siteContext", site);
|
|
resetPaging();
|
|
try {
|
|
await loadDevices({ announce: false });
|
|
$("#contextDialog").close();
|
|
showToast("会话已建立;token 仅驻留当前页面内存");
|
|
} catch (error) {
|
|
errorBox.textContent = error.status === 401 ? "认证失败,请检查 token 后重试。" : "无法读取该站点,请按页面提示检查权限或服务状态。";
|
|
errorBox.hidden = false;
|
|
$("#contextDialog").showModal();
|
|
}
|
|
}
|
|
|
|
async function initialize() {
|
|
try {
|
|
const response = await fetch("/sense-console/config", { cache: "no-store" });
|
|
if (!response.ok) throw new Error("config unavailable");
|
|
state.config = await response.json();
|
|
if (state.config.page_size !== 16 || state.config.max_active_previews !== 4 || state.config.recording_and_playback !== false) {
|
|
throw new Error("unsafe console bounds");
|
|
}
|
|
} catch (_) {
|
|
showDegraded("控制台配置不可用", "页面保持只读且不会尝试连接视频,请检查 Sense 启动配置。");
|
|
$$("button, input, select").forEach((control) => { control.disabled = true; });
|
|
return;
|
|
}
|
|
setConnected(false);
|
|
renderAll();
|
|
$("#contextDialog").showModal();
|
|
}
|
|
|
|
$$('[data-view]').forEach((button) => button.addEventListener("click", () => switchView(button.dataset.view)));
|
|
$$('[data-view-target]').forEach((button) => button.addEventListener("click", () => switchView(button.dataset.viewTarget)));
|
|
$("#changeContext").addEventListener("click", () => $("#contextDialog").showModal());
|
|
$("#closeContext").addEventListener("click", () => $("#contextDialog").close());
|
|
$("#cancelContext").addEventListener("click", () => $("#contextDialog").close());
|
|
$("#contextForm").addEventListener("submit", connect);
|
|
$("#refreshDevices").addEventListener("click", () => loadDevices());
|
|
$("#refreshOperations").addEventListener("click", () => loadDevices());
|
|
$("#retryLoad").addEventListener("click", () => state.token ? loadDevices() : $("#contextDialog").showModal());
|
|
$("#startPreview").addEventListener("click", startSelectedPreviews);
|
|
$("#stopAll").addEventListener("click", stopAllPreviews);
|
|
$("#deviceSearch").addEventListener("input", renderDevices);
|
|
|
|
for (const selector of ["#modalityFilter", "#desiredFilter", "#actualFilter"]) {
|
|
$(selector).addEventListener("change", () => {
|
|
resetPaging();
|
|
if (state.token) loadDevices();
|
|
});
|
|
}
|
|
|
|
$("#clearFilters").addEventListener("click", () => {
|
|
$("#deviceSearch").value = "";
|
|
$("#modalityFilter").value = "";
|
|
$("#desiredFilter").value = "";
|
|
$("#actualFilter").value = "";
|
|
resetPaging();
|
|
if (state.token) loadDevices(); else renderDevices();
|
|
});
|
|
|
|
$("#nextPage").addEventListener("click", async () => {
|
|
if (!state.nextCursor) return;
|
|
state.cursors[state.pageIndex + 1] = state.nextCursor;
|
|
state.pageIndex += 1;
|
|
try { await loadDevices(); } catch (_) { state.pageIndex -= 1; }
|
|
});
|
|
|
|
$("#prevPage").addEventListener("click", async () => {
|
|
if (state.pageIndex === 0) return;
|
|
state.pageIndex -= 1;
|
|
try { await loadDevices(); } catch (_) { state.pageIndex += 1; }
|
|
});
|
|
|
|
window.addEventListener("beforeunload", () => {
|
|
state.token = "";
|
|
for (const iframe of $$("#previewGrid iframe")) iframe.removeAttribute("src");
|
|
});
|
|
|
|
initialize();
|