Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions gearbox/internal/framework/templates/pages/overview.templ
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
<!-- Hidden container for page header content - will be moved to main header via JS.
Title + box selector live in the global header breadcrumb + chip. -->
Expand Down Expand Up @@ -57,7 +62,11 @@ templ Overview(user *models.User, servers []models.BoxConfig) {
</div>

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 {
Expand Down
54 changes: 38 additions & 16 deletions gearbox/internal/gears/haproxy/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading