2026-06-21 11:00:47 +00:00
|
|
|
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)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
Add backend skeleton: Fastify + SQLite API with auth and integrations
- Single-user JWT auth with a first-run /api/setup endpoint, gated by
GET /api/system/setup-status, to back an upcoming enrollment page
- SQLite schema for users, integrations, secrets (AES-256-GCM encrypted),
bookmarks, and bookmark categories
- Integration adapter registry with real health-check adapters for
Uptime Kuma and Docker, stubs for the rest, wired to
POST /api/integrations/:id/test
- CRUD routes for integrations and bookmarks
- backend/ as its own Docker service in docker-compose.yml, Vite dev
proxy for /api, .env.example for required secrets
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
2026-06-18 19:04:48 +00:00
|
|
|
|
|
|
|
|
export const uptimeKuma: IntegrationAdapter = {
|
2026-06-21 11:00:47 +00:00
|
|
|
async testConnection(config, secrets) {
|
Add backend skeleton: Fastify + SQLite API with auth and integrations
- Single-user JWT auth with a first-run /api/setup endpoint, gated by
GET /api/system/setup-status, to back an upcoming enrollment page
- SQLite schema for users, integrations, secrets (AES-256-GCM encrypted),
bookmarks, and bookmark categories
- Integration adapter registry with real health-check adapters for
Uptime Kuma and Docker, stubs for the rest, wired to
POST /api/integrations/:id/test
- CRUD routes for integrations and bookmarks
- backend/ as its own Docker service in docker-compose.yml, Vite dev
proxy for /api, .env.example for required secrets
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
2026-06-18 19:04:48 +00:00
|
|
|
const baseUrl = config.baseUrl?.replace(/\/$/, '')
|
2026-06-21 11:00:47 +00:00
|
|
|
const username = secrets.username
|
|
|
|
|
const password = secrets.password
|
Add backend skeleton: Fastify + SQLite API with auth and integrations
- Single-user JWT auth with a first-run /api/setup endpoint, gated by
GET /api/system/setup-status, to back an upcoming enrollment page
- SQLite schema for users, integrations, secrets (AES-256-GCM encrypted),
bookmarks, and bookmark categories
- Integration adapter registry with real health-check adapters for
Uptime Kuma and Docker, stubs for the rest, wired to
POST /api/integrations/:id/test
- CRUD routes for integrations and bookmarks
- backend/ as its own Docker service in docker-compose.yml, Vite dev
proxy for /api, .env.example for required secrets
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
2026-06-18 19:04:48 +00:00
|
|
|
if (!baseUrl) return { ok: false, message: 'Missing baseUrl' }
|
2026-06-21 11:00:47 +00:00
|
|
|
if (!username || !password) return { ok: false, message: 'Missing username/password' }
|
Add backend skeleton: Fastify + SQLite API with auth and integrations
- Single-user JWT auth with a first-run /api/setup endpoint, gated by
GET /api/system/setup-status, to back an upcoming enrollment page
- SQLite schema for users, integrations, secrets (AES-256-GCM encrypted),
bookmarks, and bookmark categories
- Integration adapter registry with real health-check adapters for
Uptime Kuma and Docker, stubs for the rest, wired to
POST /api/integrations/:id/test
- CRUD routes for integrations and bookmarks
- backend/ as its own Docker service in docker-compose.yml, Vite dev
proxy for /api, .env.example for required secrets
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
2026-06-18 19:04:48 +00:00
|
|
|
try {
|
2026-06-21 11:00:47 +00:00
|
|
|
const socket = await connectAndLogin(baseUrl, username, password)
|
|
|
|
|
socket.disconnect()
|
Add backend skeleton: Fastify + SQLite API with auth and integrations
- Single-user JWT auth with a first-run /api/setup endpoint, gated by
GET /api/system/setup-status, to back an upcoming enrollment page
- SQLite schema for users, integrations, secrets (AES-256-GCM encrypted),
bookmarks, and bookmark categories
- Integration adapter registry with real health-check adapters for
Uptime Kuma and Docker, stubs for the rest, wired to
POST /api/integrations/:id/test
- CRUD routes for integrations and bookmarks
- backend/ as its own Docker service in docker-compose.yml, Vite dev
proxy for /api, .env.example for required secrets
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
2026-06-18 19:04:48 +00:00
|
|
|
return { ok: true, message: 'Connected' }
|
|
|
|
|
} catch (err) {
|
|
|
|
|
return { ok: false, message: err instanceof Error ? err.message : 'Connection failed' }
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-06-21 11:00:47 +00:00
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
},
|
Add backend skeleton: Fastify + SQLite API with auth and integrations
- Single-user JWT auth with a first-run /api/setup endpoint, gated by
GET /api/system/setup-status, to back an upcoming enrollment page
- SQLite schema for users, integrations, secrets (AES-256-GCM encrypted),
bookmarks, and bookmark categories
- Integration adapter registry with real health-check adapters for
Uptime Kuma and Docker, stubs for the rest, wired to
POST /api/integrations/:id/test
- CRUD routes for integrations and bookmarks
- backend/ as its own Docker service in docker-compose.yml, Vite dev
proxy for /api, .env.example for required secrets
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
2026-06-18 19:04:48 +00:00
|
|
|
}
|