Merge branch 'claude/youthful-cerf-ibvxfb'

This commit is contained in:
Claude 2026-06-21 11:03:38 +00:00
commit bcdecd86d6
No known key found for this signature in database
4 changed files with 198 additions and 5 deletions

View file

@ -21,6 +21,7 @@
"fastify": "^5.2.1", "fastify": "^5.2.1",
"guacamole-lite": "^1.2.0", "guacamole-lite": "^1.2.0",
"node-pty": "^1.1.0", "node-pty": "^1.1.0",
"socket.io-client": "^4.8.3",
"ssh2": "^1.17.0", "ssh2": "^1.17.0",
"undici": "^8.5.0", "undici": "^8.5.0",
"zod": "^3.24.1" "zod": "^3.24.1"
@ -1234,6 +1235,12 @@
"node": ">=14.0.0" "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": { "node_modules/@types/bcryptjs": {
"version": "2.4.6", "version": "2.4.6",
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
@ -1530,6 +1537,23 @@
"node": ">=10.0.0" "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": { "node_modules/decompress-response": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
@ -1614,6 +1638,28 @@
"once": "^1.4.0" "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": { "node_modules/esbuild": {
"version": "0.28.1", "version": "0.28.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
@ -2074,6 +2120,12 @@
"obliterator": "^2.0.4" "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": { "node_modules/nan": {
"version": "2.27.0", "version": "2.27.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.27.0.tgz", "resolved": "https://registry.npmjs.org/nan/-/nan-2.27.0.tgz",
@ -2458,6 +2510,34 @@
"simple-concat": "^1.0.0" "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": { "node_modules/sonic-boom": {
"version": "4.2.1", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
@ -2721,6 +2801,14 @@
"node": ">=16.0.0" "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": { "node_modules/xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View file

@ -22,6 +22,7 @@
"fastify": "^5.2.1", "fastify": "^5.2.1",
"guacamole-lite": "^1.2.0", "guacamole-lite": "^1.2.0",
"node-pty": "^1.1.0", "node-pty": "^1.1.0",
"socket.io-client": "^4.8.3",
"ssh2": "^1.17.0", "ssh2": "^1.17.0",
"undici": "^8.5.0", "undici": "^8.5.0",
"zod": "^3.24.1" "zod": "^3.24.1"

View file

@ -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 = { export const uptimeKuma: IntegrationAdapter = {
async testConnection(config) { async testConnection(config, secrets) {
const baseUrl = config.baseUrl?.replace(/\/$/, '') const baseUrl = config.baseUrl?.replace(/\/$/, '')
const username = secrets.username
const password = secrets.password
if (!baseUrl) return { ok: false, message: 'Missing baseUrl' } if (!baseUrl) return { ok: false, message: 'Missing baseUrl' }
if (!username || !password) return { ok: false, message: 'Missing username/password' }
try { try {
const res = await fetch(`${baseUrl}/api/status-page/heartbeat/default`) const socket = await connectAndLogin(baseUrl, username, password)
if (!res.ok) return { ok: false, message: `HTTP ${res.status}` } socket.disconnect()
return { ok: true, message: 'Connected' } return { ok: true, message: 'Connected' }
} catch (err) { } catch (err) {
return { ok: false, message: err instanceof Error ? err.message : 'Connection failed' } 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()
}
},
} }

View file

@ -77,7 +77,8 @@ const integrationTypeDefs: { type: string; name: string; multiInstance?: boolean
] }, ] },
{ type: 'uptime_kuma', name: 'Uptime Kuma', fields: [ { type: 'uptime_kuma', name: 'Uptime Kuma', fields: [
{ key: 'baseUrl', label: 'URL', placeholder: 'https://uptime.example.com' }, { 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: 'weather', name: 'Weather API', fields: [{ key: 'location', label: 'Location' }, { key: 'units', label: 'Units' }] },
{ type: 'remote_desktop', name: 'Remote Desktop', multiInstance: true, fields: [ { type: 'remote_desktop', name: 'Remote Desktop', multiInstance: true, fields: [