Files
deploy-felhom-compose/TASK.md
T

24 KiB
Raw Blame History

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 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:

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:

  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:

.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 <a>)
  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)

// 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