TASK.md — v0.4.5: Dedicated Backup Page ("Biztonsági mentés")
This commit is contained in:
@@ -1,382 +1,616 @@
|
||||
# TASK.md — v0.4.1: App Filtering + Bugfixes
|
||||
# TASK.md — v0.4.5: Dedicated Backup Page ("Biztonsági mentés")
|
||||
|
||||
> Version bump: **v0.4.1**
|
||||
> Scope: UI feature + configuration fixes
|
||||
> Version bump: **v0.4.5**
|
||||
> Scope: New page + backend extensions for detailed backup visibility
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Three items:
|
||||
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.
|
||||
|
||||
1. **Feature**: App filtering on the Alkalmazások (Stacks) page with clickable stat cards on the Vezérlőpult (Dashboard) page
|
||||
2. **Bugfix**: felhom.demo-felhom.eu returns 404 — docker-compose.yml on demo node needs syncing
|
||||
3. **Config**: Enable backup on demo node so the backup UI section appears
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: App Filtering
|
||||
## Page Layout
|
||||
|
||||
### Concept
|
||||
### Section 1: Status Overview (top of page)
|
||||
|
||||
**Alkalmazások page** gets filter tabs at the top:
|
||||
A row of stat cards (same style as dashboard), showing at-a-glance backup health:
|
||||
|
||||
```
|
||||
[ Mind (51) ] [ Futó (4) ] [ Leállítva (0) ] [ Telepíthető (47) ]
|
||||
┌────────────────┐ ┌────────────────┐ ┌─────────────────┐ ┌───────────────┐
|
||||
│ ✅ Helyi │ │ ⬜ Távoli │ │ 3 │ │ 87.5 MB │
|
||||
│ mentés aktív │ │ nincs beállítva │ │ adatbázis mentve│ │ tároló méret │
|
||||
└────────────────┘ └────────────────┘ └─────────────────┘ └───────────────┘
|
||||
```
|
||||
|
||||
**Vezérlőpult page** stat cards become clickable — clicking navigates to the Alkalmazások page with the corresponding filter pre-selected:
|
||||
- **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
|
||||
|
||||
- Click "4 Futó alkalmazás" → `/stacks?filter=running`
|
||||
- Click "0 Leállítva" → `/stacks?filter=stopped`
|
||||
- Click "51 Összes alkalmazás" → `/stacks` (no filter = show all)
|
||||
### Section 2: Ütemezés (Schedule)
|
||||
|
||||
### Implementation: 100% client-side
|
||||
A compact info card showing backup timing:
|
||||
|
||||
No server/handler changes needed. All filtering is done via JavaScript show/hide on existing DOM elements.
|
||||
|
||||
### Changes to `stacks.html`
|
||||
|
||||
Add filter bar between the page header and the stack grid:
|
||||
|
||||
```html
|
||||
<div class="filter-bar" id="filter-bar">
|
||||
<button class="filter-btn active" data-filter="all">Mind <span class="filter-count" id="count-all"></span></button>
|
||||
<button class="filter-btn" data-filter="running">Futó <span class="filter-count" id="count-running"></span></button>
|
||||
<button class="filter-btn" data-filter="stopped">Leállítva <span class="filter-count" id="count-stopped"></span></button>
|
||||
<button class="filter-btn" data-filter="available">Telepíthető <span class="filter-count" id="count-available"></span></button>
|
||||
</div>
|
||||
```
|
||||
╔══════════════════════════════════════════════════════╗
|
||||
║ Ü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] ║
|
||||
╚══════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
Add `data-filter-state` attribute to each stack card in the `{{range .Stacks}}` loop:
|
||||
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
|
||||
|
||||
```html
|
||||
<div class="stack-detail-card stack-state-{{stateColor .State}}"
|
||||
data-filter-state="{{filterCategory .State .Deployed}}"
|
||||
...>
|
||||
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 ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
Where `filterCategory` is a new template function that maps states to filter categories:
|
||||
- `running` → states: running, starting, unhealthy, restarting (has containers, is "up")
|
||||
- `stopped` → states: stopped, exited (deployed but not running)
|
||||
- `available` → state: not_deployed (never deployed)
|
||||
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)
|
||||
|
||||
### New template function: `filterCategory`
|
||||
If a dump failed, show the error message in a tooltip or expandable row detail.
|
||||
|
||||
Add to `funcmap.go`:
|
||||
**É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
|
||||
"filterCategory": func(state stacks.ContainerState, deployed bool) string {
|
||||
switch state {
|
||||
case stacks.StateRunning, stacks.StateStarting, stacks.StateUnhealthy, stacks.StateRestarting:
|
||||
return "running"
|
||||
case stacks.StateStopped, stacks.StateExited, stacks.StatePaused:
|
||||
return "stopped"
|
||||
default:
|
||||
if deployed {
|
||||
return "stopped" // deployed but unknown state → treat as stopped
|
||||
}
|
||||
return "available"
|
||||
}
|
||||
},
|
||||
// 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
|
||||
}
|
||||
```
|
||||
|
||||
### JavaScript for stacks.html
|
||||
Extend `SnapshotInfo` with data-added fields if possible (restic's `snapshots --json` includes some stats).
|
||||
|
||||
Add a `<script>` block at the bottom of `stacks.html` (before `{{template "layout_end" .}}`):
|
||||
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.
|
||||
|
||||
```javascript
|
||||
(function() {
|
||||
// Count cards per category
|
||||
var cards = document.querySelectorAll('.stack-detail-card[data-filter-state]');
|
||||
var counts = {all: cards.length, running: 0, stopped: 0, available: 0};
|
||||
### New: Backup History (in-memory ring buffer)
|
||||
|
||||
cards.forEach(function(card) {
|
||||
var cat = card.getAttribute('data-filter-state');
|
||||
if (counts[cat] !== undefined) counts[cat]++;
|
||||
});
|
||||
The `Manager` currently stores only `lastDBDump` and `lastBackup`. Extend to keep a history:
|
||||
|
||||
// Update count badges
|
||||
['all', 'running', 'stopped', 'available'].forEach(function(f) {
|
||||
var el = document.getElementById('count-' + f);
|
||||
if (el) el.textContent = '(' + counts[f] + ')';
|
||||
});
|
||||
```go
|
||||
type Manager struct {
|
||||
// ... existing fields ...
|
||||
|
||||
// Filter logic
|
||||
function applyFilter(filter) {
|
||||
cards.forEach(function(card) {
|
||||
if (filter === 'all' || card.getAttribute('data-filter-state') === filter) {
|
||||
card.style.display = '';
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
mu sync.Mutex
|
||||
lastDBDump *DBDumpStatus
|
||||
lastBackup *BackupStatus
|
||||
running bool
|
||||
snapshotHistory []SnapshotRecord // NEW: ring buffer, last 20 entries
|
||||
}
|
||||
|
||||
// Update active button
|
||||
document.querySelectorAll('.filter-btn').forEach(function(btn) {
|
||||
btn.classList.toggle('active', btn.getAttribute('data-filter') === filter);
|
||||
});
|
||||
|
||||
// Update URL without reload
|
||||
var url = new URL(window.location);
|
||||
if (filter === 'all') {
|
||||
url.searchParams.delete('filter');
|
||||
} else {
|
||||
url.searchParams.set('filter', filter);
|
||||
}
|
||||
history.replaceState(null, '', url);
|
||||
}
|
||||
|
||||
// Button click handlers
|
||||
document.querySelectorAll('.filter-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
applyFilter(this.getAttribute('data-filter'));
|
||||
});
|
||||
});
|
||||
|
||||
// Apply filter from URL on page load
|
||||
var urlFilter = new URLSearchParams(window.location.search).get('filter');
|
||||
if (urlFilter && ['running', 'stopped', 'available'].indexOf(urlFilter) !== -1) {
|
||||
applyFilter(urlFilter);
|
||||
}
|
||||
})();
|
||||
// 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
|
||||
}
|
||||
```
|
||||
|
||||
Key behaviors:
|
||||
- Reads `?filter=running` from URL on page load → applies filter immediately
|
||||
- Clicking a filter button updates URL via `history.replaceState` (no page reload)
|
||||
- Counts are computed from the DOM on page load (no server-side count needed)
|
||||
- "Mind" button removes the filter param from URL
|
||||
- Hidden cards use `display: none` (simple, works with any layout)
|
||||
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.
|
||||
|
||||
### Changes to `dashboard.html`
|
||||
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.
|
||||
|
||||
Make the three stat cards clickable. Wrap each in an `<a>` tag or add `onclick`:
|
||||
### 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
|
||||
<div class="stats-grid">
|
||||
<a href="/stacks?filter=running" class="stat-card stat-running">
|
||||
<div class="stat-value">{{.RunningCount}}</div>
|
||||
<div class="stat-label">Futó alkalmazás</div>
|
||||
</a>
|
||||
<a href="/stacks?filter=stopped" class="stat-card stat-stopped">
|
||||
<div class="stat-value">{{.StoppedCount}}</div>
|
||||
<div class="stat-label">Leállítva</div>
|
||||
</a>
|
||||
<a href="/stacks" class="stat-card stat-total">
|
||||
<div class="stat-value">{{.TotalCount}}</div>
|
||||
<div class="stat-label">Összes alkalmazás</div>
|
||||
</a>
|
||||
<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}}
|
||||
```
|
||||
|
||||
Change `<div>` to `<a>` for the stat cards. This requires a small CSS fix to ensure `<a>` tags inherit the card styling (remove text-decoration, inherit color).
|
||||
### Auto-refresh
|
||||
|
||||
### CSS changes (`style.css`)
|
||||
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.
|
||||
|
||||
Add filter bar styles:
|
||||
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
|
||||
/* --- Filter bar --- */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
padding: 0.4rem 1rem;
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
font-family: inherit;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.filter-btn:hover {
|
||||
border-color: var(--accent-blue);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background: var(--accent-blue);
|
||||
border-color: var(--accent-blue);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.filter-count {
|
||||
opacity: 0.7;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.db-table tr:nth-child(even) { background: var(--bg-secondary); }
|
||||
.db-table tr:nth-child(odd) { background: var(--bg-card); }
|
||||
```
|
||||
|
||||
Add clickable stat card styles (for the `<a>` change):
|
||||
|
||||
```css
|
||||
a.stat-card {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: transform 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
a.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
```
|
||||
|
||||
### Edge cases
|
||||
|
||||
- If all cards in a filter category are 0 (e.g., "Leállítva (0)") → button still shows, just with (0) count. Clicking it shows empty grid (the existing "Nincs elérhető alkalmazás" empty state won't show since cards exist but are hidden — this is OK, the (0) count is self-explanatory).
|
||||
- Protected stacks (traefik, cloudflared, felhom-controller): Include them in the filter. They have `data-filter-state="running"` like any running stack.
|
||||
- Orphaned stacks: Same — categorized by their actual state.
|
||||
- Page refresh preserves filter via URL param.
|
||||
- Back/forward navigation: `history.replaceState` means no extra history entries.
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Fix felhom.demo-felhom.eu 404
|
||||
## Implementation Order
|
||||
|
||||
### Confirmed root cause
|
||||
### 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
|
||||
|
||||
The docker-compose.yml on the demo node (192.168.0.162) is **stale** — still from pre-v0.3.0:
|
||||
### 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
|
||||
|
||||
**On demo node** (confirmed from troubleshooting.txt):
|
||||
```yaml
|
||||
- "traefik.http.routers.controller.rule=Host(`dashboard.${DOMAIN}`)" # ← WRONG
|
||||
```
|
||||
Also missing:
|
||||
- `/sys:/host/sys:ro` volume mount (needed for temperature reading, added in v0.4.0)
|
||||
- `felhom.managed=true` and `felhom.component=controller` labels
|
||||
- Updated restic-password handling
|
||||
### 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
|
||||
|
||||
**In repo** (correct):
|
||||
```yaml
|
||||
- "traefik.http.routers.controller.rule=Host(`felhom.${DOMAIN}`)" # ← CORRECT
|
||||
```
|
||||
### 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/*`
|
||||
|
||||
The deploy script only does `sed -i` on the image tag, so docker-compose.yml changes from v0.3.0 and v0.4.0 were never applied to the demo node.
|
||||
### Step 5: Auto-refresh JavaScript
|
||||
1. Add polling logic for backup-in-progress state
|
||||
2. Add "Mentés most" button with inline status updates
|
||||
|
||||
**DNS is fine** — Cloudflare has a wildcard CNAME (`*` → tunnel UUID), so `felhom.demo-felhom.eu` resolves through the tunnel. The 404 comes from Traefik not finding a matching `Host()` rule.
|
||||
### 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
|
||||
|
||||
### Fix
|
||||
|
||||
**Step 1** — Sync docker-compose.yml from repo to demo node:
|
||||
```bash
|
||||
scp controller/docker-compose.yml kisfenyo@192.168.0.162:/opt/docker/felhom-controller/docker-compose.yml
|
||||
```
|
||||
|
||||
**Step 2** — Recreate the controller with updated compose:
|
||||
```bash
|
||||
ssh kisfenyo@192.168.0.162 "cd /opt/docker/felhom-controller && sudo docker compose up -d"
|
||||
```
|
||||
This applies the new Traefik label (`felhom.${DOMAIN}`) and the `/sys` mount.
|
||||
|
||||
**Step 3** — Verify Cloudflare Tunnel public hostname config includes `felhom.demo-felhom.eu`. Since DNS uses a wildcard CNAME, the tunnel should already route it — but check the tunnel's public hostname entries in the Cloudflare Zero Trust dashboard. If only `dashboard.demo-felhom.eu` is listed, update it to `felhom.demo-felhom.eu`.
|
||||
|
||||
**Step 4** — Pi-hole DNS (if applicable): Update local DNS from `dashboard.demo-felhom.eu` → `felhom.demo-felhom.eu` pointing to `192.168.0.162`.
|
||||
|
||||
**Long-term**: The deploy process should sync the full docker-compose.yml, not just the image tag. This is a known gap — consider a controller self-update mechanism in a future version.
|
||||
### Step 7: Documentation
|
||||
1. Update README, CONTEXT.md
|
||||
2. Bump version references
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Enable backup on demo node
|
||||
## New template functions (`funcmap.go`)
|
||||
|
||||
### Root cause
|
||||
```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 { ... }
|
||||
|
||||
The backup UI section (`{{if .BackupEnabled}}`) exists in `dashboard.html` but `backup.enabled` is `false` (or unset) in the demo node's `controller.yaml`.
|
||||
// dbTypeBadge returns the Hungarian display name for a DB type
|
||||
// "postgres" → "PostgreSQL", "mariadb" → "MariaDB"
|
||||
"dbTypeLabel": func(t backup.DBType) string { ... }
|
||||
|
||||
### Fix
|
||||
// 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 { ... }
|
||||
|
||||
Edit `/opt/docker/felhom-controller/controller.yaml` on demo-felhom (192.168.0.162):
|
||||
|
||||
```yaml
|
||||
backup:
|
||||
enabled: true
|
||||
restic_repo: "/srv/backups/restic-repo"
|
||||
restic_password_file: "/opt/docker/felhom-controller/data/restic-password"
|
||||
db_dump_schedule: "02:30"
|
||||
restic_schedule: "03:00"
|
||||
retention:
|
||||
keep_daily: 7
|
||||
keep_weekly: 4
|
||||
keep_monthly: 6
|
||||
prune_schedule: "sunday"
|
||||
// pruneLabel converts prune schedule config to Hungarian
|
||||
// "sunday" → "vasárnap", "daily" → "naponta", "weekly" → "hetente (vasárnap)"
|
||||
"pruneLabel": func(s string) string { ... }
|
||||
```
|
||||
|
||||
Ensure `/srv/backups` directory exists on the host:
|
||||
```bash
|
||||
sudo mkdir -p /srv/backups/db-dumps /srv/backups/restic-repo
|
||||
```
|
||||
|
||||
Restart the controller: `docker compose restart felhom-controller`
|
||||
|
||||
The restic password will be auto-generated on first backup run. The "Biztonsági mentés" section should appear on the dashboard with "Még nem futott" status until the first scheduled or manual backup.
|
||||
|
||||
**Optional**: Create Healthchecks checks in status.felhom.eu and add the UUIDs to the monitoring config for full ping integration.
|
||||
|
||||
---
|
||||
|
||||
## Implementation order
|
||||
## Files to create
|
||||
|
||||
### Step 1: Template function
|
||||
1. Add `filterCategory` to `funcmap.go`
|
||||
|
||||
### Step 2: Stacks page filtering
|
||||
1. Add `data-filter-state` attribute to stack cards in `stacks.html`
|
||||
2. Add filter bar HTML
|
||||
3. Add filter JavaScript
|
||||
4. Add filter bar CSS to `style.css`
|
||||
|
||||
### Step 3: Dashboard clickable cards
|
||||
1. Change stat card `<div>` to `<a>` with href in `dashboard.html`
|
||||
2. Add `a.stat-card` CSS styles
|
||||
|
||||
### Step 4: Build, deploy, verify
|
||||
1. Build v0.4.1
|
||||
2. Deploy to demo node (this time, sync the FULL docker-compose.yml, not just image tag)
|
||||
3. Verify filters work on Alkalmazások page
|
||||
4. Verify stat card clicks navigate correctly
|
||||
5. Verify `?filter=running` URL param works
|
||||
|
||||
### Step 5: Manual config fixes (for Viktor, NOT in code)
|
||||
1. Fix Cloudflare Tunnel hostname
|
||||
2. Enable backup in controller.yaml
|
||||
3. Create /srv/backups directories
|
||||
|
||||
---
|
||||
```
|
||||
internal/web/templates/backups.html # Backup page template
|
||||
```
|
||||
|
||||
## Files to modify
|
||||
|
||||
```
|
||||
internal/web/funcmap.go — add filterCategory function
|
||||
internal/web/templates/stacks.html — filter bar + data attributes + JS
|
||||
internal/web/templates/dashboard.html — stat cards as <a> links
|
||||
internal/web/templates/style.css — filter bar + clickable card styles
|
||||
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()
|
||||
```
|
||||
|
||||
## Files unchanged
|
||||
|
||||
No server-side handler changes. No config changes. No new files.
|
||||
|
||||
---
|
||||
|
||||
## Verification checklist
|
||||
|
||||
- [ ] Alkalmazások page shows filter bar with correct counts
|
||||
- [ ] Clicking "Futó" shows only running apps
|
||||
- [ ] Clicking "Leállítva" shows only stopped apps
|
||||
- [ ] Clicking "Telepíthető" shows only undeployed apps
|
||||
- [ ] Clicking "Mind" shows everything
|
||||
- [ ] URL updates to `?filter=running` when filter active
|
||||
- [ ] Page load with `?filter=running` in URL pre-applies filter
|
||||
- [ ] Dashboard "Futó alkalmazás" card links to `/stacks?filter=running`
|
||||
- [ ] Dashboard "Leállítva" card links to `/stacks?filter=stopped`
|
||||
- [ ] Dashboard "Összes" card links to `/stacks`
|
||||
- [ ] Stat cards show hover effect (slight lift + shadow)
|
||||
- [ ] Protected stacks appear in correct filter category
|
||||
- [ ] Filter works correctly after template sync (new apps appear in right category)
|
||||
- [ ] 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
|
||||
Reference in New Issue
Block a user