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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user