v0.9.0: Storage paths registry, per-app HDD_PATH resolution, storage management UI

- Fix backup toggles not appearing (read each app's own HDD_PATH from app.yaml)
- Storage paths registry in settings.json with auto-discovery from deployed apps
- Settings page "Adattárolók" section with disk usage, add/remove/default/schedulable
- Deploy page path field as dropdown of registered storage paths
- Health check storage monitoring (mount point, disk usage alerts)
- Mount-point validation utilities (Linux syscall + cross-platform stubs)
- Controller docker-compose mount changed to /mnt:/mnt:rw for multi-storage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 09:04:28 +01:00
parent 465dec443f
commit aca3b8680a
17 changed files with 963 additions and 33 deletions
+68 -8
View File
@@ -8,6 +8,7 @@ import (
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
@@ -60,6 +61,10 @@ func main() {
logger.Fatalf("[FATAL] Failed to load settings from %s: %v", settingsPath, err)
}
// --- Auto-discover storage paths from deployed apps ---
discoveredPaths := discoverHDDPaths(cfg.Paths.StacksDir, logger)
sett.AutoDiscoverStoragePaths(discoveredPaths, cfg.Paths.HDDPath, logger)
// --- Initialize stack manager ---
stackMgr, err := stacks.NewManager(cfg, logger)
if err != nil {
@@ -96,7 +101,11 @@ func main() {
if metricsStore != nil {
defer metricsStore.Close()
metricsCollector := metrics.NewMetricsCollector(metricsStore, cpuCollector, cfg.Paths.HDDPath, logger)
metricsHDDPath := cfg.Paths.HDDPath
if paths := sett.GetStoragePaths(); len(paths) > 0 {
metricsHDDPath = paths[0].Path
}
metricsCollector := metrics.NewMetricsCollector(metricsStore, cpuCollector, metricsHDDPath, logger)
metricsCollector.Start(ctx)
defer metricsCollector.Stop()
logger.Println("[INFO] Metrics collector started (60s interval)")
@@ -109,7 +118,10 @@ func main() {
var backupMgr *backup.Manager
if cfg.Backup.Enabled {
backupMgr = backup.NewManager(cfg, pinger, sett, logger)
backupMgr.SetStackProvider(&stackAdapter{mgr: stackMgr, hddPath: cfg.Paths.HDDPath})
backupMgr.SetStackProvider(&stackAdapter{
mgr: stackMgr,
getStoragePaths: func() []settings.StoragePath { return sett.GetStoragePaths() },
})
backupMgr.AfterBackup = func() {
nextDBDump := scheduler.NextDailyRun(cfg.Backup.DBDumpSchedule)
nextBackup := scheduler.NextDailyRun(cfg.Backup.ResticSchedule)
@@ -147,7 +159,7 @@ func main() {
healthInterval = 5 * time.Minute
}
sched.Every("system-health", healthInterval, func(ctx context.Context) error {
healthReport := monitor.RunHealthCheck(cfg, cpuCollector)
healthReport := monitor.RunHealthCheck(cfg, cpuCollector, sett.GetStoragePaths())
body := healthReport.FormatMessage()
healthUUID := cfg.Monitoring.PingUUIDs.SystemHealth
if healthReport.Status == "fail" {
@@ -220,7 +232,7 @@ func main() {
}
pusher := report.NewPusher(&cfg.Hub, logger)
sched.Every("hub-report", pushInterval, func(ctx context.Context) error {
r := report.BuildReport(cfg, stackMgr, backupMgr, cpuCollector, metricsStore, Version)
r := report.BuildReport(cfg, stackMgr, backupMgr, cpuCollector, metricsStore, Version, sett.GetStoragePaths())
return pusher.Push(r)
})
logger.Printf("[INFO] Hub reporting enabled (every %s to %s)", pushInterval, cfg.Hub.URL)
@@ -252,7 +264,7 @@ func main() {
// Initial alert refresh (so alerts appear immediately, not after first 5min health check)
go func() {
report := monitor.RunHealthCheck(cfg, cpuCollector)
report := monitor.RunHealthCheck(cfg, cpuCollector, sett.GetStoragePaths())
alertMgr.Refresh(report, cfg, backupMgr)
}()
@@ -318,8 +330,8 @@ func setupLogger(cfg *config.Config) *log.Logger {
// stackAdapter implements backup.StackDataProvider using stacks.Manager.
type stackAdapter struct {
mgr *stacks.Manager
hddPath string
mgr *stacks.Manager
getStoragePaths func() []settings.StoragePath
}
func (a *stackAdapter) GetStackComposePath(name string) (string, bool) {
@@ -351,5 +363,53 @@ func (a *stackAdapter) GetStackHDDMounts(name string) []string {
if !ok {
return nil
}
return stacks.ParseComposeHDDMounts(s.ComposePath, a.hddPath)
// Priority 1: Read the app's own HDD_PATH from its app.yaml
stackDir := filepath.Dir(s.ComposePath)
appCfg := stacks.LoadAppConfig(stackDir)
if appCfg != nil && appCfg.Env["HDD_PATH"] != "" {
return stacks.ParseComposeHDDMounts(s.ComposePath, appCfg.Env["HDD_PATH"])
}
// Priority 2: Try all registered storage paths (fallback)
var allMounts []string
seen := make(map[string]bool)
for _, sp := range a.getStoragePaths() {
mounts := stacks.ParseComposeHDDMounts(s.ComposePath, sp.Path)
for _, m := range mounts {
if !seen[m] {
seen[m] = true
allMounts = append(allMounts, m)
}
}
}
return allMounts
}
// discoverHDDPaths scans deployed apps' app.yaml for HDD_PATH env values.
func discoverHDDPaths(stacksDir string, logger *log.Logger) []string {
entries, err := os.ReadDir(stacksDir)
if err != nil {
logger.Printf("[WARN] Cannot read stacks dir for HDD path discovery: %v", err)
return nil
}
seen := make(map[string]bool)
var paths []string
for _, e := range entries {
if !e.IsDir() {
continue
}
appCfg := stacks.LoadAppConfig(filepath.Join(stacksDir, e.Name()))
if appCfg == nil || !appCfg.Deployed {
continue
}
if hddPath, ok := appCfg.Env["HDD_PATH"]; ok && hddPath != "" {
cleaned := filepath.Clean(hddPath)
if !seen[cleaned] {
seen[cleaned] = true
paths = append(paths, cleaned)
}
}
}
return paths
}