import { appendChartPoint } from "./chart.js"; import { dom } from "./dom.js"; import { prependEvent } from "./events.js"; import { formatValue } from "./points.js"; import { state } from "./state.js"; import { renderUnits } from "./units.js"; import { showToast } from "./toast.js"; function escapeHtml(text) { return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); } function parseLogLine(line) { const trimmed = line.trim(); if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null; try { return JSON.parse(trimmed); } catch { return null; } } export function appendLog(line) { if (!dom.logView) return; const atBottom = dom.logView.scrollTop + dom.logView.clientHeight >= dom.logView.scrollHeight - 10; const div = document.createElement("div"); const parsed = parseLogLine(line); if (!parsed) { div.className = "log-line"; div.textContent = line; } else { const levelRaw = (parsed.level || "").toString(); const level = levelRaw.toLowerCase(); div.className = `log-line${level ? ` level-${level}` : ""}`; div.innerHTML = [ `${escapeHtml(levelRaw || "LOG")}`, parsed.timestamp ? ` ${escapeHtml(parsed.timestamp)}` : "", parsed.target ? ` ${escapeHtml(parsed.target)}` : "", `${escapeHtml(parsed.fields?.message || parsed.message || parsed.msg || line)}`, ].join(""); } dom.logView.appendChild(div); if (atBottom) dom.logView.scrollTop = dom.logView.scrollHeight; } export function startLogs() { if (state.logSource) return; state.logSource = new EventSource("/api/logs/stream"); state.logSource.addEventListener("log", (event) => { const data = JSON.parse(event.data); (data.lines || []).forEach(appendLog); }); state.logSource.addEventListener("error", () => appendLog("[log stream error]")); } export function stopLogs() { if (state.logSource) { state.logSource.close(); state.logSource = null; } } 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("后端连接断开,正在重连…", "error", 0); } else if (connected && _disconnectToast) { _disconnectToast.dismiss(); _disconnectToast = null; showToast("连接已恢复", "success", 3000); } } 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.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 cell 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(); } if (state.chartPointId === data.point_id) { appendChartPoint(data); } return; } if (payload.type === "EventCreated" || payload.type === "event_created") { prependEvent(payload.data); } if (payload.type === "UnitRuntimeChanged") { const runtime = payload.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, runtime.auto_enabled); }); return; } } catch { // ignore malformed messages } }; ws.onclose = () => { setWsStatus(false); window.setTimeout(startPointSocket, 2000); }; ws.onerror = () => setWsStatus(false); }