## Changelog ### v0.99.0 — restore-path fixes: dead restore UI + volume dumps + blank-secret redeploy (2026-07-05) TASK C1 — fixes F1/F3/O4 from the 2026-07-04 restore drill (`felhom.eu/documentation/audits/DRILL-appdata-restore-2026-07-04.md`). F2 (one-click in-place class-C restore) deliberately NOT included — product-design work (C2). - **F1 (HIGH — the restore panel was dead):** `GET /api/backup/snapshots?stack=` now exists (`internal/api/router.go` + `backup.Manager.ListRestorePoints`, `internal/backup/restore_points.go`). The backups.html restore panel fetched this restic-era route, got the catch-all 404, so the snapshot dropdown never populated and "Visszaállítás indítása" could never enable. Returns the ONE honest keep-side restore point (the current recovery unit): `time` = newest artifact mtime (manifest / db-dumps / volume-dumps), `short_id:"helyi"`, `tier:1` always (Tier-2 copies are NOT restorable via POST /backup/restore — never listed), `drive_label` from the storage registry. Guards: traversal/empty → 400 (`validStackParam`), unknown stack → 404, no unit yet → `ok:true, data:[]`. No template change needed — the JS payload contract was honoured server-side. - **F3 — named-volume data was never backed up:** `DumpAppVolumesSafe` had no production caller. New `runVolumeDumps` loop in `runDBDumpsInternal` (`internal/backup/backup.go`), running BEFORE `captureAllRecoveryUnits` so manifests enumerate the fresh tars. Gate order is load-bearing: protected-stack + has-volumes checks precede the Safe call (which stops the stack before its own check — unconditional calls would bounce every volume-less app nightly); disconnected/decommissioned drives skip like the DB loop. Failures land in the run summary and fail the run (no silent partial). Zero-DB early return removed (volume-only apps still get dumps + unit refresh). Test seam: `dumpVolumesSafe` func field (F17-style). - **O4 — missing resettable secret redeployed blank:** the restore proceed-path now generates a replacement credential via the catalog field's `generate` spec (`stacks.Manager.GenerateSecretForField` → `backup.SetSecretGenerator` seam, wired in main.go), persisted encrypted through the existing `RecreateStackFromUnit` → `SaveAppConfig` path. Data-keys are NEVER generated (gate untouched + generator refuses `data_key` fields); values never logged. No-generator fields keep proceeding with an upgraded "may fail to start" WARN. Residual case documented: a restored volume tar carrying the OLD internal credential hash may still need a manual in-DB reset. Tests: 272 → 286 top-level test funcs (+14: api snapshots ×3, backup restore-points ×4, volume-dump gating ×3, backup secret-gen ×3, stacks secret-gen ×1); all three fixes companion-red-proofed (hollow `[]` endpoint / removed volume gate / no-generation each fail their test). Full `go build && go vet && go test ./...` green. ### docs — CLAUDE.md refresh: slim-down to stable orientation (2026-07-03) No code change, no version bump. CLAUDE.md 338 → ~160 lines: full 30-package layout map (was 7); stale bare-metal `/opt/docker` deploy steps replaced with the verified 9201 bootstrap deploy (`/etc/felhom-controller-image` + `felhom-controller-bootstrap.service`); embedded hub build section deleted (points to felhom.eu); deep runbooks/design/testing content moved to the new skills (`felhom-build-deploy`, `felhom-ui-design`, `felhom-testing` — source `felhom.eu/skills/`); "Key patterns"/"lessons" pruned to session-critical invariants (rest live in REUSE.md). Standing rule adopted: CLAUDE.md carries no version-pinned current state — that lives in CONTEXT/CHANGELOG/REUSE. ### docs — REUSE.md introduced (2026-07-03) Cross-repo reuse-map rollout (docs-only, no code change, no version bump). New `REUSE.md` at the repo root: curated map of canonical helpers (62 rows), patterns, dangerous lookalikes (rsyncMirror `--delete`, raw os.RemoveAll on drive paths, fresh agentapi.New per request…), test seams, extension points, and observed duplication (12 clusters, NOT fixed). Every entry code-verified at file+symbol; cited paths machine-checked by `felhom.eu/scripts/reuse_refs_check.py` (green). CLAUDE.md gains the "See REUSE.md before writing new code" pointer + the same-commit maintenance rule. ### v0.98.3 — hide "Eltávolítás a listából" on wizard-enrolled drives (2026-07-02) User feedback follow-up: list-removal (registry-entry delete; data + mount untouched) is only meaningful as the undo of a MANUAL path add. On a wizard-enrolled drive (/mnt/felhom-drives/) the resulting de-registered-but-still-agent-bound limbo is never what the customer wants — its real lifecycle is Biztonságos leválasztás / Végleges leszerelés. New `StoragePathView.IsEnrolled` (path-prefix check) gates the button; manually added paths keep it, and the decommissioned-branch "Eltávolítás a rendszerből" (final cleanup) is unchanged. Endpoint untouched. Test: enrolled card must not render the remove form, manual card must (TestListRemovalHiddenForEnrolledDrives). ### v0.98.2 — drive-card action clarity: dedupe + self-documenting labels (2026-07-02) User feedback: two "Leválasztás" buttons per drive, and four near-synonymous labels (Letiltás / Leválasztás / Eltávolítás / Leszerelés) for very different operations. storage.html only: - **Dedupe:** the agent-level eject no longer renders on a card that already offers the registry safe-disconnect (enrichCard passes `hasSafeDisconnect`, detected from the card's own storageDisconnect button) — one detach affordance per card. Non-USB registered drives (no safe-disconnect) and unregistered drives keep the agent eject. - **Labels + tooltips (endpoints unchanged):** Letiltás/Engedélyezés → "Új telepítések letiltása/engedélyezése"; registry Leválasztás → "Biztonságos leválasztás" (apps stop, safe unplug, reconnectable); Eltávolítás → "Eltávolítás a listából" (registry-entry removal only, data untouched); Leszerelés → "Végleges leszerelés" (permanent, optional migrate-first); agent Törlés… → "Formázás…" (that's what it does). Every action button carries an explanatory `title`. ### v0.98.1 — drive-card spacing: enrichment rows no longer touch (2026-07-02) User feedback: on the Meghajtók cards the agent tag row ("Felhasználói adat", "lassú", uuid) and the agent action row ("Leválasztás", "Törlés…") rendered with zero vertical gap. The `.drive-agent-extra` slot is now a flex column with a .6rem gap (+ .6rem top margin, hidden when empty); the inline margin in `enrichCard` dropped in favor of the slot styles; metarow horizontal gap tightened to .75rem. CSS + one JS-string line only (storage.html, style.css). ### v0.98.0 — storage IA follow-up: Meghajtók / Hálózati tárhely subpages (2026-07-02) User feedback on the D1 Tárhely page: the NAS-add button rendered directly next to the local-drive enrollment buttons ("Új meghajtó inicializálása" / "Meglévő meghajtó csatolása") — two different storage classes confusingly interleaved. The page splits into two subpages under Tárhely: - **`/storage` — Tárhely — Meghajtók** (storage.html): physical drive registry + unified agent view + migrate + wizard entry points + manual add. The enrollment buttons now live unambiguously in the local-drive context. - **`/storage/network` — Tárhely — Hálózati tárhely (NAS)** (storage_network.html, new): the NAS share list ("NAS-megosztások") + add form + its JS moved verbatim (incl. its own `openDialog` copy for the remove overlay). - **layout.html:** the Tárhely main-nav item gains two always-visible nested sub-links (Meghajtók / Hálózati tárhely; `.nav-links-nested` CSS); the parent stays highlighted on both subpages. - **handlers.go / server.go:** `NetworkStoragePaths` moved from `storagePageData` into the new `networkStoragePageData` (page key `storage-network`) + `storageNetworkPageHandler`; `GET /storage/network` route. No `/api/storage/*` change. - Tests updated: `/storage` must NOT render the NAS section, `/storage/network` renders it and nothing drive-related; the section inventory + no-native-confirm scans cover the new template. Both template gates green; `go build/vet/test ./...` green (18 pkgs). - Live-validated on 9201: both subpages render with correct sidebar active states; the NAS add-form toggle + `nsToggleSmb` + `openDialog` exercised on the new page. ### v0.97.0 — TASK-D1: settings split + Tárhely page (unified drive view) (2026-07-02) The 1451-line settings monolith becomes four pages; storage is promoted to a first-class main-nav page with a unified (registry + agent) drive view; native browser dialogs migrate to the overlay pattern. **IA/appearance only** — no `/api/storage/*` payload, storage semantics, or agent-client change. Four commits (d50a919, f8e18a9, cb6f04c, 622d932). - **Routing (server.go):** new `GET /storage` (Tárhely), `GET /settings/notifications` (GET→page / POST→save split on the same path), `GET /settings/security`; the enrollment wizards move to `/storage/init` + `/storage/attach`, with **301** permanent redirects from the old `/settings/storage/{init,attach}`. - **Data builders (handlers.go):** `settingsData()` decomposed into `settingsBaseData` + `systemPageData` / `storagePageData` / `notificationsPageData` / `securityPageData`; each GET handler and each error-re-rendering POST handler uses exactly its page's builder + template. All five storage action redirects now land on `/storage?storage_msg=…`. - **Template split:** `settings.html` deleted; sections moved verbatim into `settings_system.html` (Rendszer konfiguráció, Verzió és frissítés, Vezérlő/Kiszolgáló újraindítása), `settings_notifications.html` (Értesítések, Alkalmazás-email), `settings_security.html` (Jelszó módosítás, Földrajzi korlátozás, Vészhelyzeti információk — misspelled heading + section copy accents fixed), and `storage.html`. The NAS + migrate sections (previously nested inside `{{if .StoragePaths}}` and invisible with zero drives) are now unconditional on `/storage`. - **Sidebar (layout.html):** Tárhely main-nav item (hard-drive icon) + a "Beállítások" group with Rendszer / Értesítések / Biztonság és hozzáférés sub-links (active state per page); orphaned `.sidebar-settings-link` CSS deleted, `.nav-group-label` / `.nav-links-sub` added. - **Unified drive view (storage.html):** registry cards render server-side as before; the agent `/api/disks` list ENRICHES each connected user-data card in place (role tag via `i-lock`, drive class, durable-id mono line, agent-only register/eject/wipe actions) joined on mount path — one card per drive. Two extra groups: **Rendszermeghajtók** (system/backup, read-only, lock tag, no actions) and **Nem regisztrált meghajtók** (register action only). Agent-down → one warn note (`Az ügynök nem elérhető…`), all registry cards still render (graceful degradation). Agent-view helpers emit design-system `.tag` markup (no `.badge`); the 🔒 emoji is gone. - **Overlay migration:** every native `confirm()`/`prompt()` on the four pages routes through a light `.confirm-overlay` dialog (`openDialog`; texts verbatim) — storage remove forms, netStorageRemove, storageMigrateAll, storageDisconnect, storageDecommission (migrate + the type-to-confirm anyway branch preserved like-for-like), storageReEnroll, triggerUpdate, controller/server restart, and the two geo Hungary-removal confirms. One froze a browser tab during D0 validation; none remain. - **D0 leftovers:** the D0 grep gate false-negatived multibyte emoji on Windows — a Python codepoint gate (`scripts/emoji_gate.py`) found and removed **8** survivors (📁🔄🔒📦 across backups/debug/deploy/storage; the ★ default-marker → „(alapértelmezett)"). Orphaned `.badge-lock`/`.lock-ico` CSS deleted (grep-zero first). - **Gates & tests (+8):** `scripts/template_id_gate.py` (JS element-ID integrity — every `getElementById`/`querySelector('#…')` resolves in its own template; red-proven by a misplaced function), `scripts/emoji_gate.py` (0), Go tests for the four-page render + cross-leak, the h3 section inventory (all 11 old headings accounted for), 301s, storage redirect + flash, password inline re-render, no-native-confirm scan, agent-down warn-note, and codepoint emoji scan. Redirect + inventory tests red-proven against pre-split code. `go build/vet/test ./...` green. - Live-validated on 9201 via claude-in-chrome: all four pages + the 301 redirect, the unified view (3 enriched cards with role tags + durable-ids, Rendszermeghajtók group read-only with 0 action buttons), the Leválasztás overlay opened + cancelled (drive untouched), and a full label-rename round-trip through the real UI (flash on /storage, renamed back). NOT live-validated: agent-down degradation (static/unit only — the agent must not be stopped on the live host); destructive storage ops (endpoints unchanged; the moved UI paths await a supervised session). ### v0.96.0 — TASK-D0: design system v2 re-skin (appearance only) (2026-07-02) Full customer-UI re-skin to the approved Felhom design system v2 — navy token palette, exception-based status color, vendored fonts/icons, flat metadata. **Appearance only:** no route/handler/IA changes; every page keeps its URL, sections, forms and behavior. Canonical reference: `felhom.eu/documentation/design/design-system.md`. Four commits (b073cc4, 5dc277f, f100cef, 7df061c) + a bug-fix (4906524). - **Vendored assets (`internal/web/static/fonts/`, `templates/icons.html`, `embed.go`, `server.go`):** Plus Jakarta Sans + JetBrains Mono as variable woff2 (latin + latin-ext — ő/ű), embedded and served from `/static/fonts/` (font/woff2, immutable cache); Google Fonts `@import` removed (CDN silently broke offline nodes). Vendored 30-icon Lucide sprite included at top of `
`; all emoji replaced by sprite icons or plain text (templates AND JS-built strings). - **Setup CSS fix (`internal/setup/handlers.go`):** `handleCSS` served a dataDir-derived filesystem path that never exists in the container — production setup mode silently fell back to `minimalCSS`. Now serves the embedded `web.StyleCSS()` (new accessor); minimalCSS only if the embedded read errors (logged). `minimalCSS` retokened to v2. - **`templates/style.css` (rewritten in place):** v2 `:root` tokens; single 2px radius; every `box-shadow` + the bg grid overlay deleted; new components — `.meter` (3px hairline track, blue nominal fill, neutral 70/85 ticks, warn/crit `.meter-flag` „Fogyóban a hely" / „Kritikusan kevés hely"), `.tag` (square state chip + dot, pulse on progress, reduced-motion respected), `.metarow`, `.panel`/`.list`/`.section-h`, boxless `.stats`, buttons (danger = crit outline until confirm), `:focus-visible` outlines. - **funcmap (`internal/web/funcmap.go`):** `stateColor` → `run/progress/warn/neutral/off` (**stopped/exited is neutral, NOT red** — operator-approved exception-color change; restarting = warn); `usageColor`/`tempColor` → `nominal/warn/crit` (thresholds unchanged); `stateLabel` Hungarian copy untouched (byte-identity guarded by test). New `timeAgoStr` (see fix below). - **All 19 web templates + setup templates:** bars → meters (template + JS-generated markup), badges/pills → tags, informational pills → metarows with icons, legacy `var(--*)` names in inline styles/JS renamed to v2 tokens, monitoring Chart.js palette (cpu `#2EA8F5`, memory `#8E7CE8`, temp `#E0A93E`, load `#5EC4B6`; v2 tooltip/grid/tick literals), deploy 3-step progress → sprite icons, catchall page (standalone) fully retokened with inline SVGs, login two-tone H1. - **fix(backups) 4906524:** `OffboxTarget.LastRun` is an RFC3339 *string*; backups.html passed it to `timeAgo` (expects `time.Time`) → GET /backups 500'd on any node where an off-box backup had ever run. Pre-existing since v0.93.0, exposed by the D0 click-through; fixed with `timeAgoStr`. - **Tests (+7):** §8 truth tables for stateColor/usageColor/tempColor + stateLabel guard (red-proven vs the old funcmap), font route + `StyleCSS()` accessor, setup embedded-CSS (Scenario E, red-proven vs the old handler). Grep gate: 34 banned patterns (old hexes, 999px, box-shadow, CDN import, legacy class names, emoji) at **zero** in `internal/{web,setup}` (baseline: 143 hits). - Live-validated on guest 9201 via claude-in-chrome: full click-through, no Google Fonts requests, `document.fonts.check` true, ő/ű render in PJS latin-ext, dashboard Scenario-A assertions (0 green fills, 0 shadows, 0 radii >2px) DOM-verified. NOT live-validated: setup wizard rendering (unit-tested only), warn/crit meter states on real hardware (demo node healthy; unit-tested). ### v0.95.0 — enrollment wizards use the raw-device scan `/disks/candidates` (Impl-2b) (2026-07-01) Final drive-enrollment piece: both enrollment wizards now source candidates from the agent's Impl-2a raw-device scan instead of the `Observe()`-based `/api/disks` list — so a brand-new (non-PVE-storage) drive is finally visible + enrollable end-to-end. The enroll flow (`runStorageInit`/`runStorageAttach`) and the Impl-1 guarded `mkfs` are UNCHANGED; the wizards just get the right candidate list. - **`internal/agentapi/client.go`:** `ListCandidates(ctx) (CandidatesResult, error)` → agent `GET /disks/candidates`; types `CandidatesResult{Initialize,Attach []DiskCandidate}` + `DiskCandidate{Device,SizeBytes,Model,FSType,DataBearing,Mountable,MountSource,DurableID}` mirroring the agent's `candidates.go`. - **`internal/web/agent_disk_handlers.go`:** `GET /api/disks/candidates` proxy (`agentDiskCandidatesHandler`, copy of `agentDisksListHandler`) — passthrough, NO controller-side filtering (the agent's unclaimed-disk filter is authoritative + fail-safe). - **`templates/storage_init.html`:** fetch `/api/disks/candidates` → render the `initialize` list (model/size/current-FS + a data-bearing marker); dropped the client-side "already-managed" filter (the server list already excludes OS/enrolled/claimed disks). Data-bearing → the existing wipe-confirm. - **`templates/storage_attach.html`:** fetch `/api/disks/candidates` → render the `attach` list (mountable-FS disks); selecting posts the FS-bearing node + its fstype to the existing `/api/storage/attach` (mount + bind, NO format). - **TOCTOU:** the wizard trusts the agent's Impl-1 `Format` guard as the backstop (re-checks unclaimed at format time), not the list's freshness — a device claimed between scan and enroll is refused. - Tests: `agentapi` `TestListCandidates` + `_Error`. `go build/vet/test ./...` clean. Live end-to-end raw enrollment of `/dev/sdd` validated through the real UI (see REPORT). ### v0.94.0 — pull-based config-refresh (re-pull controller.yaml + self-restart on a config change) (2026-06-30) Config delivery is now pull-based, riding the report ACK exactly like the Phase 2 version floor — the hub never connects into the box. This replaces the hub's retired "Push Config" (companion hub change v0.26.0) and is the mechanism by which an operator config edit reaches a running box. - **`internal/report/pusher.go`:** `PushResponse` gains `ConfigVersion int` (`json:"config_version"`). 0 = the hub didn't advertise it (old hub / report-only customer) → no action. - **`internal/report/config_refresh.go` (NEW) — `ConfigRefresher.Reconcile`.** The testable reconcile (all side effects injected): on a config_version change vs. the last-applied version, **Refresh** (re-pull `controller.yaml`) → **Record** → **Restart**. First-ever ACK (nothing recorded) records the baseline WITHOUT restarting (the first-boot pull already has the current config); an unchanged version is a no-op (no restart storm); a failed pull keeps the current config and does NOT record/restart (retried next cycle); record-before-restart so the restarted process sees it applied and doesn't loop. - **`internal/bootstrap/bootstrap.go` — `RefreshConfig`.** Re-pulls `controller.yaml` from the hub and rewrites it, re-merging `local_api` from the same read-only `bootstrap.json` mount (no secret stashed elsewhere). Reuses the existing `pullWithRetry`/`mergeLocalAPI`/`writeFileAtomic`. Overwrites `controller.yaml` (hub = source of truth); NEVER touches `settings.json`; fail-safe (any failure leaves the current config unchanged + returns an error). NOT first-boot-gated (unlike `MaybeIngest`). - **`internal/settings/settings.go`:** `applied_config_version` + `GetAppliedConfigVersion` / `SetAppliedConfigVersion` (persisted so the version survives the restart). - **`internal/api/selfrestart.go`:** exported `GracefulSelfRestart` (the unexported one now calls it) so the main.go reconcile reuses the one graceful-restart mechanism instead of reinventing an `os.Exit`. - **`cmd/controller/main.go`:** wires the reconcile into `OnPushResponse` beside the floor reconcile — same report cycle, no new timer, no agent involvement. The first-boot `MaybeIngest` never-clobber is untouched (the refresh is a separate explicit re-pull). - Tests: `Reconcile` (change→refresh+record+restart; **same-version no-op RED-PROOF**; baseline-no-restart; failed-pull no-record/no-restart; zero-version no-op; record-fail skips restart) + `RefreshConfig` (re-pull overwrites + re-merges local_api; failed pull leaves config unchanged; absent bootstrap errors without writing). `go build/vet/test ./...` green. ### v0.93.0 — NAS Part B: off-box backup target (restic-over-SFTP) (2026-06-30) Closes the NAS arc: back the app-data tier (each off-box app's recovery unit + DB dumps + volume tars) up to the customer's NAS as an **encrypted restic repo over SFTP** — the "1 off-site" leg of 3-2-1, distinct from the local cross-drive rsync copy and the agent's PBS whole-CT DR. No kernel mount; restic talks SFTP to the NAS directly. Spike-validated (SPIKE-nas-storage Q8). - **`Dockerfile`:** restic was dropped when cross-drive migrated restic→rsync — re-added `restic` + `openssh-client` (restic's sftp backend shells out to `ssh`); version pinned by the Debian release. - **`internal/backup/offbox.go` (NEW):** the restic-SFTP backend + orchestration. - **Fail-fast (the load-bearing spike Q8 lesson):** every restic call carries `-o sftp.args=…-oConnectTimeout=10…` so a dead NAS errors in ~10 s instead of a multi-minute TCP hang. Also `-oStrictHostKeyChecking=yes -oUserKnownHostsFile=