@muhammedaksam/easiarr 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/api/auto-setup-types.ts +62 -0
- package/src/api/bazarr-api.ts +54 -1
- package/src/api/cloudflare-api.ts +216 -17
- package/src/api/grafana-api.ts +314 -0
- package/src/api/heimdall-api.ts +209 -0
- package/src/api/homarr-api.ts +296 -0
- package/src/api/jellyfin-api.ts +61 -1
- package/src/api/jellyseerr-api.ts +49 -1
- package/src/api/overseerr-api.ts +489 -0
- package/src/api/plex-api.ts +329 -0
- package/src/api/portainer-api.ts +79 -1
- package/src/api/prowlarr-api.ts +44 -1
- package/src/api/qbittorrent-api.ts +57 -1
- package/src/api/tautulli-api.ts +277 -0
- package/src/api/uptime-kuma-api.ts +342 -0
- package/src/apps/registry.ts +32 -2
- package/src/config/homepage-config.ts +82 -38
- package/src/config/schema.ts +14 -0
- package/src/ui/screens/CloudflaredSetup.ts +225 -9
- package/src/ui/screens/FullAutoSetup.ts +496 -117
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tautulli API Client
|
|
3
|
+
* Handles Tautulli auto-setup for Plex monitoring
|
|
4
|
+
* Note: Initial Plex connection requires web wizard, but API key can be retrieved automatically
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { debugLog } from "../utils/debug"
|
|
8
|
+
import type { IAutoSetupClient, AutoSetupOptions, AutoSetupResult } from "./auto-setup-types"
|
|
9
|
+
|
|
10
|
+
interface TautulliServerInfo {
|
|
11
|
+
pms_identifier?: string
|
|
12
|
+
pms_ip?: string
|
|
13
|
+
pms_is_remote?: number
|
|
14
|
+
pms_name?: string
|
|
15
|
+
pms_platform?: string
|
|
16
|
+
pms_plexpass?: number
|
|
17
|
+
pms_port?: number
|
|
18
|
+
pms_ssl?: number
|
|
19
|
+
pms_url?: string
|
|
20
|
+
pms_url_manual?: number
|
|
21
|
+
pms_version?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface TautulliApiResponse<T = unknown> {
|
|
25
|
+
response: {
|
|
26
|
+
result: "success" | "error"
|
|
27
|
+
message?: string
|
|
28
|
+
data: T
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class TautulliClient implements IAutoSetupClient {
|
|
33
|
+
private host: string
|
|
34
|
+
private port: number
|
|
35
|
+
private apiKey?: string
|
|
36
|
+
|
|
37
|
+
constructor(host: string, port: number = 8181, apiKey?: string) {
|
|
38
|
+
this.host = host
|
|
39
|
+
this.port = port
|
|
40
|
+
this.apiKey = apiKey
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get base URL for Tautulli
|
|
45
|
+
*/
|
|
46
|
+
private get baseUrl(): string {
|
|
47
|
+
return `http://${this.host}:${this.port}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Set API key for authenticated requests
|
|
52
|
+
*/
|
|
53
|
+
setApiKey(apiKey: string): void {
|
|
54
|
+
this.apiKey = apiKey
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build API URL with command and optional params
|
|
59
|
+
*/
|
|
60
|
+
private buildApiUrl(cmd: string, params: Record<string, string> = {}): string {
|
|
61
|
+
const url = new URL(`${this.baseUrl}/api/v2`)
|
|
62
|
+
url.searchParams.set("cmd", cmd)
|
|
63
|
+
if (this.apiKey) {
|
|
64
|
+
url.searchParams.set("apikey", this.apiKey)
|
|
65
|
+
}
|
|
66
|
+
for (const [key, value] of Object.entries(params)) {
|
|
67
|
+
url.searchParams.set(key, value)
|
|
68
|
+
}
|
|
69
|
+
return url.toString()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if Tautulli is reachable
|
|
74
|
+
*/
|
|
75
|
+
async isHealthy(): Promise<boolean> {
|
|
76
|
+
try {
|
|
77
|
+
// Tautulli returns 200 OK even without API key for basic requests
|
|
78
|
+
const response = await fetch(`${this.baseUrl}/status`, {
|
|
79
|
+
method: "GET",
|
|
80
|
+
})
|
|
81
|
+
debugLog("TautulliApi", `Health check: ${response.status}`)
|
|
82
|
+
return response.ok
|
|
83
|
+
} catch (error) {
|
|
84
|
+
debugLog("TautulliApi", `Health check failed: ${error}`)
|
|
85
|
+
return false
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if Tautulli has Plex connection configured
|
|
91
|
+
*/
|
|
92
|
+
async isInitialized(): Promise<boolean> {
|
|
93
|
+
if (!this.apiKey) return false
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const serverInfo = await this.getServerInfo()
|
|
97
|
+
// If we have PMS identifier, Plex is connected
|
|
98
|
+
return !!serverInfo?.pms_identifier
|
|
99
|
+
} catch {
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get or create API key
|
|
106
|
+
* Works without authentication on first run!
|
|
107
|
+
*/
|
|
108
|
+
async getApiKey(username?: string, password?: string): Promise<string | null> {
|
|
109
|
+
debugLog("TautulliApi", "Getting/creating API key...")
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const url = new URL(`${this.baseUrl}/api/v2`)
|
|
113
|
+
url.searchParams.set("cmd", "get_apikey")
|
|
114
|
+
if (username) url.searchParams.set("username", username)
|
|
115
|
+
if (password) url.searchParams.set("password", password)
|
|
116
|
+
|
|
117
|
+
const response = await fetch(url.toString(), { method: "GET" })
|
|
118
|
+
|
|
119
|
+
if (response.ok) {
|
|
120
|
+
const data = (await response.json()) as TautulliApiResponse<string>
|
|
121
|
+
if (data.response?.result === "success" && data.response.data) {
|
|
122
|
+
const apiKey = data.response.data
|
|
123
|
+
this.apiKey = apiKey
|
|
124
|
+
debugLog("TautulliApi", "API key obtained successfully")
|
|
125
|
+
return apiKey
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const text = await response.text()
|
|
130
|
+
debugLog("TautulliApi", `Failed to get API key: ${response.status} - ${text}`)
|
|
131
|
+
} catch (error) {
|
|
132
|
+
debugLog("TautulliApi", `Error getting API key: ${error}`)
|
|
133
|
+
}
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get server info (requires API key)
|
|
139
|
+
*/
|
|
140
|
+
async getServerInfo(): Promise<TautulliServerInfo | null> {
|
|
141
|
+
if (!this.apiKey) return null
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const response = await fetch(this.buildApiUrl("get_server_info"), {
|
|
145
|
+
method: "GET",
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
if (response.ok) {
|
|
149
|
+
const data = (await response.json()) as TautulliApiResponse<TautulliServerInfo>
|
|
150
|
+
if (data.response?.result === "success") {
|
|
151
|
+
return data.response.data
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// Ignore
|
|
156
|
+
}
|
|
157
|
+
return null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get Plex Media Server info (requires API key)
|
|
162
|
+
*/
|
|
163
|
+
async getPlexServerInfo(): Promise<Record<string, unknown> | null> {
|
|
164
|
+
if (!this.apiKey) return null
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const response = await fetch(this.buildApiUrl("get_server_info"), {
|
|
168
|
+
method: "GET",
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
if (response.ok) {
|
|
172
|
+
const data = (await response.json()) as TautulliApiResponse<Record<string, unknown>>
|
|
173
|
+
if (data.response?.result === "success") {
|
|
174
|
+
return data.response.data
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
178
|
+
// Ignore
|
|
179
|
+
}
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Check server connection status
|
|
185
|
+
*/
|
|
186
|
+
async serverStatus(): Promise<boolean> {
|
|
187
|
+
if (!this.apiKey) return false
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const response = await fetch(this.buildApiUrl("server_status"), {
|
|
191
|
+
method: "GET",
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
if (response.ok) {
|
|
195
|
+
const data = (await response.json()) as TautulliApiResponse<{ connected: boolean }>
|
|
196
|
+
return data.response?.result === "success" && data.response.data?.connected === true
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
// Ignore
|
|
200
|
+
}
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get API key from settings (if accessible)
|
|
206
|
+
*/
|
|
207
|
+
async getSettings(): Promise<Record<string, unknown> | null> {
|
|
208
|
+
if (!this.apiKey) return null
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const response = await fetch(this.buildApiUrl("get_settings"), {
|
|
212
|
+
method: "GET",
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
if (response.ok) {
|
|
216
|
+
const data = (await response.json()) as TautulliApiResponse<Record<string, unknown>>
|
|
217
|
+
if (data.response?.result === "success") {
|
|
218
|
+
return data.response.data
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} catch {
|
|
222
|
+
// Ignore
|
|
223
|
+
}
|
|
224
|
+
return null
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Run the auto-setup process for Tautulli
|
|
229
|
+
* Gets API key automatically, but Plex connection requires manual wizard
|
|
230
|
+
*/
|
|
231
|
+
async setup(options: AutoSetupOptions): Promise<AutoSetupResult> {
|
|
232
|
+
try {
|
|
233
|
+
// Check if reachable
|
|
234
|
+
const healthy = await this.isHealthy()
|
|
235
|
+
if (!healthy) {
|
|
236
|
+
return { success: false, message: "Tautulli not reachable" }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Step 1: Get or create API key (works without auth initially)
|
|
240
|
+
debugLog("TautulliApi", "Step 1: Getting API key...")
|
|
241
|
+
let apiKey: string | undefined = this.apiKey
|
|
242
|
+
if (!apiKey) {
|
|
243
|
+
const newKey = await this.getApiKey(options.username, options.password)
|
|
244
|
+
if (!newKey) {
|
|
245
|
+
return { success: false, message: "Failed to get API key" }
|
|
246
|
+
}
|
|
247
|
+
apiKey = newKey
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Step 2: Check if Plex is already connected
|
|
251
|
+
debugLog("TautulliApi", "Step 2: Checking Plex connection...")
|
|
252
|
+
const serverInfo = await this.getServerInfo()
|
|
253
|
+
const plexConnected = !!serverInfo?.pms_identifier
|
|
254
|
+
|
|
255
|
+
if (plexConnected) {
|
|
256
|
+
debugLog("TautulliApi", `Plex connected: ${serverInfo?.pms_name}`)
|
|
257
|
+
return {
|
|
258
|
+
success: true,
|
|
259
|
+
message: `Connected to Plex: ${serverInfo?.pms_name}`,
|
|
260
|
+
data: { apiKey },
|
|
261
|
+
envUpdates: { API_KEY_TAUTULLI: apiKey },
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Plex not connected - requires manual wizard
|
|
266
|
+
// But we still got the API key which is useful
|
|
267
|
+
return {
|
|
268
|
+
success: true,
|
|
269
|
+
message: "API key obtained. Complete Plex connection via web wizard.",
|
|
270
|
+
data: { apiKey, requiresWizard: true },
|
|
271
|
+
envUpdates: { API_KEY_TAUTULLI: apiKey },
|
|
272
|
+
}
|
|
273
|
+
} catch (error) {
|
|
274
|
+
return { success: false, message: `${error}` }
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uptime Kuma API Client
|
|
3
|
+
* Handles Uptime Kuma auto-setup via Socket.IO including user creation and monitor management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { io, Socket } from "socket.io-client"
|
|
7
|
+
import { debugLog } from "../utils/debug"
|
|
8
|
+
import type { IAutoSetupClient, AutoSetupOptions, AutoSetupResult } from "./auto-setup-types"
|
|
9
|
+
import type { AppConfig } from "../config/schema"
|
|
10
|
+
import { getApp } from "../apps/registry"
|
|
11
|
+
|
|
12
|
+
interface MonitorConfig {
|
|
13
|
+
type: "http" | "port" | "ping" | "docker"
|
|
14
|
+
name: string
|
|
15
|
+
url?: string
|
|
16
|
+
hostname?: string
|
|
17
|
+
port?: number
|
|
18
|
+
interval: number
|
|
19
|
+
timeout?: number
|
|
20
|
+
maxretries?: number
|
|
21
|
+
active?: boolean
|
|
22
|
+
docker_container?: string
|
|
23
|
+
docker_host?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface UptimeKumaResponse {
|
|
27
|
+
ok: boolean
|
|
28
|
+
msg?: string
|
|
29
|
+
monitorID?: number
|
|
30
|
+
token?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class UptimeKumaClient implements IAutoSetupClient {
|
|
34
|
+
private host: string
|
|
35
|
+
private port: number
|
|
36
|
+
private socket: Socket | null = null
|
|
37
|
+
private authenticated = false
|
|
38
|
+
|
|
39
|
+
constructor(host: string, port: number = 3001) {
|
|
40
|
+
this.host = host
|
|
41
|
+
this.port = port
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get base URL for Uptime Kuma
|
|
46
|
+
*/
|
|
47
|
+
private get baseUrl(): string {
|
|
48
|
+
return `http://${this.host}:${this.port}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Connect to Socket.IO server
|
|
53
|
+
*/
|
|
54
|
+
private async connect(): Promise<void> {
|
|
55
|
+
if (this.socket?.connected) return
|
|
56
|
+
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const timeout = setTimeout(() => {
|
|
59
|
+
reject(new Error("Connection timeout"))
|
|
60
|
+
}, 10000)
|
|
61
|
+
|
|
62
|
+
this.socket = io(this.baseUrl, {
|
|
63
|
+
transports: ["websocket"],
|
|
64
|
+
reconnection: false,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
this.socket.on("connect", () => {
|
|
68
|
+
clearTimeout(timeout)
|
|
69
|
+
debugLog("UptimeKumaApi", "Connected to Socket.IO")
|
|
70
|
+
resolve()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
this.socket.on("connect_error", (error) => {
|
|
74
|
+
clearTimeout(timeout)
|
|
75
|
+
debugLog("UptimeKumaApi", `Connection error: ${error}`)
|
|
76
|
+
reject(error)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Disconnect from Socket.IO server
|
|
83
|
+
*/
|
|
84
|
+
disconnect(): void {
|
|
85
|
+
if (this.socket) {
|
|
86
|
+
this.socket.disconnect()
|
|
87
|
+
this.socket = null
|
|
88
|
+
this.authenticated = false
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Emit a Socket.IO event and wait for callback response
|
|
94
|
+
*/
|
|
95
|
+
private emit<T>(event: string, ...args: unknown[]): Promise<T> {
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
if (!this.socket) {
|
|
98
|
+
reject(new Error("Not connected"))
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const timeout = setTimeout(() => {
|
|
103
|
+
reject(new Error(`Timeout waiting for ${event} response`))
|
|
104
|
+
}, 15000)
|
|
105
|
+
|
|
106
|
+
this.socket.emit(event, ...args, (response: T) => {
|
|
107
|
+
clearTimeout(timeout)
|
|
108
|
+
resolve(response)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if Uptime Kuma is reachable
|
|
115
|
+
*/
|
|
116
|
+
async isHealthy(): Promise<boolean> {
|
|
117
|
+
try {
|
|
118
|
+
const response = await fetch(`${this.baseUrl}/api/status-page/heartbeat/main`, {
|
|
119
|
+
method: "GET",
|
|
120
|
+
})
|
|
121
|
+
// Even 404 means server is running
|
|
122
|
+
debugLog("UptimeKumaApi", `Health check: ${response.status}`)
|
|
123
|
+
return response.status !== 502 && response.status !== 503
|
|
124
|
+
} catch (error) {
|
|
125
|
+
// Try simple connection
|
|
126
|
+
try {
|
|
127
|
+
const response = await fetch(this.baseUrl)
|
|
128
|
+
return response.ok || response.status === 404
|
|
129
|
+
} catch {
|
|
130
|
+
debugLog("UptimeKumaApi", `Health check failed: ${error}`)
|
|
131
|
+
return false
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check if already initialized (has users)
|
|
138
|
+
*/
|
|
139
|
+
async isInitialized(): Promise<boolean> {
|
|
140
|
+
try {
|
|
141
|
+
await this.connect()
|
|
142
|
+
// Try to get info - if it needs setup, needSetup will be true
|
|
143
|
+
const response = await this.emit<{ needSetup: boolean }>("needSetup")
|
|
144
|
+
this.disconnect()
|
|
145
|
+
return !response.needSetup
|
|
146
|
+
} catch {
|
|
147
|
+
this.disconnect()
|
|
148
|
+
return true // Assume initialized if we can't check
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Setup initial admin user
|
|
154
|
+
*/
|
|
155
|
+
async setupAdmin(username: string, password: string): Promise<UptimeKumaResponse> {
|
|
156
|
+
await this.connect()
|
|
157
|
+
|
|
158
|
+
const response = await this.emit<UptimeKumaResponse>("setup", {
|
|
159
|
+
username,
|
|
160
|
+
password,
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
if (response.ok) {
|
|
164
|
+
this.authenticated = true
|
|
165
|
+
debugLog("UptimeKumaApi", "Admin user created")
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return response
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Login with username and password
|
|
173
|
+
*/
|
|
174
|
+
async login(username: string, password: string): Promise<boolean> {
|
|
175
|
+
await this.connect()
|
|
176
|
+
|
|
177
|
+
const response = await this.emit<UptimeKumaResponse>("login", {
|
|
178
|
+
username,
|
|
179
|
+
password,
|
|
180
|
+
token: "",
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
if (response.ok) {
|
|
184
|
+
this.authenticated = true
|
|
185
|
+
debugLog("UptimeKumaApi", "Logged in successfully")
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return response.ok
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Add a monitor
|
|
193
|
+
*/
|
|
194
|
+
async addMonitor(config: MonitorConfig): Promise<number | null> {
|
|
195
|
+
if (!this.authenticated) {
|
|
196
|
+
throw new Error("Not authenticated")
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const payload = {
|
|
200
|
+
type: config.type,
|
|
201
|
+
name: config.name,
|
|
202
|
+
url: config.url,
|
|
203
|
+
hostname: config.hostname,
|
|
204
|
+
port: config.port,
|
|
205
|
+
interval: config.interval || 60,
|
|
206
|
+
timeout: config.timeout || 30,
|
|
207
|
+
maxretries: config.maxretries || 3,
|
|
208
|
+
active: config.active ?? true,
|
|
209
|
+
docker_container: config.docker_container,
|
|
210
|
+
docker_host: config.docker_host,
|
|
211
|
+
accepted_statuscodes: ["200-299"],
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const response = await this.emit<UptimeKumaResponse>("add", payload)
|
|
215
|
+
|
|
216
|
+
if (response.ok && response.monitorID) {
|
|
217
|
+
debugLog("UptimeKumaApi", `Monitor "${config.name}" created with ID ${response.monitorID}`)
|
|
218
|
+
return response.monitorID
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
debugLog("UptimeKumaApi", `Failed to create monitor: ${response.msg}`)
|
|
222
|
+
return null
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get list of all monitors
|
|
227
|
+
*/
|
|
228
|
+
async getMonitors(): Promise<Record<string, unknown>[]> {
|
|
229
|
+
if (!this.authenticated) {
|
|
230
|
+
throw new Error("Not authenticated")
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return new Promise((resolve) => {
|
|
234
|
+
if (!this.socket) {
|
|
235
|
+
resolve([])
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Uptime Kuma sends monitor list via 'monitorList' event
|
|
240
|
+
const timeout = setTimeout(() => resolve([]), 5000)
|
|
241
|
+
|
|
242
|
+
this.socket.once("monitorList", (data: Record<string, Record<string, unknown>>) => {
|
|
243
|
+
clearTimeout(timeout)
|
|
244
|
+
resolve(Object.values(data) as Record<string, unknown>[])
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
// Request monitor list
|
|
248
|
+
this.socket.emit("getMonitorList")
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Auto-add monitors for enabled easiarr apps
|
|
254
|
+
*/
|
|
255
|
+
async setupEasiarrMonitors(apps: AppConfig[]): Promise<number> {
|
|
256
|
+
let addedCount = 0
|
|
257
|
+
|
|
258
|
+
// Get existing monitors to avoid duplicates
|
|
259
|
+
const existingMonitors = await this.getMonitors()
|
|
260
|
+
const existingNames = new Set(existingMonitors.map((m) => m.name as string))
|
|
261
|
+
|
|
262
|
+
for (const appConfig of apps) {
|
|
263
|
+
if (!appConfig.enabled) continue
|
|
264
|
+
|
|
265
|
+
const appDef = getApp(appConfig.id)
|
|
266
|
+
if (!appDef) continue
|
|
267
|
+
|
|
268
|
+
// Skip apps without web UI
|
|
269
|
+
if (appDef.defaultPort === 0) continue
|
|
270
|
+
|
|
271
|
+
const monitorName = `Easiarr - ${appDef.name}`
|
|
272
|
+
|
|
273
|
+
// Skip if already exists
|
|
274
|
+
if (existingNames.has(monitorName)) {
|
|
275
|
+
debugLog("UptimeKumaApi", `Monitor "${monitorName}" already exists, skipping`)
|
|
276
|
+
continue
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const port = appConfig.port || appDef.defaultPort
|
|
280
|
+
const internalPort = appDef.internalPort || port
|
|
281
|
+
|
|
282
|
+
// Create HTTP monitor for web UIs
|
|
283
|
+
const monitorId = await this.addMonitor({
|
|
284
|
+
type: "http",
|
|
285
|
+
name: monitorName,
|
|
286
|
+
url: `http://${appConfig.id}:${internalPort}`,
|
|
287
|
+
interval: 60,
|
|
288
|
+
timeout: 30,
|
|
289
|
+
maxretries: 2,
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
if (monitorId) {
|
|
293
|
+
addedCount++
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return addedCount
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Run the auto-setup process for Uptime Kuma
|
|
302
|
+
*/
|
|
303
|
+
async setup(options: AutoSetupOptions): Promise<AutoSetupResult> {
|
|
304
|
+
const { username, password } = options
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
// Check if reachable
|
|
308
|
+
const healthy = await this.isHealthy()
|
|
309
|
+
if (!healthy) {
|
|
310
|
+
return { success: false, message: "Uptime Kuma not reachable" }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Check if needs initial setup
|
|
314
|
+
const initialized = await this.isInitialized()
|
|
315
|
+
|
|
316
|
+
if (!initialized) {
|
|
317
|
+
// Create admin user
|
|
318
|
+
const setupResult = await this.setupAdmin(username, password)
|
|
319
|
+
if (!setupResult.ok) {
|
|
320
|
+
return { success: false, message: `Setup failed: ${setupResult.msg}` }
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
// Login with existing credentials
|
|
324
|
+
const loggedIn = await this.login(username, password)
|
|
325
|
+
if (!loggedIn) {
|
|
326
|
+
this.disconnect()
|
|
327
|
+
return { success: false, message: "Login failed - check credentials" }
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
this.disconnect()
|
|
332
|
+
return {
|
|
333
|
+
success: true,
|
|
334
|
+
message: initialized ? "Logged in" : "Admin created",
|
|
335
|
+
data: { adminCreated: !initialized },
|
|
336
|
+
}
|
|
337
|
+
} catch (error) {
|
|
338
|
+
this.disconnect()
|
|
339
|
+
return { success: false, message: `${error}` }
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|