12 KiB
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.<domain>.
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):
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:
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:
// 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/<name>
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
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.Sprintfwith concatenation for the backtick part (shown above) - Or use
text/templatewith 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:
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:
removeStoragePathhandler (if exists)setDefaultStoragePathhandler — 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
# 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.<domain> — 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:
<span class="badge badge-danger">Nincs csatolva!</span>
After:
<span class="badge badge-warn">Rendszermeghajtón</span>
Add badge style if badge-warn doesn't exist in style.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):
.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:
<div class="progress-bar-task">
<div class="progress-fill" id="progress-fill" style="width: 0%"></div>
</div>
<span id="progress-percent" style="margin-left: 0.5rem; font-size: 0.9rem; color: var(--text-dim)">0%</span>
Update the JS pollProgress function to set the width and text:
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.ymlfor FileBrowser — unchanged.envfor FileBrowser — unchanged (domain read from it, not written)docker-setup.shFileBrowser install function — still works for initial install; controller takes over compose management after first storage init- Go storage package — no changes