v0.4.7: Protected stack detail pages + backup page caching
Task 1: Protected stacks with .felhom.yml (slug) are now clickable on both dashboard and stacks pages. "Részletek" button added to protected stack actions section. Filebrowser .felhom.yml updated with resources metadata. Task 2: Backup page now reads from a cached FullBackupStatus that refreshes every 5 minutes in background + after each backup run. Page loads instantly instead of blocking on restic/docker subprocesses. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
|
||||
<div class="stack-list">
|
||||
{{range .Stacks}}
|
||||
<div class="stack-card stack-state-{{stateColor .State}}"{{if not .Protected}} data-href="/apps/{{.Meta.Slug}}"{{end}}>
|
||||
<div class="stack-card stack-state-{{stateColor .State}}"{{if .Meta.Slug}} data-href="/apps/{{.Meta.Slug}}"{{end}}>
|
||||
<div class="stack-info">
|
||||
<img class="stack-logo" src="{{logoURL .Meta.Slug}}"
|
||||
alt="{{.Meta.DisplayName}}" onerror="this.onerror=function(){this.style.display='none'};this.src='{{logoPNGURL .Meta.Slug}}'">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<div class="stack-grid">
|
||||
{{range .Stacks}}
|
||||
<div class="stack-detail-card stack-state-{{stateColor .State}}" data-filter-state="{{filterCategory .State .Deployed}}"{{if not .Protected}} data-href="/apps/{{.Meta.Slug}}"{{end}}>
|
||||
<div class="stack-detail-card stack-state-{{stateColor .State}}" data-filter-state="{{filterCategory .State .Deployed}}"{{if .Meta.Slug}} data-href="/apps/{{.Meta.Slug}}"{{end}}>
|
||||
<div class="stack-detail-header">
|
||||
<div class="stack-title-row">
|
||||
<img class="stack-logo-lg" src="{{logoURL .Meta.Slug}}"
|
||||
@@ -62,6 +62,9 @@
|
||||
{{if isOperational .State}}
|
||||
<button class="btn btn-warning" onclick="stackAction('{{.Name}}', 'restart')">Újraindítás</button>
|
||||
{{end}}
|
||||
{{if .Meta.Slug}}
|
||||
<a href="/apps/{{.Meta.Slug}}" class="btn btn-outline">Részletek</a>
|
||||
{{end}}
|
||||
{{else if not .Deployed}}
|
||||
<a href="/stacks/{{.Name}}/deploy" class="btn btn-primary" onclick="return checkBeforeDeploy(event, '{{.Name}}')">Telepítés</a>
|
||||
<a href="{{appPageURL .Meta.Slug}}" class="btn btn-outline">Részletek</a>
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user