dev_arc_aws/backend/src/integrations/uptimeKuma.ts

119 lines
3.9 KiB
TypeScript
Raw Normal View History

import { io, type Socket } from 'socket.io-client'
import type { IntegrationAdapter, Resource } from './types.js'
/**
* Uptime Kuma has no plain REST API for monitor data the web UI talks to it
* over a Socket.IO session (login, then the server pushes `monitorList` and
* per-monitor `importantHeartbeatList` events). This connects the same way,
* waits briefly for those events, then disconnects.
*/
interface UptimeKumaMonitor {
id: number
name: string
active: boolean
}
interface Heartbeat {
status: number // 0 = down, 1 = up, 2 = pending, 3 = maintenance
msg: string
time: string
}
function connectAndLogin(
baseUrl: string,
username: string,
password: string,
): Promise<Socket> {
return new Promise((resolve, reject) => {
const socket = io(baseUrl, { transports: ['websocket', 'polling'], reconnection: false, timeout: 10000 })
const timer = setTimeout(() => {
socket.disconnect()
reject(new Error('Timed out connecting to Uptime Kuma'))
}, 10000)
socket.on('connect_error', (err) => {
clearTimeout(timer)
socket.disconnect()
reject(new Error(err.message || 'Connection failed'))
})
socket.on('connect', () => {
socket.emit('login', { username, password, token: '' }, (res: { ok: boolean; msg?: string }) => {
clearTimeout(timer)
if (!res.ok) {
socket.disconnect()
reject(new Error(res.msg || 'Login failed'))
return
}
resolve(socket)
})
})
})
}
export const uptimeKuma: IntegrationAdapter = {
async testConnection(config, secrets) {
const baseUrl = config.baseUrl?.replace(/\/$/, '')
const username = secrets.username
const password = secrets.password
if (!baseUrl) return { ok: false, message: 'Missing baseUrl' }
if (!username || !password) return { ok: false, message: 'Missing username/password' }
try {
const socket = await connectAndLogin(baseUrl, username, password)
socket.disconnect()
return { ok: true, message: 'Connected' }
} catch (err) {
return { ok: false, message: err instanceof Error ? err.message : 'Connection failed' }
}
},
async listResources(config, secrets): Promise<Resource[]> {
const baseUrl = config.baseUrl?.replace(/\/$/, '')
const username = secrets.username
const password = secrets.password
if (!baseUrl || !username || !password) return []
const socket = await connectAndLogin(baseUrl, username, password)
try {
const monitors = new Map<number, UptimeKumaMonitor>()
const lastHeartbeat = new Map<number, Heartbeat>()
socket.on('monitorList', (list: Record<string, UptimeKumaMonitor>) => {
for (const m of Object.values(list)) monitors.set(m.id, m)
})
socket.on('importantHeartbeatList', (monitorId: number, beats: Heartbeat[]) => {
const latest = beats[beats.length - 1]
if (latest) lastHeartbeat.set(monitorId, latest)
})
socket.on('heartbeat', (beat: Heartbeat & { monitorID: number }) => {
lastHeartbeat.set(beat.monitorID, beat)
})
// Server pushes these events asynchronously right after login — there's no
// single "done" signal, so give it a short window to arrive.
await new Promise((resolve) => setTimeout(resolve, 2500))
const resources: Resource[] = []
for (const monitor of monitors.values()) {
if (!monitor.active) continue
const beat = lastHeartbeat.get(monitor.id)
const status: Resource['status'] =
beat === undefined
? 'unknown'
: beat.status === 1
? 'healthy'
: beat.status === 0
? 'critical'
: beat.status === 2
? 'warning'
: 'unknown'
resources.push({ name: monitor.name, status, detail: beat?.msg || undefined })
}
return resources
} finally {
socket.disconnect()
}
},
}