update User, User group, IO modules
Some checks failed
Test / test (push) Has been cancelled

This commit is contained in:
2026-06-23 11:06:26 +07:00
parent 523f98b74c
commit 50a2587cef
32 changed files with 4728 additions and 52 deletions

View File

@@ -56,6 +56,7 @@
userGroups: [],
maps: [],
mapsLoaded: false,
ioModules: [],
robotPose: null,
robotPoseAt: 0,
mapPollTimer: null,
@@ -277,6 +278,167 @@
});
}
async function ensureIoModulesLoaded() {
if (store.ioModules.length) return store.ioModules;
try {
if (window.IoModulesCatalog?.refresh) {
store.ioModules = await window.IoModulesCatalog.refresh();
} else {
const res = await fetch("/api/io_modules", { credentials: "include" });
if (res.ok) {
const data = await res.json();
store.ioModules = Array.isArray(data.io_modules) ? data.io_modules : [];
}
}
} catch {
store.ioModules = [];
}
return store.ioModules;
}
function ioModuleOptions(selectedId) {
const list = store.ioModules || [];
if (!list.length) return `<option value="">${escapeHtml(t("ioModules.empty"))}</option>`;
return list
.map(
(m) =>
`<option value="${escapeHtml(m.id)}" ${m.id === selectedId ? "selected" : ""}>${escapeHtml(m.name || m.id)}</option>`
)
.join("");
}
function ioModuleById(id) {
return (store.ioModules || []).find((m) => m.id === id) || null;
}
async function setIoModuleConnected(id, connect) {
const path = connect ? "connect" : "disconnect";
const res = await fetch(`/api/io_modules/${encodeURIComponent(id)}/${path}`, {
method: "POST",
credentials: "include",
});
if (!res.ok) {
const text = await res.text();
throw new Error(text || res.statusText);
}
const data = await res.json();
const idx = store.ioModules.findIndex((m) => m.id === id);
if (idx >= 0) store.ioModules[idx] = data;
return data;
}
function renderIoConnectWidget(widget, bodyEl) {
if (!bodyEl) return;
const mod = ioModuleById(widget.io_module_id);
const connected = !!mod?.connected;
const label = widget.title || mod?.name || t("dashboard.widget.io_connect");
bodyEl.innerHTML = `
<button type="button" class="dashboardMirMissionBtn dashboardIoConnectBtn" data-io-connect="${escapeHtml(widget.io_module_id || "")}">
${escapeHtml(connected ? t("dashboard.widget.io_disconnect") : t("dashboard.widget.io_connectAction"))}
</button>
<p class="mutedNote dashboardIoWidgetMeta">${escapeHtml(label)} · ${escapeHtml(mod ? (mod.connected ? t("ioModules.status.connected") : t("ioModules.status.disconnected")) : "—")}</p>`;
const btn = bodyEl.querySelector("[data-io-connect]");
btn?.addEventListener("click", async () => {
if (!widget.io_module_id) return;
try {
await setIoModuleConnected(widget.io_module_id, !connected);
renderIoConnectWidget(widget, bodyEl);
} catch (e) {
alert(e.message);
}
});
}
function renderIoStatusWidget(widget, bodyEl) {
if (!bodyEl) return;
const mod = ioModuleById(widget.io_module_id);
const label = widget.title || mod?.name || t("dashboard.widget.io_status");
bodyEl.innerHTML = `
<div class="dashboardIoStatusCard">
<div class="dashboardIoStatusName">${escapeHtml(label)}</div>
<div class="dashboardIoStatusLine">${escapeHtml(mod?.module_type ? t(`ioModules.type.${mod.module_type}`) : "—")} · ${escapeHtml(mod?.ip_address || "—")}</div>
<div class="dashboardIoStatusBadge ${mod?.connected ? "is-on" : "is-off"}">${escapeHtml(mod ? (mod.connected ? t("ioModules.status.connected") : t("ioModules.status.disconnected")) : "—")}</div>
</div>`;
}
async function fetchIoRuntime(moduleId) {
const res = await fetch(`/api/io_modules/${encodeURIComponent(moduleId)}/runtime`, { credentials: "include" });
if (!res.ok) throw new Error(res.statusText);
return res.json();
}
async function setIoOutput(moduleId, port, value, timeoutMs = 0) {
const res = await fetch(`/api/io_modules/${encodeURIComponent(moduleId)}/output`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ output: port, value, timeout_ms: timeoutMs }),
});
if (!res.ok) {
const text = await res.text();
throw new Error(text || res.statusText);
}
return res.json();
}
function ioPortRange(mod) {
return mod?.module_type === "wise" ? [0, 1, 2, 3] : [1, 2, 3, 4];
}
function renderIoConfigurationWidget(widget, bodyEl) {
if (!bodyEl) return;
const mod = ioModuleById(widget.io_module_id);
const label = widget.title || mod?.name || t("dashboard.widget.io_configuration");
if (!mod) {
bodyEl.innerHTML = `<p class="mutedNote">${escapeHtml(t("dashboard.widget.configHint"))}</p>`;
return;
}
const ports = ioPortRange(mod);
bodyEl.innerHTML = `
<div class="dashboardIoConfigHead">
<div class="dashboardIoStatusName">${escapeHtml(label)}</div>
<div class="dashboardIoStatusBadge ${mod.connected ? "is-on" : "is-off"}">${escapeHtml(mod.connected ? t("ioModules.status.connected") : t("ioModules.status.disconnected"))}</div>
</div>
<div class="dashboardIoConfigPorts" data-io-config-ports></div>`;
const portsEl = bodyEl.querySelector("[data-io-config-ports]");
ports.forEach((port) => {
const row = document.createElement("div");
row.className = "dashboardIoConfigRow";
row.innerHTML = `
<span class="dashboardIoConfigPortLabel">${escapeHtml(t("dashboard.widget.ioPort", { port }))}</span>
<button type="button" class="dashboardIoConfigToggle" data-port="${port}" data-value="off">${escapeHtml(t("dashboard.widget.ioOff"))}</button>
<button type="button" class="dashboardIoConfigToggle is-primary" data-port="${port}" data-value="on">${escapeHtml(t("dashboard.widget.ioOn"))}</button>`;
portsEl.appendChild(row);
});
const refresh = async () => {
try {
const rt = await fetchIoRuntime(mod.id);
portsEl.querySelectorAll("[data-port]").forEach((btn) => {
const port = Number(btn.dataset.port);
const outputs = Array.isArray(rt.outputs) ? rt.outputs : [];
const idx = mod.module_type === "wise" ? port : port;
const on = !!outputs[idx];
btn.classList.toggle("is-active", (btn.dataset.value === "on") === on);
});
} catch {
/* ignore */
}
};
portsEl.addEventListener("click", async (evt) => {
const btn = evt.target.closest("[data-port]");
if (!btn || !widget.io_module_id) return;
const port = Number(btn.dataset.port);
const on = btn.dataset.value === "on";
try {
await setIoOutput(widget.io_module_id, port, on);
await refresh();
} catch (e) {
alert(e.message);
}
});
void refresh();
}
function widgetTypeLabel(type) {
return t(`dashboard.widget.${type}`) || type;
}
@@ -986,6 +1148,16 @@
<label>${t("dashboard.widget.field.title")}</label>
<input data-field="title" type="text" value="${escapeHtml(widget.title || t("dashboard.widget.robot_summary"))}" />
</div>`;
} else if (type === "io_connect" || type === "io_status" || type === "io_configuration") {
container.innerHTML = `
<div class="row rowWide">
<label>${t("dashboard.widget.field.ioModule")}</label>
<select data-field="io_module_id">${ioModuleOptions(widget.io_module_id || "")}</select>
</div>
<div class="row rowWide">
<label>${t("dashboard.widget.field.title")}</label>
<input data-field="title" type="text" value="${escapeHtml(widget.title || "")}" placeholder="${escapeHtml(widgetTypeLabel(type))}" />
</div>`;
}
}
@@ -1209,6 +1381,15 @@
case "robot_summary":
renderRobotSummaryWidget(widget, bodyEl);
break;
case "io_connect":
renderIoConnectWidget(widget, bodyEl);
break;
case "io_status":
renderIoStatusWidget(widget, bodyEl);
break;
case "io_configuration":
renderIoConfigurationWidget(widget, bodyEl);
break;
default:
bodyEl.innerHTML = `<p class="mutedNote">${t("dashboard.widget.unsupported")}</p>`;
}
@@ -1245,6 +1426,9 @@
if (widget.type === "mission_queue") refreshQueueWidget(bodyEl);
if (widget.type === "pause_continue") renderPauseContinueWidget(widget, bodyEl);
if (widget.type === "mission_action_log") renderMissionActionLogWidget(widget, bodyEl);
if (widget.type === "io_connect") renderIoConnectWidget(widget, bodyEl);
if (widget.type === "io_status") renderIoStatusWidget(widget, bodyEl);
if (widget.type === "io_configuration") renderIoConfigurationWidget(widget, bodyEl);
});
if (hasMapWidget()) refreshMapWidgets();
}
@@ -1260,6 +1444,8 @@
};
if (widget.type === "map" || widget.type === "map_locked") {
void ensureMapsLoaded().then(open);
} else if (widget.type === "io_connect" || widget.type === "io_status" || widget.type === "io_configuration") {
void ensureIoModulesLoaded().then(open);
} else {
open();
}
@@ -1484,6 +1670,7 @@
async function init() {
await loadStoreFromBackend();
await loadUserGroups();
await ensureIoModulesLoaded();
bindEvents();
setView("list");
}
@@ -1493,6 +1680,7 @@
getNavItems,
handleNav,
onPageShow() {
void ensureIoModulesLoaded();
if (store.view === "designer") {
syncDesignerEditMode();
renderDesignerChrome();