@muhammedaksam/easiarr 0.4.2 → 0.5.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.
@@ -16,6 +16,7 @@ export interface EasiarrConfig {
16
16
  network?: NetworkConfig
17
17
  traefik?: TraefikConfig
18
18
  vpn?: VpnConfig
19
+ monitor?: MonitorConfig
19
20
  createdAt: string
20
21
  updatedAt: string
21
22
  }
@@ -27,6 +28,38 @@ export interface VpnConfig {
27
28
  provider?: string // For future use (e.g. custom, airvpn, protonvpn)
28
29
  }
29
30
 
31
+ // ==========================================
32
+ // Monitoring Configuration
33
+ // ==========================================
34
+
35
+ export type MonitorCheckType = "health" | "diskspace" | "status" | "queue"
36
+
37
+ export interface MonitorOptions {
38
+ health: boolean
39
+ diskspace: boolean
40
+ status: boolean
41
+ queue: boolean
42
+ }
43
+
44
+ export interface CategoryMonitorConfig {
45
+ category: AppCategory
46
+ enabled: boolean
47
+ checks: MonitorOptions
48
+ }
49
+
50
+ export interface AppMonitorConfig {
51
+ appId: AppId
52
+ override: boolean // If true, uses app-specific settings instead of category defaults
53
+ enabled: boolean
54
+ checks: MonitorOptions
55
+ }
56
+
57
+ export interface MonitorConfig {
58
+ categories: CategoryMonitorConfig[]
59
+ apps: AppMonitorConfig[] // App-specific overrides
60
+ pollIntervalSeconds: number
61
+ }
62
+
30
63
  export interface TraefikConfig {
31
64
  enabled: boolean
32
65
  domain: string
@@ -152,6 +185,7 @@ export interface AppDefinition {
152
185
  cap_add?: string[]
153
186
  apiKeyMeta?: ApiKeyMeta
154
187
  rootFolder?: RootFolderMeta
188
+ prowlarrCategoryIds?: number[]
155
189
  /** Architecture compatibility info - omit if supports all */
156
190
  arch?: ArchCompatibility
157
191
  }
package/src/index.ts CHANGED
@@ -21,6 +21,10 @@ async function main() {
21
21
  consoleOptions: {
22
22
  startInDebugMode: false,
23
23
  },
24
+ exitOnCtrlC: true,
25
+ onDestroy: () => {
26
+ process.exit(0)
27
+ },
24
28
  })
25
29
 
