@muhammedaksam/easiarr 1.4.0 → 1.4.1

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.
Files changed (109) hide show
  1. package/dist/index.js +29837 -0
  2. package/package.json +15 -15
  3. package/src/VersionInfo.ts +0 -12
  4. package/src/api/arr-api.ts +0 -521
  5. package/src/api/auto-setup-types.ts +0 -64
  6. package/src/api/base-api.ts +0 -216
  7. package/src/api/bazarr-api.ts +0 -297
  8. package/src/api/cloudflare-api.ts +0 -493
  9. package/src/api/custom-format-api.ts +0 -224
  10. package/src/api/grafana-api.ts +0 -230
  11. package/src/api/heimdall-api.ts +0 -141
  12. package/src/api/homarr-api.ts +0 -209
  13. package/src/api/huntarr-api.ts +0 -457
  14. package/src/api/index.ts +0 -9
  15. package/src/api/jellyfin-api.ts +0 -389
  16. package/src/api/jellyseerr-api.ts +0 -559
  17. package/src/api/maintainerr-api.ts +0 -461
  18. package/src/api/naming-config.ts +0 -101
  19. package/src/api/overseerr-api.ts +0 -392
  20. package/src/api/plex-api.ts +0 -308
  21. package/src/api/portainer-api.ts +0 -374
  22. package/src/api/profilarr-api.ts +0 -391
  23. package/src/api/prowlarr-api.ts +0 -488
  24. package/src/api/qbittorrent-api.ts +0 -300
  25. package/src/api/quality-profile-api.ts +0 -272
  26. package/src/api/tautulli-api.ts +0 -244
  27. package/src/api/uptime-kuma-api.ts +0 -343
  28. package/src/apps/categories.ts +0 -14
  29. package/src/apps/index.ts +0 -2
  30. package/src/apps/registry.ts +0 -1275
  31. package/src/compose/caddy-config.ts +0 -130
  32. package/src/compose/generator.ts +0 -287
  33. package/src/compose/index.ts +0 -7
  34. package/src/compose/templates.ts +0 -78
  35. package/src/compose/traefik-config.ts +0 -187
  36. package/src/config/bookmarks-generator.ts +0 -160
  37. package/src/config/defaults.ts +0 -37
  38. package/src/config/homepage-config.ts +0 -381
  39. package/src/config/index.ts +0 -6
  40. package/src/config/manager.ts +0 -150
  41. package/src/config/recyclarr-config.ts +0 -181
  42. package/src/config/schema.ts +0 -310
  43. package/src/config/slskd-config.ts +0 -86
  44. package/src/config/soularr-config.ts +0 -105
  45. package/src/config/trash-quality-definitions.ts +0 -213
  46. package/src/data/lidarr-custom-formats.ts +0 -127
  47. package/src/data/trash-profiles.ts +0 -333
  48. package/src/docker/client.ts +0 -312
  49. package/src/docker/index.ts +0 -18
  50. package/src/index.ts +0 -77
  51. package/src/setup/actions/arr-common.ts +0 -307
  52. package/src/setup/actions/dashboards.ts +0 -79
  53. package/src/setup/actions/download-clients.ts +0 -54
  54. package/src/setup/actions/jellyseerr.ts +0 -331
  55. package/src/setup/actions/media-servers.ts +0 -68
  56. package/src/setup/actions/monitoring.ts +0 -120
  57. package/src/setup/actions/prowlarr.ts +0 -137
  58. package/src/setup/actions/utilities.ts +0 -490
  59. package/src/setup/index.ts +0 -78
  60. package/src/setup/types.ts +0 -144
  61. package/src/structure/manager.ts +0 -129
  62. package/src/ui/App.ts +0 -111
  63. package/src/ui/components/ApplicationSelector.ts +0 -275
  64. package/src/ui/components/CredentialsForm.ts +0 -180
  65. package/src/ui/components/DownloadClientForm.ts +0 -114
  66. package/src/ui/components/FileEditor.ts +0 -91
  67. package/src/ui/components/FooterHint.ts +0 -119
  68. package/src/ui/components/PageLayout.ts +0 -160
  69. package/src/ui/components/UpdateNotification.ts +0 -102
  70. package/src/ui/index.ts +0 -7
  71. package/src/ui/screens/AdvancedSettings.ts +0 -188
  72. package/src/ui/screens/ApiKeyViewer.ts +0 -511
  73. package/src/ui/screens/AppConfigurator.ts +0 -543
  74. package/src/ui/screens/AppManager.ts +0 -284
  75. package/src/ui/screens/ContainerControl.ts +0 -581
  76. package/src/ui/screens/LogsViewer.ts +0 -478
  77. package/src/ui/screens/MainMenu.ts +0 -299
  78. package/src/ui/screens/MonitorDashboard.ts +0 -881
  79. package/src/ui/screens/SecretsEditor.ts +0 -216
  80. package/src/ui/screens/SettingsScreen.ts +0 -633
  81. package/src/ui/screens/index.ts +0 -22
  82. package/src/ui/screens/setup/BaseAppSetupScreen.ts +0 -375
  83. package/src/ui/screens/setup/CloudflaredSetup.ts +0 -460
  84. package/src/ui/screens/setup/FullAutoSetup.ts +0 -766
  85. package/src/ui/screens/setup/HomepageSetup.ts +0 -165
  86. package/src/ui/screens/setup/JellyfinSetup.ts +0 -328
  87. package/src/ui/screens/setup/JellyseerrSetup.ts +0 -242
  88. package/src/ui/screens/setup/ProwlarrSetup.ts +0 -500
  89. package/src/ui/screens/setup/QBittorrentSetup.ts +0 -221
  90. package/src/ui/screens/setup/QuickSetup.ts +0 -1169
  91. package/src/ui/screens/setup/RecyclarrSetup.ts +0 -270
  92. package/src/ui/screens/setup/TRaSHProfileSetup.ts +0 -381
  93. package/src/utils/arch.ts +0 -78
  94. package/src/utils/browser.ts +0 -27
  95. package/src/utils/categories.ts +0 -61
  96. package/src/utils/debug.ts +0 -84
  97. package/src/utils/env.ts +0 -138
  98. package/src/utils/index.ts +0 -8
  99. package/src/utils/logs.ts +0 -119
  100. package/src/utils/migrations/1765626338_rename_env_variables.ts +0 -102
  101. package/src/utils/migrations/1765707135_rename_easiarr_status.ts +0 -91
  102. package/src/utils/migrations/1765732722_remove_cloudflare_dns_api_token.ts +0 -45
  103. package/src/utils/migrations/1769515650_migrate_to_xdg.ts +0 -114
  104. package/src/utils/migrations.ts +0 -165
  105. package/src/utils/password.ts +0 -42
  106. package/src/utils/paths.ts +0 -33
  107. package/src/utils/unraid.ts +0 -103
  108. package/src/utils/update-checker.ts +0 -210
  109. package/src/utils/url-utils.ts +0 -38
