Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9cc8424954 | |||
| 2487681396 |
@@ -1,5 +1,22 @@
|
|||||||
## Changelog
|
## 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)
|
### 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,
|
Live QA follow-up to v0.169.0: the agent defaults SMART to UNKNOWN on non-physical targets (PBS,
|
||||||
|
|||||||
@@ -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`
|
> 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.
|
> 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
|
> **felhom-pve is back on the home LAN (as of 2026-07-25).** The host holds `192.168.0.162` again and
|
||||||
> `192.168.0.162` is NOT reachable from DooPlex for the duration. Access via Tailscale:
|
> the **agent is UP** — `localapi` binds `192.168.0.162:8443`, the service is `active`, capabilities
|
||||||
> felhom-pve = 100.70.170.35; the `Host felhom-pve` entry in `~/.ssh/config` on DooPlex already
|
> self-check clean, and all agent-backed features (storage, PBS backup, quiesce, restore-test, DR) are
|
||||||
> points there (the direct-LAN path stays available as `Host felhom-pve-lan`). Delete this block on
|
> reachable. `ssh felhom-pve` remains a **Tailscale** alias (`100.70.170.35`, location-independent, the
|
||||||
> return. All documented `ssh felhom-pve` / `pct exec` workflows are unchanged. Path is **direct**
|
> N100 is travel-portable) with `Host felhom-pve-lan` as the direct-LAN fallback; both work. The earlier
|
||||||
> (not DERP), ~37 ms rtt per hop. At the remote site the host is on **DHCP** and currently holds
|
> "remote site — agent DOWN (binds a stale `.162`)" block was the 2026-07-20→24 vacation window and is
|
||||||
> `192.168.0.147` (the guest holds `.104`); no Pi-hole there — the guest reaches `gitea.dooplex.hu`
|
> now retired; the historical record stays in
|
||||||
> and `*.demo-felhom.eu` via public paths. **The host agent is DOWN for the duration**: its
|
> `felhom.eu/documentation/audits/AUDIT-vacation-remote-ops-2026-07-20.md`.
|
||||||
> `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`
|
|
||||||
|
|
||||||
External access via Cloudflare Tunnel → Traefik; Pi-hole forwards `*.demo-felhom.eu` → .162 locally.
|
External access via Cloudflare Tunnel → Traefik; Pi-hole forwards `*.demo-felhom.eu` → .162 locally.
|
||||||
|
|
||||||
|
|||||||
@@ -765,6 +765,7 @@ func getMariaDBPassword(ctx context.Context, containerID string) string {
|
|||||||
// - else longest known prefix → handles <stack>_postgres / <stack>-1 / compose-suffixed names.
|
// - else longest known prefix → handles <stack>_postgres / <stack>-1 / compose-suffixed names.
|
||||||
// - else → candidate (fall back to today's suffix-strip; preserves behaviour when
|
// - else → candidate (fall back to today's suffix-strip; preserves behaviour when
|
||||||
// the stack list is empty/unavailable, so nothing regresses).
|
// the stack list is empty/unavailable, so nothing regresses).
|
||||||
|
//
|
||||||
// A nil/empty `known` map = the legacy fast path (pure suffix-strip).
|
// A nil/empty `known` map = the legacy fast path (pure suffix-strip).
|
||||||
func deriveStackName(containerName string, known map[string]bool) string {
|
func deriveStackName(containerName string, known map[string]bool) string {
|
||||||
candidate := suffixStripStackName(containerName)
|
candidate := suffixStripStackName(containerName)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
// RecoveryManifest describes an app's self-contained, SECRET-FREE recovery unit (Phase 2).
|
// RecoveryManifest describes an app's self-contained, SECRET-FREE recovery unit (Phase 2).
|
||||||
//
|
//
|
||||||
// The unit on a drive is `<nsRoot>/backups/primary/<app>/` and contains:
|
// The unit on a drive is `<nsRoot>/backups/primary/<app>/` and contains:
|
||||||
|
//
|
||||||
// compose/ docker-compose.yml + .felhom.yml + a SECRET-STRIPPED app.yaml
|
// compose/ docker-compose.yml + .felhom.yml + a SECRET-STRIPPED app.yaml
|
||||||
// db-dumps/ app-consistent DB dump(s) (written by the dump flow)
|
// db-dumps/ app-consistent DB dump(s) (written by the dump flow)
|
||||||
// volume-dumps/ named-volume tars (written by the dump flow)
|
// volume-dumps/ named-volume tars (written by the dump flow)
|
||||||
|
|||||||
@@ -187,10 +187,12 @@ func (m *Manager) tier2FitsSystemDrive(sys string, unitSizeBytes int64) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tier-2 v2 layout (Task 3b, architecture §8): backups/secondary/<stack>/ holds
|
// Tier-2 v2 layout (Task 3b, architecture §8): backups/secondary/<stack>/ holds
|
||||||
|
//
|
||||||
// .felhom-tier2-layout (marker file, content "2" — written LAST, after all legs + reconcile)
|
// .felhom-tier2-layout (marker file, content "2" — written LAST, after all legs + reconcile)
|
||||||
// recovery-unit/ (the unit leg, layout-identical to v1)
|
// recovery-unit/ (the unit leg, layout-identical to v1)
|
||||||
// hdd/<relpath>/ (per-bind HDD legs, relpath-mirroring)
|
// hdd/<relpath>/ (per-bind HDD legs, relpath-mirroring)
|
||||||
// userdata/<relpath>/ (per-bind USERDATA legs)
|
// userdata/<relpath>/ (per-bind USERDATA legs)
|
||||||
|
//
|
||||||
// Relpath-mirroring represents N>1 dirs + nested binds natively (the v1 flat-appdata N>1 refusal is
|
// Relpath-mirroring represents N>1 dirs + nested binds natively (the v1 flat-appdata N>1 refusal is
|
||||||
// lifted structurally) and makes restore position-derivable (dest relpath → live path under the app's
|
// lifted structurally) and makes restore position-derivable (dest relpath → live path under the app's
|
||||||
// current HDD_PATH). The whole tree is DERIVED from live data — migration is delete-and-rebuild.
|
// current HDD_PATH). The whole tree is DERIVED from live data — migration is delete-and-rebuild.
|
||||||
|
|||||||
@@ -101,7 +101,12 @@ func (m *Manager) OnStackStart(_ context.Context, stackName string) {
|
|||||||
provStack, pOk := m.stacks.GetStack(provider)
|
provStack, pOk := m.stacks.GetStack(provider)
|
||||||
if !pOk || !provStack.Deployed || !isStackUp(provStack.State) {
|
if !pOk || !provStack.Deployed || !isStackUp(provStack.State) {
|
||||||
if m.isDebug() {
|
if m.isDebug() {
|
||||||
m.logger.Printf("[DEBUG] [integrations] OnStackStart: skipping %s — provider %s not up (found=%v deployed=%v state=%v)", key, provider, pOk, pOk && provStack.Deployed, func() stacks.ContainerState { if pOk { return provStack.State }; return "" }())
|
m.logger.Printf("[DEBUG] [integrations] OnStackStart: skipping %s — provider %s not up (found=%v deployed=%v state=%v)", key, provider, pOk, pOk && provStack.Deployed, func() stacks.ContainerState {
|
||||||
|
if pOk {
|
||||||
|
return provStack.State
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}())
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -109,7 +114,12 @@ func (m *Manager) OnStackStart(_ context.Context, stackName string) {
|
|||||||
tgtStack, tOk := m.stacks.GetStack(target)
|
tgtStack, tOk := m.stacks.GetStack(target)
|
||||||
if !tOk || !tgtStack.Deployed || !isStackUp(tgtStack.State) {
|
if !tOk || !tgtStack.Deployed || !isStackUp(tgtStack.State) {
|
||||||
if m.isDebug() {
|
if m.isDebug() {
|
||||||
m.logger.Printf("[DEBUG] [integrations] OnStackStart: skipping %s — target %s not up (found=%v deployed=%v state=%v)", key, target, tOk, tOk && tgtStack.Deployed, func() stacks.ContainerState { if tOk { return tgtStack.State }; return "" }())
|
m.logger.Printf("[DEBUG] [integrations] OnStackStart: skipping %s — target %s not up (found=%v deployed=%v state=%v)", key, target, tOk, tOk && tgtStack.Deployed, func() stacks.ContainerState {
|
||||||
|
if tOk {
|
||||||
|
return tgtStack.State
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}())
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,7 +100,12 @@ func (m *Manager) Toggle(ctx context.Context, provider, target string, enable bo
|
|||||||
// Validate: provider must be deployed and running
|
// Validate: provider must be deployed and running
|
||||||
provStack, pOk := m.stacks.GetStack(provider)
|
provStack, pOk := m.stacks.GetStack(provider)
|
||||||
if m.isDebug() {
|
if m.isDebug() {
|
||||||
m.logger.Printf("[DEBUG] [integrations] Toggle: provider %s found=%v deployed=%v state=%v", provider, pOk, pOk && provStack.Deployed, func() stacks.ContainerState { if pOk { return provStack.State }; return "" }())
|
m.logger.Printf("[DEBUG] [integrations] Toggle: provider %s found=%v deployed=%v state=%v", provider, pOk, pOk && provStack.Deployed, func() stacks.ContainerState {
|
||||||
|
if pOk {
|
||||||
|
return provStack.State
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}())
|
||||||
}
|
}
|
||||||
if !pOk || !provStack.Deployed {
|
if !pOk || !provStack.Deployed {
|
||||||
return state, fmt.Errorf("a szolgáltató alkalmazás (%s) nincs telepítve", provider)
|
return state, fmt.Errorf("a szolgáltató alkalmazás (%s) nincs telepítve", provider)
|
||||||
@@ -113,7 +118,12 @@ func (m *Manager) Toggle(ctx context.Context, provider, target string, enable bo
|
|||||||
if target != "filebrowser" {
|
if target != "filebrowser" {
|
||||||
tgtStack, tOk := m.stacks.GetStack(target)
|
tgtStack, tOk := m.stacks.GetStack(target)
|
||||||
if m.isDebug() {
|
if m.isDebug() {
|
||||||
m.logger.Printf("[DEBUG] [integrations] Toggle: target %s found=%v deployed=%v state=%v", target, tOk, tOk && tgtStack.Deployed, func() stacks.ContainerState { if tOk { return tgtStack.State }; return "" }())
|
m.logger.Printf("[DEBUG] [integrations] Toggle: target %s found=%v deployed=%v state=%v", target, tOk, tOk && tgtStack.Deployed, func() stacks.ContainerState {
|
||||||
|
if tOk {
|
||||||
|
return tgtStack.State
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}())
|
||||||
}
|
}
|
||||||
if !tOk || !tgtStack.Deployed {
|
if !tOk || !tgtStack.Deployed {
|
||||||
return state, fmt.Errorf("a célalkalmazás (%s) nincs telepítve", target)
|
return state, fmt.Errorf("a célalkalmazás (%s) nincs telepítve", target)
|
||||||
|
|||||||
@@ -321,4 +321,3 @@ func buildStacksReport(stackMgr *stacks.Manager) StacksReport {
|
|||||||
|
|
||||||
return sr
|
return sr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -842,4 +842,3 @@ var runCommandStdin = func(stdin, name string, args ...string) (string, error) {
|
|||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
return out.String(), err
|
return out.String(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ func TestAppExportDomainUsesCustomerDomainNotCSRFToken(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The empty-subdomain branch still yields '' — an app without a subdomain must not get a link to
|
// The empty-subdomain branch still yields the empty string — an app without a subdomain must not get
|
||||||
// a bare domain (the truthiness guard is what produces that, and the fix must not disturb it).
|
// a link to a bare domain (the truthiness guard is what produces that, and the fix must not disturb it).
|
||||||
func TestAppExportDomainEmptyWithoutSubdomain(t *testing.T) {
|
func TestAppExportDomainEmptyWithoutSubdomain(t *testing.T) {
|
||||||
line := exportScriptLine(t, renderExport(t, ""))
|
line := exportScriptLine(t, renderExport(t, ""))
|
||||||
|
|
||||||
|
|||||||
@@ -107,9 +107,9 @@ func TestDashboardBackupCard_ShowsLastRun(t *testing.T) {
|
|||||||
dumpAt := time.Date(2026, 7, 19, 3, 30, 0, 0, time.Local)
|
dumpAt := time.Date(2026, 7, 19, 3, 30, 0, 0, time.Local)
|
||||||
s := newDashboardServer(t, dumpAt)
|
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 {
|
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())
|
card := backupCard(t, rec.Body.String())
|
||||||
|
|
||||||
@@ -127,9 +127,9 @@ func TestDashboardBackupCard_ShowsLastRun(t *testing.T) {
|
|||||||
func TestDashboardBackupCard_FreshBoxStaysHonest(t *testing.T) {
|
func TestDashboardBackupCard_FreshBoxStaysHonest(t *testing.T) {
|
||||||
s := newDashboardServer(t, time.Time{})
|
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 {
|
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())
|
card := backupCard(t, rec.Body.String())
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ func (p *fabWebProvider) GetStackDir(string) (string, bool) { return p.stackDir,
|
|||||||
func (p *fabWebProvider) GetStackComposePath(string) (string, bool) {
|
func (p *fabWebProvider) GetStackComposePath(string) (string, bool) {
|
||||||
return filepath.Join(p.stackDir, "docker-compose.yml"), true
|
return filepath.Join(p.stackDir, "docker-compose.yml"), true
|
||||||
}
|
}
|
||||||
func (p *fabWebProvider) GetStackHDDMounts(string) []string { return []string{appbackup.UserdataDir(p.hddPath)} }
|
func (p *fabWebProvider) GetStackHDDMounts(string) []string {
|
||||||
|
return []string{appbackup.UserdataDir(p.hddPath)}
|
||||||
|
}
|
||||||
func (p *fabWebProvider) GetStackHDDPath(string) string { return p.hddPath }
|
func (p *fabWebProvider) GetStackHDDPath(string) string { return p.hddPath }
|
||||||
func (p *fabWebProvider) GetStackClassifiedBinds(string) ([]appbackup.ClassifiedBind, bool) {
|
func (p *fabWebProvider) GetStackClassifiedBinds(string) ([]appbackup.ClassifiedBind, bool) {
|
||||||
return p.binds, true
|
return p.binds, true
|
||||||
|
|||||||
@@ -49,7 +49,10 @@ func TestNetAddGate_OldAgent_RefusedUpFront(t *testing.T) {
|
|||||||
verifyErr: &agentapi.StatusError{Path: "/netstorage/verify-status", Code: http.StatusNotFound},
|
verifyErr: &agentapi.StatusError{Path: "/netstorage/verify-status", Code: http.StatusNotFound},
|
||||||
}
|
}
|
||||||
s.netAgentFn = func() (netAgent, error) { return agent, nil }
|
s.netAgentFn = func() (netAgent, error) { return agent, nil }
|
||||||
s.netProbeFn = func(context.Context, string) probeOutcome { t.Error("probe must never run on a gated add"); return probeOutcome{} }
|
s.netProbeFn = func(context.Context, string) probeOutcome {
|
||||||
|
t.Error("probe must never run on a gated add")
|
||||||
|
return probeOutcome{}
|
||||||
|
}
|
||||||
|
|
||||||
w := postNetAdd(t, s, "media")
|
w := postNetAdd(t, s, "media")
|
||||||
if w.Code != http.StatusPreconditionFailed {
|
if w.Code != http.StatusPreconditionFailed {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -363,7 +363,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
s.handleClaimSubmit(w, r)
|
s.handleClaimSubmit(w, r)
|
||||||
case path == "/claim/request-new-code" && r.Method == http.MethodPost:
|
case path == "/claim/request-new-code" && r.Method == http.MethodPost:
|
||||||
s.handleClaimRequestNewCode(w, r)
|
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)
|
s.dashboardHandler(w, r)
|
||||||
case path == "/launcher":
|
case path == "/launcher":
|
||||||
s.launcherHandler(w, r)
|
s.launcherHandler(w, r)
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ func TestRunStorageInit_Success(t *testing.T) {
|
|||||||
|
|
||||||
// F6 (VALIDATION-n100) — initialize must end in a USABLE (mounted+registered) drive even when the
|
// F6 (VALIDATION-n100) — initialize must end in a USABLE (mounted+registered) drive even when the
|
||||||
// client disconnects mid-format. Two halves:
|
// client disconnects mid-format. Two halves:
|
||||||
|
//
|
||||||
// RED-PROOF: runStorageInit on a CANCELLED context (the disconnect) aborts at the mount step →
|
// RED-PROOF: runStorageInit on a CANCELLED context (the disconnect) aborts at the mount step →
|
||||||
// the device is formatted but NOT registered (the N100-observed state). This is exactly what
|
// the device is formatted but NOT registered (the N100-observed state). This is exactly what
|
||||||
// the pre-fix handler did (it ran the chain on r.Context()).
|
// the pre-fix handler did (it ran the chain on r.Context()).
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<ul class="nav-links">
|
<ul class="nav-links">
|
||||||
<li><a href="/launcher" class="{{if eq .Page "launcher"}}active{{end}}"><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
|
<li><a href="/launcher" class="{{if eq .Page "launcher"}}active{{end}}"><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
|
||||||
<li><a href="/" class="{{if eq .Page "dashboard"}}active{{end}}"><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
|
<li><a href="/dashboard" class="{{if eq .Page "dashboard"}}active{{end}}"><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
|
||||||
<li><a href="/stacks" class="{{if eq .Page "stacks"}}active{{end}}"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
|
<li><a href="/stacks" class="{{if eq .Page "stacks"}}active{{end}}"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
|
||||||
{{$storageOpen := or (eq .Page "storage") (eq .Page "storage-network")}}
|
{{$storageOpen := or (eq .Page "storage") (eq .Page "storage-network")}}
|
||||||
<li class="nav-group{{if $storageOpen}} is-open{{end}}">
|
<li class="nav-group{{if $storageOpen}} is-open{{end}}">
|
||||||
|
|||||||
Reference in New Issue
Block a user