@muhammedaksam/easiarr 1.1.8 → 1.2.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.
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  > ⚠️ **Work In Progress** - This project is in early experimental development. Features may be incomplete, unstable, or change without notice.
12
12
 
13
- TUI tool for generating docker-compose files for the \*arr media ecosystem with 47 apps, TRaSH Guides best practices, VPN routing, and Traefik reverse proxy support.
13
+ TUI tool for generating docker-compose files for the \*arr media ecosystem with 49 apps, TRaSH Guides best practices, VPN routing, and Traefik/Caddy reverse proxy support.
14
14
 
15
15
  A terminal-based wizard that helps you set up Radarr, Sonarr, Prowlarr, and other \*arr applications with Docker Compose, following best practices from [TRaSH Guides](https://trash-guides.info/).
16
16
 
@@ -19,9 +19,13 @@ A terminal-based wizard that helps you set up Radarr, Sonarr, Prowlarr, and othe
19
19
  - 📦 **Quick Setup Wizard** - Get started in minutes with a guided setup flow
20
20
  - 🐳 **Docker Compose Generation** - Automatically generates optimized `docker-compose.yml`
21
21
  - ✅ **TRaSH Guides Compliant** - Follows best practices for folder structure and hardlinks
22
+ - 🔄 **Recyclarr & Profilarr** - Automated TRaSH Guides profile sync
22
23
  - 🎮 **Container Control** - Start, stop, and restart containers directly from the TUI
24
+ - 📋 **Container Logs Viewer** - View and save Docker container logs from the TUI
23
25
  - ⚙️ **App Management** - Add or remove apps from your stack with ease
24
26
  - 💾 **Persistent Configuration** - Settings saved to `~/.easiarr/config.json`
27
+ - 🔀 **Reverse Proxy** - Traefik or Caddy support with automatic SSL
28
+ - 🖥️ **Unraid Support** - Automatic OS detection and compatibility
25
29
 
26
30
  ## Quick Start
27
31
 
@@ -52,7 +56,7 @@ bun run start
52
56
  - [Bun](https://bun.sh/) >= 1.0
53
57
  - [Docker](https://www.docker.com/) with Docker Compose v2
54
58
 
55
- ## Supported Applications (47 apps across 10 categories)
59
+ ## Supported Applications (49 apps across 10 categories)
56
60
 
57
61
  ### Media Management (Servarr)
58
62
 
@@ -99,6 +103,8 @@ bun run start
99
103
  - **Portainer** - Docker container management UI
100
104
  - **Huntarr** - Missing content manager for \*arr apps
101
105
  - **Unpackerr** - Archive extraction for \*arr apps
106
+ - **Recyclarr** - TRaSH Guides profile sync (CLI-based)
107
+ - **Profilarr** - TRaSH Guides profile sync (Web UI)
102
108
  - **FileBot** - Media file renaming and automator
103
109
  - **Chromium** - Web browser for secure remote browsing
104
110
  - **Guacamole** - Clientless remote desktop gateway
@@ -118,6 +124,7 @@ bun run start
118
124
  ### Infrastructure
119
125
 
120
126
  - **Traefik** - Reverse proxy and load balancer
127
+ - **Caddy** - Automatic HTTPS reverse proxy
121
128
  - **Cloudflared** - Cloudflare Tunnel for secure external access
122
129
  - **Traefik Certs Dumper** - Extracts certificates from Traefik
123
130
  - **CrowdSec** - Intrusion prevention system
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muhammedaksam/easiarr",
3
- "version": "1.1.8",
3
+ "version": "1.2.1",
4
4
  "description": "TUI tool for generating docker-compose files for the *arr media ecosystem with 41 apps, TRaSH Guides best practices, VPN routing, and Traefik reverse proxy support",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -61,15 +61,15 @@
61
61
  "jiti": "^2.6.1",
62
62
  "prettier": "^3.7.4",
63
63
  "ts-jest": "^29.4.6",
64
- "typescript-eslint": "^8.50.0"
64
+ "typescript-eslint": "^8.50.1"
65
65
  },
66
66
  "peerDependencies": {
67
67
  "typescript": "^5.9.3"
68
68
  },
69
69
  "dependencies": {
70
- "@opentui/core": "^0.1.61",
70
+ "@opentui/core": "^0.1.63",
71
71
  "bcrypt": "^6.0.0",
72
- "socket.io-client": "^4.8.1",
72
+ "socket.io-client": "^4.8.2",
73
73
  "yaml": "^2.8.2"
74
74
  },
75
75
  "engines": {
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Profilarr API Client
3
+ * Handles authentication and *arr instance configuration
4
+ */
5
+
6
+ import { debugLog } from "../utils/debug"
7
+ import type { IAutoSetupClient, AutoSetupOptions, AutoSetupResult } from "./auto-setup-types"
8
+
9
+ // ==========================================
10
+ // Types
11
+ // ==========================================
12
+
13
+ export interface ProfilarrConfig {
14
+ id?: number
15
+ name: string
16
+ type: "radarr" | "sonarr"
17
+ tags: string[]
18
+ arrServer: string
19
+ apiKey: string
20
+ sync_method: "manual" | "schedule"
21
+ sync_interval: number
22
+ import_as_unique: boolean
23
+ data_to_sync: {
24
+ profiles: string[]
25
+ customFormats: string[]
26
+ }
27
+ }
28
+
29
+ interface ProfilarrSetupStatus {
30
+ needs_setup: boolean
31
+ }
32
+
33
+ interface ProfilarrSetupResponse {
34
+ message: string
35
+ username: string
36
+ api_key: string
37
+ authenticated: boolean
38
+ }
39
+
40
+ interface ProfilarrSettings {
41
+ username: string
42
+ api_key: string
43
+ }
44
+
45
+ // ==========================================
46
+ // Client
47
+ // ==========================================
48
+
49
+ export class ProfilarrApiClient implements IAutoSetupClient {
50
+ private baseUrl: string
51
+ private apiKey: string | null = null
52
+
53
+ constructor(host: string, port: number) {
54
+ this.baseUrl = `http://${host}:${port}/api`
55
+ }
56
+
57
+ setApiKey(key: string): void {
58
+ this.apiKey = key
59
+ }
60
+
61
+ private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
62
+ const url = `${this.baseUrl}${endpoint}`
63
+ const headers: Record<string, string> = {
64
+ "Content-Type": "application/json",
65
+ ...(this.apiKey ? { "X-Api-Key": this.apiKey } : {}),
66
+ ...(options.headers as Record<string, string>),
67
+ }
68
+
69
+ debugLog("ProfilarrApi", `${options.method || "GET"} ${endpoint}`)
70
+
71
+ const response = await fetch(url, { ...options, headers })
72
+
73
+ if (!response.ok) {
74
+ const text = await response.text()
75
+ debugLog("ProfilarrApi", `Error ${response.status}: ${text}`)
76
+ throw new Error(`Profilarr API error: ${response.status} - ${text}`)
77
+ }
78
+
79
+ const contentType = response.headers.get("content-type")
80
+ if (contentType?.includes("application/json")) {
81
+ return response.json()
82
+ }
83
+ return {} as T
84
+ }
85
+
86
+ // ==========================================
87
+ // Health & Status
88
+ // ==========================================
89
+
90
+ async isHealthy(): Promise<boolean> {
91
+ try {
92
+ const response = await fetch(`${this.baseUrl}/auth/setup`)
93
+ return response.status === 200 || response.status === 400
94
+ } catch {
95
+ return false
96
+ }
97
+ }
98
+
99
+ async isInitialized(): Promise<boolean> {
100
+ try {
101
+ const response = await fetch(`${this.baseUrl}/auth/setup`)
102
+ if (response.status === 400) return true // "Auth already configured"
103
+ if (response.status === 200) {
104
+ const data = (await response.json()) as ProfilarrSetupStatus
105
+ return !data.needs_setup
106
+ }
107
+ return false
108
+ } catch {
109
+ return false
110
+ }
111
+ }
112
+
113
+ // ==========================================
114
+ // Authentication
115
+ // ==========================================
116
+
117
+ /**
118
+ * Authenticate with Profilarr.
119
+ * If not set up, performs initial setup.
120
+ * If already configured, logs in and retrieves API key.
121
+ */
122
+ async authenticate(username: string, password: string): Promise<string> {
123
+ // Check if setup is needed
124
+ try {
125
+ const response = await fetch(`${this.baseUrl}/auth/setup`)
126
+ if (response.status === 200) {
127
+ const status = (await response.json()) as ProfilarrSetupStatus
128
+ if (status.needs_setup) {
129
+ debugLog("ProfilarrApi", "Performing initial setup")
130
+ const setupRes = await this.request<ProfilarrSetupResponse>("/auth/setup", {
131
+ method: "POST",
132
+ body: JSON.stringify({ username, password }),
133
+ })
134
+ this.apiKey = setupRes.api_key
135
+ return this.apiKey
136
+ }
137
+ }
138
+ } catch {
139
+ // Ignore check error, try login
140
+ }
141
+
142
+ // Already configured, login to get API key
143
+ debugLog("ProfilarrApi", "Logging in to retrieve API key")
144
+ const loginRes = await fetch(`${this.baseUrl}/auth/authenticate`, {
145
+ method: "POST",
146
+ headers: { "Content-Type": "application/json" },
147
+ body: JSON.stringify({ username, password }),
148
+ })
149
+
150
+ if (!loginRes.ok) {
151
+ throw new Error(`Login failed: ${loginRes.statusText}`)
152
+ }
153
+
154
+ const cookie = loginRes.headers.get("set-cookie")
155
+ if (!cookie) {
156
+ throw new Error("Login successful but no cookie received")
157
+ }
158
+
159
+ // Get API key from settings using the cookie
160
+ const settingsRes = await fetch(`${this.baseUrl}/settings/general`, {
161
+ headers: { Cookie: cookie },
162
+ })
163
+
164
+ if (!settingsRes.ok) {
165
+ throw new Error(`Failed to fetch settings: ${settingsRes.statusText}`)
166
+ }
167
+
168
+ const settings = (await settingsRes.json()) as ProfilarrSettings
169
+ this.apiKey = settings.api_key
170
+ return this.apiKey
171
+ }
172
+
173
+ // ==========================================
174
+ // Arr Configuration
175
+ // ==========================================
176
+
177
+ async getConfigs(): Promise<ProfilarrConfig[]> {
178
+ return this.request<ProfilarrConfig[]>("/arr/config")
179
+ }
180
+
181
+ async addConfig(config: ProfilarrConfig): Promise<ProfilarrConfig> {
182
+ return this.request<ProfilarrConfig>("/arr/config", {
183
+ method: "POST",
184
+ body: JSON.stringify(config),
185
+ })
186
+ }
187
+
188
+ /**
189
+ * Configure Radarr connection
190
+ * @returns The created/existing config, or null if failed
191
+ */
192
+ async configureRadarr(hostname: string, port: number, apiKey: string): Promise<ProfilarrConfig | null> {
193
+ try {
194
+ const existingConfigs = await this.getConfigs()
195
+ const existingConfig = existingConfigs.find((c) => c.type === "radarr")
196
+
197
+ if (existingConfig) {
198
+ debugLog("ProfilarrApi", "Radarr already configured")
199
+ return existingConfig
200
+ }
201
+
202
+ const arrServer = `http://${hostname}:${port}`
203
+ return await this.addConfig({
204
+ name: "Radarr",
205
+ type: "radarr",
206
+ tags: [],
207
+ arrServer,
208
+ apiKey,
209
+ sync_method: "manual",
210
+ sync_interval: 60,
211
+ import_as_unique: false,
212
+ data_to_sync: { profiles: [], customFormats: [] },
213
+ })
214
+ } catch (e) {
215
+ debugLog("ProfilarrApi", `Radarr config failed: ${e}`)
216
+ return null
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Configure Sonarr connection
222
+ * @returns The created/existing config, or null if failed
223
+ */
224
+ async configureSonarr(hostname: string, port: number, apiKey: string): Promise<ProfilarrConfig | null> {
225
+ try {
226
+ const existingConfigs = await this.getConfigs()
227
+ const existingConfig = existingConfigs.find((c) => c.type === "sonarr")
228
+
229
+ if (existingConfig) {
230
+ debugLog("ProfilarrApi", "Sonarr already configured")
231
+ return existingConfig
232
+ }
233
+
234
+ const arrServer = `http://${hostname}:${port}`
235
+ return await this.addConfig({
236
+ name: "Sonarr",
237
+ type: "sonarr",
238
+ tags: [],
239
+ arrServer,
240
+ apiKey,
241
+ sync_method: "manual",
242
+ sync_interval: 60,
243
+ import_as_unique: false,
244
+ data_to_sync: { profiles: [], customFormats: [] },
245
+ })
246
+ } catch (e) {
247
+ debugLog("ProfilarrApi", `Sonarr config failed: ${e}`)
248
+ return null
249
+ }
250
+ }
251
+
252
+ // ==========================================
253
+ // Auto-Setup (IAutoSetupClient)
254
+ // ==========================================
255
+
256
+ /**
257
+ * Run the auto-setup process for Profilarr.
258
+ * Only handles authentication, returns API key for env persistence.
259
+ * *arr connections are configured separately via configureRadarr/configureSonarr.
260
+ */
261
+ async setup(options: AutoSetupOptions): Promise<AutoSetupResult> {
262
+ const { username, password } = options
263
+
264
+ try {
265
+ // Check if reachable
266
+ const healthy = await this.isHealthy()
267
+ if (!healthy) {
268
+ return { success: false, message: "Profilarr not reachable" }
269
+ }
270
+
271
+ // Authenticate (setup or login)
272
+ const apiKey = await this.authenticate(username, password)
273
+
274
+ return {
275
+ success: true,
276
+ message: "Profilarr configured",
277
+ data: { apiKey },
278
+ envUpdates: { API_KEY_PROFILARR: apiKey },
279
+ }
280
+ } catch (error) {
281
+ return { success: false, message: `${error}` }
282
+ }
283
+ }
284
+ }
@@ -31,6 +31,7 @@ export const APPS: Record<AppId, AppDefinition> = {
31
31
  },
32
32
  prowlarrCategoryIds: [2000, 2010, 2020, 2030, 2040, 2045, 2050, 2060, 2070, 2080, 2090], // Movies + all sub-categories
33
33
  homepage: { icon: "radarr.png", widget: "radarr" },
34
+ logVolume: "/config/logs",
34
35
  },
35
36
 
36
37
  sonarr: {
@@ -56,6 +57,7 @@ export const APPS: Record<AppId, AppDefinition> = {
56
57
  },
57
58
  prowlarrCategoryIds: [5000, 5010, 5020, 5030, 5040, 5045, 5050, 5060, 5070, 5080, 5090], // TV + all sub-categories
58
59
  homepage: { icon: "sonarr.png", widget: "sonarr" },
60
+ logVolume: "/config/logs",
59
61
  },
60
62
 
61
63
  lidarr: {
@@ -79,6 +81,7 @@ export const APPS: Record<AppId, AppDefinition> = {
79
81
  },
80
82
  prowlarrCategoryIds: [3000, 3010, 3020, 3030, 3040, 3050, 3060], // Audio + all sub-categories
81
83
  homepage: { icon: "lidarr.png", widget: "lidarr" },
84
+ logVolume: "/config/logs",
82
85
  },
83
86
 
84
87
  readarr: {
@@ -106,6 +109,7 @@ export const APPS: Record<AppId, AppDefinition> = {
106
109
  warning: "Readarr is deprecated - no ARM64 support (project abandoned by upstream)",
107
110
  },
108
111
  homepage: { icon: "readarr.png", widget: "readarr" },
112
+ logVolume: "/config/logs",
109
113
  },
110
114
 
111
115
  bazarr: {
@@ -127,6 +131,7 @@ export const APPS: Record<AppId, AppDefinition> = {
127
131
  selector: "auth.apikey",
128
132
  },
129
133
  homepage: { icon: "bazarr.png", widget: "bazarr" },
134
+ logVolume: "/config/log",
130
135
  },
131
136
 
132
137
  mylar3: {
@@ -149,6 +154,7 @@ export const APPS: Record<AppId, AppDefinition> = {
149
154
  },
150
155
  prowlarrCategoryIds: [7030], // Comics
151
156
  homepage: { icon: "mylar.png", widget: "mylar" },
157
+ logVolume: "/app/mylar/logs",
152
158
  // Note: Mylar3 is NOT an *arr app - has different API format (?cmd=<endpoint>)
153
159
  // Root folder is configured via Web UI settings, not API
154
160
  },
@@ -174,6 +180,7 @@ export const APPS: Record<AppId, AppDefinition> = {
174
180
  },
175
181
  prowlarrCategoryIds: [6000, 6010, 6020, 6030, 6040, 6045, 6050, 6060, 6070, 6080, 6090], // XXX + all sub-categories
176
182
  homepage: { icon: "whisparr.png", widget: "sonarr" }, // Uses sonarr widget type
183
+ logVolume: "/config/logs",
177
184
  },
178
185
 
179
186
  audiobookshelf: {
@@ -192,6 +199,7 @@ export const APPS: Record<AppId, AppDefinition> = {
192
199
  `${root}/data/media/audiobookshelf-metadata:/metadata`,
193
200
  ],
194
201
  homepage: { icon: "audiobookshelf.png", widget: "audiobookshelf" },
202
+ logVolume: "/config/metadata/logs",
195
203
  },
196
204
 
197
205
  // === INDEXERS ===
@@ -213,6 +221,7 @@ export const APPS: Record<AppId, AppDefinition> = {
213
221
  selector: "<ApiKey>(.*?)</ApiKey>",
214
222
  },
215
223
  homepage: { icon: "prowlarr.png", widget: "prowlarr" },
224
+ logVolume: "/config/logs",
216
225
  },
217
226
 
218
227
  jackett: {
@@ -231,6 +240,7 @@ export const APPS: Record<AppId, AppDefinition> = {
231
240
  selector: "APIKey",
232
241
  },
233
242
  homepage: { icon: "jackett.png", widget: "jackett" },
243
+ logVolume: "/config/logs",
234
244
  },
235
245
 
236
246
  flaresolverr: {
@@ -290,6 +300,7 @@ export const APPS: Record<AppId, AppDefinition> = {
290
300
  ],
291
301
  trashGuide: "docs/Downloaders/qBittorrent/",
292
302
  homepage: { icon: "qbittorrent.png", widget: "qbittorrent" },
303
+ logVolume: "/config/qBittorrent/logs",
293
304
  },
294
305
 
295
306
  sabnzbd: {
@@ -311,6 +322,7 @@ export const APPS: Record<AppId, AppDefinition> = {
311
322
  selector: "api_key\\s*=\\s*(.+)",
312
323
  },
313
324
  homepage: { icon: "sabnzbd.png", widget: "sabnzbd" },
325
+ logVolume: "/config/logs",
314
326
  },
315
327
 
316
328
  slskd: {
@@ -352,6 +364,7 @@ export const APPS: Record<AppId, AppDefinition> = {
352
364
  type: "partial",
353
365
  description: "Generates slskd.yml with API key for Homepage widget and Soularr integration",
354
366
  },
367
+ logVolume: "/app/logs",
355
368
  },
356
369
 
357
370
  soularr: {
@@ -390,6 +403,7 @@ export const APPS: Record<AppId, AppDefinition> = {
390
403
  selector: 'PlexOnlineToken="([^"]+)"',
391
404
  },
392
405
  homepage: { icon: "plex.png", widget: "plex" },
406
+ logVolume: "/config/Library/Application Support/Plex Media Server/Logs",
393
407
  autoSetup: {
394
408
  type: "full",
395
409
  description: "Claim server with token, create media libraries",
@@ -408,6 +422,7 @@ export const APPS: Record<AppId, AppDefinition> = {
408
422
  pgid: 13000,
409
423
  volumes: (root) => [`${root}/config/jellyfin:/config`, `${root}/data/media:/data/media`],
410
424
  homepage: { icon: "jellyfin.png", widget: "jellyfin" },
425
+ logVolume: "/config/log",
411
426
  },
412
427
 
413
428
  tautulli: {
@@ -432,6 +447,7 @@ export const APPS: Record<AppId, AppDefinition> = {
432
447
  description: "Connect to Plex, enable API",
433
448
  requires: ["plex"],
434
449
  },
450
+ logVolume: "/config/logs",
435
451
  },
436
452
 
437
453
  tdarr: {
@@ -451,6 +467,7 @@ export const APPS: Record<AppId, AppDefinition> = {
451
467
  ],
452
468
  environment: { serverIP: "0.0.0.0", internalNode: "true" },
453
469
  homepage: { icon: "tdarr.png", widget: "tdarr" },
470
+ logVolume: "/app/logs",
454
471
  },
455
472
 
456
473
  // === REQUEST MANAGEMENT ===
@@ -476,6 +493,7 @@ export const APPS: Record<AppId, AppDefinition> = {
476
493
  description: "Connect to Plex, configure Radarr/Sonarr",
477
494
  requires: ["plex"],
478
495
  },
496
+ logVolume: "/app/config/logs",
479
497
  },
480
498
 
481
499
  jellyseerr: {
@@ -495,6 +513,7 @@ export const APPS: Record<AppId, AppDefinition> = {
495
513
  selector: "main.apiKey",
496
514
  },
497
515
  homepage: { icon: "jellyseerr.png", widget: "jellyseerr" },
516
+ logVolume: "/app/config/logs",
498
517
  },
499
518
 
500
519
  // === DASHBOARDS ===
@@ -514,6 +533,7 @@ export const APPS: Record<AppId, AppDefinition> = {
514
533
  "/var/run/docker.sock:/var/run/docker.sock",
515
534
  ],
516
535
  homepage: { icon: "homarr.png" }, // No widget, just icon (it's a dashboard itself)
536
+ logVolume: "/app/data/logs",
517
537
  },
518
538
 
519
539
  heimdall: {
@@ -527,6 +547,7 @@ export const APPS: Record<AppId, AppDefinition> = {
527
547
  pgid: 13000,
528
548
  volumes: (root) => [`${root}/config/heimdall:/config`],
529
549
  homepage: { icon: "heimdall.png" }, // No widget, just icon (it's a dashboard itself)
550
+ logVolume: "/config/log",
530
551
  },
531
552
 
532
553
  homepage: {
@@ -544,6 +565,7 @@ export const APPS: Record<AppId, AppDefinition> = {
544
565
  HOMEPAGE_ALLOWED_HOSTS:
545
566
  "homepage,homepage.${CLOUDFLARE_DNS_ZONE},${CLOUDFLARE_DNS_ZONE},localhost,${LOCAL_DOCKER_IP},${LOCAL_DOCKER_IP}:3009",
546
567
  },
568
+ logVolume: "/app/config/logs",
547
569
  },
548
570
 
549
571
  // === UTILITIES ===
@@ -559,6 +581,7 @@ export const APPS: Record<AppId, AppDefinition> = {
559
581
  volumes: (root) => [`${root}/config/portainer:/data`, "/var/run/docker.sock:/var/run/docker.sock"],
560
582
  minPasswordLength: 12, // Portainer requires minimum 12 character password
561
583
  homepage: { icon: "portainer.png", widget: "portainer" },
584
+ logVolume: "/data/logs",
562
585
  },
563
586
 
564
587
  huntarr: {
@@ -591,6 +614,7 @@ export const APPS: Record<AppId, AppDefinition> = {
591
614
  description: "Test connections to Sonarr, Radarr, Lidarr, Readarr, Whisparr",
592
615
  requires: ["sonarr", "radarr"],
593
616
  },
617
+ logVolume: "/config/logs",
594
618
  },
595
619
 
596
620
  unpackerr: {
@@ -603,6 +627,7 @@ export const APPS: Record<AppId, AppDefinition> = {
603
627
  puid: 0,
604
628
  pgid: 13000,
605
629
  volumes: (root) => [`${root}/config/unpackerr:/config`, `${root}/data:/data`],
630
+ logVolume: "/config/logs",
606
631
  },
607
632
 
608
633
  filebot: {
@@ -616,6 +641,7 @@ export const APPS: Record<AppId, AppDefinition> = {
616
641
  pgid: 13000,
617
642
  volumes: (root) => [`${root}/config/filebot:/data`, `${root}/data:/data`],
618
643
  environment: { DARK_MODE: "1" },
644
+ logVolume: "/data/logs",
619
645
  },
620
646
 
621
647
  chromium: {
@@ -629,6 +655,7 @@ export const APPS: Record<AppId, AppDefinition> = {
629
655
  pgid: 13000,
630
656
  volumes: (root) => [`${root}/config/chromium:/config`],
631
657
  environment: { TITLE: "Chromium" },
658
+ logVolume: "/config/logs",
632
659
  },
633
660
 
634
661
  guacamole: {
@@ -664,6 +691,7 @@ export const APPS: Record<AppId, AppDefinition> = {
664
691
  mask: true,
665
692
  },
666
693
  ],
694
+ logVolume: "/guacamole/logs",
667
695
  },
668
696
 
669
697
  guacd: {
@@ -677,6 +705,7 @@ export const APPS: Record<AppId, AppDefinition> = {
677
705
  pgid: 13000,
678
706
  volumes: (root) => [`${root}/config/guacd:/config`], // Not really used but keeps structure
679
707
  dependsOn: ["postgresql"],
708
+ logVolume: "/guacd/logs",
680
709
  },
681
710
 
682
711
  "ddns-updater": {
@@ -689,6 +718,7 @@ export const APPS: Record<AppId, AppDefinition> = {
689
718
  puid: 13000,
690
719
  pgid: 13000,
691
720
  volumes: (root) => [`${root}/config/ddns-updater:/data`],
721
+ logVolume: "/data/logs",
692
722
  },
693
723
 
694
724
  easiarr: {
@@ -723,6 +753,43 @@ export const APPS: Record<AppId, AppDefinition> = {
723
753
  },
724
754
  },
725
755
 
756
+ recyclarr: {
757
+ id: "recyclarr",
758
+ name: "Recyclarr",
759
+ description: "Automatic TRaSH Guides profile sync for *arr apps",
760
+ category: "utility",
761
+ defaultPort: 0, // No web UI - runs as cron job
762
+ image: "ghcr.io/recyclarr/recyclarr:latest",
763
+ puid: 0, // Uses Docker user: directive
764
+ pgid: 0,
765
+ useDockerUser: true,
766
+ volumes: (root) => [`${root}/config/recyclarr:/config`],
767
+ environment: {
768
+ RECYCLARR_CREATE_CONFIG: "false", // We generate the config ourselves
769
+ CRON_SCHEDULE: "@daily", // Run sync daily at midnight
770
+ },
771
+ dependsOn: ["radarr", "sonarr"],
772
+ autoSetup: {
773
+ type: "full",
774
+ description: "Generate recyclarr.yml config for enabled *arr apps with TRaSH profiles",
775
+ requires: ["radarr", "sonarr"],
776
+ },
777
+ },
778
+ profilarr: {
779
+ id: "profilarr",
780
+ name: "Profilarr",
781
+ description: "Web UI for managing TRaSH Guides profiles (alternative to Recyclarr)",
782
+ category: "utility",
783
+ defaultPort: 6868,
784
+ image: "santiagosayshey/profilarr:latest",
785
+ puid: 1000,
786
+ pgid: 1000,
787
+ volumes: (root) => [`${root}/config/profilarr:/config`],
788
+ dependsOn: ["radarr", "sonarr"],
789
+ homepage: {
790
+ icon: "profilarr",
791
+ },
792
+ },
726
793
  // === VPN ===
727
794
  gluetun: {
728
795
  id: "gluetun",
@@ -770,6 +837,7 @@ export const APPS: Record<AppId, AppDefinition> = {
770
837
  },
771
838
  ],
772
839
  homepage: { icon: "gluetun.png", widget: "gluetun" },
840
+ logVolume: "/gluetun",
773
841
  },
774
842
 
775
843
  // === MONITORING ===
@@ -789,6 +857,7 @@ export const APPS: Record<AppId, AppDefinition> = {
789
857
  description: "Setup admin user, configure Prometheus datasource",
790
858
  requires: ["prometheus"],
791
859
  },
860
+ logVolume: "/var/log/grafana",
792
861
  },
793
862
 
794
863
  prometheus: {
@@ -802,6 +871,7 @@ export const APPS: Record<AppId, AppDefinition> = {
802
871
  pgid: 13000,
803
872
  volumes: (root) => [`${root}/config/prometheus:/prometheus`],
804
873
  homepage: { icon: "prometheus.png", widget: "prometheus" },
874
+ logVolume: "/prometheus/logs",
805
875
  },
806
876
 
807
877
  dozzle: {
@@ -834,6 +904,7 @@ export const APPS: Record<AppId, AppDefinition> = {
834
904
  type: "full",
835
905
  description: "Create admin user, add monitors for enabled apps",
836
906
  },
907
+ logVolume: "/app/data/logs",
837
908
  },
838
909
 
839
910
  // === INFRASTRUCTURE ===
@@ -862,6 +933,27 @@ export const APPS: Record<AppId, AppDefinition> = {
862
933
  },
863
934
  ],
864
935
  homepage: { icon: "traefik.png", widget: "traefik" },
936
+ logVolume: "/etc/traefik/logs",
937
+ },
938
+
939
+ caddy: {
940
+ id: "caddy",
941
+ name: "Caddy",
942
+ description: "Web server with automatic HTTPS and reverse proxy",
943
+ category: "infrastructure",
944
+ defaultPort: 80,
945
+ internalPort: 80,
946
+ secondaryPorts: ["443:443"],
947
+ image: "caddy:latest",
948
+ puid: 0,
949
+ pgid: 0,
950
+ volumes: (root) => [
951
+ `${root}/config/caddy/Caddyfile:/etc/caddy/Caddyfile`,
952
+ `${root}/config/caddy/data:/data`,
953
+ `${root}/config/caddy/config:/config`,
954
+ ],
955
+ homepage: { icon: "caddy.png" },
956
+ logVolume: "/data/logs",
865
957
  },
866
958
 
867
959
  cloudflared: {