@@ -1,881 +0,0 @@
1
- /**
2
- * Monitor Dashboard Screen
3
- * Three-panel master-detail layout for configuring health monitoring
4
- */
5
-
6
- import type { CliRenderer, KeyEvent } from "@opentui/core"
7
-
8
- import {
9
- BoxRenderable,
10
- TabSelectRenderable,
11
- TabSelectRenderableEvents,
12
- TextRenderable,
13
- } from "@opentui/core"
14
-
15
- import type {
16
- DiskSpaceResource,
17
- HealthResource,
18
- QueueStatusResource,
19
- SystemResource,
20
- } from "~/api/arr-api"
21
- import type {
22
- AppCategory,
23
- AppId,
24
- EasiarrConfig,
25
- MonitorConfig,
26
- MonitorOptions,
27
- } from "~/config/schema"
28
- import { ArrApiClient } from "~/api/arr-api"
29
- import { APPS } from "~/apps/registry"
30
- import { saveConfig } from "~/config/manager"
31
- import { APP_CATEGORIES } from "~/config/schema"
32
- import { createPageLayout } from "~/ui/components/PageLayout"
33
-
34
- // Default monitoring options
35
- const DEFAULT_CHECKS: MonitorOptions = {
36
- health: true,
37
- diskspace: true,
38
- status: true,
39
- queue: true,
40
- }
41
-
42
- // Categories that support monitoring (have compatible APIs)
43
- const MONITORABLE_CATEGORIES: AppCategory[] = ["servarr", "indexer"]
44
-
45
- // Check type labels
46
- const CHECK_LABELS: Record<keyof MonitorOptions, string> = {
47
- health: "Health Warnings",
48
- diskspace: "Disk Space",
49
- status: "System Status",
50
- queue: "Queue Info",
51
- }
52
-
53
- export class MonitorDashboard extends BoxRenderable {
54
- private config: EasiarrConfig
55
- private onBack: () => void
56
- private _renderer: CliRenderer
57
-
58
- private page!: BoxRenderable
59
- private modeTabs!: TabSelectRenderable
60
- private categoriesPanel!: BoxRenderable
61
- private appsPanel!: BoxRenderable
62
- private checksPanel!: BoxRenderable
63
-
64
- // State
65
- private currentMode: "configure" | "status" = "configure"
66
- private currentPanel: 0 | 1 | 2 = 0 // 0=categories, 1=apps, 2=checks
67
- private categoryIndex = 0
68
- private appIndex = 0
69
- private checkIndex = 0
70
-
71
- // Data
72
- private categoryConfigs: Map<AppCategory, { enabled: boolean; checks: MonitorOptions }>
73
- private appConfigs: Map<AppId, { override: boolean; enabled: boolean; checks: MonitorOptions }>
74
- private availableCategories: AppCategory[] = []
75
- private keyHandler: (key: KeyEvent) => void
76
-
77
- // Status data
78
- private statusData: Map<
79
- AppId,
80
- {
81
- loading: boolean
82
- error?: string
83
- health?: HealthResource[]
84
- disk?: DiskSpaceResource[]
85
- system?: SystemResource
86
- queue?: QueueStatusResource
87
- }
88
- > = new Map()
89
-
90
- constructor(renderer: CliRenderer, config: EasiarrConfig, onBack: () => void) {
91
- super(renderer, {
92
- width: "100%",
93
- height: "100%",
94
- flexDirection: "column",
95
- })
96
-
97
- this._renderer = renderer
98
- this.config = config
99
- this.onBack = onBack
100
-
101
- this.categoryConfigs = new Map()
102
- this.appConfigs = new Map()
103
- this.loadFromConfig()
104
- this.computeAvailableCategories()
105
-
106
- this.keyHandler = this.handleKey.bind(this)
107
- this.buildUI()
108
- }
109
-
110
- private loadFromConfig(): void {
111
- const monitor = this.config.monitor
112
-
113
- for (const cat of MONITORABLE_CATEGORIES) {
114
- const existing = monitor?.categories?.find((c) => c.category === cat)
115
- if (existing) {
116
- this.categoryConfigs.set(cat, {
117
- enabled: existing.enabled,
118
- checks: { ...existing.checks },
119
- })
120
- } else {
121
- this.categoryConfigs.set(cat, {
122
- enabled: true,
123
- checks: { ...DEFAULT_CHECKS },
124
- })
125
- }
126
- }
127
-
128
- const arrApps = this.getMonitorableApps()
129
- for (const app of arrApps) {
130
- const existing = monitor?.apps?.find((a) => a.appId === app.id)
131
- if (existing) {
132
- this.appConfigs.set(app.id, {
133
- override: existing.override,
134
- enabled: existing.enabled,
135
- checks: { ...existing.checks },
136
- })
137
- } else {
138
- this.appConfigs.set(app.id, {
139
- override: false,
140
- enabled: true,
141
- checks: { ...DEFAULT_CHECKS },
142
- })
143
- }
144
- }
145
- }
146
-
147
- private computeAvailableCategories(): void {
148
- // Only show categories that have at least one enabled app
149
- this.availableCategories = MONITORABLE_CATEGORIES.filter((cat) => {
150
- const apps = this.getAppsInCategory(cat)
151
- return apps.length > 0
152
- })
153
- }
154
-
155
- private getMonitorableApps() {
156
- return this.config.apps
157
- .filter((a) => a.enabled)
158
- .map((a) => APPS[a.id])
159
- .filter((app) => app && MONITORABLE_CATEGORIES.includes(app.category))
160
- }
161
-
162
- private getAppsInCategory(category: AppCategory) {
163
- return this.config.apps
164
- .filter((a) => a.enabled)
165
- .map((a) => APPS[a.id])
166
- .filter((app) => app && app.category === category)
167
- }
168
-
169
- private buildUI(): void {
170
- const { container: page, content } = createPageLayout(this._renderer, {
171
- title: "Monitor Dashboard",
172
- stepInfo: "Configure Health Monitoring",
173
- footerHint: [
174
- { type: "key", key: "Tab", value: "Panel" },
175
- { type: "key", key: "↑↓", value: "Navigate" },
176
- { type: "key", key: "Space", value: "Toggle" },
177
- { type: "key", key: "1-4", value: "Checks" },
178
- { type: "key", key: "←→", value: "Mode" },
179
- { type: "key", key: "s", value: "Save" },
180
- { type: "key", key: "q", value: "Back" },
181
- ],
182
- })
183
- this.page = page
184
-
185
- // Mode tabs
186
- this.modeTabs = new TabSelectRenderable(this._renderer, {
187
- width: "100%",
188
- options: [
189
- { name: "Configure", value: "configure", description: "" },
190
- { name: "Status", value: "status", description: "" },
191
- ],
192
- tabWidth: 12,
193
- showUnderline: false,
194
- showDescription: false,
195
- selectedBackgroundColor: "#4a9eff",
196
- textColor: "#555555",
197
- })
198
-
199
- this.modeTabs.on(TabSelectRenderableEvents.ITEM_SELECTED, (index) => {
200
- this.currentMode = index === 0 ? "configure" : "status"
201
- this.updateAllPanels()
202
- })
203
-
204
- content.add(this.modeTabs)
205
- content.add(new TextRenderable(this._renderer, { content: " " }))
206
-
207
- // Three-panel row
208
- const panelRow = new BoxRenderable(this._renderer, {
209
- width: "100%",
210
- flexGrow: 1,
211
- flexDirection: "row",
212
- })
213
-
214
- // Categories panel (left)
215
- this.categoriesPanel = new BoxRenderable(this._renderer, {
216
- width: "25%",
217
- height: "100%",
218
- flexDirection: "column",
219
- border: true,
220
- borderStyle: "single",
221
- borderColor: "#4a9eff",
222
- title: "Categories",
223
- titleAlignment: "left",
224
- backgroundColor: "#151525",
225
- })
226
-
227
- // Apps panel (middle)
228
- this.appsPanel = new BoxRenderable(this._renderer, {
229
- width: "35%",
230
- height: "100%",
231
- flexDirection: "column",
232
- border: true,
233
- borderStyle: "single",
234
- borderColor: "#555555",
235
- title: "Apps",
236
- titleAlignment: "left",
237
- backgroundColor: "#151525",
238
- })
239
-
240
- // Checks panel (right)
241
- this.checksPanel = new BoxRenderable(this._renderer, {
242
- width: "40%",
243
- height: "100%",
244
- flexDirection: "column",
245
- border: true,
246
- borderStyle: "single",
247
- borderColor: "#555555",
248
- title: "Checks",
249
- titleAlignment: "left",
250
- backgroundColor: "#151525",
251
- })
252
-
253
- panelRow.add(this.categoriesPanel)
254
- panelRow.add(this.appsPanel)
255
- panelRow.add(this.checksPanel)
256
- content.add(panelRow)
257
-
258
- this._renderer.keyInput.on("keypress", this.keyHandler)
259
- this.add(page)
260
-
261
- this.updateAllPanels()
262
- }
263
-
264
- private updateAllPanels(): void {
265
- this.renderCategoriesPanel()
266
- this.renderAppsPanel()
267
- this.renderChecksPanel()
268
- this.updatePanelBorders()
269
- }
270
-
271
- private updatePanelBorders(): void {
272
- // Highlight focused panel
273
- this.categoriesPanel.borderColor = this.currentPanel === 0 ? "#4a9eff" : "#555555"
274
- this.appsPanel.borderColor = this.currentPanel === 1 ? "#4a9eff" : "#555555"
275
- this.checksPanel.borderColor = this.currentPanel === 2 ? "#4a9eff" : "#555555"
276
- }
277
-
278
- private clearPanel(panel: BoxRenderable): void {
279
- const children = panel.getChildren()
280
- children.forEach((c) => panel.remove(c.id))
281
- }
282
-
283
- private renderCategoriesPanel(): void {
284
- this.clearPanel(this.categoriesPanel)
285
-
286
- if (this.availableCategories.length === 0) {
287
- this.categoriesPanel.add(
288
- new TextRenderable(this._renderer, {
289
- content: "No apps enabled",
290
- fg: "#888888",
291
- })
292
- )
293
- return
294
- }
295
-
296
- this.availableCategories.forEach((cat, idx) => {
297
- const apps = this.getAppsInCategory(cat)
298
- const cfg = this.categoryConfigs.get(cat)!
299
- const pointer = idx === this.categoryIndex ? "▶ " : " "
300
- const enabledIcon = cfg.enabled ? "●" : "○"
301
- const fg = idx === this.categoryIndex ? "#50fa7b" : "#aaaaaa"
302
-
303
- this.categoriesPanel.add(
304
- new TextRenderable(this._renderer, {
305
- id: `cat-${idx}`,
306
- content: `${pointer}${enabledIcon} ${APP_CATEGORIES[cat]} (${apps.length})`,
307
- fg,
308
- })
309
- )
310
- })
311
- }
312
-
313
- private renderAppsPanel(): void {
314
- this.clearPanel(this.appsPanel)
315
-
316
- if (this.availableCategories.length === 0) {
317
- return
318
- }
319
-
320
- const selectedCat = this.availableCategories[this.categoryIndex]
321
- if (!selectedCat) return
322
-
323
- const apps = this.getAppsInCategory(selectedCat)
324
-
325
- if (apps.length === 0) {
326
- this.appsPanel.add(
327
- new TextRenderable(this._renderer, {
328
- content: "No apps in category",
329
- fg: "#888888",
330
- })
331
- )
332
- return
333
- }
334
-
335
- // Clamp appIndex
336
- if (this.appIndex >= apps.length) {
337
- this.appIndex = 0
338
- }
339
-
340
- apps.forEach((app, idx) => {
341
- const cfg = this.appConfigs.get(app.id)
342
- const pointer = idx === this.appIndex ? "▶ " : " "
343
- const overrideIcon = cfg?.override ? "⚙" : " "
344
- const enabledIcon = cfg?.enabled ? "●" : "○"
345
- const fg = idx === this.appIndex ? "#50fa7b" : "#aaaaaa"
346
-
347
- this.appsPanel.add(
348
- new TextRenderable(this._renderer, {
349
- id: `app-${idx}`,
350
- content: `${pointer}${overrideIcon}${enabledIcon} ${app.name}`,
351
- fg,
352
- })
353
- )
354
- })
355
- }
356
-
357
- private renderChecksPanel(): void {
358
- this.clearPanel(this.checksPanel)
359
-
360
- if (this.currentMode === "status") {
361
- this.renderStatusView()
362
- return
363
- }
364
-
365
- if (this.availableCategories.length === 0) {
366
- return
367
- }
368
-
369
- const selectedCat = this.availableCategories[this.categoryIndex]
370
- if (!selectedCat) return
371
-
372
- // When Categories panel is focused, show category-level settings
373
- if (this.currentPanel === 0) {
374
- this.renderCategoryChecks(selectedCat)
375
- return
376
- }
377
-
378
- // When Apps or Checks panel is focused, show app-level settings
379
- const apps = this.getAppsInCategory(selectedCat)
380
- const selectedApp = apps[this.appIndex]
381
- if (!selectedApp) return
382
-
383
- const cfg = this.appConfigs.get(selectedApp.id)
384
- if (!cfg) return
385
-
386
- // Title
387
- this.checksPanel.add(
388
- new TextRenderable(this._renderer, {
389
- id: "checks-title",
390
- content: `${selectedApp.name}`,
391
- fg: "#4a9eff",
392
- attributes: 1, // bold
393
- })
394
- )
395
-
396
- this.checksPanel.add(
397
- new TextRenderable(this._renderer, {
398
- id: "checks-divider",
399
- content: "─".repeat(20),
400
- fg: "#555555",
401
- })
402
- )
403
-
404
- // Check toggles
405
- const checks: (keyof MonitorOptions)[] = ["health", "diskspace", "status", "queue"]
406
- const effectiveChecks = cfg.override
407
- ? cfg.checks
408
- : this.categoryConfigs.get(selectedCat)!.checks
409
-
410
- checks.forEach((check, idx) => {
411
- const isEnabled = effectiveChecks[check]
412
- const pointer = idx === this.checkIndex ? "▶ " : " "
413
- const icon = isEnabled ? "[✓]" : "[ ]"
414
- const fg = idx === this.checkIndex ? "#50fa7b" : "#aaaaaa"
415
- const dimmed = !cfg.override && idx === this.checkIndex ? " (category)" : ""
416
-
417
- this.checksPanel.add(
418
- new TextRenderable(this._renderer, {
419
- id: `check-${idx}`,
420
- content: `${pointer}${icon} ${CHECK_LABELS[check]}${dimmed}`,
421
- fg,
422
- })
423
- )
424
- })
425
-
426
- // Override toggle
427
- this.checksPanel.add(new TextRenderable(this._renderer, { content: " " }))
428
-
429
- const overridePointer = this.checkIndex === 4 ? "▶ " : " "
430
- const overrideIcon = cfg.override ? "[✓]" : "[ ]"
431
- const overrideFg = this.checkIndex === 4 ? "#50fa7b" : "#8be9fd"
432
-
433
- this.checksPanel.add(
434
- new TextRenderable(this._renderer, {
435
- id: "override-toggle",
436
- content: `${overridePointer}${overrideIcon} Override Category`,
437
- fg: overrideFg,
438
- })
439
- )
440
- }
441
-
442
- private renderCategoryChecks(category: AppCategory): void {
443
- const cfg = this.categoryConfigs.get(category)
444
- if (!cfg) return
445
-
446
- // Title
447
- this.checksPanel.add(
448
- new TextRenderable(this._renderer, {
449
- id: "cat-checks-title",
450
- content: `${APP_CATEGORIES[category]} Defaults`,
451
- fg: "#4a9eff",
452
- attributes: 1, // bold
453
- })
454
- )
455
-
456
- this.checksPanel.add(
457
- new TextRenderable(this._renderer, {
458
- id: "cat-checks-subtitle",
459
- content: "Default checks for all apps in category",
460
- fg: "#888888",
461
- })
462
- )
463
-
464
- this.checksPanel.add(
465
- new TextRenderable(this._renderer, {
466
- id: "cat-checks-divider",
467
- content: "─".repeat(25),
468
- fg: "#555555",
469
- })
470
- )
471
-
472
- // Check toggles for category defaults
473
- const checks: (keyof MonitorOptions)[] = ["health", "diskspace", "status", "queue"]
474
-
475
- checks.forEach((check, idx) => {
476
- const isEnabled = cfg.checks[check]
477
- const pointer = idx === this.checkIndex ? "▶ " : " "
478
- const icon = isEnabled ? "[✓]" : "[ ]"
479
- const fg = idx === this.checkIndex ? "#50fa7b" : "#aaaaaa"
480
-
481
- this.checksPanel.add(
482
- new TextRenderable(this._renderer, {
483
- id: `cat-check-${idx}`,
484
- content: `${pointer}${icon} ${CHECK_LABELS[check]}`,
485
- fg,
486
- })
487
- )
488
- })
489
-
490
- // Info about inheritance
491
- this.checksPanel.add(new TextRenderable(this._renderer, { content: " " }))
492
- this.checksPanel.add(
493
- new TextRenderable(this._renderer, {
494
- id: "cat-checks-info",
495
- content: "Apps inherit these unless overridden",
496
- fg: "#555555",
497
- })
498
- )
499
- }
500
-
501
- private renderStatusView(): void {
502
- if (this.availableCategories.length === 0) {
503
- return
504
- }
505
-
506
- const selectedCat = this.availableCategories[this.categoryIndex]
507
- if (!selectedCat) return
508
-
509
- const apps = this.getAppsInCategory(selectedCat)
510
- const selectedApp = apps[this.appIndex]
511
- if (!selectedApp) return
512
-
513
- const status = this.statusData.get(selectedApp.id)
514
-
515
- // Title
516
- this.checksPanel.add(
517
- new TextRenderable(this._renderer, {
518
- content: `📊 ${selectedApp.name} Status`,
519
- fg: "#4a9eff",
520
- attributes: 1,
521
- })
522
- )
523
-
524
- this.checksPanel.add(
525
- new TextRenderable(this._renderer, {
526
- content: "─".repeat(25),
527
- fg: "#555555",
528
- })
529
- )
530
-
531
- if (!status) {
532
- this.checksPanel.add(
533
- new TextRenderable(this._renderer, {
534
- content: "\nPress 'r' to fetch status",
535
- fg: "#888888",
536
- })
537
- )
538
- return
539
- }
540
-
541
- if (status.loading) {
542
- this.checksPanel.add(
543
- new TextRenderable(this._renderer, {
544
- content: "\n⏳ Loading...",
545
- fg: "#f1fa8c",
546
- })
547
- )
548
- return
549
- }
550
-
551
- if (status.error) {
552
- this.checksPanel.add(
553
- new TextRenderable(this._renderer, {
554
- content: `\n❌ Error: ${status.error}`,
555
- fg: "#ff5555",
556
- })
557
- )
558
- return
559
- }
560
-
561
- // Health warnings
562
- if (status.health && status.health.length > 0) {
563
- this.checksPanel.add(
564
- new TextRenderable(this._renderer, {
565
- content: `\n⚠️ Health Warnings: ${status.health.length}`,
566
- fg: "#ffb86c",
567
- })
568
- )
569
- status.health.slice(0, 3).forEach((h) => {
570
- this.checksPanel.add(
571
- new TextRenderable(this._renderer, {
572
- content: ` • ${h.message}`,
573
- fg: "#f1fa8c",
574
- })
575
- )
576
- })
577
- } else {
578
- this.checksPanel.add(
579
- new TextRenderable(this._renderer, {
580
- content: "\n✓ Health: OK",
581
- fg: "#50fa7b",
582
- })
583
- )
584
- }
585
-
586
- // Disk space
587
- if (status.disk && status.disk.length > 0) {
588
- const totalFree = status.disk.reduce((sum, d) => sum + (d.freeSpace || 0), 0)
589
- const freeGB = (totalFree / 1024 / 1024 / 1024).toFixed(1)
590
- const freeColor = totalFree < 10 * 1024 * 1024 * 1024 ? "#ff5555" : "#50fa7b"
591
- this.checksPanel.add(
592
- new TextRenderable(this._renderer, {
593
- content: `\n💾 Disk Free: ${freeGB} GB`,
594
- fg: freeColor,
595
- })
596
- )
597
- }
598
-
599
- // System status
600
- if (status.system) {
601
- this.checksPanel.add(
602
- new TextRenderable(this._renderer, {
603
- content: `\n📦 Version: ${status.system.version || "Unknown"}`,
604
- fg: "#8be9fd",
605
- })
606
- )
607
- }
608
-
609
- // Queue
610
- if (status.queue) {
611
- const queueCount = status.queue.totalCount || 0
612
- this.checksPanel.add(
613
- new TextRenderable(this._renderer, {
614
- content: `\n📥 Queue: ${queueCount} items`,
615
- fg: queueCount > 0 ? "#f1fa8c" : "#50fa7b",
616
- })
617
- )
618
- }
619
-
620
- this.checksPanel.add(
621
- new TextRenderable(this._renderer, {
622
- content: "\n\nPress 'r' to refresh",
623
- fg: "#555555",
624
- })
625
- )
626
- }
627
-
628
- private async fetchStatus(): Promise<void> {
629
- if (this.availableCategories.length === 0) return
630
-
631
- const selectedCat = this.availableCategories[this.categoryIndex]
632
- if (!selectedCat) return
633
-
634
- const apps = this.getAppsInCategory(selectedCat)
635
- const selectedApp = apps[this.appIndex]
636
- if (!selectedApp) return
637
-
638
- // Read API key from env file
639
- const { readEnv } = await import("~/utils/env")
640
- const env = await readEnv()
641
- const apiKey = env[`API_KEY_${selectedApp.id.toUpperCase()}`]
642
-
643
- if (!apiKey) {
644
- this.statusData.set(selectedApp.id, {
645
- loading: false,
646
- error: "No API key in .env",
647
- })
648
- this.renderChecksPanel()
649
- return
650
- }
651
-
652
- // Mark as loading
653
- this.statusData.set(selectedApp.id, { loading: true })
654
- this.renderChecksPanel()
655
-
656
- try {
657
- const client = new ArrApiClient("localhost", selectedApp.defaultPort, apiKey)
658
-
659
- const [health, disk, system, queue] = await Promise.allSettled([
660
- client.getHealth(),
661
- client.getDiskSpace(),
662
- client.getSystemStatus(),
663
- client.getQueueStatus(),
664
- ])
665
-
666
- this.statusData.set(selectedApp.id, {
667
- loading: false,
668
- health: health.status === "fulfilled" ? health.value : undefined,
669
- disk: disk.status === "fulfilled" ? disk.value : undefined,
670
- system: system.status === "fulfilled" ? system.value : undefined,
671
- queue: queue.status === "fulfilled" ? queue.value : undefined,
672
- })
673
- } catch (err) {
674
- this.statusData.set(selectedApp.id, {
675
- loading: false,
676
- error: err instanceof Error ? err.message : String(err),
677
- })
678
- }
679
-
680
- this.renderChecksPanel()
681
- }
682
-
683
- private handleKey(key: KeyEvent): void {
684
- if (!this.page.visible) return
685
-
686
- // Quit
687
- if (key.name === "q" || key.name === "escape") {
688
- this.cleanup()
689
- this.onBack()
690
- return
691
- }
692
-
693
- // Save
694
- if (key.name === "s") {
695
- this.saveConfiguration()
696
- return
697
- }
698
-
699
- // Refresh status (in status mode)
700
- if (key.name === "r" && this.currentMode === "status") {
701
- this.fetchStatus()
702
- return
703
- }
704
-
705
- // Mode switch
706
- if (key.name === "left" || key.name === "right") {
707
- const newMode = key.name === "left" ? 0 : 1
708
- this.modeTabs.setSelectedIndex(newMode)
709
- this.currentMode = newMode === 0 ? "configure" : "status"
710
- this.updateAllPanels()
711
- return
712
- }
713
-
714
- // Tab to switch panels
715
- if (key.name === "tab") {
716
- if (key.shift) {
717
- this.currentPanel = ((this.currentPanel - 1 + 3) % 3) as 0 | 1 | 2
718
- } else {
719
- this.currentPanel = ((this.currentPanel + 1) % 3) as 0 | 1 | 2
720
- }
721
- this.updatePanelBorders()
722
- return
723
- }
724
-
725
- // Navigation within panel
726
- if (key.name === "up" || key.name === "down") {
727
- const delta = key.name === "up" ? -1 : 1
728
- this.navigatePanel(delta)
729
- return
730
- }
731
-
732
- // Toggle
733
- if (key.name === "space" || key.name === "return") {
734
- this.toggleCurrentItem()
735
- return
736
- }
737
-
738
- // Number keys 1-4 for quick check toggling
739
- const numKey = parseInt(key.sequence || "", 10)
740
- if (numKey >= 1 && numKey <= 4) {
741
- const checkIdx = numKey - 1 // 0-indexed
742
- if (this.currentPanel === 0) {
743
- // Toggle category-level check directly
744
- this.toggleCategoryCheck(checkIdx)
745
- } else {
746
- // Toggle app-level check (or category if not overriding)
747
- this.checkIndex = checkIdx
748
- this.toggleCurrentItem()
749
- this.currentPanel = 2 // Move to checks panel for visual feedback
750
- this.updatePanelBorders()
751
- }
752
- this.renderChecksPanel()
753
- return
754
- }
755
- }
756
-
757
- private navigatePanel(delta: number): void {
758
- if (this.currentPanel === 0) {
759
- // Categories
760
- const max = this.availableCategories.length - 1
761
- this.categoryIndex = Math.max(0, Math.min(max, this.categoryIndex + delta))
762
- this.appIndex = 0 // Reset app selection
763
- this.checkIndex = 0
764
- this.updateAllPanels()
765
- } else if (this.currentPanel === 1) {
766
- // Apps
767
- const cat = this.availableCategories[this.categoryIndex]
768
- const apps = cat ? this.getAppsInCategory(cat) : []
769
- const max = apps.length - 1
770
- this.appIndex = Math.max(0, Math.min(max, this.appIndex + delta))
771
- this.checkIndex = 0
772
- this.renderAppsPanel()
773
- this.renderChecksPanel()
774
- } else {
775
- // Checks (0-3 for checks, 4 for override)
776
- this.checkIndex = Math.max(0, Math.min(4, this.checkIndex + delta))
777
- this.renderChecksPanel()
778
- }
779
- }
780
-
781
- private toggleCurrentItem(): void {
782
- const cat = this.availableCategories[this.categoryIndex]
783
- if (!cat) return
784
-
785
- if (this.currentPanel === 0) {
786
- // When in Categories panel, toggle category enabled status
787
- const cfg = this.categoryConfigs.get(cat)!
788
- cfg.enabled = !cfg.enabled
789
- this.renderCategoriesPanel()
790
- this.renderChecksPanel()
791
- } else if (this.currentPanel === 1) {
792
- // Toggle app enabled
793
- const apps = this.getAppsInCategory(cat)
794
- const app = apps[this.appIndex]
795
- if (app) {
796
- const cfg = this.appConfigs.get(app.id)!
797
- cfg.enabled = !cfg.enabled
798
- this.renderAppsPanel()
799
- }
800
- } else if (this.currentPanel === 2) {
801
- // Toggle check or override - depends on whether we're viewing category or app
802
- // Since we show category checks when panel 0 is focused, but we're now in panel 2,
803
- // we're viewing an app's checks
804
- const apps = this.getAppsInCategory(cat)
805
- const app = apps[this.appIndex]
806
- if (!app) return
807
-
808
- const cfg = this.appConfigs.get(app.id)!
809
-
810
- if (this.checkIndex === 4) {
811
- // Toggle override
812
- cfg.override = !cfg.override
813
- } else {
814
- // Toggle specific check
815
- const checks: (keyof MonitorOptions)[] = ["health", "diskspace", "status", "queue"]
816
- const checkKey = checks[this.checkIndex]
817
-
818
- if (cfg.override) {
819
- cfg.checks[checkKey] = !cfg.checks[checkKey]
820
- } else {
821
- // Toggle on category level (affects all apps without override)
822
- const catCfg = this.categoryConfigs.get(cat)!
823
- catCfg.checks[checkKey] = !catCfg.checks[checkKey]
824
- }
825
- }
826
- this.renderChecksPanel()
827
- }
828
- }
829
-
830
- /**
831
- * Toggle category-level check by number key (1-4)
832
- * This allows direct toggling of category defaults from any panel
833
- */
834
- private toggleCategoryCheck(checkIdx: number): void {
835
- const cat = this.availableCategories[this.categoryIndex]
836
- if (!cat) return
837
-
838
- const catCfg = this.categoryConfigs.get(cat)
839
- if (!catCfg) return
840
-
841
- const checks: (keyof MonitorOptions)[] = ["health", "diskspace", "status", "queue"]
842
- if (checkIdx >= 0 && checkIdx < checks.length) {
843
- catCfg.checks[checks[checkIdx]] = !catCfg.checks[checks[checkIdx]]
844
- this.renderChecksPanel()
845
- }
846
- }
847
-
848
- private async saveConfiguration(): Promise<void> {
849
- const monitor: MonitorConfig = {
850
- categories: Array.from(this.categoryConfigs.entries()).map(([category, cfg]) => ({
851
- category,
852
- enabled: cfg.enabled,
853
- checks: cfg.checks,
854
- })),
855
- apps: Array.from(this.appConfigs.entries())
856
- .filter(([, cfg]) => cfg.override)
857
- .map(([appId, cfg]) => ({
858
- appId,
859
- override: cfg.override,
860
- enabled: cfg.enabled,
861
- checks: cfg.checks,
862
- })),
863
- pollIntervalSeconds: this.config.monitor?.pollIntervalSeconds ?? 60,
864
- }
865
-
866
- this.config.monitor = monitor
867
- await saveConfig(this.config)
868
- }
869
-
870
- private cleanup(): void {
871
- this._renderer.keyInput.off("keypress", this.keyHandler)
872
- // Remove self from parent container
873
- if (this.parent && this.id) {
874
- try {
875
- this.parent.remove(this.id)
876
- } catch {
877
- /* ignore removal errors */
878
- }
879
- }
880
- }
881
- }