import { appendChartPoint } from "./platform/chart.js"; import { dom } from "./platform/dom.js"; import { prependEvent } from "./platform/events.js"; import { formatValue } from "./platform/points.js"; import { state } from "./platform/state.js"; import { loadUnits, renderUnits } from "./units.js"; import { loadEquipments } from "./platform/equipment.js"; import { showToast } from "./platform/api.js"; // Real-time SSE log stream lives in the shared platform module (also used by ops). export { startLogs, stopLogs } from "./platform/log-stream.js"; let _disconnectToast = null; function setWsStatus(connected) { if (dom.wsDot) { dom.wsDot.className = `ws-dot ${connected ? "connected" : "disconnected"}`; } if (dom.wsLabel) { dom.wsLabel.textContent = connected ? "已连接" : "连接断开,重连中…"; } if (!connected && !_disconnectToast) { _disconnectToast = showToast("后端连接断开", { message: "正在重连,请稍候…", level: "error", duration: 0, shake: true, }); } else if (connected && _disconnectToast) { _disconnectToast.dismiss(); _disconnectToast = null; showToast("连接已恢复", { level: "success", duration: 3000 }); } } let _reconnectDelay = 1000; let _connectedOnce = false; 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); _reconnectDelay = 1000; if (_connectedOnce) { loadUnits().catch(() => {}); if (state.activeView === "config") loadEquipments().catch(() => {}); } _connectedOnce = true; }; ws.onmessage = (event) => { try { const payload = JSON.parse(event.data); if (payload.type === "PointNewValue" || payload.type === "point_new_value") { const data = payload.data; // config view point table const entry = state.pointEls.get(data.point_id); if (entry) { entry.value.textContent = formatValue(data); entry.quality.className = `badge quality-${(data.quality || "unknown").toLowerCase()}`; entry.quality.textContent = (data.quality || "unknown").toUpperCase(); entry.time.textContent = data.timestamp || "--"; } // ops view signal pill const opsEntry = state.opsPointEls.get(data.point_id); if (opsEntry) { const { pillEl, syncBtns } = opsEntry; state.opsSignalCache.set(data.point_id, { quality: data.quality, value_text: data.value_text }); const role = pillEl.dataset.opsRole; import("./ops.js").then(({ sigPillClass }) => { pillEl.className = sigPillClass(role, data.quality, data.value_text); syncBtns?.(); }); } if (state.chartPointId === data.point_id) { appendChartPoint(data); } return; } if (payload.type === "EventCreated" || payload.type === "event_created") { prependEvent(payload.data); } if (payload.type === "AppEvent" || payload.type === "app_event") { const envelope = payload.data || {}; if (envelope.app !== "feeder") return; if (envelope.event_type === "unit_runtime_changed") { const runtime = envelope.data; state.runtimes.set(runtime.unit_id, runtime); renderUnits(); // lazy import to avoid circular dep (ops.js -> logs.js -> ops.js) import("./ops.js").then(({ renderOpsUnits, syncEquipmentButtonsForUnit }) => { renderOpsUnits(); syncEquipmentButtonsForUnit(runtime.unit_id); }); } return; } } catch { // ignore malformed messages } }; ws.onclose = () => { setWsStatus(false); window.setTimeout(startPointSocket, _reconnectDelay); _reconnectDelay = Math.min(_reconnectDelay * 2, 30000); }; ws.onerror = () => setWsStatus(false); }