TASK.md — v0.4.5: Dedicated Backup Page ("Biztonsági mentés")

This commit is contained in:
2026-02-16 07:35:23 +01:00
parent 599560454b
commit 0985339e6c
+534 -300
View File
@@ -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** > Version bump: **v0.4.5**
> Scope: UI feature + configuration fixes > Scope: New page + backend extensions for detailed backup visibility
--- ---
## Overview ## 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 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.
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
--- ---
## 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` ### Section 2: Ütemezés (Schedule)
- Click "0 Leállítva" → `/stacks?filter=stopped`
- Click "51 Összes alkalmazás" → `/stacks` (no filter = show all)
### 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` ║ Ütemezés ║
╠══════════════════════════════════════════════════════╣
Add filter bar between the page header and the stack grid: ║ ║
║ Adatbázis mentés 02:30 Következő: ma 02:30 ║
```html ║ Restic pillanatkép 03:00 Következő: ma 03:00 ║
<div class="filter-bar" id="filter-bar"> ║ Karbantartás vasárnap Következő: 2026-02-22 ║
<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> ║ Utolsó sikeres mentés: 2026-02-16 03:01 (15 órája) ║
<button class="filter-btn" data-filter="stopped">Leállítva <span class="filter-count" id="count-stopped"></span></button> ║ Mentés időtartam: 12s ║
<button class="filter-btn" data-filter="available">Telepíthető <span class="filter-count" id="count-available"></span></button> ║ ║
</div> ║ 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 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.
<div class="stack-detail-card stack-state-{{stateColor .State}}"
data-filter-state="{{filterCategory .State .Deployed}}" ### 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: Table columns:
- `running` → states: running, starting, unhealthy, restarting (has containers, is "up") - **Alkalmazás** (Application): stack name (derived from container name)
- `stopped` → states: stopped, exited (deployed but not running) - **Típus** (Type): PostgreSQL or MariaDB, with a small icon/badge
- `available` → state: not_deployed (never deployed) - **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 ```go
"filterCategory": func(state stacks.ContainerState, deployed bool) string { // ListSnapshots returns all snapshots, newest first.
switch state { func (r *ResticManager) ListSnapshots(limit int) ([]SnapshotInfo, error) {
case stacks.StateRunning, stacks.StateStarting, stacks.StateUnhealthy, stacks.StateRestarting: ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
return "running" defer cancel()
case stacks.StateStopped, stacks.StateExited, stacks.StatePaused:
return "stopped" cmd := r.command(ctx, "snapshots", "--json")
default: out, err := cmd.Output()
if deployed { // ... parse JSON array, reverse for newest-first, limit to N
return "stopped" // deployed but unknown state → treat as stopped }
}
return "available"
}
},
``` ```
### 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 ### New: Backup History (in-memory ring buffer)
(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};
cards.forEach(function(card) { The `Manager` currently stores only `lastDBDump` and `lastBackup`. Extend to keep a history:
var cat = card.getAttribute('data-filter-state');
if (counts[cat] !== undefined) counts[cat]++;
});
// Update count badges ```go
['all', 'running', 'stopped', 'available'].forEach(function(f) { type Manager struct {
var el = document.getElementById('count-' + f); // ... existing fields ...
if (el) el.textContent = '(' + counts[f] + ')';
});
// Filter logic mu sync.Mutex
function applyFilter(filter) { lastDBDump *DBDumpStatus
cards.forEach(function(card) { lastBackup *BackupStatus
if (filter === 'all' || card.getAttribute('data-filter-state') === filter) { running bool
card.style.display = ''; snapshotHistory []SnapshotRecord // NEW: ring buffer, last 20 entries
} else { }
card.style.display = 'none';
}
});
// Update active button // SnapshotRecord combines restic snapshot metadata with our run stats.
document.querySelectorAll('.filter-btn').forEach(function(btn) { type SnapshotRecord struct {
btn.classList.toggle('active', btn.getAttribute('data-filter') === filter); SnapshotID string
}); Time time.Time
FilesNew int
// Update URL without reload FilesChanged int
var url = new URL(window.location); DataAdded string
if (filter === 'all') { Duration time.Duration
url.searchParams.delete('filter'); Success bool
} else { DBDumpCount int // how many DBs were dumped in this run
url.searchParams.set('filter', filter); DBDumpSize int64 // total DB dump size
} }
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);
}
})();
``` ```
Key behaviors: 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.
- 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)
### 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 ```html
<div class="stats-grid"> <ul class="nav-links">
<a href="/stacks?filter=running" class="stat-card stat-running"> <li><a href="/" class="{{if eq .Page "dashboard"}}active{{end}}">Vezérlőpult</a></li>
<div class="stat-value">{{.RunningCount}}</div> <li><a href="/stacks" class="{{if eq .Page "stacks"}}active{{end}}">Alkalmazások</a></li>
<div class="stat-label">Futó alkalmazás</div> <li><a href="/backups" class="{{if eq .Page "backups"}}active{{end}}">Biztonsági mentés</a></li>
</a> </ul>
<a href="/stacks?filter=stopped" class="stat-card stat-stopped"> ```
<div class="stat-value">{{.StoppedCount}}</div>
<div class="stat-label">Leállítva</div> ### Web route (server.go ServeHTTP)
</a>
<a href="/stacks" class="stat-card stat-total"> Add case:
<div class="stat-value">{{.TotalCount}}</div> ```go
<div class="stat-label">Összes alkalmazás</div> case path == "/backups":
</a> 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> </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 ```css
/* --- Filter bar --- */ .db-table tr:nth-child(even) { background: var(--bg-secondary); }
.filter-bar { .db-table tr:nth-child(odd) { background: var(--bg-card); }
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;
}
``` ```
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): ### Step 3: Server wiring
```yaml 1. Pass `*scheduler.Scheduler` to `NewServer()` and store in `Server` struct
- "traefik.http.routers.controller.rule=Host(`dashboard.${DOMAIN}`)" # ← WRONG 2. Add `backupsHandler()` to `handlers.go`
``` 3. Add `/backups` route to `ServeHTTP` in `server.go`
Also missing: 4. Update `layout.html` — add sidebar nav item
- `/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
**In repo** (correct): ### Step 4: Template + CSS
```yaml 1. Create `internal/web/templates/backups.html`
- "traefik.http.routers.controller.rule=Host(`felhom.${DOMAIN}`)" # ← CORRECT 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 7: Documentation
1. Update README, CONTEXT.md
**Step 1** — Sync docker-compose.yml from repo to demo node: 2. Bump version references
```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.
--- ---
## 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): // pruneLabel converts prune schedule config to Hungarian
// "sunday" → "vasárnap", "daily" → "naponta", "weekly" → "hetente (vasárnap)"
```yaml "pruneLabel": func(s string) string { ... }
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"
``` ```
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` internal/web/templates/backups.html # Backup page template
```
### 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
---
## Files to modify ## Files to modify
``` ```
internal/web/funcmap.go — add filterCategory function internal/backup/backup.go # SnapshotRecord, snapshotHistory, GetFullStatus(), check tracking
internal/web/templates/stacks.html — filter bar + data attributes + JS internal/backup/dbdump.go # DumpValidation, ValidateDump(), ListDumpFiles(), DumpFileInfo
internal/web/templates/dashboard.html — stat cards as <a> links internal/backup/restic.go # ListSnapshots(), extend SnapshotInfo
internal/web/templates/style.css — filter bar + clickable card styles 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 ## Verification checklist
- [ ] Alkalmazások page shows filter bar with correct counts - [ ] Sidebar shows 3 nav items: Vezérlőpult, Alkalmazások, Biztonsági mentés
- [ ] Clicking "Futó" shows only running apps - [ ] Active nav highlight works on backup page
- [ ] Clicking "Leállítva" shows only stopped apps - [ ] Backup page loads with all 5 sections
- [ ] Clicking "Telepíthető" shows only undeployed apps - [ ] Status overview cards show correct colors (green/yellow/red/gray)
- [ ] Clicking "Mind" shows everything - [ ] "Távoli mentés" shows gray "nincs beállítva" placeholder
- [ ] URL updates to `?filter=running` when filter active - [ ] Schedule section shows correct times and "Következő" dates
- [ ] Page load with `?filter=running` in URL pre-applies filter - [ ] "Mentés most" button works and shows loading state
- [ ] Dashboard "Futó alkalmazás" card links to `/stacks?filter=running` - [ ] Database table lists all discovered DBs with type, size, status
- [ ] Dashboard "Leállítva" card links to `/stacks?filter=stopped` - [ ] Validation column shows table counts for successful dumps
- [ ] Dashboard "Összes" card links to `/stacks` - [ ] Snapshot history table shows recent snapshots with stats
- [ ] Stat cards show hover effect (slight lift + shadow) - [ ] Historical snapshots (from before controller restart) show "n/a" for delta stats
- [ ] Protected stacks appear in correct filter category - [ ] Repository card shows path, size, integrity status
- [ ] Filter works correctly after template sync (new apps appear in right category) - [ ] 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