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 {