Uptime Kuma has no REST API for monitor data; connect over the same Socket.IO session the web UI uses (login, then read monitorList and heartbeat events) so connected monitors now surface as Resources. Switches the integration's credentials from an API key to username/password, matching what Uptime Kuma's session login expects.
118 lines
3.9 KiB
TypeScript
118 lines
3.9 KiB
TypeScript
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()
|
|
}
|
|
},
|
|
}
|