feat(v0.11.6): FileBrowser auto-mount sync + UI polish

- Add syncFileBrowserMounts() and generateFileBrowserCompose() to handlers.go
- Call syncFileBrowserMounts() after storage path add (storage init) and remove
- settings.html: red 'Nincs csatolva!' badge → yellow 'Rendszermeghajtón' (badge-warn)
- settings.html: 'Alapértelmezett' button → 'Legyen alapértelmezett' (action clarity)
- storage_init.html: replace disk-usage zone gradient bar with clean progress-bar-task
- style.css: add .badge-warn and .progress-bar-task CSS classes

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 12:04:06 +01:00
parent d42a676522
commit 12eaf5b47e
5 changed files with 145 additions and 8 deletions
+112
View File
@@ -5,6 +5,7 @@ import (
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
@@ -795,6 +796,8 @@ func (s *Server) settingsStorageRemoveHandler(w http.ResponseWriter, r *http.Req
}
s.logger.Printf("[INFO] Storage path removed: %s", path)
// Sync FileBrowser mounts after storage path removal
go s.syncFileBrowserMounts()
http.Redirect(w, r, "/settings?storage_msg=success&storage_detail="+url.QueryEscape("Adattároló eltávolítva: "+path), http.StatusFound)
}
@@ -846,3 +849,112 @@ func (s *Server) settingsStorageLabelHandler(w http.ResponseWriter, r *http.Requ
s.logger.Printf("[INFO] Storage label updated: %s → %q", path, label)
http.Redirect(w, r, "/settings?storage_msg=success&storage_detail="+url.QueryEscape("Megnevezés módosítva: "+label), http.StatusFound)
}
// 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()
// 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"
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))
}
}
// generateFileBrowserCompose returns a FileBrowser docker-compose.yml string
// with the given domain and storage volume mount lines.
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)
}
@@ -250,6 +250,8 @@ func (s *Server) storageInitAPIHandler(w http.ResponseWriter, r *http.Request) {
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()
}
}()
@@ -86,7 +86,7 @@
<div class="storage-path-badges">
{{if .IsDefault}}<span class="badge state-green">Alapértelmezett</span>{{end}}
{{if .Schedulable}}<span class="badge" style="background:rgba(0,136,204,0.15);color:var(--accent-light)">Aktív</span>{{else}}<span class="badge state-gray">Inaktív</span>{{end}}
{{if not .IsMounted}}<span class="badge state-red">Nincs csatolva!</span>{{end}}
{{if not .IsMounted}}<span class="badge badge-warn">Rendszermeghajtón</span>{{end}}
</div>
</div>
<div class="storage-path-details">
@@ -131,7 +131,7 @@
{{if not .IsDefault}}
<form method="POST" action="/settings/storage/default" style="display:inline">
<input type="hidden" name="storage_path" value="{{.Path}}">
<button type="submit" class="btn btn-xs btn-outline">Alapértelmezett</button>
<button type="submit" class="btn btn-xs btn-outline">Legyen alapértelmezett</button>
</form>
{{end}}
{{if .Schedulable}}
@@ -87,11 +87,11 @@
<div class="disk-step" id="pstep-done"><span class="disk-step-icon"></span> Regisztráció</div>
</div>
<div class="disk-progress-bar-wrap" style="margin-top:1.5rem">
<div class="system-bar" style="height:12px;border-radius:6px">
<div class="system-bar-fill system-bar-green" id="progress-bar" style="width:0%;transition:width .4s ease;height:12px;border-radius:6px"></div>
<div style="margin-top:1.5rem;display:flex;align-items:center;gap:1rem">
<div class="progress-bar-task" style="flex:1">
<div class="progress-fill" id="progress-fill" style="width:0%"></div>
</div>
<span class="mono form-hint" id="progress-pct">0%</span>
<span id="progress-percent" style="font-size:0.9rem;color:var(--text-muted);font-family:'JetBrains Mono',monospace;white-space:nowrap">0%</span>
</div>
<div id="progress-msg" class="form-hint" style="margin-top:.75rem"></div>
@@ -297,8 +297,8 @@ function updateProgressUI(data) {
// Progress bar
var pct = data.pct || 0;
document.getElementById('progress-bar').style.width = pct + '%';
document.getElementById('progress-pct').textContent = pct + '%';
document.getElementById('progress-fill').style.width = pct + '%';
document.getElementById('progress-percent').textContent = pct + '%';
document.getElementById('progress-msg').textContent = data.msg || '';
if (data.step === 'error' || data.error) {
@@ -2090,6 +2090,29 @@ a.stat-card:hover {
.storage-app-link:hover {
text-decoration: underline;
}
/* Badge variants */
.badge-warn {
background: rgba(250, 204, 21, 0.15);
color: #facc15;
}
/* Task progress bar (storage init — not disk usage zone gradient) */
.progress-bar-task {
height: 8px;
background: var(--border-color);
border-radius: 4px;
overflow: hidden;
width: 100%;
}
.progress-bar-task .progress-fill {
height: 100%;
background: var(--accent-blue);
border-radius: 4px;
transition: width 0.3s ease;
}
.storage-path-label-wrap {
display: flex;
align-items: center;