diff --git a/TASK.md b/TASK.md index 62113af..9029123 100644 --- a/TASK.md +++ b/TASK.md @@ -1,616 +1,301 @@ -# TASK.md — v0.4.5: Dedicated Backup Page ("Biztonsági mentés") +# TASK.md — v0.4.6: MariaDB Validation Fix + Dashboard & Protected Stack UX -> Version bump: **v0.4.5** -> Scope: New page + backend extensions for detailed backup visibility +> Version bump: **v0.4.6** +> Scope: Bug fix + 2 UI improvements --- ## 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. +Three items: -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. +1. **Bugfix**: MariaDB dump validation false positive — header check fails because MariaDB 11.4+ prepends a sandbox comment before the dump header +2. **UI**: Dashboard should only show deployed apps (not 47 "Nincs telepítve" entries) +3. **UI**: Protected stacks (especially FileBrowser) should show subdomain URL + allow restart --- -## Page Layout +## Task 1: Fix MariaDB dump validation false positive -### Section 1: Status Overview (top of page) +### Root cause -A row of stat cards (same style as dashboard), showing at-a-glance backup health: +MariaDB 11.4+ prepends a sandbox directive before the header comment: -``` -┌────────────────┐ ┌────────────────┐ ┌─────────────────┐ ┌───────────────┐ -│ ✅ Helyi │ │ ⬜ Távoli │ │ 3 │ │ 87.5 MB │ -│ mentés aktív │ │ nincs beállítva │ │ adatbázis mentve│ │ tároló méret │ -└────────────────┘ └────────────────┘ └─────────────────┘ └───────────────┘ +```sql +/*M!999999\- enable the sandbox mode */ +-- MariaDB dump 10.19-11.4.10-MariaDB, for debian-linux-gnu (x86_64) ``` -- **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 +The validation code checks only the first line for `-- MariaDB dump` or `-- MySQL dump`, but line 1 is now `/*M!999999...*/`. So the header check fails with "MariaDB dump missing comment header". -### Section 2: Ütemezés (Schedule) +### Fix -A compact info card showing backup timing: +In `ValidateDump()` (file: `internal/backup/dbdump.go`), change the header check to scan the **first 10 lines** (not just line 1) for the expected pattern. Also accept `/*` and `/*!` lines as valid preamble. -``` -╔══════════════════════════════════════════════════════╗ -║ Ü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] ║ -╚══════════════════════════════════════════════════════╝ -``` +For MariaDB/MySQL dumps, valid header patterns (any of these in the first 10 lines): +- `-- MariaDB dump` +- `-- MySQL dump` +- `-- mysqldump` -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 +For PostgreSQL dumps, valid header patterns: +- `-- PostgreSQL database dump` -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. +### Implementation -### 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): +Find the header validation logic in `ValidateDump()`. Replace the "first line must be" check with a loop over the first 10 lines: ```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}} +// Check header — scan first 10 lines for expected dump header +scanner := bufio.NewScanner(file) +headerFound := false +linesChecked := 0 +for scanner.Scan() && linesChecked < 10 { + line := scanner.Text() + linesChecked++ + switch dbType { + case DBTypeMariaDB: + if strings.HasPrefix(line, "-- MariaDB dump") || + strings.HasPrefix(line, "-- MySQL dump") || + strings.HasPrefix(line, "-- mysqldump") { + headerFound = true + } + case DBTypePostgres: + if strings.HasPrefix(line, "-- PostgreSQL database dump") { + headerFound = true + } + } + if headerFound { + break } - - s.render(w, "backups", data) } ``` -### Server struct changes +### Verification -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`. +After deploying, trigger a manual backup ("Mentés most") and check the Adatbázisok table on the backup page. The romm MariaDB entry should show a green validation badge with table count instead of "Hiba". --- -## Dashboard card update +## Task 2: Dashboard — show only deployed apps -The existing "Biztonsági mentés" card on the Vezérlőpult page stays, but make it clickable — clicking navigates to `/backups`: +### Current behavior + +The "Alkalmazások állapota" section on the Vezérlőpult page shows **all** apps (deployed + not deployed), meaning 47+ "Nincs telepítve / Telepítés" entries clutter the dashboard. + +### New behavior + +Only show **deployed** apps (including protected infra stacks) on the dashboard. Non-deployed apps remain accessible via the Alkalmazások page. + +### Implementation + +In `dashboardHandler()` (`internal/web/handlers.go`), filter the stack list before passing to the template: + +```go +// Filter to deployed-only for dashboard +stackList := s.stackMgr.GetStacks() +var deployedStacks []stacks.Stack +for _, st := range stackList { + if st.Deployed || st.Protected { + deployedStacks = append(deployedStacks, st) + } +} +``` + +Pass `deployedStacks` as `data["Stacks"]` to the template instead of the full `stackList`. + +**Important**: The `TotalCount` stat card should still show the total count of all apps (deployed + not deployed), not just the filtered list. Keep using `len(stackList)` for that. + +### Template change + +In `dashboard.html`, update the section heading: ```html - - ...existing content... - +