gx
chenyc
2026-05-24 a43f8991d3f5fa2ef4e0f3eeeca00fb4afc263c0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
"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 };