372 lines
12 KiB
Markdown
372 lines
12 KiB
Markdown
# 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`):
|
|
```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/<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
|
|
|
|
```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.<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:**
|
|
```html
|
|
<span class="badge badge-danger">Nincs csatolva!</span>
|
|
```
|
|
|
|
**After:**
|
|
```html
|
|
<span class="badge badge-warn">Rendszermeghajtón</span>
|
|
```
|
|
|
|
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
|
|
<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:
|
|
```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 |