130 lines
3.6 KiB
JavaScript
130 lines
3.6 KiB
JavaScript
const express = require('express');
|
|
const http = require('http');
|
|
const WebSocket = require('ws');
|
|
const Database = require('better-sqlite3');
|
|
const path = require('path');
|
|
|
|
const app = express();
|
|
const server = http.createServer(app);
|
|
const wss = new WebSocket.Server({ server });
|
|
|
|
const db = new Database('status.db');
|
|
|
|
// 初始化数据库
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS nodes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT UNIQUE NOT NULL,
|
|
secret TEXT NOT NULL,
|
|
last_seen INTEGER,
|
|
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS stats (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
node_id INTEGER,
|
|
cpu REAL,
|
|
mem_used INTEGER,
|
|
mem_total INTEGER,
|
|
disk_used INTEGER,
|
|
disk_total INTEGER,
|
|
net_in INTEGER,
|
|
net_out INTEGER,
|
|
uptime INTEGER,
|
|
load1 REAL,
|
|
load5 REAL,
|
|
load15 REAL,
|
|
ts INTEGER DEFAULT (strftime('%s', 'now')),
|
|
FOREIGN KEY (node_id) REFERENCES nodes(id)
|
|
);
|
|
`);
|
|
|
|
// 生成密钥
|
|
function genSecret() {
|
|
return Math.random().toString(36).substr(2, 16);
|
|
}
|
|
|
|
// API: 获取所有节点状态
|
|
app.get('/api/nodes', (req, res) => {
|
|
const nodes = db.prepare(`
|
|
SELECT n.*, s.*
|
|
FROM nodes n
|
|
LEFT JOIN stats s ON n.id = s.node_id
|
|
AND s.ts = (SELECT MAX(ts) FROM stats WHERE node_id = n.id)
|
|
`).all();
|
|
res.json(nodes);
|
|
});
|
|
|
|
// API: 添加节点
|
|
app.post('/api/nodes', express.json(), (req, res) => {
|
|
const { name } = req.body;
|
|
if (!name) return res.status(400).json({ error: 'name required' });
|
|
|
|
const secret = genSecret();
|
|
try {
|
|
const result = db.prepare('INSERT INTO nodes (name, secret) VALUES (?, ?)').run(name, secret);
|
|
res.json({ id: result.lastInsertRowid, name, secret });
|
|
} catch (e) {
|
|
res.status(400).json({ error: 'name exists' });
|
|
}
|
|
});
|
|
|
|
// API: 删除节点
|
|
app.delete('/api/nodes/:id', (req, res) => {
|
|
db.prepare('DELETE FROM nodes WHERE id = ?').run(req.params.id);
|
|
db.prepare('DELETE FROM stats WHERE node_id = ?').run(req.params.id);
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
// Agent 上报接口
|
|
app.post('/report', express.json(), (req, res) => {
|
|
const { secret, cpu, mem_used, mem_total, disk_used, disk_total, net_in, net_out, uptime, load1, load5, load15 } = req.body;
|
|
|
|
const node = db.prepare('SELECT * FROM nodes WHERE secret = ?').get(secret);
|
|
if (!node) return res.status(403).json({ error: 'invalid secret' });
|
|
|
|
const now = Math.floor(Date.now() / 1000);
|
|
db.prepare('UPDATE nodes SET last_seen = ? WHERE id = ?').run(now, node.id);
|
|
|
|
db.prepare(`
|
|
INSERT INTO stats (node_id, cpu, mem_used, mem_total, disk_used, disk_total, net_in, net_out, uptime, load1, load5, load15)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run(node.id, cpu, mem_used, mem_total, disk_used, disk_total, net_in, net_out, uptime, load1, load5, load15);
|
|
|
|
// 清理7天前的数据
|
|
db.prepare('DELETE FROM stats WHERE ts < ?').run(now - 86400 * 7);
|
|
|
|
// WebSocket 广播
|
|
broadcast({ type: 'update', node_id: node.id, data: req.body });
|
|
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
// WebSocket 广播
|
|
function broadcast(data) {
|
|
const msg = JSON.stringify(data);
|
|
wss.clients.forEach(client => {
|
|
if (client.readyState === WebSocket.OPEN) {
|
|
client.send(msg);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Agent 脚本下载
|
|
app.get('/agent.sh', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'public', 'agent.sh'));
|
|
});
|
|
|
|
// 静态文件
|
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
|
|
// 首页
|
|
app.get('/', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
|
});
|
|
|
|
const PORT = process.env.PORT || 3800;
|
|
server.listen(PORT, () => {
|
|
console.log(`Status panel running on http://0.0.0.0:${PORT}`);
|
|
});
|