syco.me Homelab Dashboard
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { Router } from 'express'
|
||||
import { io, Socket } from 'socket.io-client'
|
||||
|
||||
const router = Router()
|
||||
|
||||
interface MonitorInfo { id: number; name: string; type: string; active: boolean }
|
||||
interface HeartbeatInfo { status: number; ping: number | null; beats: number[] }
|
||||
|
||||
let socket: Socket | null = null
|
||||
let monitors: Record<number, MonitorInfo> = {}
|
||||
let heartbeats: Record<number, HeartbeatInfo> = {}
|
||||
let ready = false
|
||||
|
||||
const BEAT_HISTORY = 30
|
||||
|
||||
function startSocket() {
|
||||
const host = process.env.KUMA_HOST
|
||||
const user = process.env.KUMA_USER
|
||||
const pass = process.env.KUMA_PASSWORD
|
||||
if (!host || !user || !pass) return
|
||||
|
||||
socket = io(host, { transports: ['websocket'], reconnection: true })
|
||||
|
||||
socket.on('connect', () => {
|
||||
socket!.emit('login', { username: user, password: pass, token: '' }, (res: { ok: boolean }) => {
|
||||
if (!res.ok) console.error('[kuma] login failed')
|
||||
})
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
socket.on('monitorList', (data: Record<string, any>) => {
|
||||
monitors = {}
|
||||
for (const m of Object.values(data)) {
|
||||
monitors[m.id] = { id: m.id, name: m.name, type: m.type, active: m.active === true || m.active === 1 }
|
||||
}
|
||||
ready = true
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
socket.on('heartbeatList', (monitorId: number, beats: any[]) => {
|
||||
if (!Array.isArray(beats) || beats.length === 0) return
|
||||
const last = beats.at(-1)
|
||||
const slice = beats.slice(-BEAT_HISTORY).map(b => b.status)
|
||||
heartbeats[monitorId] = { status: last.status, ping: last.ping ?? null, beats: slice }
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
socket.on('heartbeat', (beat: any) => {
|
||||
if (beat?.monitorID == null) return
|
||||
const prev = heartbeats[beat.monitorID]
|
||||
const beats = [...(prev?.beats ?? []), beat.status].slice(-BEAT_HISTORY)
|
||||
heartbeats[beat.monitorID] = { status: beat.status, ping: beat.ping ?? null, beats }
|
||||
})
|
||||
|
||||
socket.on('disconnect', () => { ready = false })
|
||||
socket.on('connect_error', (err) => console.error('[kuma] connect error:', err.message))
|
||||
}
|
||||
|
||||
function waitReady(timeoutMs = 6000): Promise<void> {
|
||||
if (ready) return Promise.resolve()
|
||||
return new Promise((resolve, reject) => {
|
||||
const deadline = Date.now() + timeoutMs
|
||||
const poll = setInterval(() => {
|
||||
if (ready) { clearInterval(poll); resolve() }
|
||||
else if (Date.now() > deadline) { clearInterval(poll); reject(new Error('Kuma not ready')) }
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
router.get('/monitors', async (_req, res) => {
|
||||
try {
|
||||
if (!socket) startSocket()
|
||||
await waitReady()
|
||||
|
||||
const list = Object.values(monitors)
|
||||
.filter(m => m.active)
|
||||
.map(m => ({
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
type: m.type,
|
||||
status: heartbeats[m.id]?.status ?? -1,
|
||||
ping: heartbeats[m.id]?.ping ?? null,
|
||||
beats: heartbeats[m.id]?.beats ?? [],
|
||||
}))
|
||||
.sort((a, b) => a.status - b.status)
|
||||
|
||||
res.json({
|
||||
total: list.length,
|
||||
up: list.filter(m => m.status === 1).length,
|
||||
down: list.filter(m => m.status === 0).length,
|
||||
monitors: list,
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : 'Unknown error'
|
||||
res.status(500).json({ error: msg })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
Reference in New Issue
Block a user