- Add sync.RWMutex to config_manager.go for file operation synchronization - Add deprecation warning to unsafe GetDB() function in database.go - Fix UninstallMiner map modification during iteration in manager.go - Add server readiness verification via TCP dial in service.go - Add mutex-protected httpClient getter/setter in xmrig.go - Update GetLatestVersion to use synchronized HTTP client in ttminer.go - Update MockMiner in service_test.go to match context-aware GetStats interface 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
799 lines
25 KiB
Go
799 lines
25 KiB
Go
package mining
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Masterminds/semver/v3"
|
|
"github.com/Snider/Mining/docs"
|
|
"github.com/adrg/xdg"
|
|
"github.com/gin-contrib/cors"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/shirou/gopsutil/v4/mem"
|
|
"github.com/swaggo/swag"
|
|
|
|
swaggerFiles "github.com/swaggo/files"
|
|
ginSwagger "github.com/swaggo/gin-swagger"
|
|
)
|
|
|
|
// Service encapsulates the gin-gonic router and the mining manager.
|
|
type Service struct {
|
|
Manager ManagerInterface
|
|
ProfileManager *ProfileManager
|
|
NodeService *NodeService
|
|
Router *gin.Engine
|
|
Server *http.Server
|
|
DisplayAddr string
|
|
SwaggerInstanceName string
|
|
APIBasePath string
|
|
SwaggerUIPath string
|
|
}
|
|
|
|
// NewService creates a new mining service
|
|
func NewService(manager ManagerInterface, listenAddr string, displayAddr string, swaggerNamespace string) (*Service, error) {
|
|
apiBasePath := "/" + strings.Trim(swaggerNamespace, "/")
|
|
swaggerUIPath := apiBasePath + "/swagger"
|
|
|
|
docs.SwaggerInfo.Title = "Mining Module API"
|
|
docs.SwaggerInfo.Version = "1.0"
|
|
docs.SwaggerInfo.Host = displayAddr
|
|
docs.SwaggerInfo.BasePath = apiBasePath
|
|
instanceName := "swagger_" + strings.ReplaceAll(strings.Trim(swaggerNamespace, "/"), "/", "_")
|
|
swag.Register(instanceName, docs.SwaggerInfo)
|
|
|
|
profileManager, err := NewProfileManager()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to initialize profile manager: %w", err)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
return &Service{
|
|
Manager: manager,
|
|
ProfileManager: profileManager,
|
|
NodeService: nodeService,
|
|
Server: &http.Server{
|
|
Addr: listenAddr,
|
|
},
|
|
DisplayAddr: displayAddr,
|
|
SwaggerInstanceName: instanceName,
|
|
APIBasePath: apiBasePath,
|
|
SwaggerUIPath: swaggerUIPath,
|
|
}, nil
|
|
}
|
|
|
|
// 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() {
|
|
s.Router = gin.Default()
|
|
|
|
// 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",
|
|
"http://localhost:" + strings.Split(s.Server.Addr, ":")[len(strings.Split(s.Server.Addr, ":"))-1],
|
|
"http://127.0.0.1:" + strings.Split(s.Server.Addr, ":")[len(strings.Split(s.Server.Addr, ":"))-1],
|
|
"wails://wails", // Wails desktop app
|
|
},
|
|
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
|
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
|
|
ExposeHeaders: []string{"Content-Length"},
|
|
AllowCredentials: true,
|
|
MaxAge: 12 * time.Hour,
|
|
}
|
|
s.Router.Use(cors.New(corsConfig))
|
|
s.SetupRoutes()
|
|
}
|
|
|
|
// 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()
|
|
s.Server.Handler = s.Router
|
|
|
|
// Channel to capture server startup errors
|
|
errChan := make(chan error, 1)
|
|
|
|
go func() {
|
|
if err := s.Server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
log.Printf("Server error on %s: %v", s.Server.Addr, err)
|
|
errChan <- err
|
|
}
|
|
close(errChan) // Prevent goroutine leak
|
|
}()
|
|
|
|
go func() {
|
|
<-ctx.Done()
|
|
s.Manager.Stop()
|
|
ctxShutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if err := s.Server.Shutdown(ctxShutdown); err != nil {
|
|
log.Printf("Server shutdown error: %v", err)
|
|
}
|
|
}()
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("server failed to start listening on %s within timeout", s.Server.Addr)
|
|
}
|
|
|
|
// 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() {
|
|
apiGroup := s.Router.Group(s.APIBasePath)
|
|
{
|
|
apiGroup.GET("/info", s.handleGetInfo)
|
|
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)
|
|
minersGroup.GET("/:miner_name/hashrate-history", s.handleGetMinerHashrateHistory)
|
|
minersGroup.GET("/:miner_name/logs", s.handleGetMinerLogs)
|
|
minersGroup.POST("/:miner_name/stdin", s.handleMinerStdin)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// Add P2P node endpoints if node service is available
|
|
if s.NodeService != nil {
|
|
s.NodeService.SetupRoutes(apiGroup)
|
|
}
|
|
}
|
|
|
|
// Serve the embedded web component
|
|
componentFS, err := GetComponentFS()
|
|
if err == nil {
|
|
s.Router.StaticFS("/component", componentFS)
|
|
}
|
|
|
|
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
|
|
// @Summary Get live miner installation information
|
|
// @Description Retrieves live installation details for all miners, along with system information.
|
|
// @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) {
|
|
systemInfo, err := s.updateInstallationCache()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get system info", "details": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, systemInfo)
|
|
}
|
|
|
|
// updateInstallationCache performs a live check and updates the cache file.
|
|
func (s *Service) updateInstallationCache() (*SystemInfo, error) {
|
|
// 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)
|
|
}
|
|
|
|
for _, availableMiner := range s.Manager.ListAvailableMiners() {
|
|
var miner Miner
|
|
switch availableMiner.Name {
|
|
case "xmrig":
|
|
miner = NewXMRigMiner()
|
|
case "tt-miner":
|
|
miner = NewTTMiner()
|
|
default:
|
|
continue
|
|
}
|
|
details, err := miner.CheckInstallation()
|
|
if err != nil {
|
|
log.Printf("Warning: failed to check installation for %s: %v", availableMiner.Name, err)
|
|
}
|
|
systemInfo.InstalledMinersInfo = append(systemInfo.InstalledMinersInfo, details)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
if err := os.WriteFile(configPath, data, 0600); err != nil {
|
|
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)
|
|
}
|
|
|
|
// 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() {
|
|
var miner Miner
|
|
switch availableMiner.Name {
|
|
case "xmrig":
|
|
miner = NewXMRigMiner()
|
|
case "tt-miner":
|
|
miner = NewTTMiner()
|
|
default:
|
|
continue
|
|
}
|
|
|
|
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")
|
|
if err := s.Manager.UninstallMiner(minerType); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
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."})
|
|
}
|
|
|
|
// 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")
|
|
var miner Miner
|
|
switch minerType {
|
|
case "xmrig":
|
|
miner = NewXMRigMiner()
|
|
case "tt-miner":
|
|
miner = NewTTMiner()
|
|
default:
|
|
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
|
|
}
|
|
|
|
if _, err := s.updateInstallationCache(); err != nil {
|
|
log.Printf("Warning: failed to update cache after install: %v", err)
|
|
}
|
|
|
|
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})
|
|
}
|
|
|
|
// handleStartMinerWithProfile godoc
|
|
// @Summary Start a new miner using a profile
|
|
// @Description Start a new miner with the configuration from a saved profile
|
|
// @Tags profiles
|
|
// @Produce json
|
|
// @Param id path string true "Profile ID"
|
|
// @Success 200 {object} XMRigMiner
|
|
// @Router /profiles/{id}/start [post]
|
|
func (s *Service) handleStartMinerWithProfile(c *gin.Context) {
|
|
profileID := c.Param("id")
|
|
profile, exists := s.ProfileManager.GetProfile(profileID)
|
|
if !exists {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "profile not found"})
|
|
return
|
|
}
|
|
|
|
var config Config
|
|
if err := json.Unmarshal(profile.Config, &config); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to parse profile config", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
miner, err := s.Manager.StartMiner(profile.MinerType, &config)
|
|
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")
|
|
if err := s.Manager.StopMiner(minerName); err != nil {
|
|
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 {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "miner not found"})
|
|
return
|
|
}
|
|
stats, err := miner.GetStats(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, stats)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// handleGetMinerLogs godoc
|
|
// @Summary Get miner log output
|
|
// @Description Get the captured stdout/stderr output from a running miner. Log lines are base64 encoded to preserve ANSI escape codes and special characters.
|
|
// @Tags miners
|
|
// @Produce json
|
|
// @Param miner_name path string true "Miner Name"
|
|
// @Success 200 {array} string "Base64 encoded log lines"
|
|
// @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 {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "miner not found"})
|
|
return
|
|
}
|
|
logs := miner.GetLogs()
|
|
// 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 {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "miner not found"})
|
|
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})
|
|
}
|
|
|
|
// 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 {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "profile not found"})
|
|
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"})
|
|
}
|
|
|
|
// 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)
|
|
}
|