314 lines
11 KiB
Markdown
314 lines
11 KiB
Markdown
# TASK.md — v0.4.7: Protected Stack Detail Pages + Backup Page Caching
|
|
|
|
> Version bump: **v0.4.7**
|
|
> Scope: UX fix + performance fix
|
|
|
|
---
|
|
|
|
## Task 1: Protected stacks — enable detail page + click-through
|
|
|
|
### Problem
|
|
|
|
Protected stacks (filebrowser, traefik, cloudflared, felhom-controller) are excluded from the app detail page in two ways:
|
|
|
|
1. **No click-through**: `data-href="/apps/{{.Meta.Slug}}"` is gated behind `{{if not .Protected}}` on both `dashboard.html` and `stacks.html`
|
|
2. **No "Részletek" button**: The protected section in `stacks.html` only shows "Védett rendszerkomponens" badge + restart button — no "Részletek" link
|
|
3. **Detail page works**: Manually navigating to `/apps/filebrowser` renders fine — so the handler and template already support it
|
|
|
|
### Fix — Templates
|
|
|
|
**stacks.html**: Add `data-href` for protected stacks that have a slug, and add "Részletek" button:
|
|
|
|
Change the card opening div from:
|
|
```html
|
|
<div class="stack-detail-card ..." {{if not .Protected}} data-href="/apps/{{.Meta.Slug}}"{{end}}>
|
|
```
|
|
To:
|
|
```html
|
|
<div class="stack-detail-card ..." {{if .Meta.Slug}} data-href="/apps/{{.Meta.Slug}}"{{end}}>
|
|
```
|
|
|
|
This lets any stack with a slug (including protected ones with `.felhom.yml`) be clickable. Stacks without `.felhom.yml` (no slug) won't have the click handler — which is correct.
|
|
|
|
In the protected actions section, add "Részletek" link:
|
|
```html
|
|
{{if .Protected}}
|
|
<span class="badge badge-protected">Védett rendszerkomponens</span>
|
|
{{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}}
|
|
```
|
|
|
|
**dashboard.html**: Same `data-href` fix for the compact card:
|
|
|
|
Change from:
|
|
```html
|
|
<div class="stack-card ..." {{if not .Protected}} data-href="/apps/{{.Meta.Slug}}"{{end}}>
|
|
```
|
|
To:
|
|
```html
|
|
<div class="stack-card ..." {{if .Meta.Slug}} data-href="/apps/{{.Meta.Slug}}"{{end}}>
|
|
```
|
|
|
|
### Fix — FileBrowser .felhom.yml (resources)
|
|
|
|
The manually created `.felhom.yml` on the demo node is missing `resources`. Update it to include:
|
|
|
|
```yaml
|
|
resources:
|
|
mem_request: "128M"
|
|
mem_limit: "256M"
|
|
pi_compatible: true
|
|
needs_hdd: true
|
|
```
|
|
|
|
Also add this to the `.felhom.yml` created by `install_filebrowser()` in `scripts/docker-setup.sh`.
|
|
|
|
**Manual fix for demo node** (run after deploy):
|
|
```bash
|
|
cat >> /opt/docker/stacks/filebrowser/.felhom.yml << 'EOF'
|
|
resources:
|
|
mem_request: "128M"
|
|
mem_limit: "256M"
|
|
pi_compatible: true
|
|
needs_hdd: true
|
|
EOF
|
|
```
|
|
|
|
### Verification
|
|
|
|
- Clicking FileBrowser card on Alkalmazások page opens `/apps/filebrowser` detail page
|
|
- "Részletek" button appears next to "Újraindítás" on FileBrowser
|
|
- Detail page shows memory badges (~128M, HDD szükséges, Pi kompatibilis)
|
|
- App info section shows use cases, first steps, prerequisites
|
|
- Other protected stacks without `.felhom.yml` (traefik, cloudflared) don't show "Részletek" (no slug)
|
|
|
|
---
|
|
|
|
## Task 2: Backup page — cache expensive data
|
|
|
|
### Problem
|
|
|
|
`GetFullStatus()` is called synchronously on every page load of `/backups` and runs three blocking subprocess calls:
|
|
|
|
1. `m.restic.Stats()` — executes `restic stats --json` + `restic snapshots --json` (~2-3 seconds)
|
|
2. `ListDumpFiles()` — directory listing (fast, ~1ms)
|
|
3. `DiscoverDatabases()` — `docker ps` + `docker inspect` per container (~0.5s)
|
|
|
|
Total: 3-4 seconds per page load. This is unacceptable for a dashboard page.
|
|
|
|
### Solution: Background cache with periodic refresh
|
|
|
|
Add a cached `FullBackupStatus` to the Manager that refreshes periodically via the scheduler, instead of computing on every page load.
|
|
|
|
#### New fields in Manager:
|
|
|
|
```go
|
|
type Manager struct {
|
|
// ... existing fields ...
|
|
|
|
mu sync.Mutex
|
|
lastDBDump *DBDumpStatus
|
|
lastBackup *BackupStatus
|
|
running bool
|
|
snapshotHistory []SnapshotRecord
|
|
|
|
// Cached status for page rendering
|
|
cachedStatus *FullBackupStatus
|
|
cacheTime time.Time
|
|
}
|
|
```
|
|
|
|
#### New method: `RefreshCache()`
|
|
|
|
```go
|
|
// 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) {
|
|
// Same logic as current GetFullStatus() — run restic stats, list dump files, discover DBs
|
|
status := &FullBackupStatus{ ... }
|
|
|
|
// ... all the expensive calls ...
|
|
|
|
m.mu.Lock()
|
|
m.cachedStatus = status
|
|
m.cacheTime = time.Now()
|
|
m.mu.Unlock()
|
|
|
|
m.logger.Printf("[INFO] Backup status cache refreshed")
|
|
}
|
|
```
|
|
|
|
#### Modified `GetFullStatus()`: read from cache
|
|
|
|
```go
|
|
// 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)
|
|
return m.cachedStatus
|
|
}
|
|
|
|
// 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",
|
|
},
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Scheduler integration (in `main.go`):
|
|
|
|
Register a periodic job that refreshes the backup cache:
|
|
|
|
```go
|
|
if cfg.Backup.Enabled && backupMgr != nil {
|
|
// ... existing daily jobs ...
|
|
|
|
// 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
|
|
})
|
|
}
|
|
```
|
|
|
|
#### Refresh after backup completion:
|
|
|
|
At the end of `RunFullBackup()` and `RunBackup()`, call `RefreshCache()` so the page shows updated data immediately after a backup:
|
|
|
|
```go
|
|
func (m *Manager) RunFullBackup(ctx context.Context) error {
|
|
// ... existing logic ...
|
|
|
|
// Refresh cache after backup completes
|
|
m.RefreshCache(
|
|
scheduler.NextDailyRun(m.cfg.Backup.DBDumpSchedule),
|
|
scheduler.NextDailyRun(m.cfg.Backup.ResticSchedule),
|
|
)
|
|
return nil // or the backup error
|
|
}
|
|
```
|
|
|
|
**Note**: `RefreshCache()` needs to import `scheduler.NextDailyRun`. To avoid a circular import (backup → scheduler → backup), either:
|
|
- Pass the next run times as parameters (already the pattern used)
|
|
- Make `NextDailyRun` a standalone utility function that both packages can import
|
|
- Or just call `RefreshCache` from `main.go` via a callback
|
|
|
|
Simplest approach: `RefreshCache` takes `nextDBDump, nextBackup time.Time` params (same as `GetFullStatus`). The scheduler job and the post-backup refresh both compute the times before calling.
|
|
|
|
#### Initial cache population on startup:
|
|
|
|
In `main.go`, after scheduler starts, trigger initial cache refresh in a goroutine (don't block startup):
|
|
|
|
```go
|
|
if cfg.Backup.Enabled && backupMgr != nil {
|
|
go func() {
|
|
nextDBDump := scheduler.NextDailyRun(cfg.Backup.DBDumpSchedule)
|
|
nextBackup := scheduler.NextDailyRun(cfg.Backup.ResticSchedule)
|
|
backupMgr.RefreshCache(nextDBDump, nextBackup)
|
|
}()
|
|
}
|
|
```
|
|
|
|
### Result
|
|
|
|
- Page load: instant (reads cached struct)
|
|
- Cache refresh: every 5 minutes in background (user never waits)
|
|
- After manual backup: cache refreshes immediately
|
|
- First page load after startup: may show minimal data for a few seconds until goroutine completes
|
|
|
|
### Cache staleness indicator (optional)
|
|
|
|
Add `CacheTime time.Time` to `FullBackupStatus`. The template can optionally show "Utolsó frissítés: X perccel ezelőtt" at the bottom of the page in a muted font. Not critical, but helpful for debugging.
|
|
|
|
---
|
|
|
|
## Implementation order
|
|
|
|
### Step 1: Protected stack detail pages
|
|
1. Fix `data-href` gating in `stacks.html` and `dashboard.html` — use `{{if .Meta.Slug}}` instead of `{{if not .Protected}}`
|
|
2. Add "Részletek" button to protected stack section in `stacks.html`
|
|
3. Update `install_filebrowser()` in `docker-setup.sh` — add `resources` to `.felhom.yml`
|
|
|
|
### Step 2: Backup page caching
|
|
1. Add `cachedStatus`, `cacheTime` fields to `Manager`
|
|
2. Create `RefreshCache()` method
|
|
3. Modify `GetFullStatus()` to read from cache
|
|
4. Register `backup-cache` scheduler job in `main.go`
|
|
5. Call `RefreshCache()` at end of `RunFullBackup()` and `RunBackup()`
|
|
6. Add initial cache goroutine in `main.go`
|
|
|
|
### Step 3: Build, deploy, verify
|
|
1. Build v0.4.7
|
|
2. Deploy to demo node
|
|
3. Update `/opt/docker/stacks/filebrowser/.felhom.yml` on demo node (add resources)
|
|
4. Verify FileBrowser card is clickable → detail page with memory badges
|
|
5. Verify backup page loads instantly
|
|
6. Trigger manual backup → verify page updates after completion
|
|
|
|
### Step 4: Documentation
|
|
1. Update CONTEXT.md, README
|
|
2. Bump version
|
|
|
|
---
|
|
|
|
## Files to modify
|
|
|
|
```
|
|
internal/backup/backup.go — add cachedStatus, RefreshCache(), modify GetFullStatus()
|
|
internal/web/templates/stacks.html — fix data-href gating, add Részletek button
|
|
internal/web/templates/dashboard.html — fix data-href gating
|
|
scripts/docker-setup.sh — add resources to filebrowser .felhom.yml
|
|
cmd/controller/main.go — register backup-cache job, initial goroutine
|
|
```
|
|
|
|
---
|
|
|
|
## Verification checklist
|
|
|
|
- [ ] FileBrowser card on Alkalmazások page is clickable → opens `/apps/filebrowser`
|
|
- [ ] FileBrowser has "Részletek" button next to "Újraindítás"
|
|
- [ ] FileBrowser detail page shows ~128M / HDD szükséges / Pi kompatibilis badges
|
|
- [ ] FileBrowser detail page shows use cases, first steps, prerequisites
|
|
- [ ] Traefik/cloudflared cards do NOT show "Részletek" (no .felhom.yml/slug)
|
|
- [ ] Felhom-controller card does NOT show "Részletek" (no .felhom.yml)
|
|
- [ ] Backup page loads in <500ms (instant, cached)
|
|
- [ ] Backup page shows correct data after initial cache population
|
|
- [ ] Manual backup → page shows updated data after completion
|
|
- [ ] Cache refreshes every 5 minutes (check logs for "[INFO] Backup status cache refreshed")
|
|
- [ ] No regressions on dashboard, app detail pages, deploy flow |