26
30
  const app = new App(renderer)
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Footer Hint Types and Utilities
3
+ * Provides a flexible array-based structure for footer hints
4
+ */
5
+
6
+ /** Base interface for all footer hint types with common styling */
7
+ export interface FooterHintBase {
8
+ /** Foreground color */
9
+ fg?: string
10
+ /** Background color */
11
+ bg?: string
12
+ }
13
+
14
+ /** A simple text message hint (e.g., "Press ? for help.") */
15
+ export interface FooterHintText extends FooterHintBase {
16
+ type: "text"
17
+ value: string
18
+ }
19
+
20
+ /** A keyboard shortcut hint (e.g., key: "↓", value: "Down") */
21
+ export interface FooterHintKey extends FooterHintBase {
22
+ type: "key"
23
+ key: string
24
+ value: string
25
+ /** Color for the key part (default: bright/highlighted) */
26
+ keyColor?: string
27
+ /** Color for the value part (default: dimmer) */
28
+ valueColor?: string
29
+ /** Background color for the key badge */
30
+ keyBgColor?: string
31
+ /** Whether to show brackets around the key */
32
+ withBrackets?: boolean
33
+ }
34
+
35
+ /** Separator between hint groups */
36
+ export interface FooterHintSeparator extends FooterHintBase {
37
+ type: "separator"
38
+ char?: string
39
+ }
40
+
41
+ /** Union type for all hint item types */
42
+ export type FooterHintItem = FooterHintText | FooterHintKey | FooterHintSeparator
43
+
44
+ /** Array of hint items */
45
+ export type FooterHint = FooterHintItem[]
46
+
47
+ /** Default separator character */
48
+ const DEFAULT_SEPARATOR = " "
49
+
50
+ /**
51
+ * Render footer hints to a plain string
52
+ * Used by PageLayout for backward-compatible rendering
53
+ */
54
+ export function renderFooterHint(hints: FooterHint): string {
55
+ return hints
56
+ .map((item) => {
57
+ switch (item.type) {
58
+ case "text":
59
+ return item.value
60
+ case "key":
61
+ return `${item.key}: ${item.value}`
62
+ case "separator":
63
+ return item.char ?? DEFAULT_SEPARATOR
64
+ default:
65
+ return ""
66
+ }
67
+ })
68
+ .join(DEFAULT_SEPARATOR)
69
+ }
70
+
71
+ /**
72
+ * Parse legacy string format to FooterHint array
73
+ * Supports both "Key: Action" and "Key Action" formats
74
+ */
75
+ export function parseFooterHintString(hint: string): FooterHint {
76
+ // Split on double spaces (common separator in existing hints)
77
+ const parts = hint.split(/\s{2,}/)
78
+
79
+ return parts.map((part): FooterHintItem => {
80
+ // Try to parse as "Key: Action" or "Key Action" format
81
+ const colonMatch = part.match(/^([^\s:]+):\s*(.+)$/)
82
+ if (colonMatch) {
83
+ return { type: "key", key: colonMatch[1], value: colonMatch[2] }
84
+ }
85
+
86
+ const spaceMatch = part.match(/^([^\s]+)\s+(.+)$/)
87
+ if (spaceMatch) {
88
+ return { type: "key", key: spaceMatch[1], value: spaceMatch[2] }
89
+ }
90
+
91
+ // Fallback to text
92
+ return { type: "text", value: part }
93
+ })
94
+ }
95
+
96
+ /**
97
+ * Helper to create a key hint
98
+ */
99
+ export function key(
100
+ key: string,
101
+ value: string,
102
+ options?: { keyColor?: string; valueColor?: string; keyBgColor?: string; withBrackets?: boolean }
103
+ ): FooterHintKey {
104
+ return { type: "key", key, value, ...options }
105
+ }
106
+
107
+ /**
108
+ * Helper to create a text hint
109
+ */
110
+ export function text(value: string, fg?: string): FooterHintText {
111
+ return { type: "text", value, fg }
112
+ }
113
+
114
+ /**
115
+ * Helper to create a separator
116
+ */
117
+ export function separator(char?: string): FooterHintSeparator {
118
+ return { type: "separator", char }
119
+ }
@@ -1,10 +1,11 @@
1
- import { BoxRenderable, TextRenderable, type CliRenderer } from "@opentui/core"
1
+ import { BoxRenderable, TextRenderable, TextNodeRenderable, type CliRenderer } from "@opentui/core"
2
2
  import { getVersion } from "../../VersionInfo"
3
+ import { type FooterHint } from "./FooterHint"
3
4
 
4
5
  export interface PageLayoutOptions {
5
6
  title: string
6
7
  stepInfo?: string
7
- footerHint?: string
8
+ footerHint?: FooterHint | string
8
9
  }
9
10
 
10
11
  export interface PageLayoutResult {
@@ -79,15 +80,62 @@ export function createPageLayout(renderer: CliRenderer, options: PageLayoutOptio
79
80
  backgroundColor: "#1a1a2e",
80
81
  })
81
82
 
