"use strict"; const http = require("http"); const fs = require("fs"); const path = require("path"); const { WebSocketServer } = require("ws"); function createDashboard({ port, dataCache, deviceManager, logger, config, uploadManager }) { let server; let wss; let pushTimer; const MIME = { ".html": "text/html; charset=utf-8", ".css": "text/css", ".js": "application/javascript", }; function serveStatic(req, res) { let filePath = req.url === "/" ? "/index.html" : req.url; filePath = path.normalize(filePath).replace(/^(\.\.[\/\\])+/, ""); const fullPath = path.join(__dirname, "public", filePath); if (!fullPath.startsWith(path.join(__dirname, "public"))) { res.writeHead(403); res.end("Forbidden"); return; } fs.readFile(fullPath, (err, data) => { if (err) { res.writeHead(404); res.end("Not Found"); return; } const ext = path.extname(fullPath); res.writeHead(200, { "Content-Type": MIME[ext] || "application/octet-stream" }); res.end(data); }); } function buildSnapshot() { const devices = dataCache.getAll().map((dev) => { const { rawFrame, ...rest } = dev; return rest; }); return { type: "snapshot", timestamp: new Date().toISOString(), summary: dataCache.getSummary(), devices, config: { pollIntervalMs: config.pollIntervalMs, reconnectBaseMs: config.reconnectBaseMs, reconnectMaxMs: config.reconnectMaxMs, }, upload: uploadManager ? uploadManager.getStatus() : null, }; } function broadcast() { if (!wss) return; const payload = JSON.stringify(buildSnapshot()); for (const ws of wss.clients) { if (ws.readyState === 1) ws.send(payload); } } function start() { server = http.createServer(serveStatic); wss = new WebSocketServer({ server }); wss.on("connection", (ws) => { logger.sys(`Dashboard 客户端已连接 (当前 ${wss.clients.size} 个)`); ws.send(JSON.stringify(buildSnapshot())); ws.on("close", () => { logger.sys(`Dashboard 客户端断开 (剩余 ${wss.clients.size} 个)`); }); }); // 数据变更防抖推送 let pending = false; dataCache.setUpdateListener(() => { if (pending) return; pending = true; setTimeout(() => { pending = false; broadcast(); }, 100); }); // 定期推送确保时间戳更新 pushTimer = setInterval(() => { if (pending) return; broadcast(); }, 2000); pushTimer.unref(); server.listen(port, () => { logger.sys(`Dashboard 已启动: http://0.0.0.0:${port}`); }); } function stop() { if (pushTimer) clearInterval(pushTimer); if (wss) { for (const ws of wss.clients) ws.close(); } if (server) server.close(); } return { start, stop }; } module.exports = { createDashboard };