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 = {} let heartbeats: Record = {} 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) => { 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 { 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