@muhammedaksam/easiarr 0.8.5 → 0.9.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/README.md +24 -0
- package/package.json +1 -1
- package/src/api/cloudflare-api.ts +368 -0
- package/src/apps/registry.ts +34 -6
- package/src/compose/generator.ts +26 -3
- package/src/compose/index.ts +1 -0
- package/src/compose/templates.ts +5 -0
- package/src/compose/traefik-config.ts +174 -0
- package/src/config/schema.ts +11 -0
- package/src/ui/screens/AppConfigurator.ts +24 -1
- package/src/ui/screens/AppManager.ts +1 -1
- package/src/ui/screens/CloudflaredSetup.ts +758 -0
- package/src/ui/screens/FullAutoSetup.ts +72 -0
- package/src/ui/screens/MainMenu.ts +14 -0
- package/src/ui/screens/QuickSetup.ts +4 -4
- package/src/ui/screens/SettingsScreen.ts +571 -0
- package/src/utils/migrations/1765732722_remove_cloudflare_dns_api_token.ts +44 -0
- package/src/utils/migrations.ts +12 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings Screen
|
|
3
|
+
* Edit Traefik, VPN, and system configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { CliRenderer, KeyEvent } from "@opentui/core"
|
|
7
|
+
import {
|
|
8
|
+
BoxRenderable,
|
|
9
|
+
TextRenderable,
|
|
10
|
+
SelectRenderable,
|
|
11
|
+
SelectRenderableEvents,
|
|
12
|
+
InputRenderable,
|
|
13
|
+
InputRenderableEvents,
|
|
14
|
+
} from "@opentui/core"
|
|
15
|
+
import type { EasiarrConfig, VpnMode } from "../../config/schema"
|
|
16
|
+
import { saveConfig } from "../../config"
|
|
17
|
+
import { saveCompose } from "../../compose"
|
|
18
|
+
import { createPageLayout } from "../components/PageLayout"
|
|
19
|
+
|
|
20
|
+
type SettingsSection = "traefik" | "vpn" | "system"
|
|
21
|
+
|
|
22
|
+
export class SettingsScreen extends BoxRenderable {
|
|
23
|
+
private config: EasiarrConfig
|
|
24
|
+
private onBack: () => void
|
|
25
|
+
private keyHandler: ((key: KeyEvent) => void) | null = null
|
|
26
|
+
private activeSection: SettingsSection = "traefik"
|
|
27
|
+
private page: BoxRenderable | null = null
|
|
28
|
+
private cliRenderer: CliRenderer
|
|
29
|
+
|
|
30
|
+
// Traefik settings (local copies to edit)
|
|
31
|
+
private traefikDomain: string
|
|
32
|
+
private traefikEntrypoint: string
|
|
33
|
+
private traefikMiddlewares: string
|
|
34
|
+
|
|
35
|
+
// VPN settings
|
|
36
|
+
private vpnMode: VpnMode
|
|
37
|
+
|
|
38
|
+
// System settings
|
|
39
|
+
private rootDir: string
|
|
40
|
+
private puid: string
|
|
41
|
+
private pgid: string
|
|
42
|
+
private timezone: string
|
|
43
|
+
private umask: string
|
|
44
|
+
|
|
45
|
+
constructor(renderer: CliRenderer, config: EasiarrConfig, onBack: () => void) {
|
|
46
|
+
super(renderer, {
|
|
47
|
+
id: "settings-screen",
|
|
48
|
+
width: "100%",
|
|
49
|
+
height: "100%",
|
|
50
|
+
flexDirection: "column",
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
this.cliRenderer = renderer
|
|
54
|
+
this.config = config
|
|
55
|
+
this.onBack = onBack
|
|
56
|
+
|
|
57
|
+
// Initialize local copies from config
|
|
58
|
+
this.traefikDomain = config.traefik?.domain || "${CLOUDFLARE_DNS_ZONE}"
|
|
59
|
+
this.traefikEntrypoint = config.traefik?.entrypoint || "web"
|
|
60
|
+
this.traefikMiddlewares = config.traefik?.middlewares?.join(",") || ""
|
|
61
|
+
|
|
62
|
+
this.vpnMode = config.vpn?.mode || "none"
|
|
63
|
+
|
|
64
|
+
this.rootDir = config.rootDir
|
|
65
|
+
this.puid = config.uid.toString()
|
|
66
|
+
this.pgid = config.gid.toString()
|
|
67
|
+
this.timezone = config.timezone
|
|
68
|
+
this.umask = config.umask
|
|
69
|
+
|
|
70
|
+
this.renderContent()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private renderContent(): void {
|
|
74
|
+
// Clear previous
|
|
75
|
+
if (this.keyHandler) {
|
|
76
|
+
this.cliRenderer.keyInput.off("keypress", this.keyHandler)
|
|
77
|
+
this.keyHandler = null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const children = this.getChildren()
|
|
81
|
+
for (const child of children) {
|
|
82
|
+
this.remove(child.id)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { container: page, content } = createPageLayout(this.cliRenderer as CliRenderer, {
|
|
86
|
+
title: "Settings",
|
|
87
|
+
stepInfo: "Edit Traefik, VPN, and System configuration",
|
|
88
|
+
footerHint: [
|
|
89
|
+
{ type: "key", key: "Tab", value: "Next" },
|
|
90
|
+
{ type: "key", key: "Enter", value: "Select/Continue" },
|
|
91
|
+
{ type: "key", key: "s", value: "Save" },
|
|
92
|
+
{ type: "key", key: "Esc", value: "Back" },
|
|
93
|
+
],
|
|
94
|
+
})
|
|
95
|
+
this.page = page
|
|
96
|
+
|
|
97
|
+
// Section tabs
|
|
98
|
+
const tabsBox = new BoxRenderable(this.cliRenderer, {
|
|
99
|
+
width: "100%",
|
|
100
|
+
height: 1,
|
|
101
|
+
flexDirection: "row",
|
|
102
|
+
marginBottom: 1,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const sections: { id: SettingsSection; label: string }[] = [
|
|
106
|
+
{ id: "traefik", label: "Traefik" },
|
|
107
|
+
{ id: "vpn", label: "VPN" },
|
|
108
|
+
{ id: "system", label: "System" },
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
for (const section of sections) {
|
|
112
|
+
const isActive = this.activeSection === section.id
|
|
113
|
+
tabsBox.add(
|
|
114
|
+
new TextRenderable(this.cliRenderer, {
|
|
115
|
+
content: ` ${section.label} `,
|
|
116
|
+
fg: isActive ? "#4a9eff" : "#666666",
|
|
117
|
+
marginRight: 2,
|
|
118
|
+
})
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
content.add(tabsBox)
|
|
123
|
+
content.add(new TextRenderable(this.cliRenderer, { content: " " }))
|
|
124
|
+
|
|
125
|
+
// Render active section
|
|
126
|
+
switch (this.activeSection) {
|
|
127
|
+
case "traefik":
|
|
128
|
+
this.renderTraefikSection(content)
|
|
129
|
+
break
|
|
130
|
+
case "vpn":
|
|
131
|
+
this.renderVpnSection(content)
|
|
132
|
+
break
|
|
133
|
+
case "system":
|
|
134
|
+
this.renderSystemSection(content)
|
|
135
|
+
break
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.add(page)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private renderTraefikSection(content: BoxRenderable): void {
|
|
142
|
+
if (!this.config.traefik?.enabled) {
|
|
143
|
+
content.add(
|
|
144
|
+
new TextRenderable(this.cliRenderer, {
|
|
145
|
+
content: "Traefik is not enabled. Enable it in App Manager first.",
|
|
146
|
+
fg: "#ff6666",
|
|
147
|
+
})
|
|
148
|
+
)
|
|
149
|
+
this.setupBackHandler()
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
content.add(
|
|
154
|
+
new TextRenderable(this.cliRenderer, {
|
|
155
|
+
content: "Configure Traefik reverse proxy settings:",
|
|
156
|
+
fg: "#888888",
|
|
157
|
+
})
|
|
158
|
+
)
|
|
159
|
+
content.add(new TextRenderable(this.cliRenderer, { content: " " }))
|
|
160
|
+
|
|
161
|
+
// Domain
|
|
162
|
+
const domainRow = new BoxRenderable(this.cliRenderer, {
|
|
163
|
+
width: "100%",
|
|
164
|
+
height: 1,
|
|
165
|
+
flexDirection: "row",
|
|
166
|
+
marginBottom: 1,
|
|
167
|
+
})
|
|
168
|
+
domainRow.add(
|
|
169
|
+
new TextRenderable(this.cliRenderer, {
|
|
170
|
+
content: "Domain:".padEnd(16),
|
|
171
|
+
fg: "#aaaaaa",
|
|
172
|
+
})
|
|
173
|
+
)
|
|
174
|
+
const domainInput = new InputRenderable(this.cliRenderer, {
|
|
175
|
+
id: "settings-traefik-domain",
|
|
176
|
+
width: 40,
|
|
177
|
+
placeholder: "${CLOUDFLARE_DNS_ZONE}",
|
|
178
|
+
backgroundColor: "#2a2a3e",
|
|
179
|
+
textColor: "#ffffff",
|
|
180
|
+
focusedBackgroundColor: "#3a3a4e",
|
|
181
|
+
})
|
|
182
|
+
domainInput.value = this.traefikDomain
|
|
183
|
+
domainInput.on(InputRenderableEvents.CHANGE, (v) => (this.traefikDomain = v))
|
|
184
|
+
domainRow.add(domainInput)
|
|
185
|
+
content.add(domainRow)
|
|
186
|
+
|
|
187
|
+
// Entrypoint
|
|
188
|
+
const epRow = new BoxRenderable(this.cliRenderer, {
|
|
189
|
+
width: "100%",
|
|
190
|
+
height: 1,
|
|
191
|
+
flexDirection: "row",
|
|
192
|
+
marginBottom: 1,
|
|
193
|
+
})
|
|
194
|
+
epRow.add(
|
|
195
|
+
new TextRenderable(this.cliRenderer, {
|
|
196
|
+
content: "Entrypoint:".padEnd(16),
|
|
197
|
+
fg: "#aaaaaa",
|
|
198
|
+
})
|
|
199
|
+
)
|
|
200
|
+
const epInput = new InputRenderable(this.cliRenderer, {
|
|
201
|
+
id: "settings-traefik-entrypoint",
|
|
202
|
+
width: 20,
|
|
203
|
+
placeholder: "web",
|
|
204
|
+
backgroundColor: "#2a2a3e",
|
|
205
|
+
textColor: "#ffffff",
|
|
206
|
+
focusedBackgroundColor: "#3a3a4e",
|
|
207
|
+
})
|
|
208
|
+
epInput.value = this.traefikEntrypoint
|
|
209
|
+
epInput.on(InputRenderableEvents.CHANGE, (v) => (this.traefikEntrypoint = v))
|
|
210
|
+
epRow.add(epInput)
|
|
211
|
+
|
|
212
|
+
// Hint for cloudflared
|
|
213
|
+
const cloudflaredEnabled = this.config.apps.some((a) => a.id === "cloudflared" && a.enabled)
|
|
214
|
+
if (cloudflaredEnabled && this.traefikEntrypoint === "websecure") {
|
|
215
|
+
epRow.add(
|
|
216
|
+
new TextRenderable(this.cliRenderer, {
|
|
217
|
+
content: " ⚠️ Use 'web' for Cloudflare Tunnel",
|
|
218
|
+
fg: "#ffcc00",
|
|
219
|
+
})
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
content.add(epRow)
|
|
223
|
+
|
|
224
|
+
// Middlewares
|
|
225
|
+
const mwRow = new BoxRenderable(this.cliRenderer, {
|
|
226
|
+
width: "100%",
|
|
227
|
+
height: 1,
|
|
228
|
+
flexDirection: "row",
|
|
229
|
+
marginBottom: 1,
|
|
230
|
+
})
|
|
231
|
+
mwRow.add(
|
|
232
|
+
new TextRenderable(this.cliRenderer, {
|
|
233
|
+
content: "Middlewares:".padEnd(16),
|
|
234
|
+
fg: "#aaaaaa",
|
|
235
|
+
})
|
|
236
|
+
)
|
|
237
|
+
const mwInput = new InputRenderable(this.cliRenderer, {
|
|
238
|
+
id: "settings-traefik-middlewares",
|
|
239
|
+
width: 50,
|
|
240
|
+
placeholder: "security-headers@file",
|
|
241
|
+
backgroundColor: "#2a2a3e",
|
|
242
|
+
textColor: "#ffffff",
|
|
243
|
+
focusedBackgroundColor: "#3a3a4e",
|
|
244
|
+
})
|
|
245
|
+
mwInput.value = this.traefikMiddlewares
|
|
246
|
+
mwInput.on(InputRenderableEvents.CHANGE, (v) => (this.traefikMiddlewares = v))
|
|
247
|
+
mwRow.add(mwInput)
|
|
248
|
+
content.add(mwRow)
|
|
249
|
+
|
|
250
|
+
content.add(new TextRenderable(this.cliRenderer, { content: " " }))
|
|
251
|
+
|
|
252
|
+
// Navigation
|
|
253
|
+
const navMenu = new SelectRenderable(this.cliRenderer, {
|
|
254
|
+
id: "settings-traefik-nav",
|
|
255
|
+
width: "100%",
|
|
256
|
+
height: 4,
|
|
257
|
+
options: [
|
|
258
|
+
{ name: "💾 Save Changes", description: "Save and regenerate docker-compose.yml" },
|
|
259
|
+
{ name: "➡️ Next: VPN", description: "Go to VPN settings" },
|
|
260
|
+
{ name: "◀ Back", description: "Return to main menu" },
|
|
261
|
+
],
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
navMenu.on(SelectRenderableEvents.ITEM_SELECTED, async (index) => {
|
|
265
|
+
if (index === 0) {
|
|
266
|
+
await this.save()
|
|
267
|
+
} else if (index === 1) {
|
|
268
|
+
this.activeSection = "vpn"
|
|
269
|
+
this.renderContent()
|
|
270
|
+
} else {
|
|
271
|
+
this.cleanup()
|
|
272
|
+
this.onBack()
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
content.add(navMenu)
|
|
277
|
+
|
|
278
|
+
// Focus management
|
|
279
|
+
const inputs = [domainInput, epInput, mwInput, navMenu]
|
|
280
|
+
let focusIndex = 0
|
|
281
|
+
inputs[0].focus()
|
|
282
|
+
|
|
283
|
+
this.keyHandler = (key: KeyEvent) => {
|
|
284
|
+
if (key.name === "escape") {
|
|
285
|
+
this.cleanup()
|
|
286
|
+
this.onBack()
|
|
287
|
+
} else if (key.name === "s" && !key.ctrl) {
|
|
288
|
+
this.save()
|
|
289
|
+
} else if (key.name === "tab" || key.name === "enter") {
|
|
290
|
+
if (key.name === "enter" && focusIndex === inputs.length - 1) return
|
|
291
|
+
inputs[focusIndex].blur()
|
|
292
|
+
focusIndex = key.shift ? (focusIndex - 1 + inputs.length) % inputs.length : (focusIndex + 1) % inputs.length
|
|
293
|
+
inputs[focusIndex].focus()
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
;(this.cliRenderer as CliRenderer).keyInput.on("keypress", this.keyHandler)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private renderVpnSection(content: BoxRenderable): void {
|
|
300
|
+
content.add(
|
|
301
|
+
new TextRenderable(this.cliRenderer, {
|
|
302
|
+
content: "Configure VPN routing through Gluetun:",
|
|
303
|
+
fg: "#888888",
|
|
304
|
+
})
|
|
305
|
+
)
|
|
306
|
+
content.add(new TextRenderable(this.cliRenderer, { content: " " }))
|
|
307
|
+
|
|
308
|
+
const gluetunEnabled = this.config.apps.some((a) => a.id === "gluetun" && a.enabled)
|
|
309
|
+
if (!gluetunEnabled) {
|
|
310
|
+
content.add(
|
|
311
|
+
new TextRenderable(this.cliRenderer, {
|
|
312
|
+
content: "Gluetun VPN is not enabled. Enable it in App Manager first.",
|
|
313
|
+
fg: "#ff6666",
|
|
314
|
+
})
|
|
315
|
+
)
|
|
316
|
+
content.add(new TextRenderable(this.cliRenderer, { content: " " }))
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Current mode display
|
|
320
|
+
const modeLabels: Record<VpnMode, string> = {
|
|
321
|
+
full: "🛡️ Full VPN",
|
|
322
|
+
mini: "⚡ Mini VPN",
|
|
323
|
+
none: "❌ No VPN Routing",
|
|
324
|
+
}
|
|
325
|
+
content.add(
|
|
326
|
+
new TextRenderable(this.cliRenderer, {
|
|
327
|
+
content: `Current mode: ${modeLabels[this.vpnMode]}`,
|
|
328
|
+
fg: "#50fa7b",
|
|
329
|
+
})
|
|
330
|
+
)
|
|
331
|
+
content.add(new TextRenderable(this.cliRenderer, { content: " " }))
|
|
332
|
+
|
|
333
|
+
// Combined menu with VPN options + navigation
|
|
334
|
+
const menu = new SelectRenderable(this.cliRenderer, {
|
|
335
|
+
id: "settings-vpn-menu",
|
|
336
|
+
width: "100%",
|
|
337
|
+
height: 8,
|
|
338
|
+
options: [
|
|
339
|
+
{ name: "🛡️ Full VPN", description: "Route Downloaders, Indexers, and Media Servers through VPN" },
|
|
340
|
+
{ name: "⚡ Mini VPN", description: "Route ONLY Downloaders through VPN (Recommended)" },
|
|
341
|
+
{ name: "❌ No VPN Routing", description: "Run container but don't route traffic" },
|
|
342
|
+
{ name: "◀ Previous: Traefik", description: "Go to Traefik settings" },
|
|
343
|
+
{ name: "➡️ Next: System", description: "Go to System settings" },
|
|
344
|
+
{ name: "💾 Save Changes", description: "Save and regenerate docker-compose.yml" },
|
|
345
|
+
{ name: "✕ Back", description: "Return to main menu" },
|
|
346
|
+
],
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
const modes: VpnMode[] = ["full", "mini", "none"]
|
|
350
|
+
|
|
351
|
+
menu.on(SelectRenderableEvents.ITEM_SELECTED, async (index) => {
|
|
352
|
+
if (index < 3) {
|
|
353
|
+
// VPN mode selection
|
|
354
|
+
this.vpnMode = modes[index]
|
|
355
|
+
this.renderContent() // Re-render to show selection
|
|
356
|
+
} else if (index === 3) {
|
|
357
|
+
this.activeSection = "traefik"
|
|
358
|
+
this.renderContent()
|
|
359
|
+
} else if (index === 4) {
|
|
360
|
+
this.activeSection = "system"
|
|
361
|
+
this.renderContent()
|
|
362
|
+
} else if (index === 5) {
|
|
363
|
+
await this.save()
|
|
364
|
+
} else {
|
|
365
|
+
this.cleanup()
|
|
366
|
+
this.onBack()
|
|
367
|
+
}
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
content.add(menu)
|
|
371
|
+
menu.focus()
|
|
372
|
+
|
|
373
|
+
this.keyHandler = (key: KeyEvent) => {
|
|
374
|
+
if (key.name === "escape") {
|
|
375
|
+
this.cleanup()
|
|
376
|
+
this.onBack()
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
this.cliRenderer.keyInput.on("keypress", this.keyHandler)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private renderSystemSection(content: BoxRenderable): void {
|
|
383
|
+
content.add(
|
|
384
|
+
new TextRenderable(this.cliRenderer, {
|
|
385
|
+
content: "Configure system-wide settings:",
|
|
386
|
+
fg: "#888888",
|
|
387
|
+
})
|
|
388
|
+
)
|
|
389
|
+
content.add(new TextRenderable(this.cliRenderer, { content: " " }))
|
|
390
|
+
|
|
391
|
+
const createField = (id: string, label: string, value: string, placeholder: string, width: number = 40) => {
|
|
392
|
+
const row = new BoxRenderable(this.cliRenderer, {
|
|
393
|
+
width: "100%",
|
|
394
|
+
height: 1,
|
|
395
|
+
flexDirection: "row",
|
|
396
|
+
marginBottom: 1,
|
|
397
|
+
})
|
|
398
|
+
row.add(
|
|
399
|
+
new TextRenderable(this.cliRenderer, {
|
|
400
|
+
content: label.padEnd(16),
|
|
401
|
+
fg: "#aaaaaa",
|
|
402
|
+
})
|
|
403
|
+
)
|
|
404
|
+
const input = new InputRenderable(this.cliRenderer, {
|
|
405
|
+
id,
|
|
406
|
+
width,
|
|
407
|
+
placeholder,
|
|
408
|
+
backgroundColor: "#2a2a3e",
|
|
409
|
+
textColor: "#ffffff",
|
|
410
|
+
focusedBackgroundColor: "#3a3a4e",
|
|
411
|
+
})
|
|
412
|
+
input.value = value
|
|
413
|
+
row.add(input)
|
|
414
|
+
content.add(row)
|
|
415
|
+
return input
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const rootInput = createField("settings-sys-root", "Root Path:", this.rootDir, "/home/user/media", 50)
|
|
419
|
+
const puidInput = createField("settings-sys-puid", "PUID:", this.puid, "1000", 10)
|
|
420
|
+
const pgidInput = createField("settings-sys-pgid", "PGID:", this.pgid, "1000", 10)
|
|
421
|
+
const tzInput = createField("settings-sys-tz", "Timezone:", this.timezone, "Europe/London", 30)
|
|
422
|
+
const umaskInput = createField("settings-sys-umask", "Umask:", this.umask, "002", 10)
|
|
423
|
+
|
|
424
|
+
rootInput.on(InputRenderableEvents.CHANGE, (v) => (this.rootDir = v))
|
|
425
|
+
puidInput.on(InputRenderableEvents.CHANGE, (v) => (this.puid = v))
|
|
426
|
+
pgidInput.on(InputRenderableEvents.CHANGE, (v) => (this.pgid = v))
|
|
427
|
+
tzInput.on(InputRenderableEvents.CHANGE, (v) => (this.timezone = v))
|
|
428
|
+
umaskInput.on(InputRenderableEvents.CHANGE, (v) => (this.umask = v))
|
|
429
|
+
|
|
430
|
+
content.add(new TextRenderable(this.cliRenderer, { content: " " }))
|
|
431
|
+
|
|
432
|
+
const navMenu = new SelectRenderable(this.cliRenderer, {
|
|
433
|
+
id: "settings-sys-nav",
|
|
434
|
+
width: "100%",
|
|
435
|
+
height: 4,
|
|
436
|
+
options: [
|
|
437
|
+
{ name: "💾 Save All Changes", description: "Save config and regenerate docker-compose.yml" },
|
|
438
|
+
{ name: "◀ Back", description: "Return to main menu" },
|
|
439
|
+
],
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
navMenu.on(SelectRenderableEvents.ITEM_SELECTED, async (index) => {
|
|
443
|
+
if (index === 0) {
|
|
444
|
+
await this.save()
|
|
445
|
+
} else {
|
|
446
|
+
this.cleanup()
|
|
447
|
+
this.onBack()
|
|
448
|
+
}
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
content.add(navMenu)
|
|
452
|
+
|
|
453
|
+
const inputs = [rootInput, puidInput, pgidInput, tzInput, umaskInput, navMenu]
|
|
454
|
+
let focusIndex = 0
|
|
455
|
+
inputs[0].focus()
|
|
456
|
+
|
|
457
|
+
this.keyHandler = (key: KeyEvent) => {
|
|
458
|
+
if (key.name === "escape") {
|
|
459
|
+
this.cleanup()
|
|
460
|
+
this.onBack()
|
|
461
|
+
} else if (key.name === "s" && !key.ctrl) {
|
|
462
|
+
this.save()
|
|
463
|
+
} else if (key.name === "tab" || key.name === "enter") {
|
|
464
|
+
if (key.name === "enter" && focusIndex === inputs.length - 1) return
|
|
465
|
+
inputs[focusIndex].blur()
|
|
466
|
+
focusIndex = key.shift ? (focusIndex - 1 + inputs.length) % inputs.length : (focusIndex + 1) % inputs.length
|
|
467
|
+
inputs[focusIndex].focus()
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
;(this.cliRenderer as CliRenderer).keyInput.on("keypress", this.keyHandler)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private setupSectionNav(content: BoxRenderable, prev: SettingsSection, next: SettingsSection): void {
|
|
474
|
+
const navMenu = new SelectRenderable(this.cliRenderer, {
|
|
475
|
+
id: "settings-section-nav",
|
|
476
|
+
width: "100%",
|
|
477
|
+
height: 4,
|
|
478
|
+
options: [
|
|
479
|
+
{ name: "◀ Previous", description: `Go to ${prev} settings` },
|
|
480
|
+
{ name: "➡️ Next", description: `Go to ${next} settings` },
|
|
481
|
+
{ name: "💾 Save", description: "Save all changes" },
|
|
482
|
+
{ name: "✕ Back", description: "Return to main menu" },
|
|
483
|
+
],
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
navMenu.on(SelectRenderableEvents.ITEM_SELECTED, async (index) => {
|
|
487
|
+
if (index === 0) {
|
|
488
|
+
this.activeSection = prev
|
|
489
|
+
this.renderContent()
|
|
490
|
+
} else if (index === 1) {
|
|
491
|
+
this.activeSection = next
|
|
492
|
+
this.renderContent()
|
|
493
|
+
} else if (index === 2) {
|
|
494
|
+
await this.save()
|
|
495
|
+
} else {
|
|
496
|
+
this.cleanup()
|
|
497
|
+
this.onBack()
|
|
498
|
+
}
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
content.add(navMenu)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private setupBackHandler(): void {
|
|
505
|
+
const navMenu = new SelectRenderable(this.cliRenderer, {
|
|
506
|
+
id: "settings-back-nav",
|
|
507
|
+
width: "100%",
|
|
508
|
+
height: 2,
|
|
509
|
+
options: [{ name: "◀ Back", description: "Return to main menu" }],
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
navMenu.on(SelectRenderableEvents.ITEM_SELECTED, () => {
|
|
513
|
+
this.cleanup()
|
|
514
|
+
this.onBack()
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
if (this.page) {
|
|
518
|
+
const content = this.page.getChildren()[0] as BoxRenderable
|
|
519
|
+
content?.add(navMenu)
|
|
520
|
+
}
|
|
521
|
+
navMenu.focus()
|
|
522
|
+
|
|
523
|
+
this.keyHandler = (key: KeyEvent) => {
|
|
524
|
+
if (key.name === "escape") {
|
|
525
|
+
this.cleanup()
|
|
526
|
+
this.onBack()
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
;(this.cliRenderer as CliRenderer).keyInput.on("keypress", this.keyHandler)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private async save(): Promise<void> {
|
|
533
|
+
// Update config with local values
|
|
534
|
+
if (this.config.traefik) {
|
|
535
|
+
this.config.traefik.domain = this.traefikDomain
|
|
536
|
+
this.config.traefik.entrypoint = this.traefikEntrypoint
|
|
537
|
+
this.config.traefik.middlewares = this.traefikMiddlewares
|
|
538
|
+
.split(",")
|
|
539
|
+
.map((m) => m.trim())
|
|
540
|
+
.filter((m) => m)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (this.config.vpn) {
|
|
544
|
+
this.config.vpn.mode = this.vpnMode
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
this.config.rootDir = this.rootDir
|
|
548
|
+
this.config.uid = parseInt(this.puid, 10) || 1000
|
|
549
|
+
this.config.gid = parseInt(this.pgid, 10) || 1000
|
|
550
|
+
this.config.timezone = this.timezone
|
|
551
|
+
this.config.umask = this.umask
|
|
552
|
+
this.config.updatedAt = new Date().toISOString()
|
|
553
|
+
|
|
554
|
+
await saveConfig(this.config)
|
|
555
|
+
await saveCompose(this.config)
|
|
556
|
+
|
|
557
|
+
this.cleanup()
|
|
558
|
+
this.onBack()
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private cleanup(): void {
|
|
562
|
+
if (this.keyHandler) {
|
|
563
|
+
;(this.cliRenderer as CliRenderer).keyInput.off("keypress", this.keyHandler)
|
|
564
|
+
this.keyHandler = null
|
|
565
|
+
}
|
|
566
|
+
const parent = this.parent
|
|
567
|
+
if (parent) {
|
|
568
|
+
parent.remove(this.id)
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration: remove cloudflare dns api token
|
|
3
|
+
* Removes the unused CLOUDFLARE_DNS_API_TOKEN from .env
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
7
|
+
import { getEnvPath } from "../env"
|
|
8
|
+
import { debugLog } from "../debug"
|
|
9
|
+
|
|
10
|
+
export const name = "remove_cloudflare_dns_api_token"
|
|
11
|
+
|
|
12
|
+
export function up(): boolean {
|
|
13
|
+
debugLog("Migrations", "Running migration: remove_cloudflare_dns_api_token")
|
|
14
|
+
|
|
15
|
+
const envPath = getEnvPath()
|
|
16
|
+
if (!existsSync(envPath)) {
|
|
17
|
+
debugLog("Migrations", ".env file not found, skipping")
|
|
18
|
+
return true
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const content = readFileSync(envPath, "utf-8")
|
|
22
|
+
|
|
23
|
+
// Check if CLOUDFLARE_DNS_API_TOKEN exists
|
|
24
|
+
if (!content.includes("CLOUDFLARE_DNS_API_TOKEN")) {
|
|
25
|
+
debugLog("Migrations", "CLOUDFLARE_DNS_API_TOKEN not found, skipping")
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Remove the line containing CLOUDFLARE_DNS_API_TOKEN
|
|
30
|
+
const lines = content.split("\n")
|
|
31
|
+
const filtered = lines.filter((line) => !line.startsWith("CLOUDFLARE_DNS_API_TOKEN="))
|
|
32
|
+
const newContent = filtered.join("\n")
|
|
33
|
+
|
|
34
|
+
writeFileSync(envPath, newContent, "utf-8")
|
|
35
|
+
debugLog("Migrations", "Removed CLOUDFLARE_DNS_API_TOKEN from .env")
|
|
36
|
+
|
|
37
|
+
return true
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function down(): boolean {
|
|
41
|
+
debugLog("Migrations", "Reverting migration: remove_cloudflare_dns_api_token")
|
|
42
|
+
// No revert needed - the token was unused
|
|
43
|
+
return true
|
|
44
|
+
}
|
package/src/utils/migrations.ts
CHANGED
|
@@ -81,6 +81,18 @@ async function loadMigrations(): Promise<Migration[]> {
|
|
|
81
81
|
debugLog("Migrations", `Failed to load migration: ${e}`)
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
try {
|
|
85
|
+
const m1765732722 = await import("./migrations/1765732722_remove_cloudflare_dns_api_token")
|
|
86
|
+
migrations.push({
|
|
87
|
+
timestamp: "1765732722",
|
|
88
|
+
name: m1765732722.name,
|
|
89
|
+
up: m1765732722.up,
|
|
90
|
+
down: m1765732722.down,
|
|
91
|
+
})
|
|
92
|
+
} catch (e) {
|
|
93
|
+
debugLog("Migrations", `Failed to load migration: ${e}`)
|
|
94
|
+
}
|
|
95
|
+
|
|
84
96
|
// Add future migrations here:
|
|
85
97
|
// const m2 = await import("./migrations/1734xxxxxx_xxx")
|
|
86
98
|
// migrations.push({ timestamp: "...", name: m2.name, up: m2.up })
|