feat(web): add runtime badge and auto/ack buttons to ops unit list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-03-25 10:47:09 +08:00
parent 4076f6575e
commit 36cfe9ecfc
2 changed files with 53 additions and 1 deletions

View File

@ -2,10 +2,20 @@ import { apiFetch } from "./api.js";
import { dom } from "./dom.js"; import { dom } from "./dom.js";
import { formatValue } from "./points.js"; import { formatValue } from "./points.js";
import { state } from "./state.js"; import { state } from "./state.js";
import { loadUnits } from "./units.js";
const SIGNAL_ROLES = ["rem", "run", "flt"]; const SIGNAL_ROLES = ["rem", "run", "flt"];
const ROLE_LABELS = { rem: "REM", run: "RUN", flt: "FLT" }; const ROLE_LABELS = { rem: "REM", run: "RUN", flt: "FLT" };
function runtimeBadge(runtime) {
if (!runtime) return '<span class="badge offline">OFFLINE</span>';
if (runtime.comm_locked) return '<span class="badge offline">COMM ERR</span>';
if (runtime.fault_locked) return '<span class="badge danger">FAULT</span>';
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 `<span class="badge ${cls[runtime.state] ?? ""}">${labels[runtime.state] ?? runtime.state}</span>`;
}
export function renderOpsUnits() { export function renderOpsUnits() {
if (!dom.opsUnitList) return; if (!dom.opsUnitList) return;
dom.opsUnitList.innerHTML = ""; dom.opsUnitList.innerHTML = "";
@ -22,11 +32,41 @@ export function renderOpsUnits() {
item.innerHTML = ` item.innerHTML = `
<div class="ops-unit-item-name">${unit.code} / ${unit.name}</div> <div class="ops-unit-item-name">${unit.code} / ${unit.name}</div>
<div class="ops-unit-item-meta"> <div class="ops-unit-item-meta">
${runtimeBadge(runtime)}
<span class="badge ${unit.enabled ? "" : "offline"}">${unit.enabled ? "EN" : "DIS"}</span> <span class="badge ${unit.enabled ? "" : "offline"}">${unit.enabled ? "EN" : "DIS"}</span>
${runtime ? `<span>Acc ${Math.floor(runtime.accumulated_run_sec / 1000)}s</span>` : ""} ${runtime ? `<span class="muted">Acc ${Math.floor(runtime.accumulated_run_sec / 1000)}s</span>` : ""}
</div> </div>
<div class="ops-unit-item-actions"></div>
`; `;
item.addEventListener("click", () => selectOpsUnit(unit.id)); 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); dom.opsUnitList.appendChild(item);
}); });
} }

View File

@ -286,9 +286,21 @@ body {
font-size: 11px; font-size: 11px;
color: var(--text-3); color: var(--text-3);
display: flex; display: flex;
align-items: center;
gap: 6px; 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 Header ───────────────────────────────── */
.panel-head { .panel-head {