Skip to content
84 changes: 42 additions & 42 deletions gearbox/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -623,8 +623,8 @@ func main() {

// Page routes (non-plugin)
r.Get("/os-updates", h.OSUpdatesPage)
r.Get("/box/{serverID}/frontend/{name}", h.FrontendDetailPage)
r.Get("/box/{serverID}/backend/{name}", h.BackendDetailPage)
r.Get("/box/{boxID}/frontend/{name}", h.FrontendDetailPage)
r.Get("/box/{boxID}/backend/{name}", h.BackendDetailPage)
Comment thread
sarg3nt marked this conversation as resolved.

// Config editor pages
r.Get("/config/haproxy/{boxID}", h.HAProxyConfigPage)
Expand All @@ -646,36 +646,36 @@ func main() {
r.Get("/user/permissions", h.APIGetCurrentUserPermissions)
r.Get("/user/pending-count", h.APIPendingUsersCount)
r.Get("/websocket/status", h.APIWebSocketStatusHandler) // WebSocket connection status
r.Get("/{serverID}/stats", h.APIStatsHandler)
r.Get("/{serverID}/metadata", h.APIMetadataHandler)
r.Get("/{serverID}/metrics", h.APISystemMetricsHandler)
r.Get("/{serverID}/logs/{logName}", h.APILogsHandler)
r.Get("/{serverID}/log-sources", h.APILogSourcesHandler) // Get enabled log sources
r.Get("/{boxID}/stats", h.APIStatsHandler)
r.Get("/{boxID}/metadata", h.APIMetadataHandler)
r.Get("/{boxID}/metrics", h.APISystemMetricsHandler)
r.Get("/{boxID}/logs/{logName}", h.APILogsHandler)
r.Get("/{boxID}/log-sources", h.APILogSourcesHandler) // Get enabled log sources
// History API endpoints
r.Get("/{serverID}/history/stats", h.APIStatsHistoryHandler)
r.Get("/{serverID}/history/metrics", h.APISystemMetricsHistoryHandler)
r.Get("/{serverID}/history/backend/{backendName}", h.APIBackendHistoryHandler)
r.Get("/{serverID}/incidents", h.APIIncidentsHandler)
r.Get("/{boxID}/history/stats", h.APIStatsHistoryHandler)
r.Get("/{boxID}/history/metrics", h.APISystemMetricsHistoryHandler)
r.Get("/{boxID}/history/backend/{backendName}", h.APIBackendHistoryHandler)
r.Get("/{boxID}/incidents", h.APIIncidentsHandler)

// Disabled entities management
r.Get("/{serverID}/disabled-entities", h.APIDisabledEntitiesHandler)
r.Post("/{serverID}/disable-entity", h.APIDisableEntityHandler)
r.Post("/{serverID}/enable-entity", h.APIEnableEntityHandler)
r.Get("/{boxID}/disabled-entities", h.APIDisabledEntitiesHandler)
r.Post("/{boxID}/disable-entity", h.APIDisableEntityHandler)
r.Post("/{boxID}/enable-entity", h.APIEnableEntityHandler)

// Certificate management
r.Get("/{serverID}/certificates", h.APICertificatesHandler)
r.Post("/{serverID}/certificates/{domain}/refresh", h.APICertificateRefreshHandler)
r.Get("/{serverID}/certificates/{domain}/download", h.APICertificateDownloadHandler)
r.Get("/{boxID}/certificates", h.APICertificatesHandler)
r.Post("/{boxID}/certificates/{domain}/refresh", h.APICertificateRefreshHandler)
r.Get("/{boxID}/certificates/{domain}/download", h.APICertificateDownloadHandler)

// Services management
r.Get("/{serverID}/services-config", h.APIServicesConfigHandler)
r.Get("/{serverID}/services", h.APIServicesHandler)
r.Post("/{serverID}/service-control", h.APIServiceControlHandler)
r.Get("/{boxID}/services-config", h.APIServicesConfigHandler)
r.Get("/{boxID}/services", h.APIServicesHandler)
r.Post("/{boxID}/service-control", h.APIServiceControlHandler)

// Traffic analysis API
r.Get("/{serverID}/traffic", h.APITrafficAnalysisHandler)
r.Get("/{serverID}/traffic/sources", h.APITrafficSourcesHandler)
r.Get("/{serverID}/traffic/network", h.APITrafficNetworkHandler)
r.Get("/{boxID}/traffic", h.APITrafficAnalysisHandler)
r.Get("/{boxID}/traffic/sources", h.APITrafficSourcesHandler)
r.Get("/{boxID}/traffic/network", h.APITrafficNetworkHandler)

// Integrations API
r.Get("/plugins", h.APIPluginsHandler)
Expand All @@ -689,23 +689,23 @@ func main() {
r.Get("/backup/download/{path}", h.APIDownloadBackup)

// Metrics storage management
r.Get("/{serverID}/metrics/storage-stats", h.APIMetricsStorageStatsHandler)
r.Post("/{serverID}/metrics/clear", h.APIClearMetricsDataHandler)
r.Get("/{boxID}/metrics/storage-stats", h.APIMetricsStorageStatsHandler)
r.Post("/{boxID}/metrics/clear", h.APIClearMetricsDataHandler)

// HAProxy config management
r.Get("/{serverID}/haproxy/config", h.APIHAProxyConfigGet)
r.Post("/{serverID}/haproxy/config", h.APIHAProxyConfigSave)
r.Post("/{serverID}/haproxy/config/validate", h.APIHAProxyConfigValidate)
r.Get("/{serverID}/haproxy/config/backups", h.APIHAProxyConfigBackups)
r.Post("/{serverID}/haproxy/config/restore", h.APIHAProxyConfigRestore)
r.Get("/{serverID}/haproxy/config/history", h.APIHAProxyConfigHistory)
r.Get("/{boxID}/haproxy/config", h.APIHAProxyConfigGet)
r.Post("/{boxID}/haproxy/config", h.APIHAProxyConfigSave)
r.Post("/{boxID}/haproxy/config/validate", h.APIHAProxyConfigValidate)
r.Get("/{boxID}/haproxy/config/backups", h.APIHAProxyConfigBackups)
r.Post("/{boxID}/haproxy/config/restore", h.APIHAProxyConfigRestore)
r.Get("/{boxID}/haproxy/config/history", h.APIHAProxyConfigHistory)

// Firewall config management
r.Get("/{serverID}/firewall/config", h.APIFirewallConfigGet)
r.Post("/{serverID}/firewall/config", h.APIFirewallConfigSave)
r.Post("/{serverID}/firewall/config/validate", h.APIFirewallConfigValidate)
r.Get("/{serverID}/firewall/config/backups", h.APIFirewallConfigBackups)
r.Post("/{serverID}/firewall/config/restore", h.APIFirewallConfigRestore)
r.Get("/{boxID}/firewall/config", h.APIFirewallConfigGet)
r.Post("/{boxID}/firewall/config", h.APIFirewallConfigSave)
r.Post("/{boxID}/firewall/config/validate", h.APIFirewallConfigValidate)
r.Get("/{boxID}/firewall/config/backups", h.APIFirewallConfigBackups)
r.Post("/{boxID}/firewall/config/restore", h.APIFirewallConfigRestore)

// User permissions API
r.Route("/users", func(r chi.Router) {
Expand All @@ -716,12 +716,12 @@ func main() {

// Alerts API
r.Get("/alerts/count", h.APIGlobalAlertCountHandler) // Global active alert count
r.Get("/{serverID}/alerts", h.APIAlertsHandler)
r.Get("/{serverID}/alerts/summary", h.APIAlertSummaryHandler)
r.Get("/{serverID}/alerts/rules", h.APIAlertRulesHandler)
r.Post("/{serverID}/alerts/rules", h.APICreateAlertRuleHandler)
r.Get("/{serverID}/alerts/retention", h.APIAlertRetentionConfigHandler)
r.Post("/{serverID}/alerts/retention", h.APIUpdateAlertRetentionConfigHandler)
r.Get("/{boxID}/alerts", h.APIAlertsHandler)
r.Get("/{boxID}/alerts/summary", h.APIAlertSummaryHandler)
r.Get("/{boxID}/alerts/rules", h.APIAlertRulesHandler)
r.Post("/{boxID}/alerts/rules", h.APICreateAlertRuleHandler)
r.Get("/{boxID}/alerts/retention", h.APIAlertRetentionConfigHandler)
r.Post("/{boxID}/alerts/retention", h.APIUpdateAlertRetentionConfigHandler)
r.Put("/alerts/rules/{ruleID}", h.APIUpdateAlertRuleHandler)
r.Delete("/alerts/rules/{ruleID}", h.APIDeleteAlertRuleHandler)
r.Post("/alerts/{alertID}/acknowledge", h.APIAcknowledgeAlertWithNoteHandler)
Expand Down
23 changes: 3 additions & 20 deletions gearbox/internal/framework/dashboard/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ func (s *Storage) CreateDefaultDashboard() error {
return nil
}

// Create default dashboard
// Create a default, empty, editable dashboard so users can configure it as needed.
dashboard := &Dashboard{
Version: "1.0",
Name: "Dashboard",
Expand All @@ -221,25 +221,8 @@ func (s *Storage) CreateDefaultDashboard() error {
Columns: 12,
Gap: 4,
},
Widgets: []Widget{
{
ID: "welcome-1",
Type: "alert-banner",
Position: WidgetPosition{
Row: 1,
Column: 1,
Width: 12,
Height: "auto",
},
Config: map[string]interface{}{
"severity": "info",
"message": "Welcome to Gearbox! This is your default dashboard. You can customize it by adding widgets.",
"icon": "info",
"dismissible": true,
},
},
},
Slug: "dashboard",
Widgets: []Widget{},
Slug: "dashboard",
}

// Save dashboard
Expand Down
4 changes: 2 additions & 2 deletions gearbox/internal/framework/database/plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ type Plugin struct {
SortOrder int `json:"sort_order"` // Order for display in UI (lower = higher priority)
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
UpdatedBy *int64 `json:"updated_by,omitempty"`
UpdatedBy *string `json:"updated_by,omitempty"`
}

// DefaultPlugins returns the default integration configurations for a server.
Expand Down Expand Up @@ -302,7 +302,7 @@ func (d *DB) initPluginsSchema() error {
sort_order INTEGER NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_by INTEGER,
updated_by TEXT,
UNIQUE(server_id, name),
FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL
);
Expand Down
5 changes: 4 additions & 1 deletion gearbox/internal/framework/handler/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,14 @@ func (h *DashboardHandler) EditDashboardPage(w http.ResponseWriter, r *http.Requ
// Get available widgets
widgets := h.widgetRegistry.List()

// Check if palette should be auto-opened (e.g., redirected from empty dashboard)
openPalette := r.URL.Query().Get("open_palette") == "1"

// Get user from context
user, _ := auth.GetUserFromContext(r.Context())

// Render editor with live content
component := pages.DashboardEditorPage(dash, content, widgets, user, r.URL.Path)
component := pages.DashboardEditorPage(dash, content, widgets, user, r.URL.Path, openPalette)
Comment thread
sarg3nt marked this conversation as resolved.
if err := component.Render(r.Context(), w); err != nil {
h.logger.Error("failed to render dashboard editor", "error", err)
http.Error(w, "Failed to render page", http.StatusInternalServerError)
Expand Down
5 changes: 3 additions & 2 deletions gearbox/internal/framework/handler/haproxy_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"

Expand Down Expand Up @@ -136,8 +137,8 @@ func (h *Handler) HAProxyBoxCreatePost(w http.ResponseWriter, r *http.Request) {
}
}

// Redirect to servers list page
http.Redirect(w, r, "/settings/boxes", http.StatusSeeOther)
// Redirect to plugins page so user can enable plugins for this new server
http.Redirect(w, r, "/settings/plugins?server="+url.QueryEscape(server.BoxID), http.StatusSeeOther)
}

// HAProxyBoxEditPage shows the form for editing a server.
Expand Down
52 changes: 51 additions & 1 deletion gearbox/internal/framework/templates/layouts/base.templ
Original file line number Diff line number Diff line change
Expand Up @@ -1216,7 +1216,6 @@ templ Sidebar(user *models.User, currentPath string) {
<!-- Navigation -->
<nav class="flex-1 py-4">
<ul id="sidebar-nav-list" class="space-y-1">
@SidebarLink("/", "Dashboard", SidebarIconDashboard(), currentPath)
@OrderedIntegrationLinks(currentPath)
</ul>
</nav>
Expand Down Expand Up @@ -1570,6 +1569,57 @@ templ SidebarLinkWithIntegration(href string, label string, icon templ.Component
}
}

// hasAnyEnabledIntegration checks if any integration (plugin) is enabled in the current context.
func hasAnyEnabledIntegration(ctx context.Context) bool {
integrations, ok := auth.GetPluginOrderFromContext(ctx)
if !ok {
return false
}
for _, integration := range integrations {
if integration.Enabled {
return true
}
}
return false
}

// integrationPath returns the URL path for a given integration name.
func integrationPath(name string) string {
switch name {
case "metrics":
return "/history"
case "logs":
return "/logs"
case "services":
return "/services"
case "certificates":
return "/certificates"
case "traffic":
return "/traffic"
case "alerts":
return "/alerts"
case "os_updates":
return "/os-updates"
default:
return "/"
}
}

// firstEnabledIntegrationPath returns the URL path for the first enabled integration.
// Returns "/" if no integrations are enabled.
func firstEnabledIntegrationPath(ctx context.Context) string {
integrations, ok := auth.GetPluginOrderFromContext(ctx)
if !ok {
return "/"
}
for _, integration := range integrations {
if integration.Enabled {
return integrationPath(integration.Name)
}
}
return "/"
}

Comment thread
sarg3nt marked this conversation as resolved.
// canViewIntegration checks if the user has permission to view an integration.
// Maps integration names to component View permissions.
func canViewIntegration(ctx context.Context, integrationName string) bool {
Expand Down
14 changes: 11 additions & 3 deletions gearbox/internal/framework/templates/pages/dashboard_editor.templ
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

// DashboardEditorPage renders the visual dashboard editor with live widget preview
templ DashboardEditorPage(dash *dashboard.Dashboard, content templ.Component, widgets []*widget.WidgetDefinition, user *models.User, currentPath string) {
templ DashboardEditorPage(dash *dashboard.Dashboard, content templ.Component, widgets []*widget.WidgetDefinition, user *models.User, currentPath string, openPalette bool) {
@layouts.Base("Edit Dashboard", user, currentPath) {
<!-- Widget Palette CSS -->
<link rel="stylesheet" href="/static/css/dashboard/palette.css"/>
Expand Down Expand Up @@ -49,8 +49,8 @@ templ DashboardEditorPage(dash *dashboard.Dashboard, content templ.Component, wi
</div>
</div>

<!-- Widget Palette Panel (collapsible) -->
<div id="widget-palette-panel" class="hidden mb-4 bg-white dark:bg-slate-900 border border-gray-300 dark:border-slate-600 rounded-lg shadow-lg">
<!-- Widget Palette Panel (collapsible, auto-opened when dashboard is empty) -->
<div id="widget-palette-panel" class={ widgetPaletteClass(openPalette) }>
<!-- Fixed Header -->
<div class="p-4 border-b border-gray-200 dark:border-slate-700 sticky top-0 bg-white dark:bg-slate-900 z-10">
<div class="flex items-center justify-between mb-3">
Expand Down Expand Up @@ -393,6 +393,14 @@ templ DashboardEditorScript(dash *dashboard.Dashboard) {
}

// Helper functions

func widgetPaletteClass(openPalette bool) string {
if openPalette {
return "mb-4 bg-white dark:bg-slate-900 border border-gray-300 dark:border-slate-600 rounded-lg shadow-lg"
}
return "hidden mb-4 bg-white dark:bg-slate-900 border border-gray-300 dark:border-slate-600 rounded-lg shadow-lg"
}

func getWidgetStyle(pos dashboard.WidgetPosition) string {
return fmt.Sprintf("grid-column: span %d;", pos.Width)
}
Expand Down
10 changes: 10 additions & 0 deletions gearbox/internal/framework/templates/pages/plugins.templ
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ templ PluginsPage(user *models.User, servers []models.BoxConfig, currentServerID
}
</div>

<!-- Done button to navigate to main dashboard -->
<div class="mt-8 flex justify-end">
<a
href="/"
class="px-6 py-2.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
>
Done
</a>
</div>

<script src="/static/js/plugins/plugins-page.js"></script>
</div>
}
Expand Down
Loading