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"
2025-12-31 02:02:57 +00:00
"net"
2025-11-09 01:02:31 +00:00
"net/http"
2025-12-31 14:07:26 +00:00
"net/url"
2025-11-09 01:02:31 +00:00
"os"
"path/filepath"
"runtime"
"strings"
2025-12-31 17:44:49 +00:00
"sync/atomic"
2025-11-09 01:02:31 +00:00
"time"
"github.com/Masterminds/semver/v3"
"github.com/Snider/Mining/docs"
2025-12-31 11:48:45 +00:00
"github.com/Snider/Mining/pkg/logging"
2025-12-07 16:26:18 +00:00
"github.com/adrg/xdg"
2025-12-31 14:12:44 +00:00
ginmcp "github.com/ckanthony/gin-mcp"
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
2025-12-31 14:07:26 +00:00
auth * DigestAuth
2025-12-31 14:12:44 +00:00
mcpServer * ginmcp . GinMCP
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?
}
2025-12-31 17:44:49 +00:00
// debugErrorsEnabled controls whether internal error details are exposed in API responses.
// In production, this should be false to prevent information disclosure.
var debugErrorsEnabled = os . Getenv ( "DEBUG_ERRORS" ) == "true" || os . Getenv ( "GIN_MODE" ) != "release"
// sanitizeErrorDetails filters potentially sensitive information from error details.
// In production mode (debugErrorsEnabled=false), returns empty string.
func sanitizeErrorDetails ( details string ) string {
if debugErrorsEnabled {
return details
}
// In production, don't expose internal error details
return ""
}
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 ,
2025-12-31 17:44:49 +00:00
Details : sanitizeErrorDetails ( details ) ,
2025-12-31 09:38:25 +00:00
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 )
}
2025-12-31 13:33:42 +00:00
// respondWithMiningError sends a structured error response from a MiningError.
// This allows using pre-built error constructors from errors.go.
func respondWithMiningError ( c * gin . Context , err * MiningError ) {
details := ""
if err . Cause != nil {
details = err . Cause . Error ( )
}
if err . Details != "" {
if details != "" {
details += "; "
}
details += err . Details
}
apiErr := APIError {
Code : err . Code ,
Message : err . Message ,
2025-12-31 17:44:49 +00:00
Details : sanitizeErrorDetails ( details ) ,
2025-12-31 13:33:42 +00:00
Suggestion : err . Suggestion ,
Retryable : err . Retryable ,
}
c . JSON ( err . StatusCode ( ) , apiErr )
}
2025-12-31 09:38:25 +00:00
// 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 16:38:48 +00:00
// securityHeadersMiddleware adds security headers to all responses.
// This helps protect against common web vulnerabilities.
func securityHeadersMiddleware ( ) gin . HandlerFunc {
return func ( c * gin . Context ) {
// Prevent MIME type sniffing
c . Header ( "X-Content-Type-Options" , "nosniff" )
// Prevent clickjacking
c . Header ( "X-Frame-Options" , "DENY" )
// Enable XSS filter in older browsers
c . Header ( "X-XSS-Protection" , "1; mode=block" )
// Restrict referrer information
c . Header ( "Referrer-Policy" , "strict-origin-when-cross-origin" )
// Content Security Policy for API responses
c . Header ( "Content-Security-Policy" , "default-src 'none'; frame-ancestors 'none'" )
c . Next ( )
}
}
// contentTypeValidationMiddleware ensures POST/PUT requests have proper Content-Type.
func contentTypeValidationMiddleware ( ) gin . HandlerFunc {
return func ( c * gin . Context ) {
method := c . Request . Method
if method != http . MethodPost && method != http . MethodPut && method != http . MethodPatch {
c . Next ( )
return
}
// Skip if no body expected
if c . Request . ContentLength == 0 {
c . Next ( )
return
}
contentType := c . GetHeader ( "Content-Type" )
// Allow JSON and form data
if strings . HasPrefix ( contentType , "application/json" ) ||
strings . HasPrefix ( contentType , "application/x-www-form-urlencoded" ) ||
strings . HasPrefix ( contentType , "multipart/form-data" ) {
c . Next ( )
return
}
respondWithError ( c , http . StatusUnsupportedMediaType , ErrCodeInvalidInput ,
"Unsupported Content-Type" ,
"Use application/json for API requests" )
c . Abort ( )
}
}
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 14:33:30 +00:00
// getRequestID extracts the request ID from gin context
func getRequestID ( c * gin . Context ) string {
if id , exists := c . Get ( "requestID" ) ; exists {
if s , ok := id . ( string ) ; ok {
return s
}
}
return ""
}
// logWithRequestID logs a message with request ID correlation
func logWithRequestID ( c * gin . Context , level string , message string , fields logging . Fields ) {
if fields == nil {
fields = logging . Fields { }
}
if reqID := getRequestID ( c ) ; reqID != "" {
fields [ "request_id" ] = reqID
}
switch level {
case "error" :
logging . Error ( message , fields )
case "warn" :
logging . Warn ( message , fields )
case "info" :
logging . Info ( message , fields )
default :
logging . Debug ( message , fields )
}
}
2025-12-31 09:51:48 +00:00
2025-12-31 15:54:37 +00:00
// csrfMiddleware protects against CSRF attacks for browser-based requests.
// For state-changing methods (POST, PUT, DELETE), it requires one of:
// - Authorization header (API clients)
// - X-Requested-With header (AJAX clients)
// - Origin header matching allowed origins (already handled by CORS)
// GET requests are always allowed as they should be idempotent.
func csrfMiddleware ( ) gin . HandlerFunc {
return func ( c * gin . Context ) {
// Only check state-changing methods
method := c . Request . Method
if method == http . MethodGet || method == http . MethodHead || method == http . MethodOptions {
c . Next ( )
return
}
// Allow if Authorization header present (API client)
if c . GetHeader ( "Authorization" ) != "" {
c . Next ( )
return
}
// Allow if X-Requested-With header present (AJAX/XHR request)
if c . GetHeader ( "X-Requested-With" ) != "" {
c . Next ( )
return
}
// Allow if Content-Type is application/json (not sent by HTML forms)
contentType := c . GetHeader ( "Content-Type" )
if strings . HasPrefix ( contentType , "application/json" ) {
c . Next ( )
return
}
// Reject the request as potential CSRF
respondWithError ( c , http . StatusForbidden , "CSRF_PROTECTION" ,
"Request blocked by CSRF protection" ,
"Include X-Requested-With header or use application/json content type" )
c . Abort ( )
}
}
// DefaultRequestTimeout is the default timeout for API requests.
const DefaultRequestTimeout = 30 * time . Second
// Cache-Control header constants
const (
CacheNoStore = "no-store"
CacheNoCache = "no-cache"
CachePublic1Min = "public, max-age=60"
CachePublic5Min = "public, max-age=300"
)
// cacheMiddleware adds Cache-Control headers based on the endpoint.
func cacheMiddleware ( ) gin . HandlerFunc {
return func ( c * gin . Context ) {
// Only cache GET requests
if c . Request . Method != http . MethodGet {
c . Header ( "Cache-Control" , CacheNoStore )
c . Next ( )
return
}
path := c . Request . URL . Path
// Static-ish resources that can be cached briefly
switch {
case strings . HasSuffix ( path , "/available" ) :
// Available miners list - can be cached for 5 minutes
c . Header ( "Cache-Control" , CachePublic5Min )
case strings . HasSuffix ( path , "/info" ) :
// System info - can be cached for 1 minute
c . Header ( "Cache-Control" , CachePublic1Min )
case strings . Contains ( path , "/swagger" ) :
// Swagger docs - can be cached
c . Header ( "Cache-Control" , CachePublic5Min )
default :
// Dynamic data (stats, miners, profiles) - don't cache
c . Header ( "Cache-Control" , CacheNoCache )
}
c . Next ( )
}
}
// requestTimeoutMiddleware adds a timeout to request handling.
// This prevents slow requests from consuming resources indefinitely.
func requestTimeoutMiddleware ( timeout time . Duration ) gin . HandlerFunc {
return func ( c * gin . Context ) {
// Skip timeout for WebSocket upgrades and streaming endpoints
if c . GetHeader ( "Upgrade" ) == "websocket" {
c . Next ( )
return
}
if strings . HasSuffix ( c . Request . URL . Path , "/events" ) {
c . Next ( )
return
}
// Create context with timeout
ctx , cancel := context . WithTimeout ( c . Request . Context ( ) , timeout )
defer cancel ( )
// Replace request context
c . Request = c . Request . WithContext ( ctx )
2025-12-31 17:44:49 +00:00
// Use atomic flag to prevent race condition between handler and timeout response
// Only one of them should write to the response
var responded int32
2025-12-31 15:54:37 +00:00
// Channel to signal completion
done := make ( chan struct { } )
go func ( ) {
c . Next ( )
2025-12-31 17:44:49 +00:00
// Mark that the handler has completed (and likely written a response)
atomic . StoreInt32 ( & responded , 1 )
2025-12-31 15:54:37 +00:00
close ( done )
} ( )
select {
case <- done :
// Request completed normally
case <- ctx . Done ( ) :
2025-12-31 17:44:49 +00:00
// Timeout occurred - only respond if handler hasn't already
if atomic . CompareAndSwapInt32 ( & responded , 0 , 1 ) {
c . Abort ( )
respondWithError ( c , http . StatusGatewayTimeout , ErrCodeTimeout ,
"Request timed out" , fmt . Sprintf ( "Request exceeded %s timeout" , timeout ) )
}
2025-12-31 15:54:37 +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 {
2025-12-31 14:07:26 +00:00
// Allow connections from localhost origins only
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
origin := r . Header . Get ( "Origin" )
if origin == "" {
2025-12-31 14:07:26 +00:00
return true // No origin header (non-browser clients)
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
}
2025-12-31 14:07:26 +00:00
// Parse the origin URL properly to prevent bypass attacks
u , err := url . Parse ( origin )
if err != nil {
return false
}
host := u . Hostname ( )
// Only allow exact localhost matches
return host == "localhost" || host == "127.0.0.1" || host == "::1" ||
host == "wails.localhost"
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
} ,
}
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 {
2025-12-31 15:32:07 +00:00
logging . Warn ( "failed to initialize profile manager" , logging . Fields { "error" : err } )
// Continue without profile manager - profile features will be degraded
// Create a minimal in-memory profile manager as fallback
profileManager = & ProfileManager {
profiles : make ( map [ string ] * MiningProfile ) ,
}
2025-12-10 22:17:38 +00:00
}
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 {
2025-12-31 11:48:45 +00:00
logging . Warn ( "failed to initialize node service" , logging . Fields { "error" : err } )
2025-12-29 19:49:33 +00:00
// 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-12-31 14:07:26 +00:00
// Initialize authentication from environment
authConfig := AuthConfigFromEnv ( )
var auth * DigestAuth
if authConfig . Enabled {
auth = NewDigestAuth ( authConfig )
logging . Info ( "API authentication enabled" , logging . Fields { "realm" : authConfig . Realm } )
}
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-31 14:07:26 +00:00
auth : auth ,
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 {
2025-12-31 14:33:30 +00:00
"http://localhost:4200" , // Angular dev server
2025-12-31 01:02:23 +00:00
"http://127.0.0.1:4200" ,
2025-12-31 14:33:30 +00:00
"http://localhost:9090" , // Default API port
2025-12-31 01:02:23 +00:00
"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 15:54:37 +00:00
AllowHeaders : [ ] string { "Origin" , "Content-Type" , "Accept" , "Authorization" , "X-Request-ID" , "X-Requested-With" } ,
2025-12-31 09:39:42 +00:00
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
2025-12-31 16:38:48 +00:00
// Add security headers (SEC-LOW-4)
s . Router . Use ( securityHeadersMiddleware ( ) )
// Add Content-Type validation for POST/PUT (API-MED-8)
s . Router . Use ( contentTypeValidationMiddleware ( ) )
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 15:54:37 +00:00
// Add CSRF protection for browser requests (SEC-MED-3)
// Requires X-Requested-With or Authorization header for state-changing methods
s . Router . Use ( csrfMiddleware ( ) )
// Add request timeout middleware (RESIL-MED-8)
s . Router . Use ( requestTimeoutMiddleware ( DefaultRequestTimeout ) )
// Add cache headers middleware (API-MED-7)
s . Router . Use ( cacheMiddleware ( ) )
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-31 14:33:30 +00:00
if s . auth != nil {
s . auth . Stop ( )
}
if s . NodeService != nil {
if err := s . NodeService . StopTransport ( ) ; err != nil {
logging . Warn ( "failed to stop node service transport" , logging . Fields { "error" : err } )
}
}
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
}
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 11:48:45 +00:00
logging . Error ( "server error" , logging . Fields { "addr" : s . Server . Addr , "error" : err } )
2025-12-31 01:15:35 +00:00
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-12-31 14:33:30 +00:00
s . Stop ( ) // Clean up service resources (auth, event hub, node service)
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 11:48:45 +00:00
logging . Error ( "server shutdown error" , logging . Fields { "error" : 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-31 14:07:26 +00:00
2025-12-31 15:32:07 +00:00
// Health endpoints (no auth required for orchestration/monitoring)
apiGroup . GET ( "/health" , s . handleHealth )
apiGroup . GET ( "/ready" , s . handleReady )
2025-12-31 14:07:26 +00:00
// Apply authentication middleware if enabled
if s . auth != nil {
apiGroup . Use ( s . auth . Middleware ( ) )
}
2025-11-09 01:02:31 +00:00
{
2025-12-07 16:26:18 +00:00
apiGroup . GET ( "/info" , s . handleGetInfo )
2025-12-31 14:33:30 +00:00
apiGroup . GET ( "/metrics" , s . handleMetrics )
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 ) ) )
2025-12-31 14:12:44 +00:00
// Initialize MCP server for AI assistant integration
// This exposes API endpoints as MCP tools for Claude, Cursor, etc.
s . mcpServer = ginmcp . New ( s . Router , & ginmcp . Config {
Name : "Mining API" ,
Description : "Mining dashboard API exposed via Model Context Protocol (MCP)" ,
BaseURL : fmt . Sprintf ( "http://%s" , s . DisplayAddr ) ,
} )
s . mcpServer . Mount ( s . APIBasePath + "/mcp" )
logging . Info ( "MCP server enabled" , logging . Fields { "endpoint" : s . APIBasePath + "/mcp" } )
2025-11-09 01:02:31 +00:00
}
2025-12-31 15:32:07 +00:00
// HealthResponse represents the health check response
type HealthResponse struct {
Status string ` json:"status" `
Components map [ string ] string ` json:"components,omitempty" `
}
// handleHealth godoc
// @Summary Health check endpoint
// @Description Returns service health status. Used for liveness probes.
// @Tags system
// @Produce json
// @Success 200 {object} HealthResponse
// @Router /health [get]
func ( s * Service ) handleHealth ( c * gin . Context ) {
c . JSON ( http . StatusOK , HealthResponse {
Status : "healthy" ,
} )
}
// handleReady godoc
// @Summary Readiness check endpoint
// @Description Returns service readiness with component status. Used for readiness probes.
// @Tags system
// @Produce json
// @Success 200 {object} HealthResponse
// @Success 503 {object} HealthResponse
// @Router /ready [get]
func ( s * Service ) handleReady ( c * gin . Context ) {
components := make ( map [ string ] string )
allReady := true
// Check manager
if s . Manager != nil {
components [ "manager" ] = "ready"
} else {
components [ "manager" ] = "not initialized"
allReady = false
}
// Check profile manager
if s . ProfileManager != nil {
components [ "profiles" ] = "ready"
} else {
components [ "profiles" ] = "degraded"
// Don't fail readiness for degraded profile manager
}
// Check event hub
if s . EventHub != nil {
components [ "events" ] = "ready"
} else {
components [ "events" ] = "not initialized"
allReady = false
}
// Check node service (optional)
if s . NodeService != nil {
components [ "p2p" ] = "ready"
} else {
components [ "p2p" ] = "disabled"
}
status := "ready"
httpStatus := http . StatusOK
if ! allReady {
status = "not ready"
httpStatus = http . StatusServiceUnavailable
}
c . JSON ( httpStatus , HealthResponse {
Status : status ,
Components : components ,
} )
}
2025-11-09 01:02:31 +00:00
// 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-31 15:45:25 +00:00
respondWithMiningError ( c , ErrInternal ( "failed to get system info" ) . WithCause ( err ) )
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 {
2025-12-31 11:48:45 +00:00
logging . Warn ( "failed to check installation" , logging . Fields { "miner" : availableMiner . Name , "error" : err } )
2025-12-31 01:15:35 +00:00
}
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 {
2025-12-31 15:45:25 +00:00
respondWithMiningError ( c , ErrInternal ( "failed to update cache" ) . WithCause ( err ) )
2025-12-07 16:26:18 +00:00
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-12-31 15:45:25 +00:00
respondWithMiningError ( c , ErrInternal ( "failed to uninstall miner" ) . WithCause ( err ) )
2025-11-09 01:02:31 +00:00
return
}
2025-12-07 16:26:18 +00:00
if _ , err := s . updateInstallationCache ( ) ; err != nil {
2025-12-31 11:48:45 +00:00
logging . Warn ( "failed to update cache after uninstall" , logging . Fields { "error" : err } )
2025-12-07 16:26:18 +00:00
}
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-12-31 15:45:25 +00:00
respondWithMiningError ( c , ErrUnsupportedMiner ( minerType ) )
2025-11-09 01:02:31 +00:00
return
}
if err := miner . Install ( ) ; err != nil {
2025-12-31 15:45:25 +00:00
respondWithMiningError ( c , ErrInstallFailed ( minerType ) . WithCause ( err ) )
2025-11-09 01:02:31 +00:00
return
}
2025-12-07 16:26:18 +00:00
if _ , err := s . updateInstallationCache ( ) ; err != nil {
2025-12-31 11:48:45 +00:00
logging . Warn ( "failed to update cache after install" , logging . Fields { "error" : err } )
2025-12-07 16:26:18 +00:00
}
2025-11-09 01:02:31 +00:00
details , err := miner . CheckInstallation ( )
if err != nil {
2025-12-31 15:45:25 +00:00
respondWithMiningError ( c , ErrInternal ( "failed to verify installation" ) . WithCause ( err ) )
2025-11-09 01:02:31 +00:00
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 13:33:42 +00:00
respondWithMiningError ( c , ErrProfileNotFound ( profileID ) )
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 {
2025-12-31 15:45:25 +00:00
respondWithMiningError ( c , ErrInvalidConfig ( "failed to parse profile config" ) . WithCause ( err ) )
2025-11-09 01:02:31 +00:00
return
}
2025-12-10 22:17:38 +00:00
2025-12-31 17:44:49 +00:00
// Validate config from profile to prevent shell injection and other issues
if err := config . Validate ( ) ; err != nil {
respondWithMiningError ( c , ErrInvalidConfig ( "profile config validation failed" ) . WithCause ( err ) )
return
}
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 {
2025-12-31 15:45:25 +00:00
respondWithMiningError ( c , ErrStartFailed ( profile . Name ) . WithCause ( err ) )
2025-11-09 01:02:31 +00:00
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-12-31 15:45:25 +00:00
respondWithMiningError ( c , ErrStopFailed ( minerName ) . WithCause ( err ) )
2025-11-09 01:02:31 +00:00
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 13:33:42 +00:00
respondWithMiningError ( c , ErrMinerNotFound ( minerName ) . WithCause ( err ) )
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 {
2025-12-31 15:45:25 +00:00
respondWithMiningError ( c , ErrInternal ( "failed to get miner stats" ) . WithCause ( err ) )
2025-11-09 01:02:31 +00:00
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 {
2025-12-31 15:45:25 +00:00
respondWithMiningError ( c , ErrMinerNotFound ( minerName ) . WithCause ( err ) )
2025-11-09 04:08:10 +00:00
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 13:33:42 +00:00
respondWithMiningError ( c , ErrMinerNotFound ( minerName ) . WithCause ( err ) )
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 {
2025-12-31 15:45:25 +00:00
respondWithMiningError ( c , ErrInvalidConfig ( "invalid input format" ) . WithCause ( err ) )
2025-12-29 23:30:19 +00:00
return
}
if err := miner . WriteStdin ( input . Input ) ; err != nil {
2025-12-31 15:45:25 +00:00
respondWithMiningError ( c , ErrInternal ( "failed to write to stdin" ) . WithCause ( err ) )
2025-12-29 23:30:19 +00:00
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
2025-12-31 14:33:30 +00:00
// @Failure 400 {object} APIError "Invalid profile data"
2025-12-10 22:17:38 +00:00
// @Router /profiles [post]
func ( s * Service ) handleCreateProfile ( c * gin . Context ) {
var profile MiningProfile
if err := c . ShouldBindJSON ( & profile ) ; err != nil {
2025-12-31 14:33:30 +00:00
respondWithError ( c , http . StatusBadRequest , ErrCodeInvalidInput , "invalid profile data" , err . Error ( ) )
return
}
// Validate required fields
if profile . Name == "" {
respondWithError ( c , http . StatusBadRequest , ErrCodeInvalidInput , "profile name is required" , "" )
return
}
if profile . MinerType == "" {
respondWithError ( c , http . StatusBadRequest , ErrCodeInvalidInput , "miner type is required" , "" )
2025-12-10 22:17:38 +00:00
return
}
createdProfile , err := s . ProfileManager . CreateProfile ( & profile )
if err != nil {
2025-12-31 14:33:30 +00:00
respondWithError ( c , http . StatusInternalServerError , ErrCodeInternal , "failed to create profile" , err . Error ( ) )
2025-12-10 22:17:38 +00:00
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
2025-12-31 14:33:30 +00:00
// @Failure 404 {object} APIError "Profile not found"
2025-12-10 22:17:38 +00:00
// @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 {
2025-12-31 14:33:30 +00:00
respondWithError ( c , http . StatusBadRequest , ErrCodeInvalidInput , "invalid profile data" , err . Error ( ) )
2025-12-10 22:17:38 +00:00
return
}
profile . ID = profileID
if err := s . ProfileManager . UpdateProfile ( & profile ) ; err != nil {
2025-12-31 14:33:30 +00:00
// Check if error is "not found"
if strings . Contains ( err . Error ( ) , "not found" ) {
respondWithError ( c , http . StatusNotFound , ErrCodeProfileNotFound , "profile not found" , err . Error ( ) )
return
}
respondWithError ( c , http . StatusInternalServerError , ErrCodeInternal , "failed to update profile" , err . Error ( ) )
2025-12-10 22:17:38 +00:00
return
}
c . JSON ( http . StatusOK , profile )
}
// handleDeleteProfile godoc
// @Summary Delete a mining profile
2025-12-31 14:33:30 +00:00
// @Description Delete a mining profile by its ID. Idempotent - returns success even if profile doesn't exist.
2025-12-10 22:17:38 +00:00
// @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 {
2025-12-31 14:33:30 +00:00
// Make DELETE idempotent - if profile doesn't exist, still return success
if strings . Contains ( err . Error ( ) , "not found" ) {
c . JSON ( http . StatusOK , gin . H { "status" : "profile deleted" } )
return
}
respondWithError ( c , http . StatusInternalServerError , ErrCodeInternal , "failed to delete profile" , err . Error ( ) )
2025-12-10 22:17:38 +00:00
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 {
2025-12-31 15:45:25 +00:00
respondWithMiningError ( c , ErrInternal ( "manager type not supported" ) )
2025-12-29 22:10:45 +00:00
return
}
stats , err := manager . GetAllMinerHistoricalStats ( )
if err != nil {
2025-12-31 15:45:25 +00:00
respondWithMiningError ( c , ErrDatabaseError ( "get historical stats" ) . WithCause ( err ) )
2025-12-29 22:10:45 +00:00
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 {
2025-12-31 15:45:25 +00:00
respondWithMiningError ( c , ErrInternal ( "manager type not supported" ) )
2025-12-29 22:10:45 +00:00
return
}
stats , err := manager . GetMinerHistoricalStats ( minerName )
if err != nil {
2025-12-31 15:45:25 +00:00
respondWithMiningError ( c , ErrDatabaseError ( "get miner stats" ) . WithCause ( err ) )
2025-12-29 22:10:45 +00:00
return
}
if stats == nil {
2025-12-31 15:45:25 +00:00
respondWithMiningError ( c , ErrMinerNotFound ( minerName ) . WithDetails ( "no historical data found" ) )
2025-12-29 22:10:45 +00:00
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 {
2025-12-31 15:45:25 +00:00
respondWithMiningError ( c , ErrInternal ( "manager type not supported" ) )
2025-12-29 22:10:45 +00:00
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 {
2025-12-31 15:45:25 +00:00
respondWithMiningError ( c , ErrDatabaseError ( "get hashrate history" ) . WithCause ( err ) )
2025-12-29 22:10:45 +00:00
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 {
2025-12-31 11:48:45 +00:00
logging . Error ( "failed to upgrade WebSocket connection" , logging . Fields { "error" : err } )
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
return
}
2025-12-31 11:48:45 +00:00
logging . Info ( "new WebSocket connection" , logging . Fields { "remote" : c . Request . RemoteAddr } )
2025-12-31 17:44:49 +00:00
// Only record connection after successful registration to avoid metrics race
if s . EventHub . ServeWs ( conn ) {
RecordWSConnection ( true )
} else {
2025-12-31 11:48:45 +00:00
logging . Warn ( "WebSocket connection rejected" , logging . Fields { "remote" : c . Request . RemoteAddr , "reason" : "limit reached" } )
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
}
2025-12-31 14:33:30 +00:00
// handleMetrics godoc
// @Summary Get internal metrics
// @Description Returns internal metrics for monitoring and debugging
// @Tags system
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /metrics [get]
func ( s * Service ) handleMetrics ( c * gin . Context ) {
c . JSON ( http . StatusOK , GetMetricsSnapshot ( ) )
}