diff --git a/CHANGELOG.md b/CHANGELOG.md index 1da3eea..4903c7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ ## Changelog +### v0.161.0 — R-70: the hub-managed offsite empty state tells the truth (2026-07-23) + +No agent coupling; MinAgent unchanged. Origin: `felhom.eu/documentation/audits/DIAG-f10-demo-hp-offsite-2026-07-23.md` +— a box stuck pre-apply (burned credential) rendered the SAME „Még nincs beállítva távoli mentési +cél." empty state as a box that was never provisioned, and the „igényelhető szolgáltatás" card +offered to order a service that was already ordered. The ambiguity hid a dead offsite tier for +2 days on demo-hp. + +**The change (XS):** `backupsOffboxData` exposes `OffsiteHubEnabled` (= `cfg.Offsite.Enabled`, +the hub descriptor in controller.yaml). On Távoli mentés, when hub-managed offsite is enabled but +no `offbox` target exists yet, BOTH empty surfaces switch to the truth: „Felhom offsite tárhely +kiépítve — a beállítás automatikus, folyamatban. Ha egy napon belül nem áll be, jelezd az +üzemeltetőnek." (the status card AND the target empty-state line). Without a hub-managed offsite, +today's copy is byte-identical; a configured target renders the status block as before. The +own-NAS setup button is untouched. + +Render tests per branch of the gate (the v0.70.1 template-gate lesson): hub-enabled+no-offbox → +banner + old copy asserted GONE; not-enabled → old copy asserted intact; configured → no banner. +All design-v2 template gates green (`docker_run_volume_path_gate` stays red on the pre-existing +R-29 allowlist item, untouched by this change). Hub-side sibling: felhom-hub v0.72.0 (delivery-state +detector + stuck event + R-71c self-heal). + ### v0.160.0 — R-67: the NAS share appears in FileBrowser (2026-07-22) No agent coupling; MinAgent unchanged. Origin: the R-64 pairing drill — the share said „Elérhető" diff --git a/CONTEXT.md b/CONTEXT.md index d3e04dd..d7be201 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -7,7 +7,17 @@ > > Ask Claude Code: "Please update CONTEXT.md with what we did today" -Last updated: 2026-07-22 (v0.160.0 — R-67: the NAS share appears in FileBrowser) +Last updated: 2026-07-23 (v0.161.0 — R-70: truthful hub-managed offsite empty state) + +> **2026-07-23 — v0.161.0 (R-70 controller leg), SHIPPED + deployed BOTH boxes.** When +> `offsite.enabled` is in controller.yaml but no `offbox` target exists (pre-apply window / burned +> credential — the F10 shape), Távoli mentés now shows „Felhom offsite tárhely kiépítve — a +> beállítás automatikus, folyamatban…" on BOTH empty surfaces (status card + target line) instead +> of „igényelhető" / „Még nincs beállítva". Data key `OffsiteHubEnabled` (from `Server.cfg`, no new +> wiring); render tests per gate branch; banner leg is unit-proven/live-pending (no healthy box +> occupies the window; next fresh onboarding is the natural live leg). Hub sibling v0.72.0 carries +> the detector + `offsite_delivery_stuck` + the R-71c self-heal. Origin + rulings: +> `felhom.eu/documentation/audits/DIAG-f10-demo-hp-offsite-2026-07-23.md`. > **2026-07-22 — v0.160.0 (R-67), SHIPPED + deployed BOTH boxes, full live leg on demo-hp.** > Network shares now bind their share ROOT into FileBrowser (`…/:/srv/:rslave`) — no diff --git a/REPORT.md b/REPORT.md index ca5b157..edc5a21 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,97 +1,44 @@ -# REPORT — R-67: the NAS share appears in FileBrowser (controller v0.160.0, 2026-07-22) +# REPORT — R-70 controller leg: the hub-managed offsite empty state tells the truth (v0.161.0, 2026-07-23) -**Spec:** the R-67 prompt (Phase-0 probe + implementation, controller-only, v0.159.0 → v0.160.0). -**Commit:** `59cd260` on `main` · image `felhom-controller:0.160.0` · deployed to **demo-felhom -9201** and **demo-hp 9201**, both healthy. No agent changes; MinAgent unchanged. +**Spec:** R-70 + R-71(c) prompt §4d (controller banner, XS; the detector/event/self-heal live in +felhom-hub v0.72.0 — see `felhom.eu/REPORT.md`). Origin: +`felhom.eu/documentation/audits/DIAG-f10-demo-hp-offsite-2026-07-23.md`. -## 1. Phase-0 probe — GO +## What shipped -On demo-hp, with the Felhom-Share automount confirmed IDLE (autofs trigger present in the guest's -/proc/mounts, `timeout=60`, no cifs mount): -`docker run --rm -v /mnt/felhom-drives/Felhom-Share:/probe:rslave alpine ls -la /probe` -listed the REAL share content (demo-felhom's `teszt/` + macOS dotfiles) and left cifs mounted in -the guest. **An in-container access through an rslave bind wakes the idle trigger** — one -namespace further than the spike's in-guest proof. The design shipped exactly as specified; the -fallback fork was not needed. +One data key + one template gate. `backupsOffboxData` now exposes `OffsiteHubEnabled` +(= `s.cfg.Offsite.Enabled`, the hub descriptor already loaded from controller.yaml — +`internal/web/handlers.go`, no new wiring; `Server.cfg` was always there). On Távoli mentés, when +hub-managed offsite is enabled but no `offbox` target exists (the pre-apply window, or a burned +credential — the F10 shape), both empty surfaces switch to the truth: -## 2. What shipped +> „Felhom offsite tárhely kiépítve — a beállítás automatikus, folyamatban. Ha egy napon belül nem +> áll be, jelezd az üzemeltetőnek." -`syncFileBrowserMounts`' path loop extracted into the pure **`buildFileBrowserPaths`** -(`fbPathDeps`: mount probe / FS classifier / skeleton fn / logger — every edge seamed), returning -BOTH the mount lines and the config source set so a source can never render without its mount. +- the v0.124.0 status card no longer offers „igényelhető szolgáltatás" for a service that is + already ordered (it contradicted the new banner — coherence extension of the spec's letter, + gate `{{if and (not .OffboxConfigured) .OffsiteHubEnabled}}`); +- the `{{if .Offbox}}` else-branch replaces the bare „Még nincs beállítva távoli mentési cél."; +- NOT hub-enabled → today's copy byte-identical; configured target → status block as before; + the own-NAS setup form untouched. -- **Network branch:** bind = the share ROOT, `…/:/srv/:rslave` (`:rslave` - load-bearing — automount wake / idle-unmount propagate into the RUNNING container). No - `EnsureUserdataSkeleton`, no userdata scoping, nothing written toward the NAS. The drive-absent - gate does NOT apply (idle is healthy — the old gate skipped an idle share forever); the gate is - the **stub** classifier verdict via the existing `Server.classifyFSPath` seam: stub ⇒ excluded - from mounts AND sources this pass + WARN (`namespace sees a local stub, not the NAS`). autofs / - network / unknown / nil-classifier include (fail open). **The stub rationale:** an exposed local - stub dir accepts uploads that the real mount later SHADOWS — the customer's files silently - vanish from view; exclusion-until-recovery is the only safe rendering. -- **Drives:** behavior byte-identical (userdata scoping, skeleton, drive-absent gate, always in - the source list) — asserted by test, and observed live (§4). -- **Triggers:** NAS add-success (`runNetAdd` done) + remove (`handleNetStorageRemove`) now call - `SyncFileBrowserMounts()` (same debounced/H5-mutexed path as the drive flows). Removal drops - source + mount next sync; F2 change detection forces the recreate. -- Renderers unchanged: `RenderFileBrowserCompose` already takes fully-formed mount lines - (propagation rides in the line), `RenderFileBrowserConfig` already names sources by `Label`. +## Tests & gates -## 3. Tests + red-proofs +Render tests per branch of the gate (`internal/web/backups_remote_hubenabled_test.go`, the +v0.70.1 template-gate lesson): banner branch (+ old copy asserted GONE + own-NAS button asserted +present), legacy branch (old copy intact, banner absent), configured branch (no banner). Green +gate: `go build && go vet && go test ./...` — 25 packages ok. Design-v2 gates: emoji, mojibake, +template_id, native_confirm, offbox_rename, app_row_dedup all OK; `docker_run_volume_path_gate` +remains red on the pre-existing R-29 allowlist item (`appexport/estimate.go:179`, named volume, +benign, filed 2026-07-18) — untouched by this change, not fixed inline per R-29's own row. -`filebrowser_network_test.go`: **A** (drive line byte-identical with/without the share — the B′ -invariant; share ROOT `:rslave` line exact; both sources in config; skeleton called for the drive -ONLY), **B** (stub ⇒ absent from mounts + sources, drive intact, warn logged), **C** (autofs / -network / unknown / nil classifier all include — idle is not a detached drive), **D** (removal -leaves no trace; `fbNeedsRecreate` sees the change). +No red-proof leg: no defect was fixed here (new truthful copy behind a new gate); the per-branch +render tests are the non-hollow coverage. -**Red-proof A** (network paths routed through the drive branch): FAIL — -`skeleton calls = [/mnt/felhom-drives/hdd_1 /mnt/felhom-drives/Felhom-Share]` — the NAS path -visibly receiving a skeleton — plus the share line degrading to a `userdata` subtree without -`:rslave`. Restored, green. -**Red-proof B** (stub gate dropped): FAIL — the stub share leaked into mounts AND the source -list, and the warn disappeared. Restored, green. +## Live state -Green gate (`go build ./... && go vet ./... && go test ./...`): clean. No template changes → no -template gates in play. - -## 4. Live leg (real surfaces; methods named) - -**demo-hp (the mounting side):** -- Startup sync recreated FileBrowser: compose carries - `/mnt/felhom-drives/Felhom-Share:/srv/Felhom-Share:rslave`, config carries - `path: "/srv/Felhom-Share"` named „Hálózati tárhely: Felhom-Share"; log: - `FileBrowser mounts synced (recreated) — 2 storage path(s)`; container healthy. -- **Content:** `docker exec filebrowser ls /srv/Felhom-Share` → demo-felhom's real content - (`teszt/` + dotfiles), uid/gid 1000, group-writable. -- **Upload round-trip:** wrote `r67-test.txt` as **uid 1000 inside the filebrowser container** - (the exact mount namespace + process identity FileBrowser uses) → the file appeared on - demo-felhom at `/mnt/felhom-drives/hdd_1/shares/Share/r67-test.txt` (19 B, uid 1000) → deleted - from demo-hp → gone on demo-felhom. **Residual:** FileBrowser's HTTP/auth layer was NOT driven — - its admin credential is customer/day-0-held (admin/admin and the operator demo password both - 401); the browser click-through is the operator's residual, as is a look at the sidebar. -- **Dead-NAS:** samba stack stopped on demo-felhom → demo-hp's in-container access returned a - clean `ls: /srv/Felhom-Share: Host is down` in ~11 s wall (incl. ssh) — an error, not a hang - (the soft/retry=0 recipe's promise held). `docker compose start` on samba → demo-hp listed the - share again immediately, **zero intervention** (the `:rslave` + automount recovery working as - designed). - -**demo-felhom (drives-only, the regression side):** startup sync logged -`no config/compose change, ensured running without recreate (1 storage path(s))` — the render was -byte-identical, FileBrowser untouched (Up 22 hours), zero `:rslave` lines in its compose. The B′ -invariant held in production. - -**Screenshots by description:** demo-hp's FileBrowser sidebar now has a second entry „Hálózati -tárhely: Felhom-Share" beside the NVMe drive source; entering it shows the `teszt` folder from -demo-felhom. During the dead-NAS minute the source errors instead of spinning. - -**Access notes:** demo-hp via the hub-vaulted G1 break-glass again (file→file, shredded after, -incl. the hub-DB copy). Probe + all execs left nothing behind (`r67-test.txt` deleted; alpine -image remains in the guest's docker cache — inert). - -## 5. Follow-ups - -- Operator: FileBrowser click-through on demo-hp (sidebar + upload via the UI) — CC lacks the - FileBrowser admin credential by design. -- ROADMAP: **R-67 → SHIPPED**, coupled to R-64 (browsing was its missing UX half); - `network-storage-nas.md` gained the „Browsing (FileBrowser)" paragraph. +Deployed to both boxes (demo-felhom 9201 + demo-hp 9201). Both live boxes have offbox CONFIGURED, +so the new banner is **unit-proven, live-pending** — it renders only in the enabled+no-offbox +window, which no healthy box occupies (and we do not break one to see it — F9 rule). The natural +live leg is the next fresh onboarding's pre-apply window. Live verification on both boxes: +configured view unchanged (no banner), version 0.161.0 healthy. diff --git a/controller/README.md b/controller/README.md index 541a421..ceb938f 100644 --- a/controller/README.md +++ b/controller/README.md @@ -344,6 +344,13 @@ Each app can define rich metadata in `.felhom.yml`: the absolute unit path; `PlaceOffsiteRestore` merges a full scratch into live via `rsync --ignore-existing` (never `--delete`), refusing on the pure `mapOffsiteRestorePaths` guards. It restores FILES ONLY — no database, no stack restart — and the UI now says so. + - **Truthful hub-managed empty state (v0.161.0, R-70):** when `controller.yaml`'s `offsite.enabled` + is true but no `offbox` target exists yet (the pre-apply window — or a burned one-time credential, + DIAG-f10), the Távoli mentés status card AND the target empty-state line say „Felhom offsite + tárhely kiépítve — a beállítás automatikus, folyamatban…" instead of „igényelhető szolgáltatás" / + „Még nincs beállítva…". Data key `OffsiteHubEnabled` from `backupsOffboxData`; own-NAS setup form + unchanged. The hub side (v0.72.0) watches the same window from its end (delivery-state detector, + stuck event, R-71c credential self-heal). - **Coherent snapshot pairs (v0.148.0, R-44):** every offsite run — manual AND nightly — refreshes the DB/volume dumps and recovery units (`offsitePreDump` → `runDBDumpsInternal`) BEFORE the restic capture, so each snapshot is an internally coherent `{DB@T, files@T}` bundle and diff --git a/controller/internal/web/backups_remote_hubenabled_test.go b/controller/internal/web/backups_remote_hubenabled_test.go new file mode 100644 index 0000000..31c6c03 --- /dev/null +++ b/controller/internal/web/backups_remote_hubenabled_test.go @@ -0,0 +1,65 @@ +package web + +import ( + "strings" + "testing" +) + +// R-70 (v0.161.0) — the hub-enabled empty state tells the truth. Render test per branch of the +// gate (the v0.70.1 hub seam-wiring lesson: template gates need render tests, handler tests prove +// nothing about reachability). Three branches: +// 1. hub-enabled + no offbox → the "kiépítve — automatikus, folyamatban" banner (card + empty +// state), the "igényelhető"/"Még nincs beállítva" copy GONE (a provisioned service is not +// orderable), the own-NAS setup button untouched; +// 2. not hub-enabled + no offbox → today's copy byte-for-byte; +// 3. offbox configured → no banner (the configured status block renders instead). + +const hubBannerText = "Felhom offsite tárhely kiépítve — a beállítás automatikus, folyamatban." + +func remoteEmptyStateData(hubEnabled bool) map[string]interface{} { + d := splitTestData() + d["Offbox"] = nil + d["OffboxConfigured"] = false + d["OffboxToggledCount"] = 0 + d["OffsiteHubEnabled"] = hubEnabled + return d +} + +func TestBackupsRemote_HubEnabledEmptyState_ShowsProvisioningBanner(t *testing.T) { + html := renderBackupPage(t, "backups_remote", remoteEmptyStateData(true)) + if !strings.Contains(html, hubBannerText) { + t.Fatalf("hub-enabled empty state must show the provisioning banner:\n%s", html) + } + if !strings.Contains(html, "jelezd az üzemeltetőnek") { + t.Fatal("the banner must carry the one-day escalation pointer") + } + if strings.Contains(html, "igényelhető szolgáltatás") { + t.Fatal("'igényelhető' must be GONE when the service is already provisioned (it would be a lie)") + } + if strings.Contains(html, "Még nincs beállítva távoli mentési cél") { + t.Fatal("the bare empty state must be replaced by the truthful banner (DIAG-f10)") + } + // the own-NAS path stays untouched + if !strings.Contains(html, "Távoli mentési cél beállítása") { + t.Fatal("the own-NAS setup button must remain") + } +} + +func TestBackupsRemote_NotHubEnabledEmptyState_KeepsTodaysCopy(t *testing.T) { + html := renderBackupPage(t, "backups_remote", remoteEmptyStateData(false)) + if !strings.Contains(html, "igényelhető szolgáltatás") || !strings.Contains(html, "Még nincs beállítva távoli mentési cél") { + t.Fatalf("non-hub-enabled empty state must keep today's copy:\n%s", html) + } + if strings.Contains(html, hubBannerText) { + t.Fatal("the provisioning banner must not render without a hub-managed offsite") + } +} + +func TestBackupsRemote_OffboxConfigured_NoBanner(t *testing.T) { + d := splitTestData() // configured target, escrowed + d["OffsiteHubEnabled"] = true + html := renderBackupPage(t, "backups_remote", d) + if strings.Contains(html, hubBannerText) { + t.Fatalf("configured target must render the status block, never the provisioning banner:\n%s", html) + } +} diff --git a/controller/internal/web/handlers.go b/controller/internal/web/handlers.go index 762d85c..6d54771 100644 --- a/controller/internal/web/handlers.go +++ b/controller/internal/web/handlers.go @@ -671,6 +671,11 @@ func (s *Server) backupsOffboxData(data map[string]interface{}) { offboxTgt := s.settings.GetOffboxTarget() data["Offbox"] = offboxTgt data["OffboxConfigured"] = s.backupMgr != nil && s.backupMgr.OffboxConfigured() + // R-70 (v0.161.0): hub-managed offsite is enabled in controller.yaml — the descriptor exists + // and the apply-bridge WILL configure the target automatically. The empty state must say so + // instead of the bare "nincs beállítva" (a box stuck pre-apply looked identical to one that + // was never provisioned — DIAG-f10: that ambiguity hid a burned credential for 2 days). + data["OffsiteHubEnabled"] = s.cfg != nil && s.cfg.Offsite.Enabled offboxApps := s.buildOffboxApps() data["OffboxApps"] = offboxApps // Zero-toggle hint (take-two obs.): configured + escrowed but no app selected — nothing is diff --git a/controller/internal/web/templates/backups_remote.html b/controller/internal/web/templates/backups_remote.html index 537be28..afc7caf 100644 --- a/controller/internal/web/templates/backups_remote.html +++ b/controller/internal/web/templates/backups_remote.html @@ -15,7 +15,11 @@ -{{if not .OffboxConfigured}} +{{if and (not .OffboxConfigured) .OffsiteHubEnabled}} + +
Felhom offsite tárhely kiépítve — a beállítás automatikus, folyamatban. Ha egy napon belül nem áll be, jelezd az üzemeltetőnek.
+{{else if not .OffboxConfigured}}
Felhom offsite tárhely — igényelhető szolgáltatás. Titkosított, házon kívüli másolat a mentéseidről. Érdeklődj az üzemeltetőnél.
{{else if eq .OffboxToggledCount 0}}
Aktív — nincs kijelölt alkalmazás
@@ -157,8 +161,12 @@ {{else}}

Nincs telepített alkalmazás.

{{end}} {{end}} {{else}} + {{if .OffsiteHubEnabled}} +

Felhom offsite tárhely kiépítve — a beállítás automatikus, folyamatban. Ha egy napon belül nem áll be, jelezd az üzemeltetőnek.

+ {{else}}

Még nincs beállítva távoli mentési cél.

{{end}} + {{end}}
Távoli mentési cél beállítása