Version: 0.2.0 | Last Updated: 2026-01-31
Terminology: In Gearbox, we call modular components "gears" — our term for what are traditionally called plugins.
Gearbox uses a compile-time gear architecture for both the gearbox (client) and gearbox-agent (server agent) applications. This document describes the gear system, how to create gears, and how feature flags control gear availability.
- Overview
- Terminology
- Architecture Principles
- Gear Types
- Gearbox Client Gears
- Gearbox Agent Gears
- Agent Client Communication
- Feature Flags
- Creating a New Gear
- Testing Gears
- Examples
A gear is a self-contained module that provides specific functionality to the Gearbox system. Gears:
- Register themselves at compile time via
init()functions - Implement a common
Gearinterface - Can define HTTP routes, permissions, UI pages, and background tasks
- Are isolated from other gears but share framework services
- Modularity: Features are cleanly separated into independent modules
- Extensibility: New functionality can be added without modifying the core
- Maintainability: Each gear owns its domain logic
- Testability: Gears can be tested in isolation
- Feature Management: Gears can be enabled/disabled per server
To avoid confusion, this document uses the following terms consistently:
| Term | Meaning | Location |
|---|---|---|
| Gearbox | The entire monitoring system (both client and agent) | - |
| Gearbox Client | The web application (gearbox) that admins use |
Runs on admin's machine or server |
| Gearbox Agent | The server agent (gearbox-agent) that collects data |
Runs on monitored servers |
| Client Gear | A gear for the web application | gearbox/internal/gears/ |
| Agent Gear | A gear for the server agent | gearbox-agent/internal/gears/ |
| Dashboard Gear | The specific gear named "dashboard" (main overview page) | gearbox/internal/gears/dashboard/ |
Why "Client" instead of "Dashboard"?
The web application is the client that connects to agents. Using "client gear" avoids confusion with the dashboard gear (one specific gear that shows the overview page).
Gearbox uses compile-time gears (similar to Caddy) rather than runtime dynamic loading:
- Gears are compiled into the binary
- No runtime gear loading or shared libraries
- Type safety and performance of static compilation
- Easier to reason about and debug
gearbox/
├── internal/
│ ├── framework/ # Shared infrastructure
│ │ ├── agent/ # Agent client (infrastructure)
│ │ ├── auth/ # Authentication/authorization
│ │ ├── collector/ # Data collection
│ │ ├── database/ # Database access
│ │ ├── events/ # Event bus
│ │ ├── handler/ # HTTP handlers (legacy)
│ │ ├── models/ # Data models
│ │ ├── gear/ # Gear system
│ │ ├── services/ # Shared services
│ │ └── templates/ # Shared templates
│ │
│ └── gears/ # Feature implementations
│ ├── alerts/
│ ├── certificates/
│ ├── dashboard/
│ ├── logs/
│ ├── metrics/
│ ├── services/
│ └── traffic/Key principle: The framework provides infrastructure and services. The gears implement features.
Gears are isolated from each other but can:
- Access framework services via
Dependencies - Publish/subscribe to events via the event bus
- Register HTTP routes under their namespace
- Define permissions and UI components
Gears cannot directly call methods on other gears.
Client gears run in the gearbox web application (the monitoring client) and provide UI pages and API endpoints for monitoring and managing servers.
Location: gearbox/internal/gears/
Purpose: Render UI pages, handle user interactions, display data from agents
Terminology Note: These are called "client gears" to distinguish them from the dashboard gear (which provides the main overview page).
Examples:
home- Box-agnostic app dashboard (start page with launcher tiles, widgets, bookmarks, search)dashboard- Main monitoring overview page (the homepage)certificates- SSL/TLS certificate monitoring pagelogs- Log viewing and analysis pagetraffic- Traffic analysis and visualization pagealerts- Alert management pageservices- Service status and control pagemetrics- System metrics and history page
Most gears are box-scoped — they monitor a specific server, and the gears table holds one row per (server_id, name). A few gears are system-scoped — they apply to the whole install, with a single row keyed by the sentinel server_id = '__system__'.
Declare the scope on Info:
func (g *Gear) Info() gear.Info {
return gear.Info{
Name: "home",
Scope: gear.ScopeSystem, // omit or set ScopeBox for the default per-box behaviour
// ...
}
}System gears are seeded once at startup via database.EnsureSystemGears() (called from cmd/server/main.go). Box gears are seeded lazily per-server via EnsureServerGears(boxID) on first access, just like before. The two seeding lists are kept disjoint by construction (DefaultGears vs DefaultSystemGears).
The home gear is the first system-scoped gear and a useful reference. Its config lives in database.HomeConfig and is loaded at startup via the standard gears.config JSON column.
Agent gears run on monitored servers (in the gearbox-agent application) and collect data, expose APIs, and respond to management commands.
Location: gearbox-agent/internal/gears/
Purpose: Collect data, expose REST APIs, publish events
Examples:
haproxy- HAProxy stats collectionmetrics- System metrics (CPU, memory, disk)logs- Log access and streamingcerts- Certificate managementsecurity- Firewall and fail2ban integrationtraffic- Traffic analysis data collectionupdates- OS package management
Client gears run in the gearbox web application and provide full-page UIs for specific features.
Client gears implement the gear.Gear interface:
type Gear interface {
Info() Info // Metadata
Initialize(ctx, deps) error // Setup
RegisterRoutes(r chi.Router) // HTTP routes
SidebarItem() *SidebarConfig // Navigation
SettingsPage(config) templ.Component // Settings UI
Permissions() []PermissionDef // Required permissions
}internal/gears/mygear/
├── gear.go # Gear definition and interface implementation
├── handlers.go # HTTP request handlers
├── icons.go # SVG icons for UI
├── agent.go # Agent client interface (if needed)
├── pages.templ # Templ templates for pages
└── README.md # Gear documentationGears register themselves in gear.go via init():
package mygear
import "github.com/sarg3nt/gearbox/internal/framework/gear"
func init() {
gear.Register(&Gear{})
}
type Gear struct {
gear.BaseGear
handlers *Handlers
}
func (g *Gear) Info() gear.Info {
return gear.Info{
Name: "mygear",
DisplayName: "My Gear",
Description: "What this gear does",
Version: "1.0.0",
Icon: "icon-name",
Category: "monitoring",
Core: false, // true = cannot be disabled
}
}The Initialize method receives dependencies from the framework:
func (g *Gear) Initialize(ctx context.Context, deps gear.Dependencies) error {
if err := g.BaseGear.Initialize(ctx, deps); err != nil {
return err
}
g.handlers = NewHandlers(deps)
return nil
}type Dependencies struct {
DB *sql.DB // Database connection
Logger *slog.Logger // Structured logger
EventHub EventPublisher // Event bus
Auth AuthChecker // Auth and permissions
Agent *agent.Client // Agent API client
Servers ServerRegistry // Server configurations
HTTPClient *http.Client // HTTP client for external requests
Config map[string]any // Gear configuration from DB
}Gears register HTTP routes that are mounted under their path:
func (g *Gear) RegisterRoutes(r chi.Router) {
// Mounted at /mygear/
r.Get("/", g.handlers.IndexPage)
r.Get("/details/{id}", g.handlers.DetailsPage)
}Note: API routes typically remain in framework/handler for now and access collectors directly. This is a transitional state.
Gears define their sidebar navigation item:
func (g *Gear) SidebarItem() *gear.SidebarConfig {
return &gear.SidebarConfig{
Path: "/mygear",
Icon: MyGearIcon(), // templ component
DefaultOrder: 50, // Sort order
RequiresPermission: "mygear:view", // Optional permission check
ShowAlways: false, // Show even when disabled
}
}Gears declare the permissions they use:
func (g *Gear) Permissions() []gear.PermissionDef {
return []gear.PermissionDef{
{
Component: "mygear",
Actions: []string{"view", "manage", "delete"},
Description: "View and manage gear features",
},
}
}Gears can provide a settings UI:
func (g *Gear) SettingsPage(config map[string]any) templ.Component {
return MyGearSettings(config) // templ component
}Return nil if the gear has no configurable settings.
Agent gears implement the gear.Gear interface:
type Gear interface {
Info() Info // Metadata
Initialize(ctx, deps) error // Setup
Start(ctx) error // Start background tasks
Stop(ctx) error // Cleanup
Health() HealthStatus // Health check
RegisterRoutes(r chi.Router) // HTTP API routes
EventTypes() []EventType // Events published
}The agent runs on hosts with very different software stacks. A gear can optionally implement ProbeableGear to declare whether its prerequisites are present on this host. Gears that probe non-available are skipped for the rest of the lifecycle — no Initialize, no Start, no routes, no collectors, no streamers.
type ProbeableGear interface {
Gear
// Probe runs before Initialize. It must be side-effect-free and fast.
Probe(ctx context.Context, deps Dependencies) ProbeResult
}
type ProbeStatus string
const (
ProbeStatusAvailable ProbeStatus = "available"
ProbeStatusNotInstalled ProbeStatus = "not_installed"
ProbeStatusInaccessible ProbeStatus = "inaccessible"
ProbeStatusDisabled ProbeStatus = "disabled"
)Gears that do not implement ProbeableGear are treated as always-available, so adoption is incremental.
The lifecycle becomes:
Probe → Initialize → Start → (later) Stop
After all gears are probed, the manager writes a single human-readable summary table to the journal so an operator can see at startup which gears apply on this box:
Gear probe summary:
GEAR STATUS REASON
certificates enabled
haproxy enabled
logs enabled
metrics enabled
security disabled neither fail2ban-client nor nft found on PATH
traffic enabled
updates enabled
The full reference — status semantics, per-gear contracts, container-mode considerations, and the rules for writing a good Probe() — lives in gearbox-agent/docs/gear-probes.md.
Important
"Skipped" means the gear's Initialize and Start are not called, so its collectors, streamers, and HTTP routes do not run. The compiled binary still contains the gear's code (gears are linked in via blank imports in cmd/gearbox-agent/main.go); Go does not support module unloading. The wins are runtime CPU/IO and heap allocations, not binary size.
Gears that collect data periodically also implement CollectorGear:
type CollectorGear interface {
Gear
Collectors() []Collector // Periodic data collection tasks
}
type Collector struct {
Name string // Collector name
Interval time.Duration // Collection interval
Collect func(ctx context.Context) (any, error) // Collection function
OnData func(data any) error // Data handler (publish events)
}internal/gears/mygear/
├── gear.go # Gear definition
├── handlers.go # HTTP API handlers
├── collector.go # Data collection logic (if CollectorGear)
└── README.md # Gear documentationpackage mygear
import "github.com/sarg3nt/gearbox-agent/internal/framework/gear"
func init() {
gear.Register(&Gear{})
}
type Gear struct {
gear.BaseGear
}
func (g *Gear) Info() gear.Info {
return gear.Info{
Name: "mygear",
DisplayName: "My Gear",
Description: "Collects data about X",
Version: "1.0.0",
Category: "monitoring",
Core: true,
}
}// Probe is optional. Implement it if your gear can be skipped on hosts
// that lack its prerequisites; see the "Probe Phase" section above.
func (g *Gear) Probe(ctx context.Context, deps gear.Dependencies) gear.ProbeResult {
if _, err := exec.LookPath("mytool"); err != nil {
return gear.ProbeNotInstalled("mytool not found on PATH")
}
return gear.ProbeAvailable("mytool found", nil)
}
func (g *Gear) Initialize(ctx context.Context, deps gear.Dependencies) error {
if err := g.BaseGear.Initialize(ctx, deps); err != nil {
return err
}
// Setup gear state
return nil
}
func (g *Gear) Start(ctx context.Context) error {
// Start background goroutines if needed
return nil
}
func (g *Gear) Stop(ctx context.Context) error {
// Cleanup resources
return nil
}type Dependencies struct {
Logger *slog.Logger // Structured logger
EventBus *events.Bus // Event bus for publishing
Config Config // Agent configuration
HAProxyStatsSocket string // HAProxy socket path
HAProxyStatsURL string // HAProxy stats URL
HAProxyConfigPath string // HAProxy config path
StatsInterval time.Duration // Stats collection interval
MetricsInterval time.Duration // Metrics collection interval
}Implement Collectors() to define periodic data collection:
func (g *Gear) Collectors() []gear.Collector {
return []gear.Collector{
{
Name: "mygear-data",
Interval: g.Deps().StatsInterval,
Collect: func(ctx context.Context) (any, error) {
return g.collectData()
},
OnData: func(data any) error {
return g.publishData(data)
},
},
}
}Gears publish events to notify the dashboard:
func (g *Gear) publishData(data any) error {
g.EventBus().Publish(events.Event{
Type: "mygear.updated",
Timestamp: time.Now(),
Data: map[string]any{
"collected_at": time.Now().UTC().Format(time.RFC3339),
"data": data,
},
})
return nil
}
func (g *Gear) EventTypes() []gear.EventType {
return []gear.EventType{
{
Name: "mygear.updated",
Description: "Published when data is collected",
Payload: "Data object with collected information",
},
}
}Gears expose REST APIs for the dashboard to query:
func (g *Gear) RegisterRoutes(r chi.Router) {
r.Get("/api/v1/mygear/data", g.handleGetData)
r.Post("/api/v1/mygear/action", g.handleAction)
}
func (g *Gear) handleGetData(w http.ResponseWriter, r *http.Request) {
data, err := g.collectData()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(data)
}The framework/agent package provides an HTTP client with 80+ methods for calling the gearbox-agent API (stats, certificates, logs, metrics, etc.). This is shared infrastructure, not gear code.
Challenge: How do gears use the agent client without tight coupling?
Each client gear defines an interface for the agent methods it needs. This provides:
- Self-documentation: Shows exactly what agent APIs the gear uses
- Testability: Easy to mock in tests
- Isolation: Changes to the agent client don't affect gears if their interface doesn't change
Step 1: Define your gear's agent interface in agent.go:
// internal/gears/certificates/agent.go
package certificates
import "github.com/sarg3nt/gearbox/internal/framework/agent"
// AgentClient defines the agent operations needed by the certificates gear.
type AgentClient interface {
GetCertificates() (*agent.CertificatesResponse, error)
RefreshCertificate(domain string) (*agent.RefreshCertificateResponse, error)
DownloadCertificate(domain string) ([]byte, string, error)
}
// Compile-time check that framework client implements our interface
var _ AgentClient = (*agent.Client)(nil)Step 2: Use the interface in your handlers:
// internal/gears/certificates/handlers.go
type Handlers struct {
agent AgentClient // Your interface, not *agent.Client
deps gear.Dependencies
}
func NewHandlers(deps gear.Dependencies) *Handlers {
return &Handlers{
agent: deps.Agent, // Satisfies AgentClient interface
deps: deps,
}
}Step 3: Use it in your code:
func (h *Handlers) GetCertificates(serverID string) {
certs, err := h.agent.GetCertificates()
// ...
}See internal/framework/agent/client.go for the complete API. Common categories:
- HAProxy:
GetStats(),GetInfo(),GetMetadata(),GetStickTables() - Certificates:
GetCertificates(),RefreshCertificate(),DownloadCertificate() - Logs:
GetLogSources(),GetLogs(name, lines) - Metrics:
GetMetrics() - Services:
GetServices(),ServiceControl(),GetAvailableServices() - Security:
GetSecuritySummary(),GetFail2BanStats(),GetFirewallStats() - Firewall:
GetBlockedIPs(),BlockIP(),UnblockIP(),CheckIPBlocked() - Traffic:
GetTraffic(),GetTrafficSummary() - Config Management:
GetHAProxyConfig(),UpdateHAProxyConfig(),GetFirewallConfig() - OS Updates:
GetUpdateStatus(),InstallUpdates(),GetUpdateHistory()
The agent client is infrastructure, like database/sql or http.Client. It:
- Provides connectivity to gearbox-agent
- Handles authentication, TLS, retries, WebSocket management
- Is used by multiple gears (stats, metrics, certificates all need it)
Moving it into individual gears would cause massive code duplication.
Feature flags control which gears are included in the compiled binary and their default state. This allows gradual rollout of experimental features without affecting production stability.
| State | Build | Default | Visible | Label | Description |
|---|---|---|---|---|---|
disabled |
❌ Not built | N/A | ❌ No | - | Excluded from binary entirely |
alpha |
✅ Built | ❌ Off | ✅ Yes | 🔬 Alpha | Early development, may change significantly |
beta |
✅ Built | ❌ Off | ✅ Yes | 🧪 Beta | Feature-complete, testing phase |
production |
✅ Built | Varies | ✅ Yes | - | Stable, ready for use |
Gears in alpha or beta state automatically display labels in the UI:
- Alpha: 🔬 "This feature is in early development and may change significantly"
- Beta: 🧪 "This feature is being tested and may have bugs"
These labels appear as tooltips/popovers when hovering over the gear name.
For Gearbox Client: gearbox/internal/gears/<gear>/gear.go
For Gearbox Agent: gearbox-agent/internal/gears/<gear>/gear.go
Add a feature flag constant at the top of gear.go:
package mygear
import "github.com/sarg3nt/gearbox/internal/framework/gear"
// Feature flag: disabled | alpha | beta | production
const featureFlag = "beta"
func init() {
gear.RegisterWithFlag(&Gear{}, featureFlag)
}Gears with featureFlag = "disabled" use Go build tags to exclude them:
In gear.go:
//go:build mygear
package mygear
const featureFlag = "disabled"
func init() {
gear.RegisterWithFlag(&Gear{}, featureFlag)
}Building:
# Standard build (excludes mygear)
go build -o server cmd/server/main.go
# Explicitly enable mygear
go build -tags mygear -o server cmd/server/main.goStandard Build (production features only):
FROM golang:1.25 AS builder
WORKDIR /app
COPY . .
RUN go build -o gearbox cmd/server/main.goBuild with Alpha/Beta Features:
FROM golang:1.25 AS builder
ARG ENABLE_ALPHA=false
ARG ENABLE_BETA=false
WORKDIR /app
COPY . .
RUN if [ "$ENABLE_ALPHA" = "true" ]; then \
BUILD_TAGS="alpha beta"; \
elif [ "$ENABLE_BETA" = "true" ]; then \
BUILD_TAGS="beta"; \
fi && \
go build -tags "$BUILD_TAGS" -o gearbox cmd/server/main.goUsing:
# Production build
docker build -t gearbox:latest .
# Beta build
docker build --build-arg ENABLE_BETA=true -t gearbox:beta .
# Alpha build (includes alpha + beta)
docker build --build-arg ENABLE_ALPHA=true -t gearbox:alpha .Gears in alpha and beta are disabled by default on first run. Admins must explicitly enable them in settings.
Gears in production can be:
- Enabled by default: Set
Core: trueinInfo()(cannot be disabled) - Disabled by default: Set
Core: falseand handle default state in database migrations
disabled → alpha → beta → production
↓ ↓ ↓ ↓
Not built Built Built Built
Off Off On/Off
🔬 🧪 Stable- Client gear? Add to
gearbox/internal/gears/(runs in web application) - Agent gear? Add to
gearbox-agent/internal/gears/(runs on servers)
# Client gear (web application)
mkdir -p gearbox/internal/gears/mygear
cd gearbox/internal/gears/mygear
# Agent gear (server-side)
mkdir -p gearbox-agent/internal/gears/mygear
cd gearbox-agent/internal/gears/mygearFor Client Gear:
// gear.go
package mygear
import (
"context"
"github.com/a-h/templ"
"github.com/go-chi/chi/v5"
"github.com/sarg3nt/gearbox/internal/framework/gear"
)
const featureFlag = "alpha" // disabled | alpha | beta | production
func init() {
gear.RegisterWithFlag(&Gear{}, featureFlag)
}
type Gear struct {
gear.BaseGear
handlers *Handlers
}
func (g *Gear) Info() gear.Info {
return gear.Info{
Name: "mygear",
DisplayName: "My Gear",
Description: "What this gear does",
Version: "1.0.0",
Icon: "icon-name",
Category: "monitoring",
Core: false,
}
}
func (g *Gear) Initialize(ctx context.Context, deps gear.Dependencies) error {
if err := g.BaseGear.Initialize(ctx, deps); err != nil {
return err
}
g.handlers = NewHandlers(deps)
return nil
}
func (g *Gear) RegisterRoutes(r chi.Router) {
r.Get("/", g.handlers.IndexPage)
}
func (g *Gear) SidebarItem() *gear.SidebarConfig {
return &gear.SidebarConfig{
Path: "/mygear",
Icon: MyGearIcon(),
DefaultOrder: 100,
RequiresPermission: "mygear:view",
}
}
func (g *Gear) SettingsPage(config map[string]any) templ.Component {
return nil // No settings page
}
func (g *Gear) Permissions() []gear.PermissionDef {
return []gear.PermissionDef{
{
Component: "mygear",
Actions: []string{"view"},
Description: "View gear data",
},
}
}// handlers.go
package mygear
import (
"net/http"
"github.com/sarg3nt/gearbox/internal/framework/gear"
)
type Handlers struct {
deps gear.Dependencies
}
func NewHandlers(deps gear.Dependencies) *Handlers {
return &Handlers{deps: deps}
}
func (h *Handlers) IndexPage(w http.ResponseWriter, r *http.Request) {
// Check permission
if !h.deps.Auth.HasPermission(r, "mygear", "view") {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
// Render page
w.Write([]byte("My Gear Page"))
}For Agent Gear:
// gear.go
package mygear
import (
"context"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/sarg3nt/gearbox-agent/internal/framework/gear"
)
const featureFlag = "production"
func init() {
gear.RegisterWithFlag(&Gear{}, featureFlag)
}
type Gear struct {
gear.BaseGear
}
func (g *Gear) Info() gear.Info {
return gear.Info{
Name: "mygear",
DisplayName: "My Gear",
Description: "Collects data about X",
Version: "1.0.0",
Category: "monitoring",
Core: true,
}
}
func (g *Gear) Initialize(ctx context.Context, deps gear.Dependencies) error {
return g.BaseGear.Initialize(ctx, deps)
}
func (g *Gear) Start(ctx context.Context) error {
return nil
}
func (g *Gear) Stop(ctx context.Context) error {
return nil
}
func (g *Gear) Health() gear.HealthStatus {
return gear.NewHealthyStatus("operational")
}
func (g *Gear) RegisterRoutes(r chi.Router) {
r.Get("/api/v1/mygear/data", g.handleGetData)
}
func (g *Gear) EventTypes() []gear.EventType {
return nil
}
func (g *Gear) handleGetData(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"ok"}`))
}The gear auto-registers via init(), but you must import it:
// cmd/server/main.go (gearbox)
package main
import (
_ "github.com/sarg3nt/gearbox/internal/gears/mygear"
// ... other imports
)// cmd/gearbox-agent/main.go (gearbox-agent)
package main
import (
_ "github.com/sarg3nt/gearbox-agent/internal/gears/mygear"
// ... other imports
)# Build
make templ-generate && make build
# Run locally
make dev-local
# Visit http://localhost:3000/mygear// handlers_test.go
package mygear
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/sarg3nt/gearbox/internal/framework/gear"
)
type mockAuth struct{}
func (m *mockAuth) HasPermission(r *http.Request, component, action string) bool {
return true // Always allow for testing
}
func TestIndexPage(t *testing.T) {
deps := gear.Dependencies{
Auth: &mockAuth{},
}
handlers := NewHandlers(deps)
req := httptest.NewRequest("GET", "/mygear", nil)
w := httptest.NewRecorder()
handlers.IndexPage(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
}
}// agent_test.go
package mygear
import "github.com/sarg3nt/gearbox/internal/framework/agent"
type mockAgentClient struct{}
func (m *mockAgentClient) GetCertificates() (*agent.CertificatesResponse, error) {
return &agent.CertificatesResponse{
Certificates: []agent.CertificateInfo{
{Domain: "example.com", DaysUntilExpiry: 30},
},
}, nil
}
// Use in tests:
// handlers := NewHandlers(gear.Dependencies{Agent: &mockAgentClient{}})// internal/gears/status/gear.go
package status
import (
"context"
"net/http"
"github.com/a-h/templ"
"github.com/go-chi/chi/v5"
"github.com/sarg3nt/gearbox/internal/framework/gear"
)
const featureFlag = "production"
func init() {
gear.RegisterWithFlag(&Gear{}, featureFlag)
}
type Gear struct {
gear.BaseGear
}
func (g *Gear) Info() gear.Info {
return gear.Info{
Name: "status",
DisplayName: "Status",
Description: "System status overview",
Version: "1.0.0",
Category: "core",
Core: true,
}
}
func (g *Gear) Initialize(ctx context.Context, deps gear.Dependencies) error {
return g.BaseGear.Initialize(ctx, deps)
}
func (g *Gear) RegisterRoutes(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Status: OK"))
})
}
func (g *Gear) SidebarItem() *gear.SidebarConfig {
return &gear.SidebarConfig{
Path: "/status",
DefaultOrder: 5,
ShowAlways: true,
}
}
func (g *Gear) SettingsPage(config map[string]any) templ.Component {
return nil
}
func (g *Gear) Permissions() []gear.PermissionDef {
return nil // Public
}// internal/gears/uptime/gear.go (gearbox-agent)
package uptime
import (
"context"
"encoding/json"
"net/http"
"os/exec"
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/sarg3nt/gearbox-agent/internal/framework/events"
"github.com/sarg3nt/gearbox-agent/internal/framework/gear"
)
const featureFlag = "production"
func init() {
gear.RegisterWithFlag(&Gear{}, featureFlag)
}
type Gear struct {
gear.BaseGear
}
func (g *Gear) Info() gear.Info {
return gear.Info{
Name: "uptime",
DisplayName: "System Uptime",
Description: "Tracks system uptime",
Version: "1.0.0",
Category: "monitoring",
Core: false,
}
}
func (g *Gear) Initialize(ctx context.Context, deps gear.Dependencies) error {
return g.BaseGear.Initialize(ctx, deps)
}
func (g *Gear) Start(ctx context.Context) error {
return nil
}
func (g *Gear) Stop(ctx context.Context) error {
return nil
}
func (g *Gear) Health() gear.HealthStatus {
return gear.NewHealthyStatus("operational")
}
func (g *Gear) Collectors() []gear.Collector {
return []gear.Collector{
{
Name: "uptime",
Interval: 60 * time.Second,
Collect: g.collectUptime,
OnData: g.publishUptime,
},
}
}
func (g *Gear) collectUptime() (any, error) {
out, err := exec.Command("uptime", "-p").Output()
if err != nil {
return nil, err
}
return strings.TrimSpace(string(out)), nil
}
func (g *Gear) publishUptime(data any) error {
g.EventBus().Publish(events.Event{
Type: "uptime.updated",
Timestamp: time.Now(),
Data: map[string]any{
"uptime": data,
},
})
return nil
}
func (g *Gear) RegisterRoutes(r chi.Router) {
r.Get("/api/v1/uptime", func(w http.ResponseWriter, r *http.Request) {
uptime, _ := g.collectUptime()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{"uptime": uptime})
})
}
func (g *Gear) EventTypes() []gear.EventType {
return []gear.EventType{
{
Name: "uptime.updated",
Description: "System uptime collected",
Payload: "uptime string",
},
}
}
var (
_ gear.Gear = (*Gear)(nil)
_ gear.CollectorGear = (*Gear)(nil)
)- Gears are compiled into the binary - No runtime loading, full type safety
- Framework provides infrastructure - Gears implement features
- Agent facades prevent coupling - Gears define their own agent interfaces
- Feature flags control rollout - Disabled, alpha, beta, production states
- Both apps use the same pattern - Gearbox and gearbox-agent have parallel gear systems
Create a gear when you want to:
- Add a new monitoring page to the web application (client gear)
- Collect new types of data on servers (agent gear)
- Expose new agent APIs (agent gear)
- Add a feature that can be enabled/disabled per server
Don't create a gear for:
- Shared infrastructure (auth, database, HTTP client)
- Cross-cutting concerns (logging, metrics)
- Framework modifications
- See examples/ for complete gear implementations
- Check existing gears in
internal/gears/for patterns - Read TASKS.md for planned gear features
Last Updated: 2026-01-31 | Document Version: 0.2.0