diff --git a/TASK.md b/TASK.md
index bc967e6..71d1d15 100644
--- a/TASK.md
+++ b/TASK.md
@@ -1,174 +1,340 @@
-# TASK: Bug fixes from v0.6.2 code scan
+# TASK: Phase 1 — Authentication, Persistence & Settings Page
-## Context
+**Version target:** 0.7.0
+**Repo:** `deploy-felhom-compose` (controller)
-Comprehensive code scan of felhom-controller v0.6.2 found 4 minor bugs across templates, shell scripts, and Go code. None are critical, but all should be fixed for correctness.
+## Overview
-**Current state:** Controller v0.6.2 running on demo-felhom.eu.
+Three workstreams in this phase:
+1. Implement login/logout authentication for the controller dashboard
+2. Persist DB validation results across container restarts
+3. Add "Beállítások" (Settings) page with config display and password change
-All changes in this task are in the **deploy-felhom-compose** repo only.
+All user-editable state is stored in a single `settings.json` file at:
+`/opt/docker/felhom-controller/data/settings.json`
+
+This path is already bind-mounted into the container (the `data/` dir holds `restic-password` etc.).
+The controller.yaml config remains the source of truth for operator-provisioned values;
+`settings.json` holds customer-modifiable overrides.
---
-## Bug 1: Missing `require_arg` for `--hdd-path` in docker-setup.sh
+## 1. settings.json — Shared Persistence Layer
-**File:** `scripts/docker-setup.sh`
-
-**Problem:** The `--hdd-path` flag parsing doesn't use `require_arg` validation like all other flags do. Under `set -u`, if `--hdd-path` is the last argument and has no value, `$2` is unbound and the script crashes with a cryptic bash error instead of a friendly message.
-
-**Current code** (in the argument parsing `while` loop):
-
-```bash
---hdd-path) HDD_PATH="$2"; shift 2 ;;
+### 1.1 Create `internal/settings/settings.go`
+```
+File: controller/internal/settings/settings.go
```
-**Fix:** Add `require_arg` call, matching the pattern used by all other flags:
-
-```bash
---hdd-path)
- require_arg "$1" "${2:-}"
- HDD_PATH="$2"; shift 2 ;;
-```
-
-**Verification:** Search for other flags in the same `while` loop — they all use `require_arg`. Confirm `require_arg` is defined earlier in the script (it is).
-
----
-
-## Bug 2: Implicit `event` variable in `stackAction()` (layout.html)
-
-**File:** `controller/internal/web/templates/layout.html`
-
-**Problem:** The `stackAction` JavaScript function references `event.currentTarget` to get the clicked button, but `event` is never passed as a parameter. It relies on the implicit global `window.event` object, which is non-standard and deprecated. Works in Chrome/Firefox today but is not guaranteed.
-
-**Current code:**
-
-```javascript
-async function stackAction(name, action) {
- const btn = event.currentTarget;
-```
-
-**Fix — Step 1:** Change the function signature to accept `event`:
-
-```javascript
-async function stackAction(event, name, action) {
- const btn = event.currentTarget;
-```
-
-**Fix — Step 2:** Update ALL `onclick` call sites in the same file that call `stackAction` to pass `event` as the first argument. Search for `stackAction(` in the template — each call looks like:
-
-```html
-onclick="stackAction('{{.Name}}', 'start')"
-```
-
-Change each to:
-
-```html
-onclick="stackAction(event, '{{.Name}}', 'start')"
-```
-
-There are multiple call sites (start, stop, restart buttons in the stacks section). Update **all** of them.
-
-**Verification:** Search the entire file for `stackAction(` — every call site must pass `event` as the first argument. No other functions in the codebase call `stackAction`.
-
----
-
-## Bug 3: Missing separator in page title (layout.html)
-
-**File:** `controller/internal/web/templates/layout.html`
-
-**Problem:** The `
` tag concatenates `.Title` and "Felhom.eu" with no separator, rendering as e.g. `"VezérlőpultFelhom.eu"` instead of `"Vezérlőpult — Felhom.eu"`.
-
-**Current code:**
-
-```html
-{{.Title}}Felhom.eu
-```
-
-**Fix:**
-
-```html
-{{.Title}} — Felhom.eu
-```
-
-Uses em dash (U+2014) with spaces on both sides. This is a single-character change in the template.
-
-**Edge case:** If `.Title` is empty, the title becomes ` — Felhom.eu` (leading space + dash). Check if any handler sets an empty `.Title`. If so, consider using a conditional:
-
-```html
-{{if .Title}}{{.Title}} — {{end}}Felhom.eu
-```
-
-Check all handlers that call `render()` or `renderTemplate()` — if every handler always sets a non-empty `.Title`, the simple fix (without conditional) is fine.
-
----
-
-## Bug 4: `nextPruneLabel` edge case on Sunday before 4am (funcmap.go)
-
-**File:** `controller/internal/web/funcmap.go`
-
-**Problem:** The `nextPruneLabel` function calculates when the next weekly prune (Sunday 4:00) will occur. On Sunday before 4am, `daysUntilSunday` computes to 0, but the function returns the date in `"2006-01-02"` format instead of `"ma"` (Hungarian for "today"). Every other "today" scenario in the codebase uses the `"ma"` label.
-
-**Current code:**
-
+Define the settings struct and load/save logic:
```go
-daysUntilSunday := (7 - int(now.Weekday())) % 7
-if daysUntilSunday == 0 && now.Hour() >= 4 {
- daysUntilSunday = 7
+type Settings struct {
+ mu sync.RWMutex `json:"-"`
+
+ // Auth
+ PasswordHash string `json:"password_hash,omitempty"` // bcrypt hash, overrides controller.yaml
+
+ // Notification preferences (Phase 2 — define struct now, leave empty)
+ Notifications *NotificationPrefs `json:"notifications,omitempty"`
+
+ // Cached state
+ DBValidations map[string]DBValidationCache `json:"db_validations,omitempty"`
+}
+
+type NotificationPrefs struct {
+ // placeholder for Phase 2
+}
+
+type DBValidationCache struct {
+ ValidatedAt string `json:"validated_at"` // RFC3339
+ TableCount int `json:"table_count"`
+ HasHeader bool `json:"has_header"`
+ Error string `json:"error,omitempty"`
}
-next := time.Date(now.Year(), now.Month(), now.Day()+daysUntilSunday, 4, 0, 0, 0, now.Location())
-return next.Format("2006-01-02")
```
-The logic breakdown:
-- Sunday, hour >= 4: `daysUntilSunday` = 0 → set to 7 (next week). Correct.
-- Sunday, hour < 4: `daysUntilSunday` = 0 → stays 0, returns today's date as `"2006-01-02"`. Should return `"ma"`.
-- Any other day: `daysUntilSunday` > 0 → returns future date. Correct.
+**Behavior:**
+- `Load(path string) (*Settings, error)` — reads file, returns empty Settings{} if file doesn't exist (not an error)
+- `Save() error` — atomic write (write to `.tmp`, rename). Path stored internally from Load.
+- Use `sync.RWMutex` for concurrent access (backup goroutine writes validations, web reads them)
+- Log on every save: `[DEBUG] Settings saved to `
-**Fix:**
+**File location:** The controller's docker-compose.yml already mounts `./data:/opt/docker/felhom-controller/data`.
+The settings file path should be passed to the Settings manager on init.
-```go
-daysUntilSunday := (7 - int(now.Weekday())) % 7
-if daysUntilSunday == 0 {
- if now.Hour() >= 4 {
- daysUntilSunday = 7 // Already ran today, next week
- } else {
- return "ma" // Today (Sunday), hasn't run yet
- }
-}
-next := time.Date(now.Year(), now.Month(), now.Day()+daysUntilSunday, 4, 0, 0, 0, now.Location())
-return next.Format("2006-01-02")
-```
-
-**Verification:** Mentally walk through all cases:
-- Monday–Saturday: `daysUntilSunday` is 1–6, returns future date ✓
-- Sunday 03:00: returns `"ma"` ✓
-- Sunday 04:00: `daysUntilSunday` = 7, returns next Sunday ✓
-- Sunday 23:00: `daysUntilSunday` = 7, returns next Sunday ✓
+**Testing:** On startup, if file doesn't exist, log `[INFO] No settings.json found, using defaults`. Do NOT create the file until something actually needs saving.
---
-## Build & Deploy
+## 2. Authentication (Login / Logout)
-After all fixes, commit and deploy as v0.6.3:
+### 2.1 Password resolution
-```bash
-# 1. Commit
-cd /e/git/deploy-felhom-compose
-git add -A && git commit -m "fix: require_arg for --hdd-path, explicit event in stackAction, title separator, nextPruneLabel Sunday edge case" && git push
+The effective password hash is determined with this priority:
+1. `settings.json` → `password_hash` (customer changed it)
+2. `controller.yaml` → `web.password_hash` (operator provisioned)
+3. Empty string → no auth required (current testing behavior)
-# 2. Build (only needed for bugs 2-4 which affect the controller binary/templates)
-ssh kisfenyo@192.168.0.180 "cd ~/build/felhom-controller && ./build.sh 0.6.3 --push"
-
-# 3. Deploy to demo node
-ssh kisfenyo@192.168.0.162 "docker pull gitea.dooplex.hu/admin/felhom-controller:0.6.3 && cd /opt/docker && docker compose up -d"
-
-# 4. Verify
-ssh kisfenyo@192.168.0.162 "docker logs felhom-controller --tail 5"
+On startup, log which source is active:
+```
+[INFO] Auth: using password from settings.json
+[INFO] Auth: using password from controller.yaml
+[INFO] Auth: no password configured — dashboard is open
```
-## Post-deploy checklist
+### 2.2 Session management
-- [ ] Page title shows separator: "Vezérlőpult — Felhom.eu" (check browser tab)
-- [ ] Stack start/stop/restart buttons still work (Bug 2 didn't break onclick handlers)
-- [ ] `docker-setup.sh --hdd-path` without value shows friendly error (test locally)
-- [ ] Backup page shows "ma" on Sunday before 4am (only testable at that time, or adjust system clock)
\ No newline at end of file
+Use a **signed session cookie** approach (not just `"authenticated"` string):
+- Generate a random 32-byte session secret on startup (store in memory only, not persisted — restarts invalidate sessions, which is fine)
+- Cookie name: `felhom_session`
+- Cookie value: `.` — HMAC of expiry timestamp with session secret
+- `HttpOnly: true`, `SameSite: Strict`, `Secure: false` (local network, self-signed certs)
+- `MaxAge: 7 days` (configurable later)
+- `Path: /`
+
+### 2.3 Routes
+
+Add to `internal/web/server.go` routing:
+
+| Method | Path | Auth? | Handler |
+|--------|-------------|-------|--------------------|
+| GET | `/login` | No | Show login form |
+| POST | `/login` | No | Validate + set cookie + redirect to `/` |
+| GET/POST | `/logout` | No | Clear cookie + redirect to `/login` |
+
+**Auth middleware** (wrap all routes except `/login`, `/logout`, `/style.css`, `/assets/*`, `/api/*`):
+- Check `felhom_session` cookie → validate HMAC + check expiry
+- If invalid/missing → redirect to `/login` (for browser) or return 401 (for API)
+- If no password configured → pass through (no auth)
+
+### 2.4 Login page template
+```
+File: controller/internal/web/templates/login.html
+```
+
+- Standalone page (no sidebar), same dark theme as rest of dashboard
+- Felhom logo centered at top
+- Single password field + "Bejelentkezés" button
+- On error: "Hibás jelszó" message (red, below form)
+- Customer name shown below logo (from config)
+- No username field — single-user system
+
+### 2.5 Logout
+
+- `GET /logout` or `POST /logout` → set cookie MaxAge=-1 → redirect to `/login`
+- Add logout link to sidebar bottom (near version display):
+```
+ | Kijelentkezés
+```
+ Only show "Kijelentkezés" if auth is enabled.
+
+### 2.6 Edge cases
+
+- If password_hash is empty in both sources → no auth, no login page, no logout link
+- If user is on a page and session expires → next request redirects to `/login`, after login redirect back to original page (use `?next=/backups` query param)
+- Cookie cleared on logout must work even if server secret rotated (clear by MaxAge=-1)
+
+---
+
+## 3. DB Validation Persistence
+
+### 3.1 Problem
+
+After container restart, the backup page "Érvényesítés" column shows "–" for all databases until the next backup cycle runs validation. The v0.6.2 cross-check helps once RefreshCache runs, but the initial load after restart still shows no data.
+
+### 3.2 Solution
+
+**On validation completion** (`internal/backup/dbdump.go` → `ValidateDump`):
+- After successful validation, save result to `settings.json` via settings package:
+```go
+ settings.SetDBValidation("immich-postgres.sql", DBValidationCache{
+ ValidatedAt: time.Now().Format(time.RFC3339),
+ TableCount: 60,
+ HasHeader: true,
+ })
+ // SetDBValidation acquires write lock, updates map, calls Save()
+```
+
+**On startup / RefreshCache** (`internal/backup/backup.go`):
+- Load cached validations from `settings.json`
+- For each dump file that exists on disk AND has a cached validation:
+ - Use the cached validation data
+ - Set `DumpValidation.Valid = true/false` based on cached result
+ - Set `DumpValidation.Message` to include cached info: e.g., `"60 tábla (utolsó: 08:04)"`
+- The next actual validation run overwrites the cache with fresh data
+
+**Important:** The cache is keyed by dump filename (e.g., `immich-postgres.sql`). If a dump file no longer exists, its cached validation is ignored (stale data cleanup).
+
+### 3.3 Template update
+
+No template changes needed — the existing 4-branch guard from v0.6.2 already handles showing validation status correctly. The only difference is now the data will be populated from cache on startup instead of being zero-valued.
+
+---
+
+## 4. "Beállítások" (Settings) Page
+
+### 4.1 Sidebar menu item
+
+Add "Beállítások" as the last menu item, visually separated from the main navigation — placed at the bottom of the sidebar, just above the version/logout section. Use a gear/cog icon (⚙ or SVG).
+
+Sidebar order:
+```
+Vezérlőpult
+Alkalmazások
+Biztonsági mentés
+Rendszermonitor
+─── (spacer / flex-grow) ───
+⚙ Beállítások ← new, pinned to bottom
+v0.7.0 | Kijelentkezés
+```
+
+### 4.2 Route
+
+| Method | Path | Auth? | Handler |
+|--------|-------------|-------|---------------------|
+| GET | `/settings` | Yes | Show settings page |
+| POST | `/settings/password` | Yes | Change password |
+
+### 4.3 Settings page layout
+
+The page has two sections:
+
+#### Section A: "Rendszer konfiguráció" (System Configuration) — Read-only
+
+Display key values from `controller.yaml` in a clean info-grid. These are read-only — the customer can see what's configured but can't change it here.
+
+| Label (Hungarian) | Source in controller.yaml | Display format |
+|--------------------------|----------------------------------|----------------|
+| Ügyfél azonosító | `customer.id` | `demo-felhom` |
+| Ügyfél neve | `customer.name` | `Demo Ügyfél` |
+| Domain | `customer.domain` | `demo-felhom.eu` |
+| Alkalmazás sablon forrás | `git.repo_url` | URL (truncated) |
+| Sablon szinkronizálás | `git.sync_interval` | `15m` |
+| Biztonsági mentés | `backup.enabled` | ✅ Aktív / ❌ Inaktív |
+| Mentés ütemezés | `backup.db_dump_schedule` + `backup.restic_schedule` | `02:30 / 03:00` |
+| Monitoring | `monitoring.enabled` | ✅ Aktív / ❌ Inaktív |
+| Healthchecks URL | `monitoring.healthchecks_base` | URL or "–" |
+| Hub jelentés | `hub.enabled` (if exists) | ✅ Aktív / ❌ Inaktív / "–" |
+| Controller verzió | built-in Version constant | `0.7.0` |
+
+Use green checkmark / red X styling for boolean states, consistent with the backup page.
+
+#### Section B: "Jelszó módosítás" (Change Password) — Editable
+
+Only shown if auth is enabled (password_hash is set). If no auth, show an info message:
+"A jelszavas védelem nincs beállítva. Kérd az üzemeltetőt a beállításhoz."
+
+Form fields:
+- Jelenlegi jelszó (current password) — required
+- Új jelszó (new password) — required, min 8 chars
+- Új jelszó megerősítése (confirm new password) — required, must match
+
+**POST `/settings/password` handler:**
+1. Validate current password against effective hash (bcrypt compare)
+2. Validate new password: min 8 chars, both fields match
+3. Generate bcrypt hash (cost 10) for new password
+4. Save to `settings.json` → `password_hash`
+5. Invalidate current session (regenerate session secret so all cookies become invalid)
+6. Redirect to `/login` with success flash message: "Jelszó sikeresen módosítva. Kérjük, jelentkezzen be az új jelszóval."
+
+**Error handling:**
+- Wrong current password → "Hibás jelenlegi jelszó" (stay on page)
+- Passwords don't match → "A két jelszó nem egyezik"
+- Too short → "A jelszónak legalább 8 karakter hosszúnak kell lennie"
+- Show errors inline, don't clear form
+
+### 4.4 Template
+```
+File: controller/internal/web/templates/settings.html
+```
+
+Follow the same card-based layout as the backup page. Two cards:
+1. "Rendszer konfiguráció" — info-grid with labels + values
+2. "Jelszó módosítás" — form card
+
+---
+
+## 5. Implementation Order
+
+Follow this sequence to keep each step testable:
+
+### Step 1: settings.json package
+- Create `internal/settings/settings.go` with Settings struct, Load/Save
+- Add settings instance to the controller's main app struct
+- Load on startup, log result
+- **Test:** Start controller, check logs for settings load message
+
+### Step 2: Authentication
+- Add session secret generation on startup
+- Add auth middleware
+- Add login.html template
+- Add login/logout handlers
+- Add logout link to sidebar
+- Wire up routes in server.go
+- Set `web.password_hash` in controller.yaml on demo to test
+- **Test:** Navigate to dashboard → redirected to /login → enter password → dashboard loads → /logout → back to /login
+
+### Step 3: DB validation persistence
+- After ValidateDump completes, save results to settings.json
+- On RefreshCache, load cached validations for initial display
+- **Test:** Deploy apps with DBs → trigger backup → check Érvényesítés column shows data → restart container → check column still shows data
+
+### Step 4: Settings page
+- Add settings.html template
+- Add settings route + handler
+- Add sidebar menu item with bottom-pinning
+- Implement password change POST handler
+- **Test:** Open /settings → see config values → change password → re-login with new password
+
+### Step 5: Cleanup & version bump
+- Update CONTEXT.md
+- Bump version to 0.7.0
+- Build + deploy + verify on demo-felhom.eu
+
+---
+
+## 6. Files to Create / Modify
+
+### New files:
+- `controller/internal/settings/settings.go` — Settings persistence
+- `controller/internal/web/templates/login.html` — Login page
+- `controller/internal/web/templates/settings.html` — Settings page
+
+### Modified files:
+- `controller/internal/web/server.go` — Add auth middleware, new routes (/login, /logout, /settings, /settings/password), session management, settings page handler
+- `controller/internal/web/templates/` sidebar partial or base template — Add "Beállítások" menu item at bottom, logout link
+- `controller/internal/backup/backup.go` — Load cached validations in RefreshCache
+- `controller/internal/backup/dbdump.go` — Save validation results to settings.json
+- `controller/internal/config/config.go` — Possibly add data_dir path helper
+- `controller/cmd/controller/main.go` — Initialize settings, pass to web server and backup manager
+
+---
+
+## 7. Design Decisions & Notes
+
+### Why settings.json (not SQLite)?
+- Single file, human-debuggable, easy to backup/restore
+- Tiny data volume (password hash + a few validation entries)
+- No query needs — just load/save whole struct
+- Customers or operators can inspect/reset it easily
+
+### Why not modify controller.yaml for password changes?
+- controller.yaml is operator-provisioned config, risky to programmatically rewrite YAML
+- settings.json is a clean override layer: operator sets initial password in yaml, customer changes it in json
+- If settings.json is deleted, system falls back to controller.yaml password (recovery path)
+
+### Session secret is ephemeral (memory only)
+- Container restart = all sessions invalidated = users must re-login
+- This is acceptable and actually desirable for security
+- No need to persist session state
+
+### Notifications (Phase 2 prep)
+- The Settings struct includes a `Notifications` field placeholder
+- Phase 2 will add: email relay via k3s (similar to contact-mailer pattern), notification preferences UI
+- The relay approach: customer controller sends HTTP POST to a central notification API on k3s, which handles Resend delivery. Avoids storing Resend API keys on customer hardware.
+- This keeps secrets centralized and customer nodes lightweight.
+
+### Settings page scope — what goes where
+- "Beállítások" = actual settings (things the user can configure or needs to know about their setup)
+- "Rendszermonitor" = live system state (hostname, uptime, CPU, RAM, disk, Docker containers)
+- No overlap — config is static, monitoring is dynamic
\ No newline at end of file