From e3986fb064da5cf822e7f487d99f8f8758de316f Mon Sep 17 00:00:00 2001 From: Virgil Date: Sat, 4 Apr 2026 04:00:30 +0000 Subject: [PATCH] Refine AX naming and usage comments --- cmd/mining/cmd/doctor.go | 13 ++-- pkg/mining/manager.go | 71 ++++++++++---------- pkg/mining/repository.go | 14 ++-- pkg/mining/service.go | 138 +++++++++++++++++++-------------------- 4 files changed, 115 insertions(+), 121 deletions(-) diff --git a/cmd/mining/cmd/doctor.go b/cmd/mining/cmd/doctor.go index fd3db25..84fab2a 100644 --- a/cmd/mining/cmd/doctor.go +++ b/cmd/mining/cmd/doctor.go @@ -15,16 +15,13 @@ import ( const signpostFilename = ".installed-miners" -// validateConfigPath validates that a config path is within the expected XDG config directory -// This prevents path traversal attacks via manipulated signpost files +// validateConfigPath("/home/alice/.config/lethean-desktop/miners/config.json") // nil +// validateConfigPath("/tmp/config.json") // error func validateConfigPath(configPath string) error { - // Get the expected XDG config base directory expectedBase := filepath.Join(xdg.ConfigHome, "lethean-desktop") - // Clean and resolve the config path cleanPath := filepath.Clean(configPath) - // Check if the path is within the expected directory if !strings.HasPrefix(cleanPath, expectedBase+string(os.PathSeparator)) && cleanPath != expectedBase { return fmt.Errorf("invalid config path: must be within %s", expectedBase) } @@ -45,7 +42,7 @@ var doctorCmd = &cobra.Command{ if err := updateDoctorCache(); err != nil { return fmt.Errorf("failed to run doctor check: %w", err) } - // After updating the cache, display the fresh results + // loadAndDisplayCache() // prints the refreshed miner summary after updateDoctorCache() _, err := loadAndDisplayCache() return err }, @@ -69,7 +66,7 @@ func loadAndDisplayCache() (bool, error) { } configPath := strings.TrimSpace(string(configPathBytes)) - // Security: Validate that the config path is within the expected directory + // validateConfigPath("/home/alice/.config/lethean-desktop/miners/config.json") // blocks path traversal if err := validateConfigPath(configPath); err != nil { return false, fmt.Errorf("security error: %w", err) } @@ -93,7 +90,7 @@ func loadAndDisplayCache() (bool, error) { fmt.Println() for _, details := range systemInfo.InstalledMinersInfo { - // Infer miner name from path for display purposes + // details.Path = "/home/alice/.local/share/lethean-desktop/miners/xmrig" -> "XMRig" var minerName string if details.Path != "" { if strings.Contains(details.Path, "xmrig") { diff --git a/pkg/mining/manager.go b/pkg/mining/manager.go index 3955bce..c66ba51 100644 --- a/pkg/mining/manager.go +++ b/pkg/mining/manager.go @@ -13,20 +13,20 @@ import ( "forge.lthn.ai/Snider/Mining/pkg/logging" ) -// equalFold("xmrig", "XMRig") == true -// equalFold("tt-miner", "TT-Miner") == true +// equalFold("xmrig", "XMRig") // true +// equalFold("tt-miner", "TT-Miner") // true func equalFold(left, right string) bool { return bytes.EqualFold([]byte(left), []byte(right)) } -// hasPrefix("xmrig-rx0", "xmrig") == true -// hasPrefix("ttminer-rtx", "xmrig") == false +// hasPrefix("xmrig-rx0", "xmrig") // true +// hasPrefix("ttminer-rtx", "xmrig") // false func hasPrefix(input, prefix string) bool { return len(input) >= len(prefix) && input[:len(prefix)] == prefix } -// containsStr("peer not found", "not found") == true -// containsStr("connection ok", "not found") == false +// containsStr("peer not found", "not found") // true +// containsStr("connection ok", "not found") // false func containsStr(haystack, needle string) bool { if len(needle) == 0 { return true @@ -42,7 +42,7 @@ func containsStr(haystack, needle string) bool { return false } -// safe := instanceNameRegex.ReplaceAllString("my algo!", "_") // => "my_algo_" +// safeName := instanceNameRegex.ReplaceAllString("my algo!", "_") // "my_algo_" var instanceNameRegex = regexp.MustCompile(`[^a-zA-Z0-9_/-]`) // var managerInterface ManagerInterface = mining.NewManager() @@ -96,7 +96,7 @@ func (m *Manager) emitEvent(eventType EventType, data interface{}) { var _ ManagerInterface = (*Manager)(nil) // manager := mining.NewManager() -// defer manager.Stop() +// defer manager.Stop() // stops miner goroutines and the hourly database cleanup loop func NewManager() *Manager { manager := &Manager{ miners: make(map[string]Miner), @@ -111,7 +111,7 @@ func NewManager() *Manager { } // manager := mining.NewManagerForSimulation() -// manager.StartMiner(ctx, "xmrig", &mining.Config{Algo: "rx/0"}) +// manager.StartMiner(ctx, "xmrig", &Config{Algo: "rx/0"}) func NewManagerForSimulation() *Manager { manager := &Manager{ miners: make(map[string]Miner), @@ -122,7 +122,7 @@ func NewManagerForSimulation() *Manager { return manager } -// m.initDatabase() // called in NewManager(); enables persistence if MinersConfig.Database.Enabled == true +// m.initDatabase() // NewManager() calls this after loading miners.json, for example with Database.Enabled = true func (m *Manager) initDatabase() { minersConfiguration, err := LoadMinersConfig() if err != nil { @@ -154,11 +154,11 @@ func (m *Manager) initDatabase() { logging.Info("database persistence enabled", logging.Fields{"retention_days": m.databaseRetention}) - // Start periodic cleanup + // m.startDBCleanup() // keeps database.Cleanup(30) running after persistence is enabled m.startDBCleanup() } -// m.startDBCleanup() // called after initDatabase(); purges rows older than m.databaseRetention days once per hour +// m.startDBCleanup() // runs database.Cleanup(30) once an hour after persistence is enabled func (m *Manager) startDBCleanup() { m.waitGroup.Add(1) go func() { @@ -168,11 +168,11 @@ func (m *Manager) startDBCleanup() { logging.Error("panic in database cleanup goroutine", logging.Fields{"panic": r}) } }() - // Run cleanup once per hour + // ticker := time.NewTicker(time.Hour) // checks for expired rows every 60 minutes ticker := time.NewTicker(time.Hour) defer ticker.Stop() - // Run initial cleanup + // database.Cleanup(30) // removes rows older than 30 days during startup if err := database.Cleanup(m.databaseRetention); err != nil { logging.Warn("database cleanup failed", logging.Fields{"error": err}) } @@ -190,8 +190,7 @@ func (m *Manager) startDBCleanup() { }() } -// m.syncMinersConfig() // called on startup; adds {MinerType: "xmrig", Autostart: false} for any -// registered miner not yet present in miners.json — existing entries are left unchanged. +// m.syncMinersConfig() // when miners.json only contains tt-miner, this adds xmrig with Autostart=false func (m *Manager) syncMinersConfig() { minersConfiguration, err := LoadMinersConfig() if err != nil { @@ -214,7 +213,7 @@ func (m *Manager) syncMinersConfig() { minersConfiguration.Miners = append(minersConfiguration.Miners, MinerAutostartConfig{ MinerType: availableMiner.Name, Autostart: false, - Config: nil, // No default config + Config: nil, // keep the new miner disabled until the user saves a profile }) configUpdated = true logging.Info("added default config for missing miner", logging.Fields{"miner": availableMiner.Name}) @@ -228,7 +227,7 @@ func (m *Manager) syncMinersConfig() { } } -// m.autostartMiners() // called in NewManager(); reads miners.json and starts any entry with Autostart == true +// m.autostartMiners() // NewManager() uses this to start xmrig when miners.json contains Autostart=true func (m *Manager) autostartMiners() { minersConfiguration, err := LoadMinersConfig() if err != nil { @@ -262,7 +261,7 @@ func findAvailablePort() (int, error) { return listener.Addr().(*net.TCPAddr).Port, nil } -// miner, err := manager.StartMiner(ctx, "xmrig", &mining.Config{Algo: "rx/0"}) +// miner, err := manager.StartMiner(ctx, "xmrig", &Config{Algo: "rx/0"}) func (m *Manager) StartMiner(ctx context.Context, minerType string, config *Config) (Miner, error) { // Check for cancellation before acquiring lock select { @@ -324,13 +323,13 @@ func (m *Manager) StartMiner(ctx context.Context, minerType string, config *Conf } } - // Emit starting event before actually starting + // m.emitEvent(EventMinerStarting, MinerEventData{Name: "xmrig-rx_0"}) // fires before miner.Start(config) m.emitEvent(EventMinerStarting, MinerEventData{ Name: instanceName, }) if err := miner.Start(config); err != nil { - // Emit error event + // m.emitEvent(EventMinerError, MinerEventData{Name: "xmrig-rx_0", Error: err.Error()}) m.emitEvent(EventMinerError, MinerEventData{ Name: instanceName, Error: err.Error(), @@ -414,7 +413,7 @@ func (m *Manager) UninstallMiner(ctx context.Context, minerType string) error { }) } -// m.updateMinerConfig("xmrig", true, config) // persists autostart=true and last-used config to miners.json +// m.updateMinerConfig("xmrig", true, config) // saves Autostart=true and the last-used config back to miners.json func (m *Manager) updateMinerConfig(minerType string, autostart bool, config *Config) error { return UpdateMinersConfig(func(configuration *MinersConfig) error { found := false @@ -498,7 +497,7 @@ func (m *Manager) StopMiner(ctx context.Context, name string) error { return nil } -// miner, err := m.GetMiner("xmrig-randomx") +// miner, err := m.GetMiner("xmrig-randomx") // returns ErrMinerNotFound when the name is missing // if err != nil { /* miner not found */ } func (m *Manager) GetMiner(name string) (Miner, error) { m.mutex.RLock() @@ -522,8 +521,8 @@ func (m *Manager) ListMiners() []Miner { return miners } -// sim := NewSimulatedMiner(SimulatedMinerConfig{Name: "sim-rx0"}) -// if err := manager.RegisterMiner(sim); err != nil { return err } +// simulatedMiner := NewSimulatedMiner(SimulatedMinerConfig{Name: "sim-rx0"}) +// if err := manager.RegisterMiner(simulatedMiner); err != nil { return err } func (m *Manager) RegisterMiner(miner Miner) error { name := miner.GetName() @@ -545,7 +544,7 @@ func (m *Manager) RegisterMiner(miner Miner) error { return nil } -// for _, available := range manager.ListAvailableMiners() { logging.Info(available.Name, nil) } +// for _, availableMiner := range manager.ListAvailableMiners() { logging.Info(availableMiner.Name, nil) } func (m *Manager) ListAvailableMiners() []AvailableMiner { return []AvailableMiner{ { @@ -559,7 +558,7 @@ func (m *Manager) ListAvailableMiners() []AvailableMiner { } } -// m.startStatsCollection() // called in NewManager(); polls all active miners every HighResolutionInterval +// m.startStatsCollection() // NewManager() uses this to poll each running miner every HighResolutionInterval func (m *Manager) startStatsCollection() { m.waitGroup.Add(1) go func() { @@ -586,7 +585,7 @@ func (m *Manager) startStatsCollection() { // ctx, cancel := context.WithTimeout(ctx, statsCollectionTimeout) const statsCollectionTimeout = 5 * time.Second -// m.collectMinerStats() // called by startStatsCollection ticker; gathers stats from all active miners in parallel +// m.collectMinerStats() // the stats ticker calls this to poll all running miners in parallel func (m *Manager) collectMinerStats() { // Take a snapshot of miners under read lock - minimize lock duration m.mutex.RLock() @@ -610,11 +609,11 @@ func (m *Manager) collectMinerStats() { now := time.Now() // Collect stats from all miners in parallel - var waitGroup sync.WaitGroup + var collectionWaitGroup sync.WaitGroup for _, entry := range miners { - waitGroup.Add(1) + collectionWaitGroup.Add(1) go func(miner Miner, minerType string) { - defer waitGroup.Done() + defer collectionWaitGroup.Done() defer func() { if r := recover(); r != nil { logging.Error("panic in single miner stats collection", logging.Fields{ @@ -626,7 +625,7 @@ func (m *Manager) collectMinerStats() { m.collectSingleMinerStats(miner, minerType, now, databaseEnabled) }(entry.miner, entry.minerType) } - waitGroup.Wait() + collectionWaitGroup.Wait() } // for attempt := 0; attempt <= statsRetryCount; attempt++ { ... } @@ -693,9 +692,9 @@ func (m *Manager) collectSingleMinerStats(miner Miner, minerType string, now tim Timestamp: point.Timestamp, Hashrate: point.Hashrate, } - // Create a new context for DB writes (original context is from retry loop) - databaseContext, databaseCancel := context.WithTimeout(context.Background(), statsCollectionTimeout) - if err := database.InsertHashratePoint(databaseContext, minerName, minerType, databasePoint, database.ResolutionHigh); err != nil { + // database.InsertHashratePoint(ctx, "xmrig-rx_0", databasePoint, database.ResolutionHigh) // persists a single sample + databaseWriteContext, databaseCancel := context.WithTimeout(context.Background(), statsCollectionTimeout) + if err := database.InsertHashratePoint(databaseWriteContext, minerName, minerType, databasePoint, database.ResolutionHigh); err != nil { logging.Warn("failed to persist hashrate", logging.Fields{"miner": minerName, "error": err}) } databaseCancel() @@ -728,7 +727,7 @@ func (m *Manager) GetMinerHashrateHistory(name string) ([]HashratePoint, error) // ctx, cancel := context.WithTimeout(context.Background(), ShutdownTimeout) const ShutdownTimeout = 10 * time.Second -// defer manager.Stop() // safe in main() or test cleanup; subsequent calls are no-ops +// defer manager.Stop() // stops miners, waits for goroutines, and closes the database during shutdown func (m *Manager) Stop() { m.stopOnce.Do(func() { // Stop all running miners first diff --git a/pkg/mining/repository.go b/pkg/mining/repository.go index 28bc0a2..6dbcdc8 100644 --- a/pkg/mining/repository.go +++ b/pkg/mining/repository.go @@ -7,7 +7,7 @@ import ( "sync" ) -// var repo Repository[MinersConfig] = NewFileRepository[MinersConfig](path) +// repo := NewFileRepository[MinersConfig]("/home/alice/.config/lethean-desktop/miners.json") // data, err := repo.Load() type Repository[T any] interface { // data, err := repo.Load() @@ -20,7 +20,7 @@ type Repository[T any] interface { Update(modifier func(*T) error) error } -// repo := NewFileRepository[MinersConfig](path, WithDefaults(defaultMinersConfig)) +// repo := NewFileRepository[MinersConfig]("/home/alice/.config/lethean-desktop/miners.json", WithDefaults(defaultMinersConfig)) // data, err := repo.Load() type FileRepository[T any] struct { mutex sync.RWMutex @@ -28,17 +28,17 @@ type FileRepository[T any] struct { defaults func() T } -// repo := NewFileRepository[MinersConfig](path, WithDefaults(defaultMinersConfig), myOption) +// repo := NewFileRepository[MinersConfig]("/home/alice/.config/lethean-desktop/miners.json", WithDefaults(defaultMinersConfig), myOption) type FileRepositoryOption[T any] func(*FileRepository[T]) -// repo := NewFileRepository[MinersConfig](path, WithDefaults(defaultMinersConfig)) +// repo := NewFileRepository[MinersConfig]("/home/alice/.config/lethean-desktop/miners.json", WithDefaults(defaultMinersConfig)) func WithDefaults[T any](defaultsProvider func() T) FileRepositoryOption[T] { return func(repo *FileRepository[T]) { repo.defaults = defaultsProvider } } -// repo := NewFileRepository[MinersConfig](path, WithDefaults(defaultMinersConfig)) +// repo := NewFileRepository[MinersConfig]("/home/alice/.config/lethean-desktop/miners.json", WithDefaults(defaultMinersConfig)) func NewFileRepository[T any](path string, options ...FileRepositoryOption[T]) *FileRepository[T] { repo := &FileRepository[T]{ path: path, @@ -83,7 +83,7 @@ func (repository *FileRepository[T]) Save(data T) error { return repository.saveUnlocked(data) } -// return repository.saveUnlocked(data) // called by Save and Update while mutex is held +// repository.saveUnlocked(updatedConfig) // used by Save() and Update() while the mutex is already held func (repository *FileRepository[T]) saveUnlocked(data T) error { dir := filepath.Dir(repository.path) if err := os.MkdirAll(dir, 0755); err != nil { @@ -132,7 +132,7 @@ func (repository *FileRepository[T]) Update(modifier func(*T) error) error { return repository.saveUnlocked(data) } -// path := repo.Path() // => "/home/user/.config/lethean-desktop/miners.json" +// path := repo.Path() // path == "/home/alice/.config/lethean-desktop/miners.json" func (repository *FileRepository[T]) Path() string { return repository.path } diff --git a/pkg/mining/service.go b/pkg/mining/service.go index 9d374d1..a1aa007 100644 --- a/pkg/mining/service.go +++ b/pkg/mining/service.go @@ -133,7 +133,7 @@ func isRetryableError(status int) bool { status == http.StatusGatewayTimeout } -// router.Use(securityHeadersMiddleware()) // X-Content-Type-Options, X-Frame-Options, CSP on every response +// router.Use(securityHeadersMiddleware()) // sets X-Content-Type-Options=nosniff and CSP=default-src 'none' on every response func securityHeadersMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // Prevent MIME type sniffing @@ -288,7 +288,7 @@ const ( CachePublic5Min = "public, max-age=300" ) -// router.Use(cacheMiddleware()) // sets Cache-Control: no-store on mutating routes, max-age=30 on read-only routes +// router.Use(cacheMiddleware()) // serves /miners/available with Cache-Control: public, max-age=300 func cacheMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // Only cache GET requests @@ -320,7 +320,7 @@ func cacheMiddleware() gin.HandlerFunc { } } -// router.Use(requestTimeoutMiddleware(30 * time.Second)) +// router.Use(requestTimeoutMiddleware(30 * time.Second)) // aborts a slow /history/miners/xmrig request after 30 seconds func requestTimeoutMiddleware(timeout time.Duration) gin.HandlerFunc { return func(c *gin.Context) { // Skip timeout for WebSocket upgrades and streaming endpoints @@ -407,29 +407,29 @@ func NewService(manager ManagerInterface, listenAddr string, displayAddr string, } } - // Initialize node service (optional - only fails if XDG paths are broken) + // NewNodeService() // falls back to miner-only API mode when XDG paths are unavailable nodeService, err := NewNodeService() if err != nil { logging.Warn("failed to initialize node service", logging.Fields{"error": err}) // Continue without node service - P2P features will be unavailable } - // Initialize event hub for WebSocket real-time updates + // NewEventHub() // broadcasts miner events to /ws/events clients eventHub := NewEventHub() go eventHub.Run() - // Wire up event hub to manager for miner events + // concreteManager.SetEventHub(eventHub) // lets Manager broadcast start/stop/stat events if concreteManager, ok := manager.(*Manager); ok { concreteManager.SetEventHub(eventHub) } - // Set up state provider for WebSocket state sync on reconnect + // eventHub.SetStateProvider(...) // returns running miner state after a reconnect eventHub.SetStateProvider(func() interface{} { miners := manager.ListMiners() if len(miners) == 0 { return nil } - // Return current state of all miners + // state := []map[string]interface{}{{"name": "xmrig-rx_0", "status": "running"}} state := make([]map[string]interface{}, 0, len(miners)) for _, miner := range miners { stats, _ := miner.GetStats(context.Background()) @@ -450,7 +450,7 @@ func NewService(manager ManagerInterface, listenAddr string, displayAddr string, } }) - // Initialize authentication from environment + // AuthConfigFromEnv() // enables digest auth when MINING_API_USERNAME and MINING_API_PASSWORD are set authConfig := AuthConfigFromEnv() var auth *DigestAuth if authConfig.Enabled { @@ -479,11 +479,11 @@ func NewService(manager ManagerInterface, listenAddr string, displayAddr string, } // service.InitRouter() -// http.Handle("/", service.Router) // embed as handler in Wails or another HTTP server +// http.Handle("/", service.Router) // embeds the API under a parent HTTP server in Wails func (s *Service) InitRouter() { s.Router = gin.Default() - // Extract port safely from server address for CORS + // s.Server.Addr = ":9090" -> serverPort = "9090" for local CORS origins serverPort := "9090" // default fallback if s.Server.Addr != "" { if _, port, err := net.SplitHostPort(s.Server.Addr); err == nil && port != "" { @@ -510,39 +510,38 @@ func (s *Service) InitRouter() { } s.Router.Use(cors.New(corsConfig)) - // Add security headers (SEC-LOW-4) + // s.Router.Use(securityHeadersMiddleware()) // sets security headers on every API response s.Router.Use(securityHeadersMiddleware()) - // Add Content-Type validation for POST/PUT (API-MED-8) + // s.Router.Use(contentTypeValidationMiddleware()) // rejects POST /miners/xmrig/install without application/json s.Router.Use(contentTypeValidationMiddleware()) - // Add request body size limit middleware (1MB max) + // c.Request.Body = http.MaxBytesReader(..., 1<<20) // caps request bodies at 1 MiB s.Router.Use(func(c *gin.Context) { c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 1<<20) // 1MB c.Next() }) - // Add CSRF protection for browser requests (SEC-MED-3) - // Requires X-Requested-With or Authorization header for state-changing methods + // s.Router.Use(csrfMiddleware()) // allows API clients with Authorization or X-Requested-With s.Router.Use(csrfMiddleware()) - // Add request timeout middleware (RESIL-MED-8) + // s.Router.Use(requestTimeoutMiddleware(DefaultRequestTimeout)) // aborts stalled requests after 30 seconds s.Router.Use(requestTimeoutMiddleware(DefaultRequestTimeout)) - // Add cache headers middleware (API-MED-7) + // s.Router.Use(cacheMiddleware()) // returns Cache-Control: public, max-age=300 for /miners/available s.Router.Use(cacheMiddleware()) - // Add X-Request-ID middleware for request tracing + // s.Router.Use(requestIDMiddleware()) // preserves the incoming X-Request-ID or creates a new one s.Router.Use(requestIDMiddleware()) - // Add rate limiting (10 requests/second with burst of 20) + // NewRateLimiter(10, 20) // allows bursts of 20 with a 10 requests/second refill rate s.rateLimiter = NewRateLimiter(10, 20) s.Router.Use(s.rateLimiter.Middleware()) s.SetupRoutes() } -// service.Stop() // called by ServiceStartup on ctx.Done(); also safe to call directly for embedded use +// service.Stop() // stops rate limiting, auth, event hub, and node transport during shutdown func (s *Service) Stop() { if s.rateLimiter != nil { s.rateLimiter.Stop() @@ -560,29 +559,29 @@ func (s *Service) Stop() { } } -// service.ServiceStartup(ctx) // blocks until ctx is cancelled; use InitRouter() to embed without a server +// service.ServiceStartup(ctx) // starts the HTTP server and stops it when ctx.Done() fires func (s *Service) ServiceStartup(ctx context.Context) error { s.InitRouter() s.Server.Handler = s.Router - // Channel to capture server startup errors - errChan := make(chan error, 1) + // serverErrorChannel captures ListenAndServe failures without blocking startup + serverErrorChannel := make(chan error, 1) go func() { if err := s.Server.ListenAndServe(); err != nil && err != http.ErrServerClosed { logging.Error("server error", logging.Fields{"addr": s.Server.Addr, "error": err}) - errChan <- err + serverErrorChannel <- err } - close(errChan) // Prevent goroutine leak + close(serverErrorChannel) // prevent goroutine leak }() go func() { <-ctx.Done() s.Stop() // Clean up service resources (auth, event hub, node service) s.Manager.Stop() - ctxShutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second) + shutdownContext, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - if err := s.Server.Shutdown(ctxShutdown); err != nil { + if err := s.Server.Shutdown(shutdownContext); err != nil { logging.Error("server shutdown error", logging.Fields{"error": err}) } }() @@ -591,7 +590,7 @@ func (s *Service) ServiceStartup(ctx context.Context) error { maxRetries := 50 // 50 * 100ms = 5 seconds max for i := 0; i < maxRetries; i++ { select { - case err := <-errChan: + case err := <-serverErrorChannel: if err != nil { return ErrInternal("failed to start server").WithCause(err) } @@ -613,64 +612,64 @@ func (s *Service) ServiceStartup(ctx context.Context) error { // service.InitRouter() // service.SetupRoutes() // re-call after adding middleware manually func (s *Service) SetupRoutes() { - apiGroup := s.Router.Group(s.APIBasePath) + apiRouterGroup := s.Router.Group(s.APIBasePath) // Health endpoints (no auth required for orchestration/monitoring) - apiGroup.GET("/health", s.handleHealth) - apiGroup.GET("/ready", s.handleReady) + apiRouterGroup.GET("/health", s.handleHealth) + apiRouterGroup.GET("/ready", s.handleReady) // Apply authentication middleware if enabled if s.auth != nil { - apiGroup.Use(s.auth.Middleware()) + apiRouterGroup.Use(s.auth.Middleware()) } { - apiGroup.GET("/info", s.handleGetInfo) - apiGroup.GET("/metrics", s.handleMetrics) - apiGroup.POST("/doctor", s.handleDoctor) - apiGroup.POST("/update", s.handleUpdateCheck) + apiRouterGroup.GET("/info", s.handleGetInfo) + apiRouterGroup.GET("/metrics", s.handleMetrics) + apiRouterGroup.POST("/doctor", s.handleDoctor) + apiRouterGroup.POST("/update", s.handleUpdateCheck) - minersGroup := apiGroup.Group("/miners") + minersRouterGroup := apiRouterGroup.Group("/miners") { - minersGroup.GET("", s.handleListMiners) - minersGroup.GET("/available", s.handleListAvailableMiners) - minersGroup.POST("/:miner_name/install", s.handleInstallMiner) - minersGroup.DELETE("/:miner_name/uninstall", s.handleUninstallMiner) - minersGroup.DELETE("/:miner_name", s.handleStopMiner) - minersGroup.GET("/:miner_name/stats", s.handleGetMinerStats) - minersGroup.GET("/:miner_name/hashrate-history", s.handleGetMinerHashrateHistory) - minersGroup.GET("/:miner_name/logs", s.handleGetMinerLogs) - minersGroup.POST("/:miner_name/stdin", s.handleMinerStdin) + minersRouterGroup.GET("", s.handleListMiners) + minersRouterGroup.GET("/available", s.handleListAvailableMiners) + minersRouterGroup.POST("/:miner_name/install", s.handleInstallMiner) + minersRouterGroup.DELETE("/:miner_name/uninstall", s.handleUninstallMiner) + minersRouterGroup.DELETE("/:miner_name", s.handleStopMiner) + minersRouterGroup.GET("/:miner_name/stats", s.handleGetMinerStats) + minersRouterGroup.GET("/:miner_name/hashrate-history", s.handleGetMinerHashrateHistory) + minersRouterGroup.GET("/:miner_name/logs", s.handleGetMinerLogs) + minersRouterGroup.POST("/:miner_name/stdin", s.handleMinerStdin) } // Historical data endpoints (database-backed) - historyGroup := apiGroup.Group("/history") + historyRouterGroup := apiRouterGroup.Group("/history") { - historyGroup.GET("/status", s.handleHistoryStatus) - historyGroup.GET("/miners", s.handleAllMinersHistoricalStats) - historyGroup.GET("/miners/:miner_name", s.handleMinerHistoricalStats) - historyGroup.GET("/miners/:miner_name/hashrate", s.handleMinerHistoricalHashrate) + historyRouterGroup.GET("/status", s.handleHistoryStatus) + historyRouterGroup.GET("/miners", s.handleAllMinersHistoricalStats) + historyRouterGroup.GET("/miners/:miner_name", s.handleMinerHistoricalStats) + historyRouterGroup.GET("/miners/:miner_name/hashrate", s.handleMinerHistoricalHashrate) } - profilesGroup := apiGroup.Group("/profiles") + profilesRouterGroup := apiRouterGroup.Group("/profiles") { - profilesGroup.GET("", s.handleListProfiles) - profilesGroup.POST("", s.handleCreateProfile) - profilesGroup.GET("/:id", s.handleGetProfile) - profilesGroup.PUT("/:id", s.handleUpdateProfile) - profilesGroup.DELETE("/:id", s.handleDeleteProfile) - profilesGroup.POST("/:id/start", s.handleStartMinerWithProfile) + profilesRouterGroup.GET("", s.handleListProfiles) + profilesRouterGroup.POST("", s.handleCreateProfile) + profilesRouterGroup.GET("/:id", s.handleGetProfile) + profilesRouterGroup.PUT("/:id", s.handleUpdateProfile) + profilesRouterGroup.DELETE("/:id", s.handleDeleteProfile) + profilesRouterGroup.POST("/:id/start", s.handleStartMinerWithProfile) } // WebSocket endpoint for real-time events - wsGroup := apiGroup.Group("/ws") + websocketRouterGroup := apiRouterGroup.Group("/ws") { - wsGroup.GET("/events", s.handleWebSocketEvents) + websocketRouterGroup.GET("/events", s.handleWebSocketEvents) } // Add P2P node endpoints if node service is available if s.NodeService != nil { - s.NodeService.SetupRoutes(apiGroup) + s.NodeService.SetupRoutes(apiRouterGroup) } } @@ -683,8 +682,7 @@ func (s *Service) SetupRoutes() { swaggerURL := ginSwagger.URL("http://" + s.DisplayAddr + s.SwaggerUIPath + "/doc.json") s.Router.GET(s.SwaggerUIPath+"/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, swaggerURL, ginSwagger.InstanceName(s.SwaggerInstanceName))) - // Initialize MCP server for AI assistant integration - // This exposes API endpoints as MCP tools for Claude, Cursor, etc. + // ginmcp.New(s.Router, ...) // exposes the API as MCP tools for Claude or Cursor s.mcpServer = ginmcp.New(s.Router, &ginmcp.Config{ Name: "Mining API", Description: "Mining dashboard API exposed via Model Context Protocol (MCP)", @@ -725,7 +723,7 @@ func (s *Service) handleReady(c *gin.Context) { components := make(map[string]string) allReady := true - // Check manager + // s.Manager != nil -> "manager": "ready" if s.Manager != nil { components["manager"] = "ready" } else { @@ -733,15 +731,15 @@ func (s *Service) handleReady(c *gin.Context) { allReady = false } - // Check profile manager + // s.ProfileManager != nil -> "profiles": "ready" if s.ProfileManager != nil { components["profiles"] = "ready" } else { components["profiles"] = "degraded" - // Don't fail readiness for degraded profile manager + // keep readiness green when only profile loading is degraded } - // Check event hub + // s.EventHub != nil -> "events": "ready" if s.EventHub != nil { components["events"] = "ready" } else { @@ -749,7 +747,7 @@ func (s *Service) handleReady(c *gin.Context) { allReady = false } - // Check node service (optional) + // s.NodeService != nil -> "p2p": "ready" if s.NodeService != nil { components["p2p"] = "ready" } else { @@ -796,7 +794,7 @@ func (s *Service) updateInstallationCache() (*SystemInfo, error) { Architecture: runtime.GOARCH, GoVersion: runtime.Version(), AvailableCPUCores: runtime.NumCPU(), - InstalledMinersInfo: []*InstallationDetails{}, // Initialize as empty slice + InstalledMinersInfo: []*InstallationDetails{}, // keep /info responses stable when no miners are installed } vMem, err := mem.VirtualMemory()