Files
deploy-felhom-compose/TASK.md
T

616 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
<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:
```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
<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:
```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 `<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`)
```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