From 36cfe9ecfc0b2e9fad1fb79b23683ae76cbaf2aa Mon Sep 17 00:00:00 2001 From: caoqianming Date: Wed, 25 Mar 2026 10:47:09 +0800 Subject: [PATCH] feat(web): add runtime badge and auto/ack buttons to ops unit list Co-Authored-By: Claude Sonnet 4.6 --- web/js/ops.js | 42 +++++++++++++++++++++++++++++++++++++++++- web/styles.css | 12 ++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/web/js/ops.js b/web/js/ops.js index 1a166d3..2deb666 100644 --- a/web/js/ops.js +++ b/web/js/ops.js @@ -2,10 +2,20 @@ import { apiFetch } from "./api.js"; import { dom } from "./dom.js"; import { formatValue } from "./points.js"; import { state } from "./state.js"; +import { loadUnits } from "./units.js"; const SIGNAL_ROLES = ["rem", "run", "flt"]; const ROLE_LABELS = { rem: "REM", run: "RUN", flt: "FLT" }; +function runtimeBadge(runtime) { + if (!runtime) return 'OFFLINE'; + if (runtime.comm_locked) return 'COMM ERR'; + if (runtime.fault_locked) return 'FAULT'; + const labels = { stopped: "STOPPED", running: "RUNNING", distributor_running: "DIST RUN", fault_locked: "FAULT", comm_locked: "COMM ERR" }; + const cls = { stopped: "", running: "online", distributor_running: "online", fault_locked: "danger", comm_locked: "offline" }; + return `${labels[runtime.state] ?? runtime.state}`; +} + export function renderOpsUnits() { if (!dom.opsUnitList) return; dom.opsUnitList.innerHTML = ""; @@ -22,11 +32,41 @@ export function renderOpsUnits() { item.innerHTML = `
${unit.code} / ${unit.name}
+ ${runtimeBadge(runtime)} ${unit.enabled ? "EN" : "DIS"} - ${runtime ? `Acc ${Math.floor(runtime.accumulated_run_sec / 1000)}s` : ""} + ${runtime ? `Acc ${Math.floor(runtime.accumulated_run_sec / 1000)}s` : ""}
+
`; item.addEventListener("click", () => selectOpsUnit(unit.id)); + + const actions = item.querySelector(".ops-unit-item-actions"); + + const isAutoOn = runtime?.auto_enabled; + const autoBtn = document.createElement("button"); + autoBtn.className = isAutoOn ? "danger" : "secondary"; + autoBtn.textContent = isAutoOn ? "Stop Auto" : "Start Auto"; + autoBtn.title = isAutoOn ? "停止自动控制" : "启动自动控制"; + autoBtn.addEventListener("click", (e) => { + e.stopPropagation(); + apiFetch(`/api/control/unit/${unit.id}/${isAutoOn ? "stop-auto" : "start-auto"}`, { method: "POST" }) + .then(() => loadUnits()).catch(() => {}); + }); + actions.append(autoBtn); + + if (runtime?.manual_ack_required) { + const ackBtn = document.createElement("button"); + ackBtn.className = "danger"; + ackBtn.textContent = "Ack Fault"; + ackBtn.title = "人工确认解除故障锁定"; + ackBtn.addEventListener("click", (e) => { + e.stopPropagation(); + apiFetch(`/api/control/unit/${unit.id}/ack-fault`, { method: "POST" }) + .then(() => loadUnits()).catch(() => {}); + }); + actions.append(ackBtn); + } + dom.opsUnitList.appendChild(item); }); } diff --git a/web/styles.css b/web/styles.css index 6f6ba89..5c2dd65 100644 --- a/web/styles.css +++ b/web/styles.css @@ -286,9 +286,21 @@ body { font-size: 11px; color: var(--text-3); display: flex; + align-items: center; gap: 6px; } +.ops-unit-item-actions { + display: flex; + gap: 4px; + padding-top: 4px; +} + +.ops-unit-item-actions button { + padding: 2px 8px; + font-size: 11px; +} + /* ── Panel Header ───────────────────────────────── */ .panel-head {