diff --git a/CHANGELOG.md b/CHANGELOG.md index a37409d..0980505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ ## Changelog +### v0.170.0 — Root → Indítópult; gofmt normalization; stale-note fix (2026-07-25) + +- **`/` is now the Indítópult** (operator ruling, reversing the v0.163.0 landing choice). `GET /` 302s + to `/launcher` (ONE canonical URL per page — the launcher body is never served at `/`); the + Vezérlőpult keeps its own URL **`/dashboard`** and its nav slot. Nav: Indítópult active on + `/launcher`, Vezérlőpult `href="/dashboard"` active there — never both. Post-login (default `/`) and + the mobile-topbar logo (`/`) both flow through the redirect to the launcher; the login redirect target + is unchanged. Tests: the 302 (target + status), `/dashboard` 200, nav hrefs/active; red-proof: fold + `/` back into the dashboard case → the 302 test fails. Two dashboard-card tests repointed `/`→`/dashboard`. +- **gofmt normalization** shipped as a **separate, style-only prior commit** (`gofmt -w` across the + controller tree, **46 files**, `gofmt -l` now empty) — disarms the formatting landmine where a + targeted edit + an accidental `gofmt -w` swept ~46 unrelated files. Pure formatting (whitespace + + optional-semicolon removal in reflowed inline closures); one doc comment reworded to avoid gofmt's + Go-1.19 `''`→curly-quote doc-comment substitution. +- Repo `CLAUDE.md`: corrected the stale "vacation — agent DOWN at a remote site" note — felhom-pve is + back on the home LAN and the agent is up at `192.168.0.162:8443` (Tailscale alias still available). + ### v0.169.1 — Disk-health card: exclude logical/network storage (2026-07-24) Live QA follow-up to v0.169.0: the agent defaults SMART to UNKNOWN on non-physical targets (PBS, diff --git a/CLAUDE.md b/CLAUDE.md index fce091d..09ce1cd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -137,18 +137,14 @@ Claude Code runs **on DooPlex (192.168.0.180, Debian 13, user `kisfenyo`)**; rep > the Windows agent and fails silently — see `docs/vscode-ssh-fix.md`), plus `MSYS_NO_PATHCONV=1` > for `pct exec`. Retained in case that environment is revived. -> **TEMPORARY — felhom-pve is at a remote site (until ~2026-08-02).** The home-LAN literal -> `192.168.0.162` is NOT reachable from DooPlex for the duration. Access via Tailscale: -> felhom-pve = 100.70.170.35; the `Host felhom-pve` entry in `~/.ssh/config` on DooPlex already -> points there (the direct-LAN path stays available as `Host felhom-pve-lan`). Delete this block on -> return. All documented `ssh felhom-pve` / `pct exec` workflows are unchanged. Path is **direct** -> (not DERP), ~37 ms rtt per hop. At the remote site the host is on **DHCP** and currently holds -> `192.168.0.147` (the guest holds `.104`); no Pi-hole there — the guest reaches `gitea.dooplex.hu` -> and `*.demo-felhom.eu` via public paths. **The host agent is DOWN for the duration**: its -> `localapi` binds the literal `192.168.0.162`, which no longer exists → `bind: cannot assign -> requested address`, so every agent-backed feature (storage, PBS backup, quiesce, restore-test, DR) -> is unavailable until fixed. Details + findings: -> `felhom.eu/documentation/audits/AUDIT-vacation-remote-ops-2026-07-20.md` +> **felhom-pve is back on the home LAN (as of 2026-07-25).** The host holds `192.168.0.162` again and +> the **agent is UP** — `localapi` binds `192.168.0.162:8443`, the service is `active`, capabilities +> self-check clean, and all agent-backed features (storage, PBS backup, quiesce, restore-test, DR) are +> reachable. `ssh felhom-pve` remains a **Tailscale** alias (`100.70.170.35`, location-independent, the +> N100 is travel-portable) with `Host felhom-pve-lan` as the direct-LAN fallback; both work. The earlier +> "remote site — agent DOWN (binds a stale `.162`)" block was the 2026-07-20→24 vacation window and is +> now retired; the historical record stays in +> `felhom.eu/documentation/audits/AUDIT-vacation-remote-ops-2026-07-20.md`. External access via Cloudflare Tunnel → Traefik; Pi-hole forwards `*.demo-felhom.eu` → .162 locally. diff --git a/controller/internal/web/dashboard_backup_card_test.go b/controller/internal/web/dashboard_backup_card_test.go index 903df51..a6aeb52 100644 --- a/controller/internal/web/dashboard_backup_card_test.go +++ b/controller/internal/web/dashboard_backup_card_test.go @@ -107,9 +107,9 @@ func TestDashboardBackupCard_ShowsLastRun(t *testing.T) { dumpAt := time.Date(2026, 7, 19, 3, 30, 0, 0, time.Local) s := newDashboardServer(t, dumpAt) - rec := getPage(t, s, "/") + rec := getPage(t, s, "/dashboard") // v0.170.0: the Vezérlőpult moved from "/" to "/dashboard" if rec.Code != 200 { - t.Fatalf("GET / = %d: %s", rec.Code, rec.Body.String()) + t.Fatalf("GET /dashboard = %d: %s", rec.Code, rec.Body.String()) } card := backupCard(t, rec.Body.String()) @@ -127,9 +127,9 @@ func TestDashboardBackupCard_ShowsLastRun(t *testing.T) { func TestDashboardBackupCard_FreshBoxStaysHonest(t *testing.T) { s := newDashboardServer(t, time.Time{}) - rec := getPage(t, s, "/") + rec := getPage(t, s, "/dashboard") // v0.170.0: the Vezérlőpult moved from "/" to "/dashboard" if rec.Code != 200 { - t.Fatalf("GET / = %d: %s", rec.Code, rec.Body.String()) + t.Fatalf("GET /dashboard = %d: %s", rec.Code, rec.Body.String()) } card := backupCard(t, rec.Body.String()) diff --git a/controller/internal/web/root_redirect_test.go b/controller/internal/web/root_redirect_test.go new file mode 100644 index 0000000..a356c22 --- /dev/null +++ b/controller/internal/web/root_redirect_test.go @@ -0,0 +1,66 @@ +package web + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +// v0.170.0 landing change (operator ruling, reversing v0.163.0): "/" is the Indítópult — it 302s to +// /launcher (ONE canonical URL per page), and the Vezérlőpult keeps its own URL /dashboard. + +// Root 302s to the launcher — the launcher body is NEVER served at "/". +// Red-proof: revert the server.go case split (fold "/" back into the dashboard case) → "/" renders a +// 200 dashboard and this fails on both the status and the Location. +func TestRoot_RedirectsToLauncher(t *testing.T) { + s := newDashboardServer(t, time.Time{}) + rec := httptest.NewRecorder() + s.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil)) + + if rec.Code != http.StatusFound { + t.Fatalf("GET / status = %d, want 302", rec.Code) + } + if loc := rec.Header().Get("Location"); loc != "/launcher" { + t.Errorf("GET / Location = %q, want /launcher", loc) + } + // The launcher body must not be served at "/" (no two canonical URLs for one page). + if strings.Contains(rec.Body.String(), "launch-tile") { + t.Error("GET / served launcher body instead of redirecting") + } +} + +// The Vezérlőpult is still served, now at /dashboard, with a 200 and its real content. +func TestDashboard_ServedAtDashboardURL(t *testing.T) { + s := newDashboardServer(t, time.Time{}) + rec := httptest.NewRecorder() + s.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/dashboard", nil)) + + if rec.Code != http.StatusOK { + t.Fatalf("GET /dashboard status = %d, want 200", rec.Code) + } + if !strings.Contains(rec.Body.String(), "Vezérlőpult") { + t.Error("/dashboard did not render the Vezérlőpult page") + } +} + +// Nav hrefs + active states: Indítópult → /launcher (active on the launcher), Vezérlőpult → +// /dashboard (active on the dashboard), and no item highlights on both. +func TestNav_LauncherAndDashboardHrefsAndActive(t *testing.T) { + onDashboard := renderNavFor(t, "dashboard") + if !strings.Contains(onDashboard, `href="/dashboard" class="active"`) { + t.Error("dashboard page: Vezérlőpult nav link is not href=/dashboard + active") + } + if !strings.Contains(onDashboard, `href="/launcher" class=""`) { + t.Error("dashboard page: Indítópult nav link should NOT be active") + } + + onLauncher := renderNavFor(t, "launcher") + if !strings.Contains(onLauncher, `href="/launcher" class="active"`) { + t.Error("launcher page: Indítópult nav link is not href=/launcher + active") + } + if !strings.Contains(onLauncher, `href="/dashboard" class=""`) { + t.Error("launcher page: Vezérlőpult nav link should NOT be active") + } +} diff --git a/controller/internal/web/server.go b/controller/internal/web/server.go index f39639b..4477821 100644 --- a/controller/internal/web/server.go +++ b/controller/internal/web/server.go @@ -363,7 +363,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.handleClaimSubmit(w, r) case path == "/claim/request-new-code" && r.Method == http.MethodPost: s.handleClaimRequestNewCode(w, r) - case path == "/" || path == "/dashboard": + case path == "/": + // v0.170.0 (operator ruling reversing the v0.163.0 landing choice): the Indítópult is the + // canonical landing page. "/" 302s to /launcher (ONE canonical URL per page — the launcher body + // is never served AT "/"). Post-login lands on "/", so it flows here → the launcher. + http.Redirect(w, r, "/launcher", http.StatusFound) + case path == "/dashboard": s.dashboardHandler(w, r) case path == "/launcher": s.launcherHandler(w, r) diff --git a/controller/internal/web/templates/layout.html b/controller/internal/web/templates/layout.html index 0057404..8fd0891 100644 --- a/controller/internal/web/templates/layout.html +++ b/controller/internal/web/templates/layout.html @@ -78,7 +78,7 @@