diff --git a/controller/cmd/controller/main.go b/controller/cmd/controller/main.go index 96f3abe..7bcc84a 100644 --- a/controller/cmd/controller/main.go +++ b/controller/cmd/controller/main.go @@ -81,6 +81,11 @@ func main() { var backupMgr *backup.Manager if cfg.Backup.Enabled { backupMgr = backup.NewManager(cfg, pinger, logger) + backupMgr.AfterBackup = func() { + nextDBDump := scheduler.NextDailyRun(cfg.Backup.DBDumpSchedule) + nextBackup := scheduler.NextDailyRun(cfg.Backup.ResticSchedule) + backupMgr.RefreshCache(nextDBDump, nextBackup) + } go backupMgr.LoadSnapshotHistory() } @@ -120,11 +125,28 @@ func main() { sched.Daily("backup", cfg.Backup.ResticSchedule, func(ctx context.Context) error { return backupMgr.RunBackup(ctx) }) + + // Cache refresh: every 5 minutes + sched.Every("backup-cache", 5*time.Minute, func(ctx context.Context) error { + nextDBDump := scheduler.NextDailyRun(cfg.Backup.DBDumpSchedule) + nextBackup := scheduler.NextDailyRun(cfg.Backup.ResticSchedule) + backupMgr.RefreshCache(nextDBDump, nextBackup) + return nil + }) } sched.Start(ctx) defer sched.Stop() + // Initial backup cache population (don't block startup) + if cfg.Backup.Enabled && backupMgr != nil { + go func() { + nextDBDump := scheduler.NextDailyRun(cfg.Backup.DBDumpSchedule) + nextBackup := scheduler.NextDailyRun(cfg.Backup.ResticSchedule) + backupMgr.RefreshCache(nextDBDump, nextBackup) + }() + } + // --- Initialize API router --- apiRouter := api.NewRouter(cfg, stackMgr, syncer, cpuCollector, backupMgr, logger) diff --git a/controller/internal/backup/backup.go b/controller/internal/backup/backup.go index 4bf9ce8..2d9c31b 100644 --- a/controller/internal/backup/backup.go +++ b/controller/internal/backup/backup.go @@ -26,6 +26,14 @@ type Manager struct { snapshotHistory []SnapshotRecord // ring buffer, last 20 entries lastCheckTime time.Time lastCheckOK bool + + // Cached status for page rendering (refreshed periodically) + cachedStatus *FullBackupStatus + cacheTime time.Time + + // AfterBackup is called after a backup completes to refresh the cache. + // Set by main.go to avoid circular import with scheduler. + AfterBackup func() } // SnapshotRecord combines restic snapshot metadata with our run stats. @@ -254,6 +262,11 @@ func (m *Manager) RunBackup(ctx context.Context) error { result.SnapshotID, result.FilesNew, result.FilesChanged, result.DataAdded, duration.Round(time.Millisecond)) + // Refresh cache so the page shows updated data immediately + if m.AfterBackup != nil { + m.AfterBackup() + } + return nil } @@ -351,14 +364,11 @@ func (m *Manager) LoadSnapshotHistory() { m.logger.Printf("[INFO] Loaded %d historical snapshots", len(m.snapshotHistory)) } -// GetFullStatus returns everything the backup page needs. -func (m *Manager) GetFullStatus(nextDBDump, nextBackup time.Time) *FullBackupStatus { - m.mu.Lock() +// RefreshCache updates the cached full status. Called by scheduler every 5 minutes +// and after each backup run. +func (m *Manager) RefreshCache(nextDBDump, nextBackup time.Time) { status := &FullBackupStatus{ - Enabled: m.cfg.Backup.Enabled, - Running: m.running, - LastDBDump: m.lastDBDump, - LastBackup: m.lastBackup, + Enabled: m.cfg.Backup.Enabled, DBDumpSchedule: m.cfg.Backup.DBDumpSchedule, ResticSchedule: m.cfg.Backup.ResticSchedule, @@ -367,13 +377,38 @@ func (m *Manager) GetFullStatus(nextDBDump, nextBackup time.Time) *FullBackupSta NextBackup: nextBackup, Retention: m.cfg.Backup.Retention, - RepoPath: m.cfg.Backup.ResticRepo, - LastCheckTime: m.lastCheckTime, - LastCheckOK: m.lastCheckOK, + RepoPath: m.cfg.Backup.ResticRepo, + BackupPaths: []string{ + m.cfg.Paths.StacksDir, + m.cfg.Paths.DBDumpDir, + "/opt/docker/felhom-controller/controller.yaml", + }, } - // Copy snapshot history + + // Expensive calls (outside lock) + if stats, err := m.restic.Stats(); err == nil { + status.RepoStats = stats + } + if files, err := ListDumpFiles(m.cfg.Paths.DBDumpDir); err == nil { + status.DumpFiles = files + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if dbs, err := DiscoverDatabases(ctx, m.logger); err == nil { + status.DiscoveredDBs = dbs + } + + // Fill in dynamic fields under lock + m.mu.Lock() + status.Running = m.running + status.LastDBDump = m.lastDBDump + status.LastBackup = m.lastBackup + status.LastCheckTime = m.lastCheckTime + status.LastCheckOK = m.lastCheckOK status.SnapshotHistory = make([]SnapshotRecord, len(m.snapshotHistory)) copy(status.SnapshotHistory, m.snapshotHistory) + m.cachedStatus = status + m.cacheTime = time.Now() m.mu.Unlock() // Reverse so newest first @@ -381,31 +416,53 @@ func (m *Manager) GetFullStatus(nextDBDump, nextBackup time.Time) *FullBackupSta status.SnapshotHistory[i], status.SnapshotHistory[j] = status.SnapshotHistory[j], status.SnapshotHistory[i] } - // Backup paths - status.BackupPaths = []string{ - m.cfg.Paths.StacksDir, - m.cfg.Paths.DBDumpDir, - "/opt/docker/felhom-controller/controller.yaml", + m.logger.Printf("[INFO] Backup status cache refreshed") +} + +// GetFullStatus returns the cached backup status for page rendering. +// Returns instantly — no subprocess calls. +func (m *Manager) GetFullStatus(nextDBDump, nextBackup time.Time) *FullBackupStatus { + m.mu.Lock() + defer m.mu.Unlock() + + if m.cachedStatus != nil { + // Update dynamic fields that don't need subprocess calls + m.cachedStatus.Running = m.running + m.cachedStatus.NextDBDump = nextDBDump + m.cachedStatus.NextBackup = nextBackup + m.cachedStatus.LastDBDump = m.lastDBDump + m.cachedStatus.LastBackup = m.lastBackup + // Update snapshot history + m.cachedStatus.SnapshotHistory = make([]SnapshotRecord, len(m.snapshotHistory)) + copy(m.cachedStatus.SnapshotHistory, m.snapshotHistory) + // Reverse so newest first + for i, j := 0, len(m.cachedStatus.SnapshotHistory)-1; i < j; i, j = i+1, j-1 { + m.cachedStatus.SnapshotHistory[i], m.cachedStatus.SnapshotHistory[j] = m.cachedStatus.SnapshotHistory[j], m.cachedStatus.SnapshotHistory[i] + } + return m.cachedStatus } - // Get repo stats (non-locked) - if stats, err := m.restic.Stats(); err == nil { - status.RepoStats = stats + // No cache yet — return a minimal status (first page load before cache is populated) + return &FullBackupStatus{ + Enabled: m.cfg.Backup.Enabled, + Running: m.running, + LastDBDump: m.lastDBDump, + LastBackup: m.lastBackup, + DBDumpSchedule: m.cfg.Backup.DBDumpSchedule, + ResticSchedule: m.cfg.Backup.ResticSchedule, + PruneSchedule: m.cfg.Backup.PruneSchedule, + NextDBDump: nextDBDump, + NextBackup: nextBackup, + Retention: m.cfg.Backup.Retention, + RepoPath: m.cfg.Backup.ResticRepo, + LastCheckTime: m.lastCheckTime, + LastCheckOK: m.lastCheckOK, + BackupPaths: []string{ + m.cfg.Paths.StacksDir, + m.cfg.Paths.DBDumpDir, + "/opt/docker/felhom-controller/controller.yaml", + }, } - - // List dump files from disk - if files, err := ListDumpFiles(m.cfg.Paths.DBDumpDir); err == nil { - status.DumpFiles = files - } - - // Discover running DBs - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - if dbs, err := DiscoverDatabases(ctx, m.logger); err == nil { - status.DiscoveredDBs = dbs - } - - return status } func dbNames(dbs []DiscoveredDB) string { diff --git a/controller/internal/web/templates/dashboard.html b/controller/internal/web/templates/dashboard.html index a17c9ec..2e4c8a6 100644 --- a/controller/internal/web/templates/dashboard.html +++ b/controller/internal/web/templates/dashboard.html @@ -124,7 +124,7 @@
{{range .Stacks}} -
+
diff --git a/controller/internal/web/templates/stacks.html b/controller/internal/web/templates/stacks.html index ac01a94..c259a29 100644 --- a/controller/internal/web/templates/stacks.html +++ b/controller/internal/web/templates/stacks.html @@ -17,7 +17,7 @@
{{range .Stacks}} -
+
Újraindítás {{end}} + {{if .Meta.Slug}} + Részletek + {{end}} {{else if not .Deployed}} Telepítés Részletek diff --git a/scripts/docker-setup.sh b/scripts/docker-setup.sh index 4d75637..88817b1 100644 --- a/scripts/docker-setup.sh +++ b/scripts/docker-setup.sh @@ -1330,6 +1330,11 @@ slug: filebrowser description: Fájlkezelő a külső merevlemezhez subdomain: files category: storage +resources: + mem_request: "128M" + mem_limit: "256M" + pi_compatible: true + needs_hdd: true app_info: tagline: Web-alapú fájlkezelő a külső merevlemezhez use_cases: