# TASK: FileBrowser Auto-Mount on New Storage + UI Polish (3 fixes) **Version:** 0.11.6 **Scope:** Go handler + template/CSS changes --- ## Feature: Auto-update FileBrowser mounts when storage paths change ### Context FileBrowser Quantum is the infrastructure file manager deployed at `files.`. It's how customers access their files on external drives via the web. **Current problem:** FileBrowser's docker-compose.yml has hardcoded volume mounts pointing to `${HDD_PATH}/storage`, `${HDD_PATH}/media`, `${HDD_PATH}/Dokumentumok`. When a new drive is initialized via the storage wizard, FileBrowser doesn't get access. The user has to manually edit the compose — which defeats the managed appliance model. **Current FileBrowser compose** (`/opt/docker/stacks/filebrowser/docker-compose.yml`): ```yaml services: filebrowser: image: gtstef/filebrowser:latest container_name: filebrowser restart: unless-stopped environment: - TZ=Europe/Budapest volumes: - filebrowser_data:/home/filebrowser/data - ${HDD_PATH}/storage:/srv/storage:ro - ${HDD_PATH}/media:/srv/media - ${HDD_PATH}/Dokumentumok:/srv/Dokumentumok # ...traefik labels etc... ``` **Goal:** After a new storage path is registered (or removed), the controller automatically regenerates FileBrowser's compose with all registered storage paths as volume mounts, then recreates the container. ### New mount layout Instead of mounting specific subdirectories, mount each registered storage path as a top-level directory: ```yaml volumes: - filebrowser_data:/home/filebrowser/data # Auto-generated storage mounts: - /mnt/hdd_1:/srv/hdd_1 # "Külső HDD 1TB" - /mnt/hdd_2:/srv/hdd_2 # (if second drive added later) ``` The user sees in FileBrowser: ``` /srv/ hdd_1/ Dokumentumok/ media/ storage/ hdd_2/ ... ``` Each storage path's subdirectories (created during init: `storage/`, `Dokumentumok/`, `media/`) are visible as subdirectories under the mount name. ### Implementation #### 1. New function: `syncFileBrowserMounts()` in `handlers.go` Add a method on `*Server` that regenerates FileBrowser's compose from the current registered storage paths: ```go // syncFileBrowserMounts regenerates FileBrowser's docker-compose.yml // with volume mounts for all registered storage paths, then recreates the container. func (s *Server) syncFileBrowserMounts() { composePath := "/opt/docker/stacks/filebrowser/docker-compose.yml" // Check if FileBrowser stack exists if _, err := os.Stat(composePath); os.IsNotExist(err) { s.logger.Printf("[WARN] FileBrowser stack not found at %s — skipping mount sync", composePath) return } // Get all active storage paths paths := s.settings.GetStoragePaths() // all, not just schedulable // Read domain from .env envPath := "/opt/docker/stacks/filebrowser/.env" domain := "" if data, err := os.ReadFile(envPath); err == nil { for _, line := range strings.Split(string(data), "\n") { if strings.HasPrefix(line, "DOMAIN=") { domain = strings.TrimPrefix(line, "DOMAIN=") break } } } if domain == "" { s.logger.Printf("[WARN] Cannot read DOMAIN from FileBrowser .env — skipping mount sync") return } // Build volume mount lines var storageMounts []string for _, sp := range paths { mountName := filepath.Base(sp.Path) // "/mnt/hdd_1" → "hdd_1" // Mount each storage path as /srv/ line := fmt.Sprintf(" - %s:/srv/%s", sp.Path, mountName) storageMounts = append(storageMounts, line) } // Generate compose from template compose := generateFileBrowserCompose(domain, storageMounts) // Write compose if err := os.WriteFile(composePath, []byte(compose), 0644); err != nil { s.logger.Printf("[ERROR] Failed to write FileBrowser compose: %v", err) return } // Recreate container cmd := exec.Command("docker", "compose", "up", "-d", "--remove-orphans") cmd.Dir = filepath.Dir(composePath) if out, err := cmd.CombinedOutput(); err != nil { s.logger.Printf("[ERROR] Failed to recreate FileBrowser: %s — %v", string(out), err) } else { s.logger.Printf("[INFO] FileBrowser mounts synced — %d storage path(s)", len(paths)) } } ``` #### 2. Compose template function ```go func generateFileBrowserCompose(domain string, storageMounts []string) string { storageSection := "" if len(storageMounts) > 0 { storageSection = "\n # Storage paths (auto-generated by felhom-controller)\n" + strings.Join(storageMounts, "\n") } return fmt.Sprintf(`# FileBrowser Quantum — Infrastructure file manager # Domain: files.%s # Deployed by docker-setup.sh — managed by felhom-controller # WARNING: Volume mounts are auto-generated. Manual edits will be overwritten. services: filebrowser: image: gtstef/filebrowser:latest container_name: filebrowser restart: unless-stopped environment: - TZ=Europe/Budapest volumes: - filebrowser_data:/home/filebrowser/data%s networks: - traefik-public deploy: resources: limits: memory: 256M healthcheck: test: ["CMD", "wget", "--spider", "-q", "http://localhost:80/"] interval: 30s timeout: 5s retries: 3 start_period: 15s labels: - "traefik.enable=true" - "traefik.http.routers.filebrowser.rule=Host(` + "`" + `files.%s` + "`" + `)" - "traefik.http.routers.filebrowser.entrypoints=websecure" - "traefik.http.routers.filebrowser.tls=true" - "traefik.http.services.filebrowser.loadbalancer.server.port=80" - "traefik.docker.network=traefik-public" volumes: filebrowser_data: networks: traefik-public: external: true `, domain, storageSection, domain) } ``` **Note on Go string templating:** The backticks in the Traefik label make raw string literals tricky. You can either: - Use `fmt.Sprintf` with concatenation for the backtick part (shown above) - Or use `text/template` with a separate template string - Either approach is fine — choose whichever is cleaner in the actual code #### 3. Call syncFileBrowserMounts() after storage changes In `storageInitAPIHandler` (handlers.go), after `AddStoragePath` succeeds, add the sync call inside the goroutine: ```go if err := s.settings.AddStoragePath(sp); err != nil { s.logger.Printf("[WARN] Failed to register storage path after init: %v", err) } else { s.logger.Printf("[INFO] Storage path registered: %s (%s)", mountPath, label) // Sync FileBrowser mounts with new storage path s.syncFileBrowserMounts() } ``` Also call it in any handler that removes or modifies storage paths: - `removeStoragePath` handler (if exists) - `setDefaultStoragePath` handler — this one only changes the default flag, does NOT need a FileBrowser sync (mounts don't change) #### 4. Handle edge case: FileBrowser not deployed The `syncFileBrowserMounts` function already checks if the compose file exists. If FileBrowser isn't deployed yet, it logs a warning and returns. This is correct — the user might initialize storage before deploying FileBrowser. When FileBrowser IS later deployed (via catalog or docker-setup.sh), it uses `${HDD_PATH}` from `.env`. The controller could also call `syncFileBrowserMounts()` after deploying FileBrowser, but that's a separate enhancement. For now, the sync runs on storage changes. #### 5. Also preserve .felhom.yml and .env `syncFileBrowserMounts()` only writes `docker-compose.yml`. The `.felhom.yml` (metadata) and `.env` files should NOT be touched — they're managed separately. The generated compose does NOT use `${HDD_PATH}` or `${DOMAIN}` env vars — it bakes in the actual values (domain from .env, paths from settings). This is intentional: it makes the compose self-contained and debuggable. No hidden env var expansion. ### Test plan ```bash # 1. Before: check current FileBrowser mounts docker inspect filebrowser --format '{{range .Mounts}}{{.Source}}→{{.Destination}} {{end}}' # 2. Initialize a new drive (or use already-initialized hdd_1) # After init completes, check compose was rewritten: cat /opt/docker/stacks/filebrowser/docker-compose.yml # Should show: - /mnt/hdd_1:/srv/hdd_1 # 3. Verify FileBrowser was recreated docker inspect filebrowser --format '{{range .Mounts}}{{.Source}}→{{.Destination}} {{end}}' # Should include /mnt/hdd_1 → /srv/hdd_1 # 4. Open files. — navigate to /srv/hdd_1/ # Should see: Dokumentumok/, media/, storage/ ``` --- ## UI Fix 1: "Nincs csatolva!" badge → "Rendszermeghajtón" ### Problem `/mnt/hdd_placeholder` shows a red "Nincs csatolva!" badge. It's on the system SSD, which isn't a separate mount point. This looks like an error but it's just informational. ### Where `controller/internal/web/templates/settings.html` — find the badge rendering for storage paths that aren't mount points. Look for "Nincs csatolva!" text. ### Fix Change from red/danger badge to yellow/warning badge with new text: **Before:** ```html Nincs csatolva! ``` **After:** ```html Rendszermeghajtón ``` Add badge style if `badge-warn` doesn't exist in `style.css`: ```css .badge-warn { background: rgba(250, 204, 21, 0.15); color: #facc15; } ``` --- ## UI Fix 2: Init progress bar — remove disk usage zone gradient ### Problem The storage init progress bar uses the disk-usage bar style which has green→yellow→red gradient zones. At 30% progress it looks alarming. A task progress bar should be a simple single-color fill on neutral background. ### Where `controller/internal/web/templates/storage_init.html` — find the progress bar. `controller/internal/web/templates/style.css` — add new class. ### Fix Add a new CSS class for task progress (not disk usage): ```css .progress-bar-task { height: 8px; background: var(--border); border-radius: 4px; overflow: hidden; width: 100%; } .progress-bar-task .progress-fill { height: 100%; background: var(--accent); border-radius: 4px; transition: width 0.3s ease; } ``` In `storage_init.html`, replace the current progress bar element (which reuses the disk usage bar class) with: ```html
0% ``` Update the JS `pollProgress` function to set the width and text: ```js document.getElementById('progress-fill').style.width = data.percent + '%'; document.getElementById('progress-percent').textContent = data.percent + '%'; ``` --- ## UI Fix 3: "Alapértelmezett" button → "Legyen alapértelmezett" ### Problem Button text "Alapértelmezett" reads as a status label ("Default"), not an action. Users think the drive IS the default, not that clicking MAKES it the default. ### Where `controller/internal/web/templates/settings.html` — find the button for setting a non-default storage path as default. ### Fix Change button text from "Alapértelmezett" to "Legyen alapértelmezett". Verify the distinction is clear: - **IS default:** Green badge "Alapértelmezett" (no button, status indicator) - **NOT default:** Button "Legyen alapértelmezett" (action) --- ## Summary: All changes by file | File | Change | Type | |------|--------|------| | `handlers.go` | New `syncFileBrowserMounts()` + `generateFileBrowserCompose()` | Feature | | `handlers.go` | Call `syncFileBrowserMounts()` after `AddStoragePath` | Feature | | `settings.html` | Red "Nincs csatolva!" → yellow "Rendszermeghajtón" | UI fix | | `settings.html` | "Alapértelmezett" button → "Legyen alapértelmezett" | UI fix | | `storage_init.html` | Progress bar: zone gradient → simple fill bar | UI fix | | `style.css` | Add `.badge-warn` and `.progress-bar-task` classes | UI fix | ### What NOT to change - `.felhom.yml` for FileBrowser — unchanged - `.env` for FileBrowser — unchanged (domain read from it, not written) - `docker-setup.sh` FileBrowser install function — still works for initial install; controller takes over compose management after first storage init - Go storage package — no changes