diff --git a/server/cron.js b/server/cron.js
index e80b707..5614e8c 100644
--- a/server/cron.js
+++ b/server/cron.js
@@ -33,36 +33,4 @@ function scheduleNews() {
console.log('[cron] 已注册: 每天 03:00 拉取新闻 + 跨源报警检测')
}
-// 每 2 分钟: 拉取 komari 节点 + 服务自检
-function scheduleMonitors() {
- cron.schedule('*/2 * * * *', async () => {
- try {
- const k = require('./komari-monitor.cjs')
- const r = await k.runOnce()
- console.log('[cron] komari: nodes=' + r.nodes + ' snapshots=' + r.snapshots + ' history=' + r.history + ' failed=' + r.failed)
- } catch (e) { console.error('[cron] komari 任务失败:', e.message) }
- try {
- const h = require('./health-check.cjs')
- const arr = await h.runOnce()
- const down = arr.filter(x => x.status !== 'up').map(x => x.name)
- console.log('[cron] health: ' + arr.length + ' 项, down=' + (down.length ? down.join(',') : '无'))
- } catch (e) { console.error('[cron] health 任务失败:', e.message) }
- })
- console.log('[cron] 已注册: 每 2 分钟 komari + 服务自检')
-}
-
-// 每小时: 清理 24h 前的 komari 历史
-function scheduleMonitorCleanup() {
- cron.schedule('0 * * * *', async () => {
- try {
- const k = require('./komari-monitor.cjs')
- const n = await k.cleanupOld()
- console.log('[cron] 监控历史清理: 删除 ' + n + ' 条')
- } catch (e) { console.error('[cron] 监控清理失败:', e.message) }
- })
- console.log('[cron] 已注册: 每小时清理 24h 前监控历史')
-}
-
scheduleNews()
-scheduleMonitors()
-scheduleMonitorCleanup()
diff --git a/server/health-check.cjs b/server/health-check.cjs
index dbd636d..f4923d9 100644
--- a/server/health-check.cjs
+++ b/server/health-check.cjs
@@ -1,3 +1,6 @@
+// ===== 已停用 =====
+// 服务自检的定时任务已从 server/cron.js 移除 (服务监控暂留空)。
+// 如恢复服务监控, 重新在 cron.js 注册 runOnce 并复用本文件。
// 服务健康检查: 探测本应用各关键端点, 写入 ServiceCheck 表
const mysql = require('mysql2/promise')
diff --git a/server/index.js b/server/index.js
index 473c653..1eba465 100644
--- a/server/index.js
+++ b/server/index.js
@@ -8,6 +8,7 @@ import dataRouter from './routes/data.js'
import newsRouter from './routes/news.js'
import alertsRouter from './routes/alerts.js'
import articlesRouter from './routes/articles.js'
+import monitorsRouter from './routes/monitors.js'
const PORT = Number(process.env.PORT) || 3001
const DIST_DIR = path.join(process.cwd(), 'dist')
@@ -26,6 +27,7 @@ app.use('/api', dataRouter)
app.use('/api', newsRouter)
app.use('/api', alertsRouter)
app.use('/api', articlesRouter)
+app.use('/api', monitorsRouter)
// 静态资源 (Vite build 产物)
if (HAS_DIST) {
diff --git a/server/komari-client.js b/server/komari-client.js
new file mode 100644
index 0000000..62bd27a
--- /dev/null
+++ b/server/komari-client.js
@@ -0,0 +1,94 @@
+// komari 实时监控客户端: 公开 API, 无鉴权, 请求时实时获取, 不落库
+// GET /api/nodes 节点列表
+// GET /api/recent/{uuid} 最近实时状态 (数组, 多个时间点)
+// GET /api/records/load?uuid=... 1 分钟粒度历史 (komari 保留 24h)
+
+const BASE = process.env.KOMARI_BASE || 'https://komari.honor3.com'
+const TIMEOUT = 15000
+
+async function jget(path) {
+ const ctrl = new AbortController()
+ const timer = setTimeout(() => ctrl.abort(), TIMEOUT)
+ try {
+ const r = await fetch(BASE + path, { headers: { 'User-Agent': 'iboard-monitor/1.0' }, signal: ctrl.signal })
+ if (!r.ok) throw new Error('komari ' + path + ' HTTP ' + r.status)
+ const j = await r.json()
+ if (j.status && j.status !== 'success') throw new Error('komari ' + path + ' status: ' + j.status + ' ' + (j.message || ''))
+ return j.data
+ } finally {
+ clearTimeout(timer)
+ }
+}
+
+// 节点列表 + 每节点最新实时快照 (并行拉取, 单节点失败不拖垮整体)
+export async function fetchServers() {
+ const nodes = await jget('/api/nodes')
+ if (!Array.isArray(nodes)) return []
+ const snaps = await Promise.all(
+ nodes
+ .filter((n) => n.uuid)
+ .map(async (n) => {
+ try {
+ const recent = await jget('/api/recent/' + n.uuid)
+ return { node: n, snap: Array.isArray(recent) && recent.length ? recent[recent.length - 1] : null }
+ } catch (e) {
+ console.error('[komari] 节点 ' + n.uuid + ' 快照失败:', e.message)
+ return { node: n, snap: null }
+ }
+ }),
+ )
+ return snaps.map(({ node: n, snap }) => {
+ const uuid = n.uuid
+ return {
+ uuid,
+ name: n.name || uuid,
+ cpuName: n.cpu_name || '',
+ os: n.os || '',
+ region: n.region || '',
+ memTotal: snap ? Number(snap.ram?.total || 0) : Number(n.mem_total || 0),
+ diskTotal: snap ? Number(snap.disk?.total || 0) : Number(n.disk_total || 0),
+ cpuCores: Number(n.cpu_cores || 0),
+ cpuUsage: snap ? Number(snap.cpu?.usage || 0) : 0,
+ ramUsed: snap ? Number(snap.ram?.used || 0) : 0,
+ swapUsed: snap ? Number(snap.swap?.used || 0) : 0,
+ load1: snap ? Number(snap.load?.load1 || 0) : 0,
+ load5: snap ? Number(snap.load?.load5 || 0) : 0,
+ load15: snap ? Number(snap.load?.load15 || 0) : 0,
+ diskUsed: snap ? Number(snap.disk?.used || 0) : 0,
+ netIn: snap ? Number(snap.network?.down || 0) : 0,
+ netOut: snap ? Number(snap.network?.up || 0) : 0,
+ uptime: snap ? Number(snap.uptime || 0) : 0,
+ process: snap ? Number(snap.process || 0) : 0,
+ connections: snap ? Number(snap.connections?.tcp || 0) : 0,
+ status: snap ? 'online' : 'unknown',
+ recordedAt: snap && snap.updated_at ? new Date(snap.updated_at).toISOString() : new Date().toISOString(),
+ }
+ })
+}
+
+// 节点时序: komari 保留最近 24h, 1 分钟粒度
+// 节点未开启 record 或已下线 (komari 404) 时返回空列表, 不向上抛 500
+export async function fetchHistory(uuid) {
+ let data
+ try {
+ data = await jget('/api/records/load?uuid=' + encodeURIComponent(uuid))
+ } catch (e) {
+ console.error('[komari] ' + uuid + ' history 获取失败:', e.message)
+ return []
+ }
+ const records = data && Array.isArray(data.records) ? data.records : []
+ return records
+ .map((r) => ({
+ period: new Date(r.time).toISOString(),
+ cpu: Number(r.cpu || 0),
+ ram: Number(r.ram || 0),
+ load: Number(r.load || 0),
+ disk: Number(r.disk || 0),
+ // net_in/net_out 为 komari 原始值 (字节口径), 前端展示时再按需换算
+ netIn: Number(r.net_in || 0),
+ netOut: Number(r.net_out || 0),
+ process: Number(r.process || 0),
+ connections: Number(r.connections || 0),
+ }))
+ .filter((r) => !isNaN(Date.parse(r.period)))
+}
diff --git a/server/komari-monitor.cjs b/server/komari-monitor.cjs
index 991d933..cb69551 100644
--- a/server/komari-monitor.cjs
+++ b/server/komari-monitor.cjs
@@ -1,3 +1,6 @@
+// ===== 已停用 =====
+// 监控数据不再落库 (用户要求实时从 komari 获取), 本脚本的定时入库调用已从 server/cron.js 移除。
+// 如需恢复"数据库记录监控数据"模式, 重新在 cron.js 注册 runOnce/cleanupOld 即可。
// komari 服务器监控拉取: 公开 API (无需 key)
// GET /api/nodes 节点列表
// GET /api/recent/{uuid} 最近实时状态 (数组, 多个时间点)
diff --git a/server/routes/monitors.js b/server/routes/monitors.js
index 090ffca..7033cea 100644
--- a/server/routes/monitors.js
+++ b/server/routes/monitors.js
@@ -1,35 +1,28 @@
import { Router } from 'express'
import { prisma } from '../db.js'
import { authRequired } from '../middleware.js'
+import { fetchServers, fetchHistory } from '../komari-client.js'
const router = Router()
router.use(authRequired)
-// 服务器节点列表: 元数据 + 最新快照
+// 服务器节点列表: 实时从 komari 拉取 (不落库)
router.get('/servers', async (_req, res, next) => {
try {
- const rows = await prisma.serverNode.findMany({
- orderBy: { name: 'asc' },
- })
- res.json({ items: rows })
+ const items = await fetchServers()
+ res.json({ items })
} catch (e) { next(e) }
})
-// 节点时序: ?uuid=...&hours=24
+// 节点时序: komari 保留最近 24h, 1 分钟粒度 (不落库)
router.get('/servers/:uuid/history', async (req, res, next) => {
try {
- const hours = Math.min(Math.max(Number(req.query.hours) || 24, 1), 168)
- const since = new Date(Date.now() - hours * 3600 * 1000)
- const rows = await prisma.serverNodeHistory.findMany({
- where: { uuid: req.params.uuid, period: { gte: since } },
- orderBy: { period: 'asc' },
- take: 2000,
- })
- res.json({ items: rows })
+ const items = await fetchHistory(req.params.uuid)
+ res.json({ items })
} catch (e) { next(e) }
})
-// 服务健康列表
+// 服务健康列表 (服务监控暂留空, 接口保留)
router.get('/services', async (_req, res, next) => {
try {
const rows = await prisma.serviceCheck.findMany({
diff --git a/src/App.jsx b/src/App.jsx
index 7f60351..b76aede 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -5,6 +5,7 @@ import HomePage from './pages/HomePage.jsx'
import NewsDetail from './pages/news/NewsDetail.jsx'
import MacroPage from './pages/MacroPage.jsx'
import MonitorPage from './pages/MonitorPage.jsx'
+import ServerDetail from './pages/monitor/ServerDetail.jsx'
import MePage from './pages/MePage.jsx'
import { useAuth } from './auth/AuthContext.jsx'
@@ -34,6 +35,7 @@ export default function App() {