diff --git a/src/handler/point.rs b/src/handler/point.rs index 5ba5385..f2401f5 100644 --- a/src/handler/point.rs +++ b/src/handler/point.rs @@ -161,12 +161,14 @@ pub struct UpdatePointReq { /// Request payload for batch setting point tags. #[derive(Deserialize, Validate)] pub struct BatchSetPointTagsReq { + #[validate(length(min = 1, max = 500))] pub point_ids: Vec, pub tag_id: Option, } #[derive(Deserialize, Validate)] pub struct BatchSetPointEquipmentReq { + #[validate(length(min = 1, max = 500))] pub point_ids: Vec, pub equipment_id: Option, pub signal_role: Option, @@ -448,6 +450,7 @@ pub async fn delete_point( #[derive(Deserialize, Validate)] /// Request payload for batch point creation from node ids. pub struct BatchCreatePointsReq { + #[validate(length(min = 1, max = 500))] pub node_ids: Vec, } @@ -563,6 +566,7 @@ pub async fn batch_create_points( #[derive(Deserialize, Validate)] /// Request payload for batch point deletion. pub struct BatchDeletePointsReq { + #[validate(length(min = 1, max = 500))] pub point_ids: Vec, } diff --git a/src/handler/source.rs b/src/handler/source.rs index 1d8b361..dfb8e02 100644 --- a/src/handler/source.rs +++ b/src/handler/source.rs @@ -171,23 +171,24 @@ fn build_node_tree(nodes: Vec) -> Vec { id: Uuid, node_map: &mut HashMap, children_map: &HashMap>, - ) -> TreeNode { - let mut node = node_map.remove(&id).unwrap(); + ) -> Option { + let mut node = node_map.remove(&id)?; if let Some(child_ids) = children_map.get(&id) { for &cid in child_ids { - let child = attach_children(cid, node_map, children_map); - node.children.push(child); + if let Some(child) = attach_children(cid, node_map, children_map) { + node.children.push(child); + } } } - node + Some(node) } // ③ 生成最终树 roots .into_iter() - .map(|rid| attach_children(rid, &mut node_map, &children_map)) + .filter_map(|rid| attach_children(rid, &mut node_map, &children_map)) .collect() } diff --git a/web/js/events.js b/web/js/events.js index 91c96b2..f391b2e 100644 --- a/web/js/events.js +++ b/web/js/events.js @@ -71,6 +71,10 @@ export function prependEvent(item) { if (placeholder) placeholder.remove(); 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", () => { diff --git a/web/js/logs.js b/web/js/logs.js index ed96c38..fdf55ad 100644 --- a/web/js/logs.js +++ b/web/js/logs.js @@ -79,12 +79,17 @@ function setWsStatus(connected) { } } +let _reconnectDelay = 1000; + export function startPointSocket() { const protocol = location.protocol === "https:" ? "wss" : "ws"; const ws = new WebSocket(`${protocol}://${location.host}/ws/public`); state.pointSocket = ws; - ws.onopen = () => setWsStatus(true); + ws.onopen = () => { + setWsStatus(true); + _reconnectDelay = 1000; + }; ws.onmessage = (event) => { try { @@ -101,12 +106,14 @@ export function startPointSocket() { entry.time.textContent = data.timestamp || "--"; } - // ops view signal cell + // ops view signal dot const opsEntry = state.opsPointEls.get(data.point_id); if (opsEntry) { - opsEntry.valueEl.textContent = formatValue(data); - opsEntry.qualityEl.className = `badge quality-${(data.quality || "unknown").toLowerCase()}`; - opsEntry.qualityEl.textContent = (data.quality || "unknown").toUpperCase(); + const { dotEl } = opsEntry; + const role = dotEl.dataset.opsRole; + import("./ops.js").then(({ sigDotClass }) => { + dotEl.className = sigDotClass(role, data.quality, data.value_text); + }); } if (state.chartPointId === data.point_id) { @@ -137,7 +144,8 @@ export function startPointSocket() { ws.onclose = () => { setWsStatus(false); - window.setTimeout(startPointSocket, 2000); + window.setTimeout(startPointSocket, _reconnectDelay); + _reconnectDelay = Math.min(_reconnectDelay * 2, 30000); }; ws.onerror = () => setWsStatus(false); diff --git a/web/js/ops.js b/web/js/ops.js index 6080ef3..410c1ac 100644 --- a/web/js/ops.js +++ b/web/js/ops.js @@ -7,6 +7,14 @@ import { loadUnits } from "./units.js"; const SIGNAL_ROLES = ["rem", "run", "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) { if (!runtime) return 'OFFLINE'; if (runtime.comm_locked) return 'COMM ERR'; @@ -128,8 +136,7 @@ function renderOpsEquipments(equipments) { return `
${ROLE_LABELS[role] || role} - ? - -- +
`; }).join(""); @@ -172,15 +179,12 @@ function renderOpsEquipments(equipments) { SIGNAL_ROLES.forEach((role) => { const point = roleMap[role]; if (!point) return; - const valueEl = card.querySelector(`[data-ops-value="${point.id}"]`); - const qualityEl = card.querySelector(`[data-ops-quality="${point.id}"]`); - if (valueEl && qualityEl) { - state.opsPointEls.set(point.id, { valueEl, qualityEl }); + const dotEl = card.querySelector(`[data-ops-dot="${point.id}"]`); + if (dotEl) { + state.opsPointEls.set(point.id, { dotEl }); if (point.point_monitor) { const m = point.point_monitor; - valueEl.textContent = formatValue(m); - qualityEl.className = `badge quality-${(m.quality || "unknown").toLowerCase()}`; - qualityEl.textContent = (m.quality || "unknown").toUpperCase(); + dotEl.className = sigDotClass(role, m.quality, m.value_text); } } }); diff --git a/web/styles.css b/web/styles.css index 95cabb5..bfa9034 100644 --- a/web/styles.css +++ b/web/styles.css @@ -261,6 +261,18 @@ body { 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 { padding: 6px 10px 8px; display: flex;