82
- // Hint text
83
- const hintText = footerHint || "↑↓ Navigate Enter Select q Quit"
84
- footerBox.add(
85
- new TextRenderable(renderer, {
86
- id: `${idPrefix}-footer-hint`,
87
- content: hintText,
88
- fg: "#aaaaaa",
83
+ // Hint text - handle both array and string formats
84
+ // Always append separator + Ctrl+C Exit when hints are provided
85
+ const hintContainer = new TextRenderable(renderer, {
86
+ id: `${idPrefix}-footer-hint`,
87
+ content: "",
88
+ fg: "#aaaaaa",
89
+ })
90
+
91
+ if (!footerHint) {
92
+ hintContainer.content = "↑↓: Navigate Enter: Select Ctrl+C: Exit"
93
+ } else if (typeof footerHint === "string") {
94
+ hintContainer.content = footerHint + " Ctrl+C: Exit"
95
+ } else {
96
+ // Append separator and global Ctrl+C hint (styled red for demo)
97
+ const hintsWithExit: FooterHint = [
98
+ ...footerHint,
99
+ { type: "separator", char: " | " },
100
+ { type: "key", key: "Ctrl+C", value: "Exit", keyColor: "#ff6666", valueColor: "#888888" },
101
+ ]
102
+
103
+ // Build styled content using TextNodeRenderable
104
+ const DEFAULT_KEY_COLOR = "#8be9fd" // cyan/bright
105
+ const DEFAULT_VALUE_COLOR = "#aaaaaa" // dim
106
+ const DEFAULT_SEP_COLOR = "#555555"
107
+
108
+ // Helper to create a styled text node
109
+ const styledText = (text: string, fg?: string, bg?: string): TextNodeRenderable => {
110
+ const node = new TextNodeRenderable({ fg, bg })
111
+ node.add(text)
112
+ return node
113
+ }
114
+
115
+ hintsWithExit.forEach((item, idx) => {
116
+ if (item.type === "separator") {
117
+ hintContainer.add(styledText(item.char ?? " ", DEFAULT_SEP_COLOR))
118
+ } else if (item.type === "text") {
119
+ hintContainer.add(styledText(item.value, item.fg ?? DEFAULT_VALUE_COLOR))
120
+ // Add spacing after text (except last)
121
+ if (idx < hintsWithExit.length - 1 && hintsWithExit[idx + 1]?.type !== "separator") {
122
+ hintContainer.add(styledText(" "))
123
+ }
124
+ } else if (item.type === "key") {
125
+ const keyDisplay = item.withBrackets ? `[${item.key}]` : item.key
126
+ // Key part (styled)
127
+ hintContainer.add(styledText(keyDisplay, item.keyColor ?? DEFAULT_KEY_COLOR, item.keyBgColor))
128
+ // Colon + Value
129
+ hintContainer.add(styledText(`: ${item.value}`, item.valueColor ?? DEFAULT_VALUE_COLOR))
130
+ // Add spacing after (except last or before separator)
131
+ if (idx < hintsWithExit.length - 1 && hintsWithExit[idx + 1]?.type !== "separator") {
132
+ hintContainer.add(styledText(" "))
133
+ }
134
+ }
89
135
  })
90
- )
136
+ }
137
+
138
+ footerBox.add(hintContainer)
91
139
 
92
140
  // Version
93
141
  footerBox.add(
@@ -58,7 +58,10 @@ export class AdvancedSettings {
58
58
  const { container: page, content } = createPageLayout(this.renderer, {
59
59
  title: "Advanced Settings",
60
60
  stepInfo: "Direct File Editing",
61
- footerHint: "Enter Select Esc Back",
61
+ footerHint: [
62
+ { type: "key", key: "Enter", value: "Select" },
63
+ { type: "key", key: "Esc", value: "Back" },
64
+ ],
62
65
  })
63
66
 
64
67
  const menu = new SelectRenderable(this.renderer, {
@@ -235,7 +235,12 @@ export class ApiKeyViewer extends BoxRenderable {
235
235
  const { container, content } = createPageLayout(this.cliRenderer, {
236
236
  title: "API Key Extractor",
237
237
  stepInfo: "Found Keys",
238
- footerHint: hasFoundKeys ? "S Save to .env Esc/Enter Return" : "Esc/Enter: Return",
238
+ footerHint: hasFoundKeys
239
+ ? [
240
+ { type: "key", key: "S", value: "Save to .env" },
241
+ { type: "key", key: "Esc/Enter", value: "Return" },
242
+ ]
243
+ : [{ type: "key", key: "Esc/Enter", value: "Return" }],
239
244
  })
240
245
  this.add(container)
241
246
 
@@ -95,7 +95,12 @@ export class AppConfigurator extends BoxRenderable {
95
95
  const { container, content } = createPageLayout(this.cliRenderer, {
96
96
  title: "Configure Apps",
97
97
  stepInfo: "Global Credentials",
98
- footerHint: "Tab Cycle Fields/Shortcuts O Override Enter Continue Esc Skip",
98
+ footerHint: [
99
+ { type: "key", key: "Tab", value: "Cycle Fields/Shortcuts" },
100
+ { type: "key", key: "O", value: "Override" },
101
+ { type: "key", key: "Enter", value: "Continue" },
102
+ { type: "key", key: "Esc", value: "Skip" },
103
+ ],
99
104
  })
100
105
  this.pageContainer = container
101
106
  this.add(container)
@@ -358,7 +363,7 @@ export class AppConfigurator extends BoxRenderable {
358
363
  const { container, content } = createPageLayout(this.cliRenderer, {
359
364
  title: "Configure Apps",
360
365
  stepInfo: "Setting up root folders",
361
- footerHint: "Please wait...",
366
+ footerHint: [{ type: "text", value: "Please wait..." }],
362
367
  })
363
368
  this.pageContainer = container
364
369
  this.contentBox = content
@@ -427,7 +432,10 @@ export class AppConfigurator extends BoxRenderable {
427
432
  const { container, content } = createPageLayout(this.cliRenderer, {
428
433
  title: "Configure Apps",
429
434
  stepInfo: "qBittorrent Credentials",
430
- footerHint: "Enter credentials from qBittorrent WebUI Esc Skip",
435
+ footerHint: [
436
+ { type: "text", value: "Enter credentials from qBittorrent WebUI" },
437
+ { type: "key", key: "Esc", value: "Skip" },
438
+ ],
431
439
  })
432
440
  this.pageContainer = container
433
441
  this.add(container)
@@ -483,7 +491,10 @@ export class AppConfigurator extends BoxRenderable {
483
491
  const { container, content } = createPageLayout(this.cliRenderer, {
484
492
  title: "Configure Apps",
485
493
  stepInfo: "SABnzbd Credentials",
486
- footerHint: "Enter API key from SABnzbd Config → General Esc Skip",
494
+ footerHint: [
495
+ { type: "text", value: "Enter API key from SABnzbd Config → General" },
496
+ { type: "key", key: "Esc", value: "Skip" },
497
+ ],
487
498
  })
488
499
  this.pageContainer = container
489
500
  this.add(container)
@@ -615,7 +626,7 @@ export class AppConfigurator extends BoxRenderable {
615
626
  const { container, content } = createPageLayout(this.cliRenderer, {
616
627
  title: "Configure Apps",
617
628
  stepInfo: "Complete",
618
- footerHint: "Press any key to return",
629
+ footerHint: [{ type: "text", value: "Press any key to return" }],
619
630
  })
620
631
  this.add(container)
621
632
 
@@ -54,7 +54,12 @@ export class AppManager {
54
54
  const { container: page, content } = createPageLayout(this.renderer as CliRenderer, {
55
55
  title: "Manage Apps",
56
56
  stepInfo: "Toggle apps linked to your configuration",
57
- footerHint: "←→ Tab Enter Toggle s Save q Back",
57
+ footerHint: [
58
+ { type: "key", key: "←→", value: "Tab" },
59
+ { type: "key", key: "Enter", value: "Toggle" },
60
+ { type: "key", key: "s", value: "Save" },
61
+ { type: "key", key: "q", value: "Back" },
62
+ ],
58
63
  })
59
64
  this.page = page
60
65
 
@@ -47,7 +47,10 @@ export class ContainerControl {
47
47
  const { container: page, content } = createPageLayout(this.renderer as CliRenderer, {
48
48
  title: "Container Control",
49
49
  stepInfo: finalStepInfo,
50
- footerHint: "Enter Select/Action q Back",
50
+ footerHint: [
51
+ { type: "key", key: "Enter", value: "Select/Action" },
52
+ { type: "key", key: "q", value: "Back" },
53
+ ],
51
54
  })
52
55
 
53
56
  if (!dockerOk) {
@@ -47,7 +47,10 @@ export class FullAutoSetup extends BoxRenderable {
47
47
  const { container: pageContainer, content: contentBox } = createPageLayout(cliRenderer, {
48
48
  title: "Full Auto Setup",
49
49
  stepInfo: "Configure all services automatically",
50
- footerHint: "Enter Start/Continue Esc Back",
50
+ footerHint: [
51
+ { type: "key", key: "Enter", value: "Start/Continue" },
52
+ { type: "key", key: "Esc", value: "Back" },
53
+ ],
51
54
  })
52
55
  super(cliRenderer, { width: "100%", height: "100%" })
53
56
  this.add(pageContainer)
@@ -15,6 +15,7 @@ import { TRaSHProfileSetup } from "./TRaSHProfileSetup"
15
15
  import { ProwlarrSetup } from "./ProwlarrSetup"
16
16
  import { QBittorrentSetup } from "./QBittorrentSetup"
17
17
  import { FullAutoSetup } from "./FullAutoSetup"
18
+ import { MonitorDashboard } from "./MonitorDashboard"
18
19
 
19
20
  export class MainMenu {
20
21
  private renderer: RenderContext
@@ -37,7 +38,7 @@ export class MainMenu {
37
38
  const { container: page, content } = createPageLayout(this.renderer as CliRenderer, {
38
39
  title: "Main Menu",
39
40
  stepInfo: "Docker Compose Generator for *arr Ecosystem",
40
- footerHint: "Enter Select Ctrl+C Exit",
41
+ footerHint: [{ type: "key", key: "Enter", value: "Select" }],
41
42
  })
42
43
  this.page = page
43
44
 
@@ -82,7 +83,7 @@ export class MainMenu {
82
83
  this.menu = new SelectRenderable(this.renderer, {
83
84
  id: "main-menu-select",
84
85
  width: "100%",
85
- height: 10,
86
+ flexGrow: 1,
86
87
  options: [
87
88
  {
88
89
  name: "📦 Manage Apps",
@@ -124,6 +125,10 @@ export class MainMenu {
124
125
  name: "🚀 Full Auto Setup",
125
126
  description: "Run all configurations (Auth, Root Folders, Prowlarr, etc.)",
126
127
  },
128
+ {
129
+ name: "📊 Monitor Dashboard",
130
+ description: "Configure app health monitoring",
131
+ },
127
132
  { name: "❌ Exit", description: "Close easiarr" },
128
133
  ],
129
134
  })
@@ -211,7 +216,18 @@ export class MainMenu {
211
216
  this.container.add(autoSetup)
212
217
  break
213
218
  }
214
- case 10:
219
+ case 10: {
220
+ // Monitor Dashboard
221
+ this.menu.blur()
222
+ this.page.visible = false
223
+ const monitor = new MonitorDashboard(this.renderer as CliRenderer, this.config, () => {
224
+ this.page.visible = true
225
+ this.menu.focus()
226
+ })
227
+ this.container.add(monitor)
228
+ break
229
+ }
230
+ case 11:
215
231
  process.exit(0)
216
232
  break
217
233
  }