diff --git a/gearbox/internal/framework/templates/pages/overview.templ b/gearbox/internal/framework/templates/pages/overview.templ index fd38fa6..f5eb8fa 100644 --- a/gearbox/internal/framework/templates/pages/overview.templ +++ b/gearbox/internal/framework/templates/pages/overview.templ @@ -7,7 +7,12 @@ import "github.com/sarg3nt/gearbox/internal/framework/ui" import "github.com/sarg3nt/gearbox/internal/framework/templates/components" import "fmt" -templ Overview(user *models.User, servers []models.BoxConfig) { +// Overview is the HAProxy dashboard page. When servers is empty, the +// emptyReason string is shown in an InfoAlert in place of the per-box +// tiles — pass "" to use the default "No servers configured" message, +// or a more specific reason when the page is reached on a deployment +// that has boxes but none with the HAProxy gear (see issue #112). +templ Overview(user *models.User, servers []models.BoxConfig, emptyReason string) { @layouts.Base("HAProxy", user, "/haproxy") { @@ -57,7 +62,11 @@ templ Overview(user *models.User, servers []models.BoxConfig) { if len(servers) == 0 { - @components.InfoAlert("No servers configured. Please check your configuration.") + if emptyReason != "" { + @components.InfoAlert(emptyReason) + } else { + @components.InfoAlert("No servers configured. Please check your configuration.") + } } for _, server := range servers { diff --git a/gearbox/internal/gears/haproxy/handlers.go b/gearbox/internal/gears/haproxy/handlers.go index 217cd59..99ecf46 100644 --- a/gearbox/internal/gears/haproxy/handlers.go +++ b/gearbox/internal/gears/haproxy/handlers.go @@ -21,51 +21,73 @@ func NewHandlers(deps gear.Dependencies) *Handlers { } // OverviewPage serves the HAProxy overview page. -// If no servers are configured, redirects to the servers settings page. // // Only servers whose agent probe table reports the haproxy gear as // available are rendered — boxes without HAProxy (e.g. a container-mode // agent on a TrueNAS host) would otherwise show empty 503-storming // stat tiles. See issue #112. +// +// Empty-state handling: +// - No enabled boxes at all → redirect to /settings/boxes so the +// operator can configure one. +// - Enabled boxes exist but none advertise the haproxy gear → render +// the page with a tailored InfoAlert pointing at the Bx fleet view +// and the boxes settings, instead of redirecting (the operator is +// on the HAProxy page on purpose; we should explain why it's empty, +// not bounce them away from it). func (h *Handlers) OverviewPage(w http.ResponseWriter, r *http.Request) { - servers := h.getHAProxyServers() + all, haproxyServers := h.getServerLists() - // If no servers configured, redirect to servers settings page - if len(servers) == 0 { + if len(all) == 0 { http.Redirect(w, r, "/settings/boxes", http.StatusSeeOther) return } user := h.getUser(r) - pages.Overview(user, servers).Render(r.Context(), w) //nolint:errcheck + emptyReason := "" + if len(haproxyServers) == 0 { + emptyReason = "None of your connected boxes report an HAProxy gear. " + + "Check the agent's probe table from the Bx fleet view, or add " + + "a box that runs HAProxy via Settings → Boxes." + } + pages.Overview(user, haproxyServers, emptyReason).Render(r.Context(), w) //nolint:errcheck } // StatusGridPage serves the status grid page. // -// Filtered to HAProxy-capable boxes for the same reason as OverviewPage. +// Same empty-state treatment as OverviewPage: redirect to /settings/boxes +// only when there are no enabled boxes; otherwise render with the +// haproxy-capable subset (which may be empty, in which case the template +// shows its own empty state). func (h *Handlers) StatusGridPage(w http.ResponseWriter, r *http.Request) { - servers := h.getHAProxyServers() + all, haproxyServers := h.getServerLists() - if len(servers) == 0 { + if len(all) == 0 { http.Redirect(w, r, "/settings/boxes", http.StatusSeeOther) return } user := h.getUser(r) - pages.StatusGrid(user, servers).Render(r.Context(), w) //nolint:errcheck + pages.StatusGrid(user, haproxyServers).Render(r.Context(), w) //nolint:errcheck } -// getHAProxyServers returns the list of enabled servers whose agent -// reports the haproxy gear available. Fail-open: boxes whose capabilities -// can't be fetched are still included so a transient agent outage doesn't -// hide the page entirely. -func (h *Handlers) getHAProxyServers() []models.BoxConfig { +// getServerLists returns (allEnabled, haproxyCapable) in one pair of +// calls so OverviewPage / StatusGridPage can distinguish "no boxes at +// all" (redirect to settings) from "boxes exist but none have HAProxy" +// (render the empty-state in place). +// +// Fail-open: a box whose capabilities aren't reachable is still counted +// as haproxy-capable, matching the behavior in +// ServerAdapter.GetEnabledServersWithGearAvailable. +func (h *Handlers) getServerLists() (all, haproxyCapable []models.BoxConfig) { serverAdapter, ok := h.deps.Servers.(*services.ServerAdapter) if !ok { h.deps.Logger.Error("failed to get server adapter - unexpected type") - return nil + return nil, nil } - return serverAdapter.GetEnabledServersWithGearAvailable("haproxy") + all = serverAdapter.GetEnabledServersAsModels() + haproxyCapable = serverAdapter.GetEnabledServersWithGearAvailable("haproxy") + return all, haproxyCapable } // getUser returns the user from the auth context, or a fallback user.