v0.4.6: MariaDB Validation Fix + Dashboard & Protected Stack UX
This commit is contained in:
@@ -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**
|
> Version bump: **v0.4.6**
|
||||||
> Scope: New page + backend extensions for detailed backup visibility
|
> Scope: Bug fix + 2 UI improvements
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Overview
|
## 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:
|
||||||
|
|
||||||
```
|
```sql
|
||||||
┌────────────────┐ ┌────────────────┐ ┌─────────────────┐ ┌───────────────┐
|
/*M!999999\- enable the sandbox mode */
|
||||||
│ ✅ Helyi │ │ ⬜ Távoli │ │ 3 │ │ 87.5 MB │
|
-- MariaDB dump 10.19-11.4.10-MariaDB, for debian-linux-gnu (x86_64)
|
||||||
│ 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
|
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".
|
||||||
- **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)
|
### 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.
|
||||||
|
|
||||||
```
|
For MariaDB/MySQL dumps, valid header patterns (any of these in the first 10 lines):
|
||||||
╔══════════════════════════════════════════════════════╗
|
- `-- MariaDB dump`
|
||||||
║ Ütemezés ║
|
- `-- MySQL dump`
|
||||||
╠══════════════════════════════════════════════════════╣
|
- `-- mysqldump`
|
||||||
║ ║
|
|
||||||
║ Adatbázis mentés 02:30 Következő: ma 02:30 ║
|
For PostgreSQL dumps, valid header patterns:
|
||||||
║ Restic pillanatkép 03:00 Következő: ma 03:00 ║
|
- `-- PostgreSQL database dump`
|
||||||
║ Karbantartás vasárnap Következő: 2026-02-22 ║
|
|
||||||
║ ║
|
### Implementation
|
||||||
║ Utolsó sikeres mentés: 2026-02-16 03:01 (15 órája) ║
|
|
||||||
║ Mentés időtartam: 12s ║
|
Find the header validation logic in `ValidateDump()`. Replace the "first line must be" check with a loop over the first 10 lines:
|
||||||
║ ║
|
|
||||||
║ Megőrzés: 7 napi · 4 heti · 6 havi ║
|
```go
|
||||||
║ ║
|
// Check header — scan first 10 lines for expected dump header
|
||||||
║ [Mentés most] ║
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Hungarian labels:
|
### Verification
|
||||||
- "Ü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.
|
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".
|
||||||
|
|
||||||
### 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
|
## Task 2: Dashboard — show only deployed apps
|
||||||
|
|
||||||
### New: Snapshot History (`ResticManager`)
|
### Current behavior
|
||||||
|
|
||||||
Add method to list all snapshots (not just latest):
|
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
|
```go
|
||||||
// ListSnapshots returns all snapshots, newest first.
|
// Filter to deployed-only for dashboard
|
||||||
func (r *ResticManager) ListSnapshots(limit int) ([]SnapshotInfo, error) {
|
stackList := s.stackMgr.GetStacks()
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
var deployedStacks []stacks.Stack
|
||||||
defer cancel()
|
for _, st := range stackList {
|
||||||
|
if st.Deployed || st.Protected {
|
||||||
cmd := r.command(ctx, "snapshots", "--json")
|
deployedStacks = append(deployedStacks, st)
|
||||||
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).
|
Pass `deployedStacks` as `data["Stacks"]` to the template instead of the full `stackList`.
|
||||||
|
|
||||||
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.
|
**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.
|
||||||
|
|
||||||
### New: Backup History (in-memory ring buffer)
|
### Template change
|
||||||
|
|
||||||
The `Manager` currently stores only `lastDBDump` and `lastBackup`. Extend to keep a history:
|
In `dashboard.html`, update the section heading:
|
||||||
|
|
||||||
```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
|
```html
|
||||||
<ul class="nav-links">
|
<h3>Telepített alkalmazások</h3>
|
||||||
<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)
|
("Telepített alkalmazások" = "Installed applications")
|
||||||
|
|
||||||
Add case:
|
No other template changes needed — the `{{range .Stacks}}` loop just iterates over fewer items.
|
||||||
```go
|
|
||||||
case path == "/backups":
|
|
||||||
s.backupsHandler(w, r)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Handler (handlers.go)
|
### Edge case
|
||||||
|
|
||||||
```go
|
If no user apps are deployed yet (fresh install), the section still shows protected infra stacks (traefik, cloudflared, felhom-controller, filebrowser) — this is reassuring to the customer.
|
||||||
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
|
## Task 3: FileBrowser — show URL + allow restart on protected stacks
|
||||||
|
|
||||||
The existing "Biztonsági mentés" card on the Vezérlőpult page stays, but make it clickable — clicking navigates to `/backups`:
|
### Current behavior
|
||||||
|
|
||||||
|
Protected stacks show only "Védett rendszerkomponens" badge with no actions or URL link, on both the dashboard compact list and the Alkalmazások detail cards.
|
||||||
|
|
||||||
|
### New behavior
|
||||||
|
|
||||||
|
For protected stacks that are **operational** (running):
|
||||||
|
- Show the subdomain URL link if configured (e.g., `files.demo-felhom.eu ↗`)
|
||||||
|
- Show "Újraindítás" (Restart) button
|
||||||
|
- Keep "Védett" badge
|
||||||
|
- NO stop/start/delete/update buttons (still protected from destructive actions)
|
||||||
|
|
||||||
|
### The subdomain problem
|
||||||
|
|
||||||
|
FileBrowser is deployed by `docker-setup.sh` as infrastructure — it may NOT have a `.felhom.yml` metadata file with the `subdomain` field set. Without it, the controller doesn't know FileBrowser's subdomain.
|
||||||
|
|
||||||
|
**Solution**: Add `.felhom.yml` creation to the `install_filebrowser()` function in `scripts/docker-setup.sh`.
|
||||||
|
|
||||||
|
For the demo node, create it manually after this deploy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo tee /opt/docker/stacks/filebrowser/.felhom.yml << 'EOF'
|
||||||
|
display_name: Filebrowser
|
||||||
|
slug: filebrowser
|
||||||
|
description: Fájlkezelő a külső merevlemezhez
|
||||||
|
subdomain: files
|
||||||
|
category: storage
|
||||||
|
app_info:
|
||||||
|
tagline: Web-alapú fájlkezelő a külső merevlemezhez
|
||||||
|
use_cases:
|
||||||
|
- Fájlok böngészése és letöltése a külső HDD-ről
|
||||||
|
- Médiafájlok megosztása családtagokkal
|
||||||
|
- Dokumentumok feltöltése és kezelése
|
||||||
|
first_steps:
|
||||||
|
- Nyisd meg a files.DOMAIN címet a böngészőben
|
||||||
|
- Jelentkezz be az admin fiókkal
|
||||||
|
- Tallózd a /srv mappákat
|
||||||
|
prerequisites:
|
||||||
|
- Külső HDD csatlakoztatva és felcsatolva
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
### Template changes — stacks.html (Alkalmazások page)
|
||||||
|
|
||||||
|
Update the protected stack actions section in the detail card:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<a href="/backups" class="backup-status-card">
|
{{if .Protected}}
|
||||||
...existing content...
|
<span class="badge badge-protected">Védett rendszerkomponens</span>
|
||||||
</a>
|
{{if isOperational .State}}
|
||||||
```
|
<button class="btn btn-warning" onclick="stackAction('{{.Name}}', 'restart')">Újraindítás</button>
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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}}
|
{{end}}
|
||||||
|
{{else if not .Deployed}}
|
||||||
|
...existing deploy button...
|
||||||
|
```
|
||||||
|
|
||||||
{{template "layout_end" .}}
|
The subdomain URL is already shown above the actions for all stacks — it uses `{{if .Meta.Subdomain}}`. If `.felhom.yml` has `subdomain: files`, it should render automatically. Verify this is not gated behind `{{if not .Protected}}` — if it is, remove that guard.
|
||||||
|
|
||||||
|
### Template changes — dashboard.html (Vezérlőpult page)
|
||||||
|
|
||||||
|
In the compact stack list, update the protected stack section similarly:
|
||||||
|
|
||||||
|
```html
|
||||||
|
{{if .Protected}}
|
||||||
|
<span class="badge badge-protected">Védett</span>
|
||||||
|
{{if isOperational .State}}
|
||||||
|
<button class="btn btn-sm btn-warning" onclick="stackAction('{{.Name}}', 'restart')">↻</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{else if not .Deployed}}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Auto-refresh
|
### Stack action handler check
|
||||||
|
|
||||||
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.
|
In `internal/stacks/manager.go`, check if the action handler blocks restart on protected stacks. If there's a guard like:
|
||||||
|
|
||||||
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
|
```go
|
||||||
// timeAgo returns a human-readable Hungarian relative time string
|
if stack.Protected {
|
||||||
// e.g., "15 perccel ezelőtt", "3 órája", "tegnap"
|
return fmt.Errorf("cannot perform action on protected stack")
|
||||||
"timeAgo": func(t time.Time) string { ... }
|
}
|
||||||
|
```
|
||||||
|
|
||||||
// dbTypeBadge returns the Hungarian display name for a DB type
|
Change it to only block destructive actions:
|
||||||
// "postgres" → "PostgreSQL", "mariadb" → "MariaDB"
|
|
||||||
"dbTypeLabel": func(t backup.DBType) string { ... }
|
|
||||||
|
|
||||||
// nextRunLabel formats the next run time relative to now
|
```go
|
||||||
// Today → "ma 02:30", Tomorrow → "holnap 02:30", else → "2026-02-22 02:30"
|
if stack.Protected && action != "restart" {
|
||||||
"nextRunLabel": func(t time.Time) string { ... }
|
return fmt.Errorf("cannot %s protected stack %s", action, name)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
// pruneLabel converts prune schedule config to Hungarian
|
**CRITICAL**: Only `restart` is allowed. `stop`, `start`, `update`, `delete` must remain blocked for protected stacks.
|
||||||
// "sunday" → "vasárnap", "daily" → "naponta", "weekly" → "hetente (vasárnap)"
|
|
||||||
"pruneLabel": func(s string) string { ... }
|
### docker-setup.sh changes
|
||||||
|
|
||||||
|
In `install_filebrowser()`, add `.felhom.yml` creation after the docker-compose.yml:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create .felhom.yml metadata
|
||||||
|
cat > "${FILEBROWSER_DIR}/.felhom.yml" << 'METAEOF'
|
||||||
|
display_name: Filebrowser
|
||||||
|
slug: filebrowser
|
||||||
|
description: Fájlkezelő a külső merevlemezhez
|
||||||
|
subdomain: files
|
||||||
|
category: storage
|
||||||
|
app_info:
|
||||||
|
tagline: Web-alapú fájlkezelő a külső merevlemezhez
|
||||||
|
use_cases:
|
||||||
|
- Fájlok böngészése és letöltése a külső HDD-ről
|
||||||
|
- Médiafájlok megosztása családtagokkal
|
||||||
|
- Dokumentumok feltöltése és kezelése
|
||||||
|
first_steps:
|
||||||
|
- Nyisd meg a files.DOMAIN címet a böngészőben
|
||||||
|
- Jelentkezz be az admin fiókkal
|
||||||
|
- Tallózd a /srv mappákat
|
||||||
|
prerequisites:
|
||||||
|
- Külső HDD csatlakoztatva és felcsatolva
|
||||||
|
METAEOF
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Files to create
|
## Implementation order
|
||||||
|
|
||||||
```
|
### Step 1: MariaDB validation fix
|
||||||
internal/web/templates/backups.html # Backup page template
|
1. Fix `ValidateDump()` in `internal/backup/dbdump.go` — scan first 10 lines for header
|
||||||
```
|
|
||||||
|
### Step 2: Dashboard deployed-only
|
||||||
|
1. Filter stacks in `dashboardHandler()` in `handlers.go`
|
||||||
|
2. Update heading in `dashboard.html` to "Telepített alkalmazások"
|
||||||
|
|
||||||
|
### Step 3: Protected stack UX
|
||||||
|
1. Update `stacks.html` protected section — add restart button, ensure URL link not gated
|
||||||
|
2. Update `dashboard.html` protected section — same (compact form)
|
||||||
|
3. Check `internal/stacks/manager.go` — allow restart on protected stacks
|
||||||
|
4. Add `.felhom.yml` creation to `install_filebrowser()` in `scripts/docker-setup.sh`
|
||||||
|
|
||||||
|
### Step 4: Build, deploy, verify
|
||||||
|
1. Build v0.4.6
|
||||||
|
2. Deploy to demo node (sync full docker-compose.yml)
|
||||||
|
3. Manually create `/opt/docker/stacks/filebrowser/.felhom.yml` on demo node
|
||||||
|
4. Trigger backup → verify MariaDB green validation
|
||||||
|
5. Check dashboard shows only deployed apps
|
||||||
|
6. Check FileBrowser shows URL and restart button
|
||||||
|
|
||||||
|
### Step 5: Documentation
|
||||||
|
1. Update CONTEXT.md, README
|
||||||
|
2. Bump version
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Files to modify
|
## Files to modify
|
||||||
|
|
||||||
```
|
```
|
||||||
internal/backup/backup.go # SnapshotRecord, snapshotHistory, GetFullStatus(), check tracking
|
internal/backup/dbdump.go — fix ValidateDump() header check
|
||||||
internal/backup/dbdump.go # DumpValidation, ValidateDump(), ListDumpFiles(), DumpFileInfo
|
internal/web/handlers.go — filter deployed-only in dashboardHandler()
|
||||||
internal/backup/restic.go # ListSnapshots(), extend SnapshotInfo
|
internal/web/templates/dashboard.html — heading + protected stack restart
|
||||||
internal/web/server.go # Accept scheduler, add /backups route
|
internal/web/templates/stacks.html — protected stack: ensure URL visible + restart button
|
||||||
internal/web/handlers.go # backupsHandler()
|
scripts/docker-setup.sh — create .felhom.yml in install_filebrowser()
|
||||||
internal/web/funcmap.go # timeAgo, dbTypeLabel, nextRunLabel, pruneLabel
|
internal/stacks/manager.go — allow restart action on protected stacks
|
||||||
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
|
## Verification checklist
|
||||||
|
|
||||||
- [ ] Sidebar shows 3 nav items: Vezérlőpult, Alkalmazások, Biztonsági mentés
|
- [ ] MariaDB (romm) shows green validation badge with table count on backup page
|
||||||
- [ ] Active nav highlight works on backup page
|
- [ ] Dashboard heading is "Telepített alkalmazások"
|
||||||
- [ ] Backup page loads with all 5 sections
|
- [ ] Dashboard shows only deployed + protected apps (no "Nincs telepítve" entries)
|
||||||
- [ ] Status overview cards show correct colors (green/yellow/red/gray)
|
- [ ] Dashboard stat cards still show correct total (52 apps)
|
||||||
- [ ] "Távoli mentés" shows gray "nincs beállítva" placeholder
|
- [ ] FileBrowser shows `files.demo-felhom.eu ↗` link on Alkalmazások page
|
||||||
- [ ] Schedule section shows correct times and "Következő" dates
|
- [ ] FileBrowser shows "Újraindítás" button on both pages
|
||||||
- [ ] "Mentés most" button works and shows loading state
|
- [ ] Restart works on FileBrowser
|
||||||
- [ ] Database table lists all discovered DBs with type, size, status
|
- [ ] Other protected stacks also show restart when operational
|
||||||
- [ ] Validation column shows table counts for successful dumps
|
- [ ] Stop/delete/update still blocked for all protected stacks
|
||||||
- [ ] Snapshot history table shows recent snapshots with stats
|
- [ ] No regressions on backup page, app detail pages, deploy flow
|
||||||
- [ ] 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
|
|
||||||
Reference in New Issue
Block a user