2025-11-09 01:02:31 +00:00
package mining
import (
"context"
2025-12-29 23:30:19 +00:00
"encoding/base64"
2025-11-09 01:02:31 +00:00
"encoding/json"
"fmt"
"log"
2025-12-31 02:02:57 +00:00
"net"
2025-11-09 01:02:31 +00:00
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/Masterminds/semver/v3"
"github.com/Snider/Mining/docs"
2025-12-07 16:26:18 +00:00
"github.com/adrg/xdg"
2025-11-09 19:06:05 +00:00
"github.com/gin-contrib/cors"
2025-11-09 01:02:31 +00:00
"github.com/gin-gonic/gin"
feat: Add WebSocket events, simulation mode, and redesigned Miners page
WebSocket Real-Time Events:
- Add EventHub for broadcasting miner events to connected clients
- New event types: miner.starting/started/stopping/stopped/stats/error
- WebSocket endpoint at /ws/events with auto-reconnect support
- Angular WebSocketService with RxJS event streams and fallback to polling
Simulation Mode (miner-ctrl simulate):
- SimulatedMiner generates realistic hashrate data for UI development
- Supports presets: cpu-low, cpu-medium, cpu-high, gpu-ethash, gpu-kawpow
- Features: variance, sine-wave fluctuation, 30s ramp-up, 98% share rate
- XMRig-compatible stats format for full UI compatibility
- NewManagerForSimulation() skips autostart of real miners
Miners Page Redesign:
- Featured cards for installed/recommended miners with gradient styling
- "Installed" (green) and "Recommended" (gold) ribbon badges
- Placeholder cards for 8 planned miners with "Coming Soon" badges
- Algorithm badges, GitHub links, and license info for each miner
- Planned miners: T-Rex, lolMiner, Rigel, BzMiner, SRBMiner, TeamRedMiner, GMiner, NBMiner
Chart Improvements:
- Hybrid data approach: live in-memory data while active, database historical when inactive
- Smoother transitions between data sources
Documentation:
- Updated DEVELOPMENT.md with simulation mode usage
- Updated ARCHITECTURE.md with WebSocket, simulation, and supported miners table
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 07:11:41 +00:00
"github.com/gorilla/websocket"
2025-12-07 18:31:27 +00:00
"github.com/shirou/gopsutil/v4/mem"
2025-11-09 01:02:31 +00:00
"github.com/swaggo/swag"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
feat: Increase test coverage for pkg/mining
This commit introduces a number of new tests for the `pkg/mining` package,
increasing the overall test coverage from 8.2% to 40.7%.
The following changes were made:
- Added tests for the `XMRigMiner` struct, including its methods for
installation, starting, stopping, and getting stats.
- Added tests for the `Service` layer, including the API endpoints for
listing, starting, stopping, and getting stats for miners.
- Added tests for the `Manager`, including starting and stopping multiple
miners, collecting stats, and getting hashrate history.
- Introduced a `ManagerInterface` to decouple the `Service` layer from the
concrete `Manager` implementation, facilitating testing with mocks.
2025-11-10 00:06:35 +00:00
// Service encapsulates the gin-gonic router and the mining manager.
type Service struct {
Manager ManagerInterface
2025-12-10 22:17:38 +00:00
ProfileManager * ProfileManager
2025-12-29 19:49:33 +00:00
NodeService * NodeService
feat: Add WebSocket events, simulation mode, and redesigned Miners page
WebSocket Real-Time Events:
- Add EventHub for broadcasting miner events to connected clients
- New event types: miner.starting/started/stopping/stopped/stats/error
- WebSocket endpoint at /ws/events with auto-reconnect support
- Angular WebSocketService with RxJS event streams and fallback to polling
Simulation Mode (miner-ctrl simulate):
- SimulatedMiner generates realistic hashrate data for UI development
- Supports presets: cpu-low, cpu-medium, cpu-high, gpu-ethash, gpu-kawpow
- Features: variance, sine-wave fluctuation, 30s ramp-up, 98% share rate
- XMRig-compatible stats format for full UI compatibility
- NewManagerForSimulation() skips autostart of real miners
Miners Page Redesign:
- Featured cards for installed/recommended miners with gradient styling
- "Installed" (green) and "Recommended" (gold) ribbon badges
- Placeholder cards for 8 planned miners with "Coming Soon" badges
- Algorithm badges, GitHub links, and license info for each miner
- Planned miners: T-Rex, lolMiner, Rigel, BzMiner, SRBMiner, TeamRedMiner, GMiner, NBMiner
Chart Improvements:
- Hybrid data approach: live in-memory data while active, database historical when inactive
- Smoother transitions between data sources
Documentation:
- Updated DEVELOPMENT.md with simulation mode usage
- Updated ARCHITECTURE.md with WebSocket, simulation, and supported miners table
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 07:11:41 +00:00
EventHub * EventHub
feat: Increase test coverage for pkg/mining
This commit introduces a number of new tests for the `pkg/mining` package,
increasing the overall test coverage from 8.2% to 40.7%.
The following changes were made:
- Added tests for the `XMRigMiner` struct, including its methods for
installation, starting, stopping, and getting stats.
- Added tests for the `Service` layer, including the API endpoints for
listing, starting, stopping, and getting stats for miners.
- Added tests for the `Manager`, including starting and stopping multiple
miners, collecting stats, and getting hashrate history.
- Introduced a `ManagerInterface` to decouple the `Service` layer from the
concrete `Manager` implementation, facilitating testing with mocks.
2025-11-10 00:06:35 +00:00
Router * gin . Engine
Server * http . Server
DisplayAddr string
SwaggerInstanceName string
APIBasePath string
SwaggerUIPath string
feat: Add rate limiter with cleanup and custom error types
Rate Limiter:
- Extract rate limiting to pkg/mining/ratelimiter.go with proper lifecycle
- Add Stop() method to gracefully shutdown cleanup goroutine
- Add RateLimiter.Middleware() for Gin integration
- Add ClientCount() for monitoring
- Fix goroutine leak in previous inline implementation
Custom Errors:
- Add pkg/mining/errors.go with MiningError type
- Define error codes: MINER_NOT_FOUND, INSTALL_FAILED, TIMEOUT, etc.
- Add predefined error constructors (ErrMinerNotFound, ErrStartFailed, etc.)
- Support error chaining with WithCause, WithDetails, WithSuggestion
- Include HTTP status codes and retry policies
Service:
- Add Service.Stop() method for graceful cleanup
- Update CLI commands to use context.Background() for Manager methods
Tests:
- Add comprehensive tests for RateLimiter (token bucket, multi-IP, refill)
- Add comprehensive tests for MiningError (codes, status, retryable)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 10:56:26 +00:00
rateLimiter * RateLimiter
feat: Increase test coverage for pkg/mining
This commit introduces a number of new tests for the `pkg/mining` package,
increasing the overall test coverage from 8.2% to 40.7%.
The following changes were made:
- Added tests for the `XMRigMiner` struct, including its methods for
installation, starting, stopping, and getting stats.
- Added tests for the `Service` layer, including the API endpoints for
listing, starting, stopping, and getting stats for miners.
- Added tests for the `Manager`, including starting and stopping multiple
miners, collecting stats, and getting hashrate history.
- Introduced a `ManagerInterface` to decouple the `Service` layer from the
concrete `Manager` implementation, facilitating testing with mocks.
2025-11-10 00:06:35 +00:00
}
2025-12-31 09:38:25 +00:00
// APIError represents a structured error response for the API
type APIError struct {
Code string ` json:"code" ` // Machine-readable error code
Message string ` json:"message" ` // Human-readable message
Details string ` json:"details,omitempty" ` // Technical details (for debugging)
Suggestion string ` json:"suggestion,omitempty" ` // What to do next
Retryable bool ` json:"retryable" ` // Can the client retry?
}
feat: Add rate limiter with cleanup and custom error types
Rate Limiter:
- Extract rate limiting to pkg/mining/ratelimiter.go with proper lifecycle
- Add Stop() method to gracefully shutdown cleanup goroutine
- Add RateLimiter.Middleware() for Gin integration
- Add ClientCount() for monitoring
- Fix goroutine leak in previous inline implementation
Custom Errors:
- Add pkg/mining/errors.go with MiningError type
- Define error codes: MINER_NOT_FOUND, INSTALL_FAILED, TIMEOUT, etc.
- Add predefined error constructors (ErrMinerNotFound, ErrStartFailed, etc.)
- Support error chaining with WithCause, WithDetails, WithSuggestion
- Include HTTP status codes and retry policies
Service:
- Add Service.Stop() method for graceful cleanup
- Update CLI commands to use context.Background() for Manager methods
Tests:
- Add comprehensive tests for RateLimiter (token bucket, multi-IP, refill)
- Add comprehensive tests for MiningError (codes, status, retryable)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 10:56:26 +00:00
// Error codes are defined in errors.go
2025-12-31 09:38:25 +00:00
// respondWithError sends a structured error response
func respondWithError ( c * gin . Context , status int , code string , message string , details string ) {
apiErr := APIError {
Code : code ,
Message : message ,
Details : details ,
Retryable : isRetryableError ( status ) ,
}
// Add suggestions based on error code
switch code {
case ErrCodeMinerNotFound :
apiErr . Suggestion = "Check the miner name or install the miner first"
case ErrCodeProfileNotFound :
apiErr . Suggestion = "Create a new profile or check the profile ID"
case ErrCodeInstallFailed :
apiErr . Suggestion = "Check your internet connection and try again"
case ErrCodeStartFailed :
apiErr . Suggestion = "Check the miner configuration and logs"
case ErrCodeInvalidInput :
apiErr . Suggestion = "Verify the request body matches the expected format"
case ErrCodeServiceUnavailable :
apiErr . Suggestion = "The service is temporarily unavailable, try again later"
apiErr . Retryable = true
}
c . JSON ( status , apiErr )
}
// isRetryableError determines if an error status code is retryable
func isRetryableError ( status int ) bool {
return status == http . StatusServiceUnavailable ||
status == http . StatusTooManyRequests ||
status == http . StatusGatewayTimeout
}
2025-12-31 09:39:42 +00:00
// requestIDMiddleware adds a unique request ID to each request for tracing
func requestIDMiddleware ( ) gin . HandlerFunc {
return func ( c * gin . Context ) {
// Use existing request ID from header if provided, otherwise generate one
requestID := c . GetHeader ( "X-Request-ID" )
if requestID == "" {
requestID = generateRequestID ( )
}
// Set in context for use by handlers
c . Set ( "requestID" , requestID )
// Set in response header
c . Header ( "X-Request-ID" , requestID )
c . Next ( )
}
}
// generateRequestID creates a unique request ID using timestamp and random bytes
func generateRequestID ( ) string {
b := make ( [ ] byte , 8 )
_ , _ = base64 . StdEncoding . Decode ( b , [ ] byte ( fmt . Sprintf ( "%d" , time . Now ( ) . UnixNano ( ) ) ) )
return fmt . Sprintf ( "%d-%x" , time . Now ( ) . UnixMilli ( ) , b [ : 4 ] )
}
2025-12-31 09:51:48 +00:00
feat: Add WebSocket events, simulation mode, and redesigned Miners page
WebSocket Real-Time Events:
- Add EventHub for broadcasting miner events to connected clients
- New event types: miner.starting/started/stopping/stopped/stats/error
- WebSocket endpoint at /ws/events with auto-reconnect support
- Angular WebSocketService with RxJS event streams and fallback to polling
Simulation Mode (miner-ctrl simulate):
- SimulatedMiner generates realistic hashrate data for UI development
- Supports presets: cpu-low, cpu-medium, cpu-high, gpu-ethash, gpu-kawpow
- Features: variance, sine-wave fluctuation, 30s ramp-up, 98% share rate
- XMRig-compatible stats format for full UI compatibility
- NewManagerForSimulation() skips autostart of real miners
Miners Page Redesign:
- Featured cards for installed/recommended miners with gradient styling
- "Installed" (green) and "Recommended" (gold) ribbon badges
- Placeholder cards for 8 planned miners with "Coming Soon" badges
- Algorithm badges, GitHub links, and license info for each miner
- Planned miners: T-Rex, lolMiner, Rigel, BzMiner, SRBMiner, TeamRedMiner, GMiner, NBMiner
Chart Improvements:
- Hybrid data approach: live in-memory data while active, database historical when inactive
- Smoother transitions between data sources
Documentation:
- Updated DEVELOPMENT.md with simulation mode usage
- Updated ARCHITECTURE.md with WebSocket, simulation, and supported miners table
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 07:11:41 +00:00
// WebSocket upgrader for the events endpoint
var wsUpgrader = websocket . Upgrader {
ReadBufferSize : 1024 ,
WriteBufferSize : 1024 ,
CheckOrigin : func ( r * http . Request ) bool {
// Allow connections from localhost origins
origin := r . Header . Get ( "Origin" )
if origin == "" {
return true
}
// Allow localhost with any port
return strings . Contains ( origin , "localhost" ) ||
strings . Contains ( origin , "127.0.0.1" ) ||
strings . Contains ( origin , "wails.localhost" )
} ,
}
2025-11-09 01:02:31 +00:00
// NewService creates a new mining service
2025-12-10 22:17:38 +00:00
func NewService ( manager ManagerInterface , listenAddr string , displayAddr string , swaggerNamespace string ) ( * Service , error ) {
2025-11-09 01:02:31 +00:00
apiBasePath := "/" + strings . Trim ( swaggerNamespace , "/" )
2025-12-07 16:26:18 +00:00
swaggerUIPath := apiBasePath + "/swagger"
2025-11-09 01:02:31 +00:00
docs . SwaggerInfo . Title = "Mining Module API"
docs . SwaggerInfo . Version = "1.0"
2025-12-07 16:26:18 +00:00
docs . SwaggerInfo . Host = displayAddr
2025-11-09 01:02:31 +00:00
docs . SwaggerInfo . BasePath = apiBasePath
instanceName := "swagger_" + strings . ReplaceAll ( strings . Trim ( swaggerNamespace , "/" ) , "/" , "_" )
swag . Register ( instanceName , docs . SwaggerInfo )
2025-12-10 22:17:38 +00:00
profileManager , err := NewProfileManager ( )
if err != nil {
return nil , fmt . Errorf ( "failed to initialize profile manager: %w" , err )
}
2025-12-29 19:49:33 +00:00
// Initialize node service (optional - only fails if XDG paths are broken)
nodeService , err := NewNodeService ( )
if err != nil {
log . Printf ( "Warning: failed to initialize node service: %v" , err )
// Continue without node service - P2P features will be unavailable
}
feat: Add WebSocket events, simulation mode, and redesigned Miners page
WebSocket Real-Time Events:
- Add EventHub for broadcasting miner events to connected clients
- New event types: miner.starting/started/stopping/stopped/stats/error
- WebSocket endpoint at /ws/events with auto-reconnect support
- Angular WebSocketService with RxJS event streams and fallback to polling
Simulation Mode (miner-ctrl simulate):
- SimulatedMiner generates realistic hashrate data for UI development
- Supports presets: cpu-low, cpu-medium, cpu-high, gpu-ethash, gpu-kawpow
- Features: variance, sine-wave fluctuation, 30s ramp-up, 98% share rate
- XMRig-compatible stats format for full UI compatibility
- NewManagerForSimulation() skips autostart of real miners
Miners Page Redesign:
- Featured cards for installed/recommended miners with gradient styling
- "Installed" (green) and "Recommended" (gold) ribbon badges
- Placeholder cards for 8 planned miners with "Coming Soon" badges
- Algorithm badges, GitHub links, and license info for each miner
- Planned miners: T-Rex, lolMiner, Rigel, BzMiner, SRBMiner, TeamRedMiner, GMiner, NBMiner
Chart Improvements:
- Hybrid data approach: live in-memory data while active, database historical when inactive
- Smoother transitions between data sources
Documentation:
- Updated DEVELOPMENT.md with simulation mode usage
- Updated ARCHITECTURE.md with WebSocket, simulation, and supported miners table
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 07:11:41 +00:00
// Initialize event hub for WebSocket real-time updates
eventHub := NewEventHub ( )
go eventHub . Run ( )
// Wire up event hub to manager for miner events
if mgr , ok := manager . ( * Manager ) ; ok {
mgr . SetEventHub ( eventHub )
}
feat: Add context propagation, state sync, and tests
- Add context.Context to ManagerInterface methods (StartMiner, StopMiner, UninstallMiner)
- Add WebSocket state sync on client connect (sends current miner states)
- Add EventStateSync event type and SetStateProvider method
- Add manager lifecycle tests (idempotent stop, context cancellation, shutdown timeout)
- Add database tests (initialization, hashrate storage, stats)
- Add EventHub tests (creation, broadcast, client count, state provider)
- Update all test files for new context-aware API
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 10:10:39 +00:00
// Set up state provider for WebSocket state sync on reconnect
eventHub . SetStateProvider ( func ( ) interface { } {
miners := manager . ListMiners ( )
if len ( miners ) == 0 {
return nil
}
// Return current state of all miners
state := make ( [ ] map [ string ] interface { } , 0 , len ( miners ) )
for _ , miner := range miners {
stats , _ := miner . GetStats ( context . Background ( ) )
minerState := map [ string ] interface { } {
"name" : miner . GetName ( ) ,
"status" : "running" ,
}
if stats != nil {
minerState [ "hashrate" ] = stats . Hashrate
minerState [ "shares" ] = stats . Shares
minerState [ "rejected" ] = stats . Rejected
minerState [ "uptime" ] = stats . Uptime
}
state = append ( state , minerState )
}
return map [ string ] interface { } {
"miners" : state ,
}
} )
2025-11-09 01:02:31 +00:00
return & Service {
2025-12-10 22:17:38 +00:00
Manager : manager ,
ProfileManager : profileManager ,
2025-12-29 19:49:33 +00:00
NodeService : nodeService ,
feat: Add WebSocket events, simulation mode, and redesigned Miners page
WebSocket Real-Time Events:
- Add EventHub for broadcasting miner events to connected clients
- New event types: miner.starting/started/stopping/stopped/stats/error
- WebSocket endpoint at /ws/events with auto-reconnect support
- Angular WebSocketService with RxJS event streams and fallback to polling
Simulation Mode (miner-ctrl simulate):
- SimulatedMiner generates realistic hashrate data for UI development
- Supports presets: cpu-low, cpu-medium, cpu-high, gpu-ethash, gpu-kawpow
- Features: variance, sine-wave fluctuation, 30s ramp-up, 98% share rate
- XMRig-compatible stats format for full UI compatibility
- NewManagerForSimulation() skips autostart of real miners
Miners Page Redesign:
- Featured cards for installed/recommended miners with gradient styling
- "Installed" (green) and "Recommended" (gold) ribbon badges
- Placeholder cards for 8 planned miners with "Coming Soon" badges
- Algorithm badges, GitHub links, and license info for each miner
- Planned miners: T-Rex, lolMiner, Rigel, BzMiner, SRBMiner, TeamRedMiner, GMiner, NBMiner
Chart Improvements:
- Hybrid data approach: live in-memory data while active, database historical when inactive
- Smoother transitions between data sources
Documentation:
- Updated DEVELOPMENT.md with simulation mode usage
- Updated ARCHITECTURE.md with WebSocket, simulation, and supported miners table
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 07:11:41 +00:00
EventHub : eventHub ,
2025-11-09 01:02:31 +00:00
Server : & http . Server {
2025-12-31 02:26:46 +00:00
Addr : listenAddr ,
ReadTimeout : 30 * time . Second ,
WriteTimeout : 30 * time . Second ,
IdleTimeout : 60 * time . Second ,
ReadHeaderTimeout : 10 * time . Second ,
2025-11-09 01:02:31 +00:00
} ,
2025-12-07 16:26:18 +00:00
DisplayAddr : displayAddr ,
2025-11-09 01:02:31 +00:00
SwaggerInstanceName : instanceName ,
APIBasePath : apiBasePath ,
SwaggerUIPath : swaggerUIPath ,
2025-12-10 22:17:38 +00:00
} , nil
2025-11-09 01:02:31 +00:00
}
2025-12-29 19:49:33 +00:00
// InitRouter initializes the Gin router and sets up all routes without starting an HTTP server.
// Use this when embedding the mining service in another application (e.g., Wails).
// After calling InitRouter, you can use the Router field directly as an http.Handler.
func ( s * Service ) InitRouter ( ) {
2025-11-09 01:02:31 +00:00
s . Router = gin . Default ( )
2025-12-31 01:02:23 +00:00
2025-12-31 02:26:46 +00:00
// Extract port safely from server address for CORS
serverPort := "9090" // default fallback
if s . Server . Addr != "" {
if _ , port , err := net . SplitHostPort ( s . Server . Addr ) ; err == nil && port != "" {
serverPort = port
}
}
2025-12-31 01:02:23 +00:00
// Configure CORS to only allow local origins
corsConfig := cors . Config {
AllowOrigins : [ ] string {
"http://localhost:4200" , // Angular dev server
"http://127.0.0.1:4200" ,
"http://localhost:9090" , // Default API port
"http://127.0.0.1:9090" ,
2025-12-31 02:26:46 +00:00
"http://localhost:" + serverPort ,
"http://127.0.0.1:" + serverPort ,
2025-12-31 04:09:11 +00:00
"http://wails.localhost" , // Wails desktop app (uses localhost origin)
2025-12-31 01:02:23 +00:00
} ,
AllowMethods : [ ] string { "GET" , "POST" , "PUT" , "DELETE" , "OPTIONS" } ,
2025-12-31 09:39:42 +00:00
AllowHeaders : [ ] string { "Origin" , "Content-Type" , "Accept" , "Authorization" , "X-Request-ID" } ,
ExposeHeaders : [ ] string { "Content-Length" , "X-Request-ID" } ,
2025-12-31 01:02:23 +00:00
AllowCredentials : true ,
MaxAge : 12 * time . Hour ,
}
s . Router . Use ( cors . New ( corsConfig ) )
2025-12-31 02:26:46 +00:00
// Add request body size limit middleware (1MB max)
s . Router . Use ( func ( c * gin . Context ) {
c . Request . Body = http . MaxBytesReader ( c . Writer , c . Request . Body , 1 << 20 ) // 1MB
c . Next ( )
} )
2025-12-31 09:39:42 +00:00
// Add X-Request-ID middleware for request tracing
s . Router . Use ( requestIDMiddleware ( ) )
2025-12-31 09:51:48 +00:00
// Add rate limiting (10 requests/second with burst of 20)
feat: Add rate limiter with cleanup and custom error types
Rate Limiter:
- Extract rate limiting to pkg/mining/ratelimiter.go with proper lifecycle
- Add Stop() method to gracefully shutdown cleanup goroutine
- Add RateLimiter.Middleware() for Gin integration
- Add ClientCount() for monitoring
- Fix goroutine leak in previous inline implementation
Custom Errors:
- Add pkg/mining/errors.go with MiningError type
- Define error codes: MINER_NOT_FOUND, INSTALL_FAILED, TIMEOUT, etc.
- Add predefined error constructors (ErrMinerNotFound, ErrStartFailed, etc.)
- Support error chaining with WithCause, WithDetails, WithSuggestion
- Include HTTP status codes and retry policies
Service:
- Add Service.Stop() method for graceful cleanup
- Update CLI commands to use context.Background() for Manager methods
Tests:
- Add comprehensive tests for RateLimiter (token bucket, multi-IP, refill)
- Add comprehensive tests for MiningError (codes, status, retryable)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 10:56:26 +00:00
s . rateLimiter = NewRateLimiter ( 10 , 20 )
s . Router . Use ( s . rateLimiter . Middleware ( ) )
2025-12-31 09:51:48 +00:00
2025-12-29 19:49:33 +00:00
s . SetupRoutes ( )
}
feat: Add rate limiter with cleanup and custom error types
Rate Limiter:
- Extract rate limiting to pkg/mining/ratelimiter.go with proper lifecycle
- Add Stop() method to gracefully shutdown cleanup goroutine
- Add RateLimiter.Middleware() for Gin integration
- Add ClientCount() for monitoring
- Fix goroutine leak in previous inline implementation
Custom Errors:
- Add pkg/mining/errors.go with MiningError type
- Define error codes: MINER_NOT_FOUND, INSTALL_FAILED, TIMEOUT, etc.
- Add predefined error constructors (ErrMinerNotFound, ErrStartFailed, etc.)
- Support error chaining with WithCause, WithDetails, WithSuggestion
- Include HTTP status codes and retry policies
Service:
- Add Service.Stop() method for graceful cleanup
- Update CLI commands to use context.Background() for Manager methods
Tests:
- Add comprehensive tests for RateLimiter (token bucket, multi-IP, refill)
- Add comprehensive tests for MiningError (codes, status, retryable)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 10:56:26 +00:00
// Stop gracefully stops the service and cleans up resources
func ( s * Service ) Stop ( ) {
if s . rateLimiter != nil {
s . rateLimiter . Stop ( )
}
if s . EventHub != nil {
s . EventHub . Stop ( )
}
}
2025-12-29 19:49:33 +00:00
// ServiceStartup initializes the router and starts the HTTP server.
// For embedding without a standalone server, use InitRouter() instead.
func ( s * Service ) ServiceStartup ( ctx context . Context ) error {
s . InitRouter ( )
2025-11-09 01:02:31 +00:00
s . Server . Handler = s . Router
2025-12-31 01:15:35 +00:00
// Channel to capture server startup errors
errChan := make ( chan error , 1 )
2025-11-09 01:02:31 +00:00
go func ( ) {
if err := s . Server . ListenAndServe ( ) ; err != nil && err != http . ErrServerClosed {
2025-12-31 01:15:35 +00:00
log . Printf ( "Server error on %s: %v" , s . Server . Addr , err )
errChan <- err
2025-11-09 01:02:31 +00:00
}
2025-12-31 02:02:57 +00:00
close ( errChan ) // Prevent goroutine leak
2025-11-09 01:02:31 +00:00
} ( )
go func ( ) {
<- ctx . Done ( )
2025-11-09 04:08:10 +00:00
s . Manager . Stop ( )
2025-11-09 01:02:31 +00:00
ctxShutdown , cancel := context . WithTimeout ( context . Background ( ) , 5 * time . Second )
defer cancel ( )
if err := s . Server . Shutdown ( ctxShutdown ) ; err != nil {
2025-12-31 01:15:35 +00:00
log . Printf ( "Server shutdown error: %v" , err )
2025-11-09 01:02:31 +00:00
}
} ( )
2025-12-31 02:02:57 +00:00
// Verify server is actually listening by attempting to connect
maxRetries := 50 // 50 * 100ms = 5 seconds max
for i := 0 ; i < maxRetries ; i ++ {
select {
case err := <- errChan :
if err != nil {
return fmt . Errorf ( "failed to start server: %w" , err )
}
return nil // Channel closed without error means server shut down
default :
// Try to connect to verify server is listening
conn , err := net . DialTimeout ( "tcp" , s . Server . Addr , 50 * time . Millisecond )
if err == nil {
conn . Close ( )
return nil // Server is ready
}
time . Sleep ( 100 * time . Millisecond )
}
2025-12-31 01:15:35 +00:00
}
2025-12-31 02:02:57 +00:00
return fmt . Errorf ( "server failed to start listening on %s within timeout" , s . Server . Addr )
2025-11-09 01:02:31 +00:00
}
2025-12-29 19:49:33 +00:00
// SetupRoutes configures all API routes on the Gin router.
// This is called automatically by ServiceStartup, but can also be called
// manually after InitRouter for embedding in other applications.
func ( s * Service ) SetupRoutes ( ) {
2025-11-09 01:02:31 +00:00
apiGroup := s . Router . Group ( s . APIBasePath )
{
2025-12-07 16:26:18 +00:00
apiGroup . GET ( "/info" , s . handleGetInfo )
2025-11-09 01:02:31 +00:00
apiGroup . POST ( "/doctor" , s . handleDoctor )
apiGroup . POST ( "/update" , s . handleUpdateCheck )
minersGroup := apiGroup . 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 )
2025-12-07 16:26:18 +00:00
minersGroup . GET ( "/:miner_name/hashrate-history" , s . handleGetMinerHashrateHistory )
2025-12-29 19:49:33 +00:00
minersGroup . GET ( "/:miner_name/logs" , s . handleGetMinerLogs )
2025-12-29 23:30:19 +00:00
minersGroup . POST ( "/:miner_name/stdin" , s . handleMinerStdin )
2025-11-09 01:02:31 +00:00
}
2025-12-10 22:17:38 +00:00
2025-12-29 22:10:45 +00:00
// Historical data endpoints (database-backed)
historyGroup := apiGroup . 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 )
}
2025-12-10 22:17:38 +00:00
profilesGroup := apiGroup . 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 )
}
2025-12-29 19:49:33 +00:00
feat: Add WebSocket events, simulation mode, and redesigned Miners page
WebSocket Real-Time Events:
- Add EventHub for broadcasting miner events to connected clients
- New event types: miner.starting/started/stopping/stopped/stats/error
- WebSocket endpoint at /ws/events with auto-reconnect support
- Angular WebSocketService with RxJS event streams and fallback to polling
Simulation Mode (miner-ctrl simulate):
- SimulatedMiner generates realistic hashrate data for UI development
- Supports presets: cpu-low, cpu-medium, cpu-high, gpu-ethash, gpu-kawpow
- Features: variance, sine-wave fluctuation, 30s ramp-up, 98% share rate
- XMRig-compatible stats format for full UI compatibility
- NewManagerForSimulation() skips autostart of real miners
Miners Page Redesign:
- Featured cards for installed/recommended miners with gradient styling
- "Installed" (green) and "Recommended" (gold) ribbon badges
- Placeholder cards for 8 planned miners with "Coming Soon" badges
- Algorithm badges, GitHub links, and license info for each miner
- Planned miners: T-Rex, lolMiner, Rigel, BzMiner, SRBMiner, TeamRedMiner, GMiner, NBMiner
Chart Improvements:
- Hybrid data approach: live in-memory data while active, database historical when inactive
- Smoother transitions between data sources
Documentation:
- Updated DEVELOPMENT.md with simulation mode usage
- Updated ARCHITECTURE.md with WebSocket, simulation, and supported miners table
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 07:11:41 +00:00
// WebSocket endpoint for real-time events
wsGroup := apiGroup . Group ( "/ws" )
{
wsGroup . GET ( "/events" , s . handleWebSocketEvents )
}
2025-12-29 19:49:33 +00:00
// Add P2P node endpoints if node service is available
if s . NodeService != nil {
s . NodeService . SetupRoutes ( apiGroup )
}
2025-11-09 01:02:31 +00:00
}
2025-12-29 19:49:33 +00:00
// Serve the embedded web component
componentFS , err := GetComponentFS ( )
if err == nil {
s . Router . StaticFS ( "/component" , componentFS )
}
2025-11-09 04:41:07 +00:00
2025-11-09 01:02:31 +00:00
swaggerURL := ginSwagger . URL ( fmt . Sprintf ( "http://%s%s/doc.json" , s . DisplayAddr , s . SwaggerUIPath ) )
s . Router . GET ( s . SwaggerUIPath + "/*any" , ginSwagger . WrapHandler ( swaggerFiles . Handler , swaggerURL , ginSwagger . InstanceName ( s . SwaggerInstanceName ) ) )
}
// handleGetInfo godoc
2025-12-10 22:17:38 +00:00
// @Summary Get live miner installation information
// @Description Retrieves live installation details for all miners, along with system information.
2025-11-09 01:02:31 +00:00
// @Tags system
// @Produce json
// @Success 200 {object} SystemInfo
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /info [get]
func ( s * Service ) handleGetInfo ( c * gin . Context ) {
2025-12-10 22:17:38 +00:00
systemInfo , err := s . updateInstallationCache ( )
2025-11-09 01:02:31 +00:00
if err != nil {
2025-12-10 22:17:38 +00:00
c . JSON ( http . StatusInternalServerError , gin . H { "error" : "failed to get system info" , "details" : err . Error ( ) } )
2025-11-09 01:02:31 +00:00
return
}
c . JSON ( http . StatusOK , systemInfo )
}
2025-12-07 16:26:18 +00:00
// updateInstallationCache performs a live check and updates the cache file.
func ( s * Service ) updateInstallationCache ( ) ( * SystemInfo , error ) {
2025-12-07 18:31:27 +00:00
// Always create a complete SystemInfo object
systemInfo := & SystemInfo {
Timestamp : time . Now ( ) ,
OS : runtime . GOOS ,
Architecture : runtime . GOARCH ,
GoVersion : runtime . Version ( ) ,
AvailableCPUCores : runtime . NumCPU ( ) ,
InstalledMinersInfo : [ ] * InstallationDetails { } , // Initialize as empty slice
}
vMem , err := mem . VirtualMemory ( )
if err == nil {
systemInfo . TotalSystemRAMGB = float64 ( vMem . Total ) / ( 1024 * 1024 * 1024 )
}
2025-11-09 01:02:31 +00:00
for _ , availableMiner := range s . Manager . ListAvailableMiners ( ) {
2025-12-31 11:12:33 +00:00
miner , err := CreateMiner ( availableMiner . Name )
if err != nil {
continue // Skip unsupported miner types
2025-11-09 01:02:31 +00:00
}
2025-12-31 01:15:35 +00:00
details , err := miner . CheckInstallation ( )
if err != nil {
log . Printf ( "Warning: failed to check installation for %s: %v" , availableMiner . Name , err )
}
2025-12-07 18:31:27 +00:00
systemInfo . InstalledMinersInfo = append ( systemInfo . InstalledMinersInfo , details )
2025-12-07 16:26:18 +00:00
}
configDir , err := xdg . ConfigFile ( "lethean-desktop/miners" )
if err != nil {
return nil , fmt . Errorf ( "could not get config directory: %w" , err )
}
if err := os . MkdirAll ( configDir , 0755 ) ; err != nil {
return nil , fmt . Errorf ( "could not create config directory: %w" , err )
}
configPath := filepath . Join ( configDir , "config.json" )
data , err := json . MarshalIndent ( systemInfo , "" , " " )
if err != nil {
return nil , fmt . Errorf ( "could not marshal cache data: %w" , err )
}
2025-12-31 00:50:06 +00:00
if err := os . WriteFile ( configPath , data , 0600 ) ; err != nil {
2025-12-07 16:26:18 +00:00
return nil , fmt . Errorf ( "could not write cache file: %w" , err )
}
return systemInfo , nil
}
// handleDoctor godoc
// @Summary Check miner installations
// @Description Performs a live check on all available miners to verify their installation status, version, and path.
// @Tags system
// @Produce json
// @Success 200 {object} SystemInfo
// @Router /doctor [post]
func ( s * Service ) handleDoctor ( c * gin . Context ) {
systemInfo , err := s . updateInstallationCache ( )
if err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : "failed to update cache" , "details" : err . Error ( ) } )
return
}
c . JSON ( http . StatusOK , systemInfo )
2025-11-09 01:02:31 +00:00
}
// handleUpdateCheck godoc
// @Summary Check for miner updates
// @Description Checks if any installed miners have a new version available for download.
// @Tags system
// @Produce json
// @Success 200 {object} map[string]string
// @Router /update [post]
func ( s * Service ) handleUpdateCheck ( c * gin . Context ) {
updates := make ( map [ string ] string )
for _ , availableMiner := range s . Manager . ListAvailableMiners ( ) {
2025-12-31 11:12:33 +00:00
miner , err := CreateMiner ( availableMiner . Name )
if err != nil {
continue // Skip unsupported miner types
2025-11-09 01:02:31 +00:00
}
details , err := miner . CheckInstallation ( )
if err != nil || ! details . IsInstalled {
continue
}
latestVersionStr , err := miner . GetLatestVersion ( )
if err != nil {
continue
}
latestVersion , err := semver . NewVersion ( latestVersionStr )
if err != nil {
continue
}
installedVersion , err := semver . NewVersion ( details . Version )
if err != nil {
continue
}
if latestVersion . GreaterThan ( installedVersion ) {
updates [ miner . GetName ( ) ] = latestVersion . String ( )
}
}
if len ( updates ) == 0 {
c . JSON ( http . StatusOK , gin . H { "status" : "All miners are up to date." } )
return
}
c . JSON ( http . StatusOK , gin . H { "updates_available" : updates } )
}
// handleUninstallMiner godoc
// @Summary Uninstall a miner
// @Description Removes all files for a specific miner.
// @Tags miners
// @Produce json
// @Param miner_type path string true "Miner Type to uninstall"
// @Success 200 {object} map[string]string
// @Router /miners/{miner_type}/uninstall [delete]
func ( s * Service ) handleUninstallMiner ( c * gin . Context ) {
minerType := c . Param ( "miner_name" )
feat: Add context propagation, state sync, and tests
- Add context.Context to ManagerInterface methods (StartMiner, StopMiner, UninstallMiner)
- Add WebSocket state sync on client connect (sends current miner states)
- Add EventStateSync event type and SetStateProvider method
- Add manager lifecycle tests (idempotent stop, context cancellation, shutdown timeout)
- Add database tests (initialization, hashrate storage, stats)
- Add EventHub tests (creation, broadcast, client count, state provider)
- Update all test files for new context-aware API
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 10:10:39 +00:00
if err := s . Manager . UninstallMiner ( c . Request . Context ( ) , minerType ) ; err != nil {
2025-11-09 01:02:31 +00:00
c . JSON ( http . StatusInternalServerError , gin . H { "error" : err . Error ( ) } )
return
}
2025-12-07 16:26:18 +00:00
if _ , err := s . updateInstallationCache ( ) ; err != nil {
log . Printf ( "Warning: failed to update cache after uninstall: %v" , err )
}
c . JSON ( http . StatusOK , gin . H { "status" : minerType + " uninstalled successfully." } )
2025-11-09 01:02:31 +00:00
}
// handleListMiners godoc
// @Summary List all running miners
// @Description Get a list of all running miners
// @Tags miners
// @Produce json
// @Success 200 {array} XMRigMiner
// @Router /miners [get]
func ( s * Service ) handleListMiners ( c * gin . Context ) {
miners := s . Manager . ListMiners ( )
c . JSON ( http . StatusOK , miners )
}
// handleListAvailableMiners godoc
// @Summary List all available miners
// @Description Get a list of all available miners
// @Tags miners
// @Produce json
// @Success 200 {array} AvailableMiner
// @Router /miners/available [get]
func ( s * Service ) handleListAvailableMiners ( c * gin . Context ) {
miners := s . Manager . ListAvailableMiners ( )
c . JSON ( http . StatusOK , miners )
}
// handleInstallMiner godoc
// @Summary Install or update a miner
// @Description Install a new miner or update an existing one.
// @Tags miners
// @Produce json
// @Param miner_type path string true "Miner Type to install/update"
// @Success 200 {object} map[string]string
// @Router /miners/{miner_type}/install [post]
func ( s * Service ) handleInstallMiner ( c * gin . Context ) {
minerType := c . Param ( "miner_name" )
2025-12-31 11:12:33 +00:00
miner , err := CreateMiner ( minerType )
if err != nil {
2025-11-09 01:02:31 +00:00
c . JSON ( http . StatusBadRequest , gin . H { "error" : "unknown miner type" } )
return
}
if err := miner . Install ( ) ; err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : err . Error ( ) } )
return
}
2025-12-07 16:26:18 +00:00
if _ , err := s . updateInstallationCache ( ) ; err != nil {
log . Printf ( "Warning: failed to update cache after install: %v" , err )
}
2025-11-09 01:02:31 +00:00
details , err := miner . CheckInstallation ( )
if err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : "failed to verify installation" , "details" : err . Error ( ) } )
return
}
c . JSON ( http . StatusOK , gin . H { "status" : "installed" , "version" : details . Version , "path" : details . Path } )
}
2025-12-10 22:17:38 +00:00
// handleStartMinerWithProfile godoc
// @Summary Start a new miner using a profile
// @Description Start a new miner with the configuration from a saved profile
// @Tags profiles
2025-11-09 01:02:31 +00:00
// @Produce json
2025-12-10 22:17:38 +00:00
// @Param id path string true "Profile ID"
2025-11-09 01:02:31 +00:00
// @Success 200 {object} XMRigMiner
2025-12-10 22:17:38 +00:00
// @Router /profiles/{id}/start [post]
func ( s * Service ) handleStartMinerWithProfile ( c * gin . Context ) {
profileID := c . Param ( "id" )
profile , exists := s . ProfileManager . GetProfile ( profileID )
if ! exists {
2025-12-31 09:38:25 +00:00
respondWithError ( c , http . StatusNotFound , ErrCodeProfileNotFound , "profile not found" , "" )
2025-12-10 22:17:38 +00:00
return
}
2025-11-09 01:02:31 +00:00
var config Config
2025-12-10 22:17:38 +00:00
if err := json . Unmarshal ( profile . Config , & config ) ; err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : "failed to parse profile config" , "details" : err . Error ( ) } )
2025-11-09 01:02:31 +00:00
return
}
2025-12-10 22:17:38 +00:00
feat: Add context propagation, state sync, and tests
- Add context.Context to ManagerInterface methods (StartMiner, StopMiner, UninstallMiner)
- Add WebSocket state sync on client connect (sends current miner states)
- Add EventStateSync event type and SetStateProvider method
- Add manager lifecycle tests (idempotent stop, context cancellation, shutdown timeout)
- Add database tests (initialization, hashrate storage, stats)
- Add EventHub tests (creation, broadcast, client count, state provider)
- Update all test files for new context-aware API
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 10:10:39 +00:00
miner , err := s . Manager . StartMiner ( c . Request . Context ( ) , profile . MinerType , & config )
2025-11-09 01:02:31 +00:00
if err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : err . Error ( ) } )
return
}
c . JSON ( http . StatusOK , miner )
}
// handleStopMiner godoc
// @Summary Stop a running miner
// @Description Stop a running miner by its name
// @Tags miners
// @Produce json
// @Param miner_name path string true "Miner Name"
// @Success 200 {object} map[string]string
// @Router /miners/{miner_name} [delete]
func ( s * Service ) handleStopMiner ( c * gin . Context ) {
minerName := c . Param ( "miner_name" )
feat: Add context propagation, state sync, and tests
- Add context.Context to ManagerInterface methods (StartMiner, StopMiner, UninstallMiner)
- Add WebSocket state sync on client connect (sends current miner states)
- Add EventStateSync event type and SetStateProvider method
- Add manager lifecycle tests (idempotent stop, context cancellation, shutdown timeout)
- Add database tests (initialization, hashrate storage, stats)
- Add EventHub tests (creation, broadcast, client count, state provider)
- Update all test files for new context-aware API
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 10:10:39 +00:00
if err := s . Manager . StopMiner ( c . Request . Context ( ) , minerName ) ; err != nil {
2025-11-09 01:02:31 +00:00
c . JSON ( http . StatusInternalServerError , gin . H { "error" : err . Error ( ) } )
return
}
c . JSON ( http . StatusOK , gin . H { "status" : "stopped" } )
}
// handleGetMinerStats godoc
// @Summary Get miner stats
// @Description Get statistics for a running miner
// @Tags miners
// @Produce json
// @Param miner_name path string true "Miner Name"
// @Success 200 {object} PerformanceMetrics
// @Router /miners/{miner_name}/stats [get]
func ( s * Service ) handleGetMinerStats ( c * gin . Context ) {
minerName := c . Param ( "miner_name" )
miner , err := s . Manager . GetMiner ( minerName )
if err != nil {
2025-12-31 09:38:25 +00:00
respondWithError ( c , http . StatusNotFound , ErrCodeMinerNotFound , "miner not found" , err . Error ( ) )
2025-11-09 01:02:31 +00:00
return
}
2025-12-31 01:55:24 +00:00
stats , err := miner . GetStats ( c . Request . Context ( ) )
2025-11-09 01:02:31 +00:00
if err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : err . Error ( ) } )
return
}
c . JSON ( http . StatusOK , stats )
}
2025-11-09 04:08:10 +00:00
// handleGetMinerHashrateHistory godoc
// @Summary Get miner hashrate history
// @Description Get historical hashrate data for a running miner
// @Tags miners
// @Produce json
// @Param miner_name path string true "Miner Name"
// @Success 200 {array} HashratePoint
// @Router /miners/{miner_name}/hashrate-history [get]
func ( s * Service ) handleGetMinerHashrateHistory ( c * gin . Context ) {
minerName := c . Param ( "miner_name" )
history , err := s . Manager . GetMinerHashrateHistory ( minerName )
if err != nil {
c . JSON ( http . StatusNotFound , gin . H { "error" : err . Error ( ) } )
return
}
c . JSON ( http . StatusOK , history )
}
2025-12-10 22:17:38 +00:00
2025-12-29 19:49:33 +00:00
// handleGetMinerLogs godoc
// @Summary Get miner log output
2025-12-29 23:30:19 +00:00
// @Description Get the captured stdout/stderr output from a running miner. Log lines are base64 encoded to preserve ANSI escape codes and special characters.
2025-12-29 19:49:33 +00:00
// @Tags miners
// @Produce json
// @Param miner_name path string true "Miner Name"
2025-12-29 23:30:19 +00:00
// @Success 200 {array} string "Base64 encoded log lines"
2025-12-29 19:49:33 +00:00
// @Router /miners/{miner_name}/logs [get]
func ( s * Service ) handleGetMinerLogs ( c * gin . Context ) {
minerName := c . Param ( "miner_name" )
miner , err := s . Manager . GetMiner ( minerName )
if err != nil {
2025-12-31 09:38:25 +00:00
respondWithError ( c , http . StatusNotFound , ErrCodeMinerNotFound , "miner not found" , err . Error ( ) )
2025-12-29 19:49:33 +00:00
return
}
logs := miner . GetLogs ( )
2025-12-29 23:30:19 +00:00
// Base64 encode each log line to preserve ANSI escape codes and special characters
encodedLogs := make ( [ ] string , len ( logs ) )
for i , line := range logs {
encodedLogs [ i ] = base64 . StdEncoding . EncodeToString ( [ ] byte ( line ) )
}
c . JSON ( http . StatusOK , encodedLogs )
}
// StdinInput represents input to send to miner's stdin
type StdinInput struct {
Input string ` json:"input" binding:"required" `
}
// handleMinerStdin godoc
// @Summary Send input to miner stdin
// @Description Send console commands to a running miner's stdin (e.g., 'h' for hashrate, 'p' for pause)
// @Tags miners
// @Accept json
// @Produce json
// @Param miner_name path string true "Miner Name"
// @Param input body StdinInput true "Input to send"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /miners/{miner_name}/stdin [post]
func ( s * Service ) handleMinerStdin ( c * gin . Context ) {
minerName := c . Param ( "miner_name" )
miner , err := s . Manager . GetMiner ( minerName )
if err != nil {
2025-12-31 09:38:25 +00:00
respondWithError ( c , http . StatusNotFound , ErrCodeMinerNotFound , "miner not found" , err . Error ( ) )
2025-12-29 23:30:19 +00:00
return
}
var input StdinInput
if err := c . ShouldBindJSON ( & input ) ; err != nil {
c . JSON ( http . StatusBadRequest , gin . H { "error" : "invalid input: " + err . Error ( ) } )
return
}
if err := miner . WriteStdin ( input . Input ) ; err != nil {
c . JSON ( http . StatusBadRequest , gin . H { "error" : err . Error ( ) } )
return
}
c . JSON ( http . StatusOK , gin . H { "status" : "sent" , "input" : input . Input } )
2025-12-29 19:49:33 +00:00
}
2025-12-10 22:17:38 +00:00
// handleListProfiles godoc
// @Summary List all mining profiles
// @Description Get a list of all saved mining profiles
// @Tags profiles
// @Produce json
// @Success 200 {array} MiningProfile
// @Router /profiles [get]
func ( s * Service ) handleListProfiles ( c * gin . Context ) {
profiles := s . ProfileManager . GetAllProfiles ( )
c . JSON ( http . StatusOK , profiles )
}
// handleCreateProfile godoc
// @Summary Create a new mining profile
// @Description Create and save a new mining profile
// @Tags profiles
// @Accept json
// @Produce json
// @Param profile body MiningProfile true "Mining Profile"
// @Success 201 {object} MiningProfile
// @Router /profiles [post]
func ( s * Service ) handleCreateProfile ( c * gin . Context ) {
var profile MiningProfile
if err := c . ShouldBindJSON ( & profile ) ; err != nil {
c . JSON ( http . StatusBadRequest , gin . H { "error" : err . Error ( ) } )
return
}
createdProfile , err := s . ProfileManager . CreateProfile ( & profile )
if err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : "failed to create profile" , "details" : err . Error ( ) } )
return
}
c . JSON ( http . StatusCreated , createdProfile )
}
// handleGetProfile godoc
// @Summary Get a specific mining profile
// @Description Get a mining profile by its ID
// @Tags profiles
// @Produce json
// @Param id path string true "Profile ID"
// @Success 200 {object} MiningProfile
// @Router /profiles/{id} [get]
func ( s * Service ) handleGetProfile ( c * gin . Context ) {
profileID := c . Param ( "id" )
profile , exists := s . ProfileManager . GetProfile ( profileID )
if ! exists {
2025-12-31 09:38:25 +00:00
respondWithError ( c , http . StatusNotFound , ErrCodeProfileNotFound , "profile not found" , "" )
2025-12-10 22:17:38 +00:00
return
}
c . JSON ( http . StatusOK , profile )
}
// handleUpdateProfile godoc
// @Summary Update a mining profile
// @Description Update an existing mining profile
// @Tags profiles
// @Accept json
// @Produce json
// @Param id path string true "Profile ID"
// @Param profile body MiningProfile true "Updated Mining Profile"
// @Success 200 {object} MiningProfile
// @Router /profiles/{id} [put]
func ( s * Service ) handleUpdateProfile ( c * gin . Context ) {
profileID := c . Param ( "id" )
var profile MiningProfile
if err := c . ShouldBindJSON ( & profile ) ; err != nil {
c . JSON ( http . StatusBadRequest , gin . H { "error" : err . Error ( ) } )
return
}
profile . ID = profileID
if err := s . ProfileManager . UpdateProfile ( & profile ) ; err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : "failed to update profile" , "details" : err . Error ( ) } )
return
}
c . JSON ( http . StatusOK , profile )
}
// handleDeleteProfile godoc
// @Summary Delete a mining profile
// @Description Delete a mining profile by its ID
// @Tags profiles
// @Produce json
// @Param id path string true "Profile ID"
// @Success 200 {object} map[string]string
// @Router /profiles/{id} [delete]
func ( s * Service ) handleDeleteProfile ( c * gin . Context ) {
profileID := c . Param ( "id" )
if err := s . ProfileManager . DeleteProfile ( profileID ) ; err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : "failed to delete profile" , "details" : err . Error ( ) } )
return
}
c . JSON ( http . StatusOK , gin . H { "status" : "profile deleted" } )
}
2025-12-29 22:10:45 +00:00
// handleHistoryStatus godoc
// @Summary Get database history status
// @Description Get the status of database persistence for historical data
// @Tags history
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /history/status [get]
func ( s * Service ) handleHistoryStatus ( c * gin . Context ) {
if manager , ok := s . Manager . ( * Manager ) ; ok {
c . JSON ( http . StatusOK , gin . H {
"enabled" : manager . IsDatabaseEnabled ( ) ,
"retentionDays" : manager . dbRetention ,
} )
return
}
c . JSON ( http . StatusOK , gin . H { "enabled" : false , "error" : "manager type not supported" } )
}
// handleAllMinersHistoricalStats godoc
// @Summary Get historical stats for all miners
// @Description Get aggregated historical statistics for all miners from the database
// @Tags history
// @Produce json
// @Success 200 {array} database.HashrateStats
// @Router /history/miners [get]
func ( s * Service ) handleAllMinersHistoricalStats ( c * gin . Context ) {
manager , ok := s . Manager . ( * Manager )
if ! ok {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : "manager type not supported" } )
return
}
stats , err := manager . GetAllMinerHistoricalStats ( )
if err != nil {
c . JSON ( http . StatusServiceUnavailable , gin . H { "error" : err . Error ( ) } )
return
}
c . JSON ( http . StatusOK , stats )
}
// handleMinerHistoricalStats godoc
// @Summary Get historical stats for a specific miner
// @Description Get aggregated historical statistics for a specific miner from the database
// @Tags history
// @Produce json
// @Param miner_name path string true "Miner Name"
// @Success 200 {object} database.HashrateStats
// @Router /history/miners/{miner_name} [get]
func ( s * Service ) handleMinerHistoricalStats ( c * gin . Context ) {
minerName := c . Param ( "miner_name" )
manager , ok := s . Manager . ( * Manager )
if ! ok {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : "manager type not supported" } )
return
}
stats , err := manager . GetMinerHistoricalStats ( minerName )
if err != nil {
c . JSON ( http . StatusServiceUnavailable , gin . H { "error" : err . Error ( ) } )
return
}
if stats == nil {
c . JSON ( http . StatusNotFound , gin . H { "error" : "no historical data found for miner" } )
return
}
c . JSON ( http . StatusOK , stats )
}
// handleMinerHistoricalHashrate godoc
// @Summary Get historical hashrate data for a specific miner
// @Description Get detailed historical hashrate data for a specific miner from the database
// @Tags history
// @Produce json
// @Param miner_name path string true "Miner Name"
// @Param since query string false "Start time (RFC3339 format)"
// @Param until query string false "End time (RFC3339 format)"
// @Success 200 {array} HashratePoint
// @Router /history/miners/{miner_name}/hashrate [get]
func ( s * Service ) handleMinerHistoricalHashrate ( c * gin . Context ) {
minerName := c . Param ( "miner_name" )
manager , ok := s . Manager . ( * Manager )
if ! ok {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : "manager type not supported" } )
return
}
// Parse time range from query params, default to last 24 hours
until := time . Now ( )
since := until . Add ( - 24 * time . Hour )
if sinceStr := c . Query ( "since" ) ; sinceStr != "" {
if t , err := time . Parse ( time . RFC3339 , sinceStr ) ; err == nil {
since = t
}
}
if untilStr := c . Query ( "until" ) ; untilStr != "" {
if t , err := time . Parse ( time . RFC3339 , untilStr ) ; err == nil {
until = t
}
}
history , err := manager . GetMinerHistoricalHashrate ( minerName , since , until )
if err != nil {
c . JSON ( http . StatusServiceUnavailable , gin . H { "error" : err . Error ( ) } )
return
}
c . JSON ( http . StatusOK , history )
}
feat: Add WebSocket events, simulation mode, and redesigned Miners page
WebSocket Real-Time Events:
- Add EventHub for broadcasting miner events to connected clients
- New event types: miner.starting/started/stopping/stopped/stats/error
- WebSocket endpoint at /ws/events with auto-reconnect support
- Angular WebSocketService with RxJS event streams and fallback to polling
Simulation Mode (miner-ctrl simulate):
- SimulatedMiner generates realistic hashrate data for UI development
- Supports presets: cpu-low, cpu-medium, cpu-high, gpu-ethash, gpu-kawpow
- Features: variance, sine-wave fluctuation, 30s ramp-up, 98% share rate
- XMRig-compatible stats format for full UI compatibility
- NewManagerForSimulation() skips autostart of real miners
Miners Page Redesign:
- Featured cards for installed/recommended miners with gradient styling
- "Installed" (green) and "Recommended" (gold) ribbon badges
- Placeholder cards for 8 planned miners with "Coming Soon" badges
- Algorithm badges, GitHub links, and license info for each miner
- Planned miners: T-Rex, lolMiner, Rigel, BzMiner, SRBMiner, TeamRedMiner, GMiner, NBMiner
Chart Improvements:
- Hybrid data approach: live in-memory data while active, database historical when inactive
- Smoother transitions between data sources
Documentation:
- Updated DEVELOPMENT.md with simulation mode usage
- Updated ARCHITECTURE.md with WebSocket, simulation, and supported miners table
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 07:11:41 +00:00
// handleWebSocketEvents godoc
// @Summary WebSocket endpoint for real-time mining events
// @Description Upgrade to WebSocket for real-time mining stats and events.
// @Description Events include: miner.starting, miner.started, miner.stopping, miner.stopped, miner.stats, miner.error
// @Tags websocket
// @Success 101 {string} string "Switching Protocols"
// @Router /ws/events [get]
func ( s * Service ) handleWebSocketEvents ( c * gin . Context ) {
conn , err := wsUpgrader . Upgrade ( c . Writer , c . Request , nil )
if err != nil {
log . Printf ( "[WebSocket] Failed to upgrade connection: %v" , err )
return
}
log . Printf ( "[WebSocket] New connection from %s" , c . Request . RemoteAddr )
2025-12-31 09:51:48 +00:00
if ! s . EventHub . ServeWs ( conn ) {
log . Printf ( "[WebSocket] Connection from %s rejected (limit reached)" , c . Request . RemoteAddr )
}
feat: Add WebSocket events, simulation mode, and redesigned Miners page
WebSocket Real-Time Events:
- Add EventHub for broadcasting miner events to connected clients
- New event types: miner.starting/started/stopping/stopped/stats/error
- WebSocket endpoint at /ws/events with auto-reconnect support
- Angular WebSocketService with RxJS event streams and fallback to polling
Simulation Mode (miner-ctrl simulate):
- SimulatedMiner generates realistic hashrate data for UI development
- Supports presets: cpu-low, cpu-medium, cpu-high, gpu-ethash, gpu-kawpow
- Features: variance, sine-wave fluctuation, 30s ramp-up, 98% share rate
- XMRig-compatible stats format for full UI compatibility
- NewManagerForSimulation() skips autostart of real miners
Miners Page Redesign:
- Featured cards for installed/recommended miners with gradient styling
- "Installed" (green) and "Recommended" (gold) ribbon badges
- Placeholder cards for 8 planned miners with "Coming Soon" badges
- Algorithm badges, GitHub links, and license info for each miner
- Planned miners: T-Rex, lolMiner, Rigel, BzMiner, SRBMiner, TeamRedMiner, GMiner, NBMiner
Chart Improvements:
- Hybrid data approach: live in-memory data while active, database historical when inactive
- Smoother transitions between data sources
Documentation:
- Updated DEVELOPMENT.md with simulation mode usage
- Updated ARCHITECTURE.md with WebSocket, simulation, and supported miners table
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 07:11:41 +00:00
}