24 KiB
TASK.md — v0.4.5: Dedicated Backup Page ("Biztonsági mentés")
Version bump: v0.4.5 Scope: New page + backend extensions for detailed backup visibility
Overview
Add a third page to the controller dashboard: Biztonsági mentés (Backup), accessible from the sidebar navigation alongside Vezérlőpult and Alkalmazások.
The page gives full visibility into the backup system — what's being backed up, when, where, and whether it's healthy. Designed so a non-technical customer can see "everything is green" at a glance, while Viktor (or a future admin panel) gets the details needed for troubleshooting.
Page Layout
Section 1: Status Overview (top of page)
A row of stat cards (same style as dashboard), showing at-a-glance backup health:
┌────────────────┐ ┌────────────────┐ ┌─────────────────┐ ┌───────────────┐
│ ✅ Helyi │ │ ⬜ Távoli │ │ 3 │ │ 87.5 MB │
│ mentés aktív │ │ nincs beállítva │ │ adatbázis mentve│ │ tároló méret │
└────────────────┘ └────────────────┘ └─────────────────┘ └───────────────┘
- Helyi mentés (Local backup): green if last backup < 36h old and successful, yellow if > 36h, red if failed, gray if disabled
- Távoli mentés (Remote backup): gray "nincs beállítva" for now (Phase 3 v1 is local-only). Placeholder for future B2/S3/SFTP offsite. When implemented: green/red status like local.
- Adatbázis mentve (Databases backed up): count of successfully dumped DBs from last run
- Tároló méret (Repository size): total restic repo size with snapshot count
Section 2: Ütemezés (Schedule)
A compact info card showing backup timing:
╔══════════════════════════════════════════════════════╗
║ Ütemezés ║
╠══════════════════════════════════════════════════════╣
║ ║
║ Adatbázis mentés 02:30 Következő: ma 02:30 ║
║ Restic pillanatkép 03:00 Következő: ma 03:00 ║
║ Karbantartás vasárnap Következő: 2026-02-22 ║
║ ║
║ Utolsó sikeres mentés: 2026-02-16 03:01 (15 órája) ║
║ Mentés időtartam: 12s ║
║ ║
║ Megőrzés: 7 napi · 4 heti · 6 havi ║
║ ║
║ [Mentés most] ║
╚══════════════════════════════════════════════════════╝
Hungarian labels:
- "Ütemezés" = Schedule
- "Adatbázis mentés" = Database backup
- "Restic pillanatkép" = Restic snapshot
- "Karbantartás" = Maintenance (prune)
- "Következő" = Next
- "ma" = today, "holnap" = tomorrow
- "Utolsó sikeres mentés" = Last successful backup
- "órája" = hours ago
- "Mentés időtartam" = Backup duration
- "Megőrzés" = Retention
- "napi" = daily, "heti" = weekly, "havi" = monthly
- "Mentés most" = Backup now
The "Mentés most" button triggers POST /api/backup/run (same as dashboard card). Show spinner/loading state while running. The button should be disabled if a backup is already in progress.
Section 3: Adatbázisok (Databases)
A table listing every discovered database container and its dump status:
╔══════════════════════════════════════════════════════════════════════╗
║ Adatbázisok ║
╠══════════════════════════════════════════════════════════════════════╣
║ ║
║ Alkalmazás Típus Méret Utolsó Állapot ║
║ ─────────────────────────────────────────────────────────────────── ║
║ paperless-ngx PostgreSQL 4.2 MB 03:01 ✅ OK ║
║ immich PostgreSQL 82.1 MB 03:01 ✅ OK ║
║ romm MariaDB 1.2 MB 03:01 ✅ OK ║
║ ║
╚══════════════════════════════════════════════════════════════════════╝
Table columns:
- Alkalmazás (Application): stack name (derived from container name)
- Típus (Type): PostgreSQL or MariaDB, with a small icon/badge
- Méret (Size): dump file size from last run
- Utolsó (Last): time of last dump (HH:MM format if today, date if older)
- Állapot (Status): ✅ OK / ❌ Hiba (error) / ⏳ Folyamatban (in progress) / ➖ Nem futott (never run)
If a dump failed, show the error message in a tooltip or expandable row detail.
Érvényesítés (Validation) column — NEW FEATURE (see Section 6 below for backend details):
Add a column showing whether the dump file passed basic structural validation:
║ Érvényesítés ║
║ ✅ 47 tábla ║ ← "47 tables" found in the dump
║ ✅ 123 tábla ║
║ ✅ 12 tábla ║
Section 4: Pillanatképek (Snapshots)
A table showing restic snapshot history (last 10-20 snapshots):
╔══════════════════════════════════════════════════════════════════════╗
║ Pillanatképek ║
╠══════════════════════════════════════════════════════════════════════╣
║ ║
║ Azonosító Időpont Méret Új fájl Változott ║
║ ─────────────────────────────────────────────────────────────────── ║
║ a3f2c91e 2026-02-16 03:01 +1.2 MB 3 2 ║
║ 8bc4d312 2026-02-15 03:01 +82.5 MB 156 0 ║ ← first snapshot
║ ║
║ Összesen: 2 pillanatkép · 87.5 MB ║
╚══════════════════════════════════════════════════════════════════════╝
Table columns:
- Azonosító (ID): short snapshot ID (8 chars)
- Időpont (Time): snapshot timestamp
- Méret (Size): data added in this snapshot ("+1.2 MB")
- Új fájl (New files): files_new count
- Változott (Changed): files_changed count
Footer: total snapshot count and total repo size.
Section 5: Tároló (Repository)
A compact info card about the restic repository:
╔══════════════════════════════════════════════════════╗
║ Tároló ║
╠══════════════════════════════════════════════════════╣
║ ║
║ Helyszín: /srv/backups/restic-repo (helyi) ║
║ Méret: 87.5 MB ║
║ Pillanatképek: 2 ║
║ Integritás: ✅ Rendben (utolsó: 2026-02-16) ║
║ ║
║ Mentett útvonalak: ║
║ • /opt/docker/stacks/ ║
║ • /srv/backups/db-dumps/ ║
║ • /opt/docker/felhom-controller/controller.yaml ║
║ ║
║ Távoli másolat: ║
║ ⬜ Nincs beállítva ║
║ (B2/S3/SFTP támogatás hamarosan) ║
║ ║
╚══════════════════════════════════════════════════════╝
Hungarian labels:
- "Tároló" = Repository/Storage
- "Helyszín" = Location
- "helyi" = local
- "Integritás" = Integrity
- "Rendben" = OK
- "Mentett útvonalak" = Backed up paths
- "Távoli másolat" = Remote copy
- "Nincs beállítva" = Not configured
- "hamarosan" = coming soon
Section 6: Backup Not Configured State
If backup.enabled is false, the entire page shows a friendly empty state:
╔══════════════════════════════════════════════════════╗
║ ║
║ 🛡️ Biztonsági mentés nincs beállítva ║
║ ║
║ A biztonsági mentés funkció nem aktív. ║
║ Kérjük, vegye fel a kapcsolatot a Felhom ║
║ csapattal a beállításhoz. ║
║ ║
╚══════════════════════════════════════════════════════╝
Backend Changes
New: Snapshot History (ResticManager)
Add method to list all snapshots (not just latest):
// ListSnapshots returns all snapshots, newest first.
func (r *ResticManager) ListSnapshots(limit int) ([]SnapshotInfo, error) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
cmd := r.command(ctx, "snapshots", "--json")
out, err := cmd.Output()
// ... parse JSON array, reverse for newest-first, limit to N
}
Extend SnapshotInfo with data-added fields if possible (restic's snapshots --json includes some stats).
Note: restic's snapshots --json returns basic info (ID, time, paths, tags, hostname). To get per-snapshot size/files data, we'd need restic diff between snapshots — this is expensive. Instead, store per-snapshot stats from the backup run in the Manager's in-memory history.
New: Backup History (in-memory ring buffer)
The Manager currently stores only lastDBDump and lastBackup. Extend to keep a history:
type Manager struct {
// ... existing fields ...
mu sync.Mutex
lastDBDump *DBDumpStatus
lastBackup *BackupStatus
running bool
snapshotHistory []SnapshotRecord // NEW: ring buffer, last 20 entries
}
// SnapshotRecord combines restic snapshot metadata with our run stats.
type SnapshotRecord struct {
SnapshotID string
Time time.Time
FilesNew int
FilesChanged int
DataAdded string
Duration time.Duration
Success bool
DBDumpCount int // how many DBs were dumped in this run
DBDumpSize int64 // total DB dump size
}
After each successful backup run, append to snapshotHistory. Cap at 20 entries (oldest dropped). This gives us per-snapshot stats without expensive restic diff calls.
On startup, populate snapshotHistory from restic snapshots --json (we'll have IDs and times but NOT the per-snapshot file/size delta — mark those as "n/a" for historical entries). Only new entries from the running controller will have full stats.
New: DB Dump Validation
After each dump, perform lightweight validation:
// ValidateDump checks a SQL dump file for basic structural integrity.
type DumpValidation struct {
Valid bool
TableCount int // number of CREATE TABLE / CREATE TABLE IF NOT EXISTS statements
Error string // validation error message, if any
FileSize int64
ModTime time.Time
}
func ValidateDump(filePath string, dbType DBType) DumpValidation
Validation logic:
- PostgreSQL dumps: Scan file for
CREATE TABLEstatements (count them). Check file starts with--(comment header). Check file ends with content (not truncated — look for final\\.or--). Check file size > 100 bytes. - MariaDB dumps: Scan for
CREATE TABLEstatements. Check starts with-- MySQL dumpor-- MariaDB dump. Check file size > 100 bytes. - This is a fast file scan (read and grep), NOT a full SQL parse.
Add Validation field to DumpResult:
type DumpResult struct {
DB DiscoveredDB
FilePath string
Size int64
Duration time.Duration
Error error
Validation DumpValidation // NEW
}
Run validation after each successful dump in DumpOne().
New: Dump File Listing from Disk
For the DB table, also scan the dump directory for existing files (even if the controller restarted and has no in-memory results):
// ListDumpFiles returns info about SQL dump files on disk.
func ListDumpFiles(dumpDir string) ([]DumpFileInfo, error)
type DumpFileInfo struct {
FileName string
StackName string // parsed from filename: "paperless-ngx-postgres.sql" → "paperless-ngx"
DBType DBType // parsed from filename: "...-postgres.sql" → postgres
Size int64
ModTime time.Time
}
This provides a fallback for the DB table even when in-memory DumpResult is empty (e.g., after container restart before first backup run).
New: Restic Check Status Tracking
Track the last restic check result:
type Manager struct {
// ... existing ...
lastCheckTime time.Time
lastCheckOK bool
}
restic check already runs during weekly prune. Store the result. Expose via GetCheckStatus().
Extended: GetStatus() → GetFullStatus()
New method that returns everything the backup page needs in one call:
type FullBackupStatus struct {
Enabled bool
Running bool
// DB Dumps
LastDBDump *DBDumpStatus
DumpFiles []DumpFileInfo // from disk scan
DiscoveredDBs []DiscoveredDB // currently running DB containers
// Restic
LastBackup *BackupStatus
SnapshotHistory []SnapshotRecord // last 20 runs
RepoStats *RepoStats
// Schedule
DBDumpSchedule string // "02:30"
ResticSchedule string // "03:00"
PruneSchedule string // "sunday"
NextDBDump time.Time // from scheduler
NextBackup time.Time // from scheduler
Retention config.RetentionConfig
// Repository health
RepoPath string
LastCheckTime time.Time
LastCheckOK bool
// Remote (placeholder for future)
RemoteEnabled bool
RemoteType string // "b2", "s3", "sftp" — empty for now
}
func (m *Manager) GetFullStatus(sched *scheduler.Scheduler) *FullBackupStatus
Extended API: GET /api/backup/status
Update the existing endpoint to return the full status when a ?detail=true query param is provided (backward compatible). Or just always return full data — the dashboard card only uses what it needs.
Add new endpoints:
GET /api/backup/snapshots → snapshot history list
GET /api/backup/databases → discovered DBs + dump file listing
Or keep it simple: one expanded GET /api/backup/status returns everything the page needs.
Routing & Navigation
Sidebar nav (layout.html)
Add third nav item:
<ul class="nav-links">
<li><a href="/" class="{{if eq .Page "dashboard"}}active{{end}}">Vezérlőpult</a></li>
<li><a href="/stacks" class="{{if eq .Page "stacks"}}active{{end}}">Alkalmazások</a></li>
<li><a href="/backups" class="{{if eq .Page "backups"}}active{{end}}">Biztonsági mentés</a></li>
</ul>
Web route (server.go ServeHTTP)
Add case:
case path == "/backups":
s.backupsHandler(w, r)
Handler (handlers.go)
func (s *Server) backupsHandler(w http.ResponseWriter, _ *http.Request) {
data := s.baseData("backups", "Biztonsági mentés")
if s.backupMgr != nil {
fullStatus := s.backupMgr.GetFullStatus(s.scheduler)
data["Backup"] = fullStatus
} else {
data["Backup"] = nil // template checks: {{if .Backup}}
}
s.render(w, "backups", data)
}
Server struct changes
The Server needs access to the scheduler for next-run times:
type Server struct {
cfg *config.Config
stackMgr *stacks.Manager
cpuCollector *system.CPUCollector
backupMgr *backup.Manager
scheduler *scheduler.Scheduler // NEW
// ...
}
Pass scheduler to NewServer() from main.go.
Dashboard card update
The existing "Biztonsági mentés" card on the Vezérlőpult page stays, but make it clickable — clicking navigates to /backups:
<a href="/backups" class="backup-status-card">
...existing content...
</a>
Template file
Create internal/web/templates/backups.html:
The template uses the {{.Backup}} data (type *FullBackupStatus).
Structure:
{{define "backups"}}
{{template "layout_start" .}}
<div class="page-header">
<h2>Biztonsági mentés</h2>
<span class="domain-badge">{{.Domain}}</span>
</div>
{{if not .Backup}}
<!-- Backup not configured empty state -->
{{else}}
<!-- Section 1: Status overview cards -->
<!-- Section 2: Schedule -->
<!-- Section 3: Database table -->
<!-- Section 4: Snapshot history table -->
<!-- Section 5: Repository info -->
{{end}}
{{template "layout_end" .}}
{{end}}
Auto-refresh
The page should auto-refresh backup status while a backup is running. Use the same pattern as the deploy page — poll /api/backup/status every 3 seconds when running: true, update the UI elements.
When a manual backup is triggered via "Mentés most", show inline progress:
- Button changes to "Mentés folyamatban..." with loading animation
- Status cards update as backup progresses
- When complete, full page data refreshes
CSS additions
New styles needed in style.css:
.backup-page-cards— the overview stat cards row (reuse.stats-gridlayout).schedule-card— schedule info card (similar to.system-info-card).schedule-row— individual schedule line.db-table— database dump table (dark theme table).db-type-badge— PostgreSQL/MariaDB badge (colored pill).snapshot-table— snapshot history table.repo-card— repository info card.backup-empty-state— empty state when backup not configured.validation-badge— green/red validation status badge.relative-time— muted color for "15 órája" type text
Table styling should match the dark theme. Alternating row backgrounds for readability:
.db-table tr:nth-child(even) { background: var(--bg-secondary); }
.db-table tr:nth-child(odd) { background: var(--bg-card); }
Implementation Order
Step 1: Backend data structures
- Add
SnapshotRecord,DumpFileInfo,DumpValidation,FullBackupStatustypes to backup package - Add
snapshotHistoryring buffer toManager - Add
ValidateDump()function todbdump.go - Add
ListDumpFiles()function todbdump.go - Add
ListSnapshots()method toResticManager - Add
GetFullStatus()method toManager - Track restic check status in Manager
Step 2: Integrate validation into dump flow
- Call
ValidateDump()after each successful dump inDumpOne() - Store validation result in
DumpResult - Append
SnapshotRecordto history after each backup run
Step 3: Server wiring
- Pass
*scheduler.SchedulertoNewServer()and store inServerstruct - Add
backupsHandler()tohandlers.go - Add
/backupsroute toServeHTTPinserver.go - Update
layout.html— add sidebar nav item
Step 4: Template + CSS
- Create
internal/web/templates/backups.html - Add new CSS styles to
style.css - Add any new template functions to
funcmap.go - Make dashboard backup card clickable (wrap in
<a>) - Update
embed.go— the new template file is auto-included by//go:embed templates/*
Step 5: Auto-refresh JavaScript
- Add polling logic for backup-in-progress state
- Add "Mentés most" button with inline status updates
Step 6: Build, deploy, verify
- Build v0.5.0
- Deploy to demo node (sync full docker-compose.yml this time)
- Verify all sections render correctly
- Trigger manual backup, verify page updates
- Check DB validation shows table counts
Step 7: Documentation
- Update README, CONTEXT.md
- Bump version references
New template functions (funcmap.go)
// timeAgo returns a human-readable Hungarian relative time string
// e.g., "15 perccel ezelőtt", "3 órája", "tegnap"
"timeAgo": func(t time.Time) string { ... }
// dbTypeBadge returns the Hungarian display name for a DB type
// "postgres" → "PostgreSQL", "mariadb" → "MariaDB"
"dbTypeLabel": func(t backup.DBType) string { ... }
// nextRunLabel formats the next run time relative to now
// Today → "ma 02:30", Tomorrow → "holnap 02:30", else → "2026-02-22 02:30"
"nextRunLabel": func(t time.Time) string { ... }
// pruneLabel converts prune schedule config to Hungarian
// "sunday" → "vasárnap", "daily" → "naponta", "weekly" → "hetente (vasárnap)"
"pruneLabel": func(s string) string { ... }
Files to create
internal/web/templates/backups.html # Backup page template
Files to modify
internal/backup/backup.go # SnapshotRecord, snapshotHistory, GetFullStatus(), check tracking
internal/backup/dbdump.go # DumpValidation, ValidateDump(), ListDumpFiles(), DumpFileInfo
internal/backup/restic.go # ListSnapshots(), extend SnapshotInfo
internal/web/server.go # Accept scheduler, add /backups route
internal/web/handlers.go # backupsHandler()
internal/web/funcmap.go # timeAgo, dbTypeLabel, nextRunLabel, pruneLabel
internal/web/templates/layout.html # Add sidebar nav item
internal/web/templates/dashboard.html # Make backup card clickable (<a> wrapper)
internal/web/templates/style.css # New backup page styles
cmd/controller/main.go # Pass scheduler to NewServer()
Verification checklist
- Sidebar shows 3 nav items: Vezérlőpult, Alkalmazások, Biztonsági mentés
- Active nav highlight works on backup page
- Backup page loads with all 5 sections
- Status overview cards show correct colors (green/yellow/red/gray)
- "Távoli mentés" shows gray "nincs beállítva" placeholder
- Schedule section shows correct times and "Következő" dates
- "Mentés most" button works and shows loading state
- Database table lists all discovered DBs with type, size, status
- Validation column shows table counts for successful dumps
- Snapshot history table shows recent snapshots with stats
- Historical snapshots (from before controller restart) show "n/a" for delta stats
- Repository card shows path, size, integrity status
- Backed up paths listed correctly
- Dashboard backup card is clickable → navigates to /backups
- Empty state shows correctly when backup.enabled is false
- Auto-refresh works during backup-in-progress
- Page renders correctly on mobile (responsive)
- All existing features still work