100 lines
3.2 KiB
TypeScript
100 lines
3.2 KiB
TypeScript
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
|