feat: ws backoff, signal dots, dom cap, unwrap fix, batch size limit
- logs.js: WS reconnect exponential backoff 1s→2s→4s…30s - ops.js: replace badge+text signal display with red/green/yellow dots (sig-on=green, sig-fault=red, sig-warn=yellow, gray=off) - events.js: cap live-prepended event cards at 100 DOM nodes - source.rs: fix attach_children unwrap() → Option<TreeNode>/filter_map - point.rs: add max=500 validation to all batch Vec<Uuid> fields Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
13c4b515d7
commit
a8d36578fa
|
|
@ -161,12 +161,14 @@ pub struct UpdatePointReq {
|
||||||
/// Request payload for batch setting point tags.
|
/// Request payload for batch setting point tags.
|
||||||
#[derive(Deserialize, Validate)]
|
#[derive(Deserialize, Validate)]
|
||||||
pub struct BatchSetPointTagsReq {
|
pub struct BatchSetPointTagsReq {
|
||||||
|
#[validate(length(min = 1, max = 500))]
|
||||||
pub point_ids: Vec<Uuid>,
|
pub point_ids: Vec<Uuid>,
|
||||||
pub tag_id: Option<Uuid>,
|
pub tag_id: Option<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Validate)]
|
#[derive(Deserialize, Validate)]
|
||||||
pub struct BatchSetPointEquipmentReq {
|
pub struct BatchSetPointEquipmentReq {
|
||||||
|
#[validate(length(min = 1, max = 500))]
|
||||||
pub point_ids: Vec<Uuid>,
|
pub point_ids: Vec<Uuid>,
|
||||||
pub equipment_id: Option<Uuid>,
|
pub equipment_id: Option<Uuid>,
|
||||||
pub signal_role: Option<String>,
|
pub signal_role: Option<String>,
|
||||||
|
|
@ -448,6 +450,7 @@ pub async fn delete_point(
|
||||||
#[derive(Deserialize, Validate)]
|
#[derive(Deserialize, Validate)]
|
||||||
/// Request payload for batch point creation from node ids.
|
/// Request payload for batch point creation from node ids.
|
||||||
pub struct BatchCreatePointsReq {
|
pub struct BatchCreatePointsReq {
|
||||||
|
#[validate(length(min = 1, max = 500))]
|
||||||
pub node_ids: Vec<Uuid>,
|
pub node_ids: Vec<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -563,6 +566,7 @@ pub async fn batch_create_points(
|
||||||
#[derive(Deserialize, Validate)]
|
#[derive(Deserialize, Validate)]
|
||||||
/// Request payload for batch point deletion.
|
/// Request payload for batch point deletion.
|
||||||
pub struct BatchDeletePointsReq {
|
pub struct BatchDeletePointsReq {
|
||||||
|
#[validate(length(min = 1, max = 500))]
|
||||||
pub point_ids: Vec<Uuid>,
|
pub point_ids: Vec<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -171,23 +171,24 @@ fn build_node_tree(nodes: Vec<Node>) -> Vec<TreeNode> {
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
node_map: &mut HashMap<Uuid, TreeNode>,
|
node_map: &mut HashMap<Uuid, TreeNode>,
|
||||||
children_map: &HashMap<Uuid, Vec<Uuid>>,
|
children_map: &HashMap<Uuid, Vec<Uuid>>,
|
||||||
) -> TreeNode {
|
) -> Option<TreeNode> {
|
||||||
let mut node = node_map.remove(&id).unwrap();
|
let mut node = node_map.remove(&id)?;
|
||||||
|
|
||||||
if let Some(child_ids) = children_map.get(&id) {
|
if let Some(child_ids) = children_map.get(&id) {
|
||||||
for &cid in child_ids {
|
for &cid in child_ids {
|
||||||
let child = attach_children(cid, node_map, children_map);
|
if let Some(child) = attach_children(cid, node_map, children_map) {
|
||||||
node.children.push(child);
|
node.children.push(child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
node
|
Some(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ③ 生成最终树
|
// ③ 生成最终树
|
||||||
roots
|
roots
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|rid| attach_children(rid, &mut node_map, &children_map))
|
.filter_map(|rid| attach_children(rid, &mut node_map, &children_map))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,10 @@ export function prependEvent(item) {
|
||||||
if (placeholder) placeholder.remove();
|
if (placeholder) placeholder.remove();
|
||||||
|
|
||||||
dom.eventList.insertBefore(makeCard(item), dom.eventList.firstChild);
|
dom.eventList.insertBefore(makeCard(item), dom.eventList.firstChild);
|
||||||
|
|
||||||
|
// Keep DOM bounded to prevent unbounded growth
|
||||||
|
const cards = dom.eventList.querySelectorAll(".event-card");
|
||||||
|
if (cards.length > 100) cards[cards.length - 1].remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
dom.eventList.addEventListener("scroll", () => {
|
dom.eventList.addEventListener("scroll", () => {
|
||||||
|
|
|
||||||
|
|
@ -79,12 +79,17 @@ function setWsStatus(connected) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _reconnectDelay = 1000;
|
||||||
|
|
||||||
export function startPointSocket() {
|
export function startPointSocket() {
|
||||||
const protocol = location.protocol === "https:" ? "wss" : "ws";
|
const protocol = location.protocol === "https:" ? "wss" : "ws";
|
||||||
const ws = new WebSocket(`${protocol}://${location.host}/ws/public`);
|
const ws = new WebSocket(`${protocol}://${location.host}/ws/public`);
|
||||||
state.pointSocket = ws;
|
state.pointSocket = ws;
|
||||||
|
|
||||||
ws.onopen = () => setWsStatus(true);
|
ws.onopen = () => {
|
||||||
|
setWsStatus(true);
|
||||||
|
_reconnectDelay = 1000;
|
||||||
|
};
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -101,12 +106,14 @@ export function startPointSocket() {
|
||||||
entry.time.textContent = data.timestamp || "--";
|
entry.time.textContent = data.timestamp || "--";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ops view signal cell
|
// ops view signal dot
|
||||||
const opsEntry = state.opsPointEls.get(data.point_id);
|
const opsEntry = state.opsPointEls.get(data.point_id);
|
||||||
if (opsEntry) {
|
if (opsEntry) {
|
||||||
opsEntry.valueEl.textContent = formatValue(data);
|
const { dotEl } = opsEntry;
|
||||||
opsEntry.qualityEl.className = `badge quality-${(data.quality || "unknown").toLowerCase()}`;
|
const role = dotEl.dataset.opsRole;
|
||||||
opsEntry.qualityEl.textContent = (data.quality || "unknown").toUpperCase();
|
import("./ops.js").then(({ sigDotClass }) => {
|
||||||
|
dotEl.className = sigDotClass(role, data.quality, data.value_text);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.chartPointId === data.point_id) {
|
if (state.chartPointId === data.point_id) {
|
||||||
|
|
@ -137,7 +144,8 @@ export function startPointSocket() {
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
setWsStatus(false);
|
setWsStatus(false);
|
||||||
window.setTimeout(startPointSocket, 2000);
|
window.setTimeout(startPointSocket, _reconnectDelay);
|
||||||
|
_reconnectDelay = Math.min(_reconnectDelay * 2, 30000);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = () => setWsStatus(false);
|
ws.onerror = () => setWsStatus(false);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,14 @@ 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" };
|
||||||
|
|
||||||
|
export function sigDotClass(role, quality, valueText) {
|
||||||
|
if (!quality || quality.toLowerCase() !== "good") return "sig-dot sig-warn";
|
||||||
|
const v = String(valueText ?? "").trim().toLowerCase();
|
||||||
|
const on = v === "1" || v === "true" || v === "on";
|
||||||
|
if (!on) return "sig-dot";
|
||||||
|
return role === "flt" ? "sig-dot sig-fault" : "sig-dot sig-on";
|
||||||
|
}
|
||||||
|
|
||||||
function runtimeBadge(runtime) {
|
function runtimeBadge(runtime) {
|
||||||
if (!runtime) return '<span class="badge offline">OFFLINE</span>';
|
if (!runtime) return '<span class="badge offline">OFFLINE</span>';
|
||||||
if (runtime.comm_locked) return '<span class="badge offline">COMM ERR</span>';
|
if (runtime.comm_locked) return '<span class="badge offline">COMM ERR</span>';
|
||||||
|
|
@ -128,8 +136,7 @@ function renderOpsEquipments(equipments) {
|
||||||
return `
|
return `
|
||||||
<div class="ops-signal-row">
|
<div class="ops-signal-row">
|
||||||
<span class="ops-signal-label">${ROLE_LABELS[role] || role}</span>
|
<span class="ops-signal-label">${ROLE_LABELS[role] || role}</span>
|
||||||
<span class="badge quality-unknown" data-ops-quality="${point.id}">?</span>
|
<span class="sig-dot sig-warn" data-ops-dot="${point.id}" data-ops-role="${role}"></span>
|
||||||
<span class="ops-signal-value" data-ops-value="${point.id}">--</span>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
|
|
||||||
|
|
@ -172,15 +179,12 @@ function renderOpsEquipments(equipments) {
|
||||||
SIGNAL_ROLES.forEach((role) => {
|
SIGNAL_ROLES.forEach((role) => {
|
||||||
const point = roleMap[role];
|
const point = roleMap[role];
|
||||||
if (!point) return;
|
if (!point) return;
|
||||||
const valueEl = card.querySelector(`[data-ops-value="${point.id}"]`);
|
const dotEl = card.querySelector(`[data-ops-dot="${point.id}"]`);
|
||||||
const qualityEl = card.querySelector(`[data-ops-quality="${point.id}"]`);
|
if (dotEl) {
|
||||||
if (valueEl && qualityEl) {
|
state.opsPointEls.set(point.id, { dotEl });
|
||||||
state.opsPointEls.set(point.id, { valueEl, qualityEl });
|
|
||||||
if (point.point_monitor) {
|
if (point.point_monitor) {
|
||||||
const m = point.point_monitor;
|
const m = point.point_monitor;
|
||||||
valueEl.textContent = formatValue(m);
|
dotEl.className = sigDotClass(role, m.quality, m.value_text);
|
||||||
qualityEl.className = `badge quality-${(m.quality || "unknown").toLowerCase()}`;
|
|
||||||
qualityEl.textContent = (m.quality || "unknown").toUpperCase();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -261,6 +261,18 @@ body {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sig-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: var(--text-3);
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.sig-dot.sig-on { background: var(--success); }
|
||||||
|
.sig-dot.sig-fault { background: var(--danger); }
|
||||||
|
.sig-dot.sig-warn { background: var(--warning); }
|
||||||
|
|
||||||
.ops-eq-card-actions {
|
.ops-eq-card-actions {
|
||||||
padding: 6px 10px 8px;
|
padding: 6px 10px 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue