Merge branch 'claude/youthful-cerf-ibvxfb'
This commit is contained in:
commit
bcdecd86d6
4 changed files with 198 additions and 5 deletions
88
backend/package-lock.json
generated
88
backend/package-lock.json
generated
|
|
@ -21,6 +21,7 @@
|
|||
"fastify": "^5.2.1",
|
||||
"guacamole-lite": "^1.2.0",
|
||||
"node-pty": "^1.1.0",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"ssh2": "^1.17.0",
|
||||
"undici": "^8.5.0",
|
||||
"zod": "^3.24.1"
|
||||
|
|
@ -1234,6 +1235,12 @@
|
|||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/bcryptjs": {
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
|
||||
|
|
@ -1530,6 +1537,23 @@
|
|||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
|
|
@ -1614,6 +1638,28 @@
|
|||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.6.6",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz",
|
||||
"integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.4.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.21.0",
|
||||
"xmlhttprequest-ssl": "~2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
||||
|
|
@ -2074,6 +2120,12 @@
|
|||
"obliterator": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nan": {
|
||||
"version": "2.27.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.27.0.tgz",
|
||||
|
|
@ -2458,6 +2510,34 @@
|
|||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-client": {
|
||||
"version": "4.8.3",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
|
||||
"integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.4.1",
|
||||
"engine.io-client": "~6.6.1",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.6",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz",
|
||||
"integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sonic-boom": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
|
||||
|
|
@ -2721,6 +2801,14 @@
|
|||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlhttprequest-ssl": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
"fastify": "^5.2.1",
|
||||
"guacamole-lite": "^1.2.0",
|
||||
"node-pty": "^1.1.0",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"ssh2": "^1.17.0",
|
||||
"undici": "^8.5.0",
|
||||
"zod": "^3.24.1"
|
||||
|
|
|
|||
|
|
@ -1,15 +1,118 @@
|
|||
import type { IntegrationAdapter } from './types.js'
|
||||
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) {
|
||||
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 res = await fetch(`${baseUrl}/api/status-page/heartbeat/default`)
|
||||
if (!res.ok) return { ok: false, message: `HTTP ${res.status}` }
|
||||
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()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,8 @@ const integrationTypeDefs: { type: string; name: string; multiInstance?: boolean
|
|||
] },
|
||||
{ type: 'uptime_kuma', name: 'Uptime Kuma', fields: [
|
||||
{ key: 'baseUrl', label: 'URL', placeholder: 'https://uptime.example.com' },
|
||||
{ key: 'apiKey', label: 'API Key', secret: true },
|
||||
{ key: 'username', label: 'Username', secret: true },
|
||||
{ key: 'password', label: 'Password', secret: true },
|
||||
] },
|
||||
{ type: 'weather', name: 'Weather API', fields: [{ key: 'location', label: 'Location' }, { key: 'units', label: 'Units' }] },
|
||||
{ type: 'remote_desktop', name: 'Remote Desktop', multiInstance: true, fields: [
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue