# 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): ```go // 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: ```go 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: ```go // 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 TABLE` statements (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 TABLE` statements. Check starts with `-- MySQL dump` or `-- 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`: ```go 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): ```go // 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: ```go 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: ```go 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: ```html ``` ### Web route (server.go ServeHTTP) Add case: ```go case path == "/backups": s.backupsHandler(w, r) ``` ### Handler (handlers.go) ```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: ```go 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`: ```html ...existing content... ``` --- ## Template file Create `internal/web/templates/backups.html`: The template uses the `{{.Backup}}` data (type `*FullBackupStatus`). Structure: ``` {{define "backups"}} {{template "layout_start" .}} {{if not .Backup}} {{else}} {{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: 1. Button changes to "Mentés folyamatban..." with loading animation 2. Status cards update as backup progresses 3. When complete, full page data refreshes --- ## CSS additions New styles needed in `style.css`: - `.backup-page-cards` — the overview stat cards row (reuse `.stats-grid` layout) - `.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: ```css .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 1. Add `SnapshotRecord`, `DumpFileInfo`, `DumpValidation`, `FullBackupStatus` types to backup package 2. Add `snapshotHistory` ring buffer to `Manager` 3. Add `ValidateDump()` function to `dbdump.go` 4. Add `ListDumpFiles()` function to `dbdump.go` 5. Add `ListSnapshots()` method to `ResticManager` 6. Add `GetFullStatus()` method to `Manager` 7. Track restic check status in Manager ### Step 2: Integrate validation into dump flow 1. Call `ValidateDump()` after each successful dump in `DumpOne()` 2. Store validation result in `DumpResult` 3. Append `SnapshotRecord` to history after each backup run ### Step 3: Server wiring 1. Pass `*scheduler.Scheduler` to `NewServer()` and store in `Server` struct 2. Add `backupsHandler()` to `handlers.go` 3. Add `/backups` route to `ServeHTTP` in `server.go` 4. Update `layout.html` — add sidebar nav item ### Step 4: Template + CSS 1. Create `internal/web/templates/backups.html` 2. Add new CSS styles to `style.css` 3. Add any new template functions to `funcmap.go` 4. Make dashboard backup card clickable (wrap in ``) 5. Update `embed.go` — the new template file is auto-included by `//go:embed templates/*` ### Step 5: Auto-refresh JavaScript 1. Add polling logic for backup-in-progress state 2. Add "Mentés most" button with inline status updates ### Step 6: Build, deploy, verify 1. Build v0.5.0 2. Deploy to demo node (sync full docker-compose.yml this time) 3. Verify all sections render correctly 4. Trigger manual backup, verify page updates 5. Check DB validation shows table counts ### Step 7: Documentation 1. Update README, CONTEXT.md 2. Bump version references --- ## New template functions (`funcmap.go`) ```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 ( 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