feat: Enhance mining configuration management and API documentation
This commit is contained in:
parent
2576d4bc1b
commit
816f860b73
18 changed files with 796 additions and 272 deletions
|
|
@ -6,6 +6,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Snider/Mining/pkg/mining"
|
||||
"github.com/adrg/xdg"
|
||||
|
|
@ -53,7 +54,6 @@ func loadAndDisplayCache() (bool, error) {
|
|||
|
||||
cacheBytes, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
// If the cache file is missing (e.g., after an uninstall), it's not a fatal error
|
||||
if os.IsNotExist(err) {
|
||||
fmt.Println("No cached data found. Run 'install' for a miner first.")
|
||||
return false, nil
|
||||
|
|
@ -61,16 +61,19 @@ func loadAndDisplayCache() (bool, error) {
|
|||
return false, fmt.Errorf("could not read cache file from %s: %w", configPath, err)
|
||||
}
|
||||
|
||||
var cachedDetails []*mining.InstallationDetails
|
||||
if err := json.Unmarshal(cacheBytes, &cachedDetails); err != nil {
|
||||
var systemInfo mining.SystemInfo
|
||||
if err := json.Unmarshal(cacheBytes, &systemInfo); err != nil {
|
||||
return false, fmt.Errorf("could not parse cache file: %w", err)
|
||||
}
|
||||
|
||||
for _, details := range cachedDetails {
|
||||
fmt.Printf("System Info (cached at %s):\n", systemInfo.Timestamp.Format(time.RFC1123))
|
||||
fmt.Printf(" OS: %s, Arch: %s\n", systemInfo.OS, systemInfo.Architecture)
|
||||
fmt.Println()
|
||||
|
||||
for _, details := range systemInfo.InstalledMinersInfo {
|
||||
// Infer miner name from path for display purposes
|
||||
var minerName string
|
||||
if details.Path != "" { // Use path to infer miner name if available
|
||||
// This is a weak heuristic, but works for now.
|
||||
// A more robust solution would store miner name in InstallationDetails.
|
||||
if details.Path != "" {
|
||||
if strings.Contains(details.Path, "xmrig") {
|
||||
minerName = "XMRig"
|
||||
} else {
|
||||
|
|
@ -85,15 +88,7 @@ func loadAndDisplayCache() (bool, error) {
|
|||
return true, nil
|
||||
}
|
||||
|
||||
func saveResultsToCache(details []*mining.InstallationDetails) error {
|
||||
// Filter out non-installed miners before saving
|
||||
var installedOnly []*mining.InstallationDetails
|
||||
for _, d := range details {
|
||||
if d.IsInstalled {
|
||||
installedOnly = append(installedOnly, d)
|
||||
}
|
||||
}
|
||||
|
||||
func saveResultsToCache(systemInfo *mining.SystemInfo) error {
|
||||
configDir, err := xdg.ConfigFile("lethean-desktop/miners")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get config directory: %w", err)
|
||||
|
|
@ -103,7 +98,7 @@ func saveResultsToCache(details []*mining.InstallationDetails) error {
|
|||
}
|
||||
configPath := filepath.Join(configDir, "config.json")
|
||||
|
||||
data, err := json.MarshalIndent(installedOnly, "", " ")
|
||||
data, err := json.MarshalIndent(systemInfo, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not marshal cache data: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package cmd
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/Snider/Mining/pkg/mining"
|
||||
|
|
@ -89,7 +91,18 @@ func updateDoctorCache() error {
|
|||
}
|
||||
allDetails = append(allDetails, details)
|
||||
}
|
||||
return saveResultsToCache(allDetails)
|
||||
|
||||
// Create the SystemInfo struct that the /info endpoint expects
|
||||
systemInfo := &mining.SystemInfo{
|
||||
Timestamp: time.Now(),
|
||||
OS: runtime.GOOS,
|
||||
Architecture: runtime.GOARCH,
|
||||
GoVersion: runtime.Version(),
|
||||
AvailableCPUCores: runtime.NumCPU(),
|
||||
InstalledMinersInfo: allDetails,
|
||||
}
|
||||
|
||||
return saveResultsToCache(systemInfo)
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package cmd
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/Snider/Mining/pkg/mining"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -11,27 +10,21 @@ import (
|
|||
var uninstallCmd = &cobra.Command{
|
||||
Use: "uninstall [miner_type]",
|
||||
Short: "Uninstall a miner",
|
||||
Long: `Remove all files associated with a specific miner.`,
|
||||
Long: `Stops the miner if it is running, removes all associated files, and updates the configuration.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
minerType := args[0]
|
||||
manager := getManager() // Assuming getManager() provides the singleton manager instance
|
||||
|
||||
var miner mining.Miner
|
||||
switch minerType {
|
||||
case "xmrig":
|
||||
miner = mining.NewXMRigMiner()
|
||||
default:
|
||||
return fmt.Errorf("unknown miner type: %s", minerType)
|
||||
}
|
||||
|
||||
fmt.Printf("Uninstalling %s...\n", miner.GetName())
|
||||
if err := miner.Uninstall(); err != nil {
|
||||
fmt.Printf("Uninstalling %s...\n", minerType)
|
||||
if err := manager.UninstallMiner(minerType); err != nil {
|
||||
return fmt.Errorf("failed to uninstall miner: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s uninstalled successfully.\n", miner.GetName())
|
||||
fmt.Printf("%s uninstalled successfully.\n", minerType)
|
||||
|
||||
// Update the cache after a successful uninstallation
|
||||
// The doctor cache is implicitly updated by the manager's actions,
|
||||
// but an explicit cache update can still be beneficial.
|
||||
fmt.Println("Updating installation cache...")
|
||||
if err := updateDoctorCache(); err != nil {
|
||||
fmt.Printf("Warning: failed to update doctor cache: %v\n", err)
|
||||
|
|
|
|||
|
|
@ -29,10 +29,7 @@ const docTemplate = `{
|
|||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/mining.InstallationDetails"
|
||||
}
|
||||
"$ref": "#/definitions/mining.SystemInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -569,6 +566,10 @@ const docTemplate = `{
|
|||
"mining.InstallationDetails": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"config_path": {
|
||||
"description": "Add path to the miner-specific config",
|
||||
"type": "string"
|
||||
},
|
||||
"is_installed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -23,10 +23,7 @@
|
|||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/mining.InstallationDetails"
|
||||
}
|
||||
"$ref": "#/definitions/mining.SystemInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -563,6 +560,10 @@
|
|||
"mining.InstallationDetails": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"config_path": {
|
||||
"description": "Add path to the miner-specific config",
|
||||
"type": "string"
|
||||
},
|
||||
"is_installed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -154,6 +154,9 @@ definitions:
|
|||
type: object
|
||||
mining.InstallationDetails:
|
||||
properties:
|
||||
config_path:
|
||||
description: Add path to the miner-specific config
|
||||
type: string
|
||||
is_installed:
|
||||
type: boolean
|
||||
miner_binary:
|
||||
|
|
@ -244,9 +247,7 @@ paths:
|
|||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/mining.InstallationDetails'
|
||||
type: array
|
||||
$ref: '#/definitions/mining.SystemInfo'
|
||||
summary: Check miner installations
|
||||
tags:
|
||||
- system
|
||||
|
|
|
|||
71
pkg/mining/config_manager.go
Normal file
71
pkg/mining/config_manager.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
package mining
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
)
|
||||
|
||||
// MinerAutostartConfig represents the configuration for a single miner's autostart settings.
|
||||
type MinerAutostartConfig struct {
|
||||
MinerType string `json:"minerType"`
|
||||
Autostart bool `json:"autostart"`
|
||||
Config *Config `json:"config,omitempty"` // Store the last used config
|
||||
}
|
||||
|
||||
// MinersConfig represents the overall configuration for all miners, including autostart settings.
|
||||
type MinersConfig struct {
|
||||
Miners []MinerAutostartConfig `json:"miners"`
|
||||
}
|
||||
|
||||
// GetMinersConfigPath returns the path to the miners configuration file.
|
||||
func GetMinersConfigPath() (string, error) {
|
||||
return xdg.ConfigFile("lethean-desktop/miners/config.json")
|
||||
}
|
||||
|
||||
// LoadMinersConfig loads the miners configuration from the file system.
|
||||
func LoadMinersConfig() (*MinersConfig, error) {
|
||||
configPath, err := GetMinersConfigPath()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not determine miners config path: %w", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return &MinersConfig{Miners: []MinerAutostartConfig{}}, nil // Return empty config if file doesn't exist
|
||||
}
|
||||
return nil, fmt.Errorf("failed to read miners config file: %w", err)
|
||||
}
|
||||
|
||||
var cfg MinersConfig
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal miners config: %w", err)
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// SaveMinersConfig saves the miners configuration to the file system.
|
||||
func SaveMinersConfig(cfg *MinersConfig) error {
|
||||
configPath, err := GetMinersConfigPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not determine miners config path: %w", err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
|
||||
return fmt.Errorf("failed to create config directory: %w", err)
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal miners config: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write miners config file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -10,10 +10,19 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// ManagerInterface defines the contract for a miner manager.
|
||||
type ManagerInterface interface {
|
||||
StartMiner(minerType string, config *Config) (Miner, error)
|
||||
StopMiner(name string) error
|
||||
GetMiner(name string) (Miner, error)
|
||||
ListMiners() []Miner
|
||||
ListAvailableMiners() []AvailableMiner
|
||||
GetMinerHashrateHistory(name string) ([]HashratePoint, error)
|
||||
UninstallMiner(minerType string) error
|
||||
Stop()
|
||||
}
|
||||
|
||||
// Manager handles the lifecycle and operations of multiple miners.
|
||||
// It provides a centralized way to start, stop, and manage different miner
|
||||
// instances, while also collecting and exposing their performance data.
|
||||
// The Manager is safe for concurrent use.
|
||||
type Manager struct {
|
||||
miners map[string]Miner
|
||||
mu sync.RWMutex
|
||||
|
|
@ -23,24 +32,80 @@ type Manager struct {
|
|||
|
||||
var _ ManagerInterface = (*Manager)(nil)
|
||||
|
||||
// NewManager creates a new miner manager.
|
||||
// NewManager creates a new miner manager and autostarts miners based on config.
|
||||
func NewManager() *Manager {
|
||||
m := &Manager{
|
||||
miners: make(map[string]Miner),
|
||||
stopChan: make(chan struct{}),
|
||||
waitGroup: sync.WaitGroup{},
|
||||
}
|
||||
m.syncMinersConfig() // Ensure config file is populated
|
||||
m.autostartMiners()
|
||||
m.startStatsCollection()
|
||||
return m
|
||||
}
|
||||
|
||||
// syncMinersConfig ensures the miners.json config file has entries for all available miners.
|
||||
func (m *Manager) syncMinersConfig() {
|
||||
cfg, err := LoadMinersConfig()
|
||||
if err != nil {
|
||||
log.Printf("Warning: could not load miners config for sync: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
availableMiners := m.ListAvailableMiners()
|
||||
configUpdated := false
|
||||
|
||||
for _, availableMiner := range availableMiners {
|
||||
found := false
|
||||
for _, configuredMiner := range cfg.Miners {
|
||||
if strings.EqualFold(configuredMiner.MinerType, availableMiner.Name) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
cfg.Miners = append(cfg.Miners, MinerAutostartConfig{
|
||||
MinerType: availableMiner.Name,
|
||||
Autostart: false,
|
||||
Config: nil, // No default config
|
||||
})
|
||||
configUpdated = true
|
||||
log.Printf("Added default config for missing miner: %s", availableMiner.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if configUpdated {
|
||||
if err := SaveMinersConfig(cfg); err != nil {
|
||||
log.Printf("Warning: failed to save updated miners config: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// autostartMiners loads the miners config and starts any miners marked for autostart.
|
||||
func (m *Manager) autostartMiners() {
|
||||
cfg, err := LoadMinersConfig()
|
||||
if err != nil {
|
||||
log.Printf("Warning: could not load miners config for autostart: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, minerCfg := range cfg.Miners {
|
||||
if minerCfg.Autostart && minerCfg.Config != nil {
|
||||
log.Printf("Autostarting miner: %s", minerCfg.MinerType)
|
||||
if _, err := m.StartMiner(minerCfg.MinerType, minerCfg.Config); err != nil {
|
||||
log.Printf("Failed to autostart miner %s: %v", minerCfg.MinerType, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// findAvailablePort finds an available TCP port on the local machine.
|
||||
func findAvailablePort() (int, error) {
|
||||
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
l, err := net.ListenTCP("tcp", addr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
|
@ -49,12 +114,11 @@ func findAvailablePort() (int, error) {
|
|||
return l.Addr().(*net.TCPAddr).Port, nil
|
||||
}
|
||||
|
||||
// StartMiner starts a new miner with the given configuration.
|
||||
// StartMiner starts a new miner and saves its configuration.
|
||||
func (m *Manager) StartMiner(minerType string, config *Config) (Miner, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// Prevent nil pointer panic if request body is empty
|
||||
if config == nil {
|
||||
config = &Config{}
|
||||
}
|
||||
|
|
@ -99,12 +163,91 @@ func (m *Manager) StartMiner(minerType string, config *Config) (Miner, error) {
|
|||
|
||||
m.miners[instanceName] = miner
|
||||
|
||||
if err := m.updateMinerConfig(minerType, true, config); err != nil {
|
||||
log.Printf("Warning: failed to save miner config for autostart: %v", err)
|
||||
}
|
||||
|
||||
logMessage := fmt.Sprintf("CryptoCurrency Miner started: %s (Binary: %s)", miner.GetName(), miner.GetBinaryPath())
|
||||
logToSyslog(logMessage)
|
||||
|
||||
return miner, nil
|
||||
}
|
||||
|
||||
// UninstallMiner stops, uninstalls, and removes a miner's configuration.
|
||||
func (m *Manager) UninstallMiner(minerType string) error {
|
||||
m.mu.Lock()
|
||||
for name, runningMiner := range m.miners {
|
||||
if rm, ok := runningMiner.(*XMRigMiner); ok && strings.EqualFold(rm.ExecutableName, minerType) {
|
||||
if err := runningMiner.Stop(); err != nil {
|
||||
log.Printf("Warning: failed to stop running miner %s during uninstall: %v", name, err)
|
||||
}
|
||||
delete(m.miners, name)
|
||||
}
|
||||
if rm, ok := runningMiner.(*TTMiner); ok && strings.EqualFold(rm.ExecutableName, minerType) {
|
||||
if err := runningMiner.Stop(); err != nil {
|
||||
log.Printf("Warning: failed to stop running miner %s during uninstall: %v", name, err)
|
||||
}
|
||||
delete(m.miners, name)
|
||||
}
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
var miner Miner
|
||||
switch strings.ToLower(minerType) {
|
||||
case "xmrig":
|
||||
miner = NewXMRigMiner()
|
||||
default:
|
||||
return fmt.Errorf("unsupported miner type: %s", minerType)
|
||||
}
|
||||
|
||||
if err := miner.Uninstall(); err != nil {
|
||||
return fmt.Errorf("failed to uninstall miner files: %w", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadMinersConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load miners config to update uninstall status: %w", err)
|
||||
}
|
||||
|
||||
var updatedMiners []MinerAutostartConfig
|
||||
for _, minerCfg := range cfg.Miners {
|
||||
if !strings.EqualFold(minerCfg.MinerType, minerType) {
|
||||
updatedMiners = append(updatedMiners, minerCfg)
|
||||
}
|
||||
}
|
||||
cfg.Miners = updatedMiners
|
||||
|
||||
return SaveMinersConfig(cfg)
|
||||
}
|
||||
|
||||
// updateMinerConfig saves the autostart and last-used config for a miner.
|
||||
func (m *Manager) updateMinerConfig(minerType string, autostart bool, config *Config) error {
|
||||
cfg, err := LoadMinersConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
found := false
|
||||
for i, minerCfg := range cfg.Miners {
|
||||
if strings.EqualFold(minerCfg.MinerType, minerType) {
|
||||
cfg.Miners[i].Autostart = autostart
|
||||
cfg.Miners[i].Config = config
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
cfg.Miners = append(cfg.Miners, MinerAutostartConfig{
|
||||
MinerType: minerType,
|
||||
Autostart: autostart,
|
||||
Config: config,
|
||||
})
|
||||
}
|
||||
|
||||
return SaveMinersConfig(cfg)
|
||||
}
|
||||
|
||||
// StopMiner stops a running miner.
|
||||
func (m *Manager) StopMiner(name string) error {
|
||||
m.mu.Lock()
|
||||
|
|
@ -138,7 +281,6 @@ func (m *Manager) StopMiner(name string) error {
|
|||
func (m *Manager) GetMiner(name string) (Miner, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
miner, exists := m.miners[name]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("miner not found: %s", name)
|
||||
|
|
@ -150,7 +292,6 @@ func (m *Manager) GetMiner(name string) (Miner, error) {
|
|||
func (m *Manager) ListMiners() []Miner {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
miners := make([]Miner, 0, len(m.miners))
|
||||
for _, miner := range m.miners {
|
||||
miners = append(miners, miner)
|
||||
|
|
@ -215,7 +356,6 @@ func (m *Manager) collectMinerStats() {
|
|||
func (m *Manager) GetMinerHashrateHistory(name string) ([]HashratePoint, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
miner, exists := m.miners[name]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("miner not found: %s", name)
|
||||
|
|
|
|||
|
|
@ -1,38 +1,5 @@
|
|||
package mining
|
||||
|
||||
// ManagerInterface defines the interface for a miner manager.
|
||||
// This interface abstracts the core functionalities of a miner manager,
|
||||
// allowing for different implementations to be used interchangeably. It provides
|
||||
// a standard way to manage the lifecycle of miners and retrieve their data.
|
||||
type ManagerInterface interface {
|
||||
// StartMiner starts a new miner with the given configuration.
|
||||
// It takes the miner type and a configuration object, and returns the
|
||||
// created miner instance or an error if the miner could not be started.
|
||||
StartMiner(minerType string, config *Config) (Miner, error)
|
||||
|
||||
// StopMiner stops a running miner.
|
||||
// It takes the name of the miner to be stopped and returns an error if the
|
||||
// miner could not be stopped.
|
||||
StopMiner(name string) error
|
||||
|
||||
// GetMiner retrieves a running miner by its name.
|
||||
// It returns the miner instance or an error if the miner is not found.
|
||||
GetMiner(name string) (Miner, error)
|
||||
|
||||
// ListMiners returns a slice of all running miners.
|
||||
ListMiners() []Miner
|
||||
|
||||
// ListAvailableMiners returns a list of available miners that can be started.
|
||||
// This provides a way to discover the types of miners supported by the manager.
|
||||
ListAvailableMiners() []AvailableMiner
|
||||
|
||||
// GetMinerHashrateHistory returns the hashrate history for a specific miner.
|
||||
// It takes the name of the miner and returns a slice of hashrate points
|
||||
// or an error if the miner is not found.
|
||||
GetMinerHashrateHistory(name string) ([]HashratePoint, error)
|
||||
|
||||
// Stop stops the manager and its background goroutines.
|
||||
// It should be called when the manager is no longer needed to ensure a
|
||||
// graceful shutdown of any background processes.
|
||||
Stop()
|
||||
}
|
||||
// This file is intentionally left with only a package declaration
|
||||
// to resolve a redeclaration error. The ManagerInterface is defined
|
||||
// in manager.go.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import (
|
|||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -125,7 +126,47 @@ func (b *BaseMiner) InstallFromURL(url string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// parseVersion parses a version string (e.g., "6.24.0") into a slice of integers for comparison.
|
||||
func parseVersion(v string) []int {
|
||||
parts := strings.Split(v, ".")
|
||||
intParts := make([]int, len(parts))
|
||||
for i, p := range parts {
|
||||
val, err := strconv.Atoi(p)
|
||||
if err != nil {
|
||||
return []int{0} // Malformed version, treat as very old
|
||||
}
|
||||
intParts[i] = val
|
||||
}
|
||||
return intParts
|
||||
}
|
||||
|
||||
// compareVersions compares two version slices. Returns 1 if v1 > v2, -1 if v1 < v2, 0 if equal.
|
||||
func compareVersions(v1, v2 []int) int {
|
||||
minLen := len(v1)
|
||||
if len(v2) < minLen {
|
||||
minLen = len(v2)
|
||||
}
|
||||
|
||||
for i := 0; i < minLen; i++ {
|
||||
if v1[i] > v2[i] {
|
||||
return 1
|
||||
}
|
||||
if v1[i] < v2[i] {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
if len(v1) > len(v2) {
|
||||
return 1
|
||||
}
|
||||
if len(v1) < len(v2) {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// findMinerBinary searches for the miner's executable file.
|
||||
// It returns the absolute path to the executable if found, prioritizing the highest versioned installation.
|
||||
func (b *BaseMiner) findMinerBinary() (string, error) {
|
||||
executableName := b.ExecutableName
|
||||
if runtime.GOOS == "windows" {
|
||||
|
|
@ -135,35 +176,40 @@ func (b *BaseMiner) findMinerBinary() (string, error) {
|
|||
baseInstallPath := b.GetPath()
|
||||
searchedPaths := []string{}
|
||||
|
||||
if _, err := os.Stat(baseInstallPath); err == nil {
|
||||
var latestModTime time.Time
|
||||
var latestDir string
|
||||
var highestVersion []int
|
||||
var highestVersionDir string
|
||||
|
||||
// 1. Check the standard installation directory first
|
||||
if _, err := os.Stat(baseInstallPath); err == nil {
|
||||
dirs, err := os.ReadDir(baseInstallPath)
|
||||
if err == nil {
|
||||
for _, d := range dirs {
|
||||
if d.IsDir() && strings.HasPrefix(d.Name(), b.ExecutableName+"-") {
|
||||
// Extract version string, e.g., "xmrig-6.24.0" -> "6.24.0"
|
||||
versionStr := strings.TrimPrefix(d.Name(), b.ExecutableName+"-")
|
||||
currentVersion := parseVersion(versionStr)
|
||||
|
||||
if highestVersionDir == "" || compareVersions(currentVersion, highestVersion) > 0 {
|
||||
highestVersion = currentVersion
|
||||
highestVersionDir = d.Name()
|
||||
}
|
||||
versionedPath := filepath.Join(baseInstallPath, d.Name())
|
||||
fullPath := filepath.Join(versionedPath, executableName)
|
||||
searchedPaths = append(searchedPaths, fullPath)
|
||||
info, err := d.Info()
|
||||
if err == nil && info.ModTime().After(latestModTime) {
|
||||
latestModTime = info.ModTime()
|
||||
latestDir = d.Name()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if latestDir != "" {
|
||||
fullPath := filepath.Join(baseInstallPath, latestDir, executableName)
|
||||
if highestVersionDir != "" {
|
||||
fullPath := filepath.Join(baseInstallPath, highestVersionDir, executableName)
|
||||
if _, err := os.Stat(fullPath); err == nil {
|
||||
log.Printf("Found miner binary at standard path: %s", fullPath)
|
||||
log.Printf("Found miner binary at highest versioned path: %s", fullPath)
|
||||
return fullPath, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback to searching the system PATH
|
||||
path, err := exec.LookPath(executableName)
|
||||
if err == nil {
|
||||
absPath, err := filepath.Abs(path)
|
||||
|
|
@ -174,6 +220,7 @@ func (b *BaseMiner) findMinerBinary() (string, error) {
|
|||
return absPath, nil
|
||||
}
|
||||
|
||||
// If not found, return a detailed error
|
||||
return "", fmt.Errorf("miner executable '%s' not found. Searched in: %s and system PATH", executableName, strings.Join(searchedPaths, ", "))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,20 +5,13 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
// HighResolutionDuration is the duration for which hashrate data is kept at high resolution (10s intervals)
|
||||
HighResolutionDuration = 5 * time.Minute
|
||||
// HighResolutionInterval is the interval at which hashrate data is collected for high resolution
|
||||
HighResolutionInterval = 10 * time.Second
|
||||
// LowResolutionInterval is the interval for aggregated hashrate data (1m averages)
|
||||
LowResolutionInterval = 1 * time.Minute
|
||||
// LowResHistoryRetention is the duration for which low-resolution hashrate data is retained
|
||||
LowResHistoryRetention = 24 * time.Hour // Example: keep 24 hours of 1-minute averages
|
||||
LowResolutionInterval = 1 * time.Minute
|
||||
LowResHistoryRetention = 24 * time.Hour
|
||||
)
|
||||
|
||||
// Miner defines the standard interface for a cryptocurrency miner.
|
||||
// This interface abstracts the core functionalities of a miner, such as installation,
|
||||
// starting, stopping, and statistics retrieval, allowing for different miner
|
||||
// implementations to be used interchangeably.
|
||||
type Miner interface {
|
||||
Install() error
|
||||
Uninstall() error
|
||||
|
|
@ -41,6 +34,7 @@ type InstallationDetails struct {
|
|||
Version string `json:"version"`
|
||||
Path string `json:"path"`
|
||||
MinerBinary string `json:"miner_binary"`
|
||||
ConfigPath string `json:"config_path,omitempty"` // Add path to the miner-specific config
|
||||
}
|
||||
|
||||
// SystemInfo provides general system and miner installation information.
|
||||
|
|
|
|||
12
pkg/mining/mining_profile.go
Normal file
12
pkg/mining/mining_profile.go
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package mining
|
||||
|
||||
// MiningProfile represents a saved configuration for a specific mining setup.
|
||||
// This allows users to define and switch between different miners, pools,
|
||||
// and wallets without re-entering information.
|
||||
type MiningProfile struct {
|
||||
Name string `json:"name"` // A user-defined name for the profile, e.g., "My XMR Rig"
|
||||
Pool string `json:"pool"` // The mining pool address
|
||||
Wallet string `json:"wallet"` // The wallet address
|
||||
Miner string `json:"miner"` // The type of miner, e.g., "xmrig"
|
||||
// This can be expanded later to include the full *Config for advanced options
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import (
|
|||
|
||||
"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" // Import mem for memory stats
|
||||
|
|
@ -37,23 +38,21 @@ type Service struct {
|
|||
// NewService creates a new mining service
|
||||
func NewService(manager ManagerInterface, listenAddr string, displayAddr string, swaggerNamespace string) *Service {
|
||||
apiBasePath := "/" + strings.Trim(swaggerNamespace, "/")
|
||||
swaggerUIPath := apiBasePath + "/swagger" // Serve Swagger UI under a distinct sub-path
|
||||
swaggerUIPath := apiBasePath + "/swagger"
|
||||
|
||||
// Dynamically configure Swagger at runtime
|
||||
docs.SwaggerInfo.Title = "Mining Module API"
|
||||
docs.SwaggerInfo.Version = "1.0"
|
||||
docs.SwaggerInfo.Host = displayAddr // Use the displayable address for Swagger UI
|
||||
docs.SwaggerInfo.Host = displayAddr
|
||||
docs.SwaggerInfo.BasePath = apiBasePath
|
||||
// Use a unique instance name to avoid conflicts in a multi-module environment
|
||||
instanceName := "swagger_" + strings.ReplaceAll(strings.Trim(swaggerNamespace, "/"), "/", "_")
|
||||
swag.Register(instanceName, docs.SwaggerInfo)
|
||||
|
||||
return &Service{
|
||||
Manager: manager,
|
||||
Server: &http.Server{
|
||||
Addr: listenAddr, // Server listens on this address
|
||||
Addr: listenAddr,
|
||||
},
|
||||
DisplayAddr: displayAddr, // Store displayable address for messages
|
||||
DisplayAddr: displayAddr,
|
||||
SwaggerInstanceName: instanceName,
|
||||
APIBasePath: apiBasePath,
|
||||
SwaggerUIPath: swaggerUIPath,
|
||||
|
|
@ -74,9 +73,7 @@ func (s *Service) ServiceStartup(ctx context.Context) error {
|
|||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
// Stop the manager's background goroutines
|
||||
s.Manager.Stop()
|
||||
|
||||
ctxShutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := s.Server.Shutdown(ctxShutdown); err != nil {
|
||||
|
|
@ -88,10 +85,9 @@ func (s *Service) ServiceStartup(ctx context.Context) error {
|
|||
}
|
||||
|
||||
func (s *Service) setupRoutes() {
|
||||
// All API routes are now relative to the service's APIBasePath
|
||||
apiGroup := s.Router.Group(s.APIBasePath)
|
||||
{
|
||||
apiGroup.GET("/info", s.handleGetInfo) // New GET endpoint for cached info
|
||||
apiGroup.GET("/info", s.handleGetInfo)
|
||||
apiGroup.POST("/doctor", s.handleDoctor)
|
||||
apiGroup.POST("/update", s.handleUpdateCheck)
|
||||
|
||||
|
|
@ -104,15 +100,12 @@ func (s *Service) setupRoutes() {
|
|||
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) // New endpoint
|
||||
minersGroup.GET("/:miner_name/hashrate-history", s.handleGetMinerHashrateHistory)
|
||||
}
|
||||
}
|
||||
|
||||
// New route to serve the custom HTML element bundle
|
||||
// This path now points to the output of the Angular project within the 'ui' directory
|
||||
s.Router.StaticFile("/component/mining-dashboard.js", "./ui/dist/ui/mbe-mining-dashboard.js")
|
||||
|
||||
// Register Swagger UI route under a distinct sub-path to avoid conflicts
|
||||
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)))
|
||||
}
|
||||
|
|
@ -126,74 +119,40 @@ func (s *Service) setupRoutes() {
|
|||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /info [get]
|
||||
func (s *Service) handleGetInfo(c *gin.Context) {
|
||||
systemInfo := SystemInfo{
|
||||
Timestamp: time.Now(),
|
||||
OS: runtime.GOOS,
|
||||
Architecture: runtime.GOARCH,
|
||||
GoVersion: runtime.Version(),
|
||||
AvailableCPUCores: runtime.NumCPU(),
|
||||
}
|
||||
|
||||
// Get total system RAM
|
||||
vMem, err := mem.VirtualMemory()
|
||||
configDir, err := xdg.ConfigFile("lethean-desktop/miners")
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to get virtual memory info: %v", err)
|
||||
systemInfo.TotalSystemRAMGB = 0.0 // Default to 0 on error
|
||||
} else {
|
||||
// Convert bytes to GB
|
||||
systemInfo.TotalSystemRAMGB = float64(vMem.Total) / (1024 * 1024 * 1024)
|
||||
}
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not get home directory"})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not get config directory"})
|
||||
return
|
||||
}
|
||||
signpostPath := filepath.Join(homeDir, ".installed-miners")
|
||||
|
||||
configPathBytes, err := os.ReadFile(signpostPath)
|
||||
if err != nil {
|
||||
// If signpost or cache doesn't exist, return SystemInfo with empty miner details
|
||||
systemInfo.InstalledMinersInfo = []*InstallationDetails{}
|
||||
c.JSON(http.StatusOK, systemInfo)
|
||||
return
|
||||
}
|
||||
configPath := string(configPathBytes)
|
||||
configPath := filepath.Join(configDir, "config.json")
|
||||
|
||||
cacheBytes, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
// If cache file is missing, return SystemInfo with empty miner details
|
||||
systemInfo.InstalledMinersInfo = []*InstallationDetails{}
|
||||
c.JSON(http.StatusOK, systemInfo)
|
||||
if os.IsNotExist(err) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "cache file not found, run setup"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not read cache file"})
|
||||
return
|
||||
}
|
||||
|
||||
var cachedDetails []*InstallationDetails
|
||||
if err := json.Unmarshal(cacheBytes, &cachedDetails); err != nil {
|
||||
var systemInfo SystemInfo
|
||||
if err := json.Unmarshal(cacheBytes, &systemInfo); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not parse cache file"})
|
||||
return
|
||||
}
|
||||
|
||||
// Filter for only installed miners
|
||||
var installedOnly []*InstallationDetails
|
||||
for _, detail := range cachedDetails {
|
||||
if detail.IsInstalled {
|
||||
installedOnly = append(installedOnly, detail)
|
||||
}
|
||||
systemInfo.Timestamp = time.Now()
|
||||
vMem, err := mem.VirtualMemory()
|
||||
if err == nil {
|
||||
systemInfo.TotalSystemRAMGB = float64(vMem.Total) / (1024 * 1024 * 1024)
|
||||
}
|
||||
systemInfo.InstalledMinersInfo = installedOnly
|
||||
|
||||
c.JSON(http.StatusOK, systemInfo)
|
||||
}
|
||||
|
||||
// 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 {array} InstallationDetails
|
||||
// @Router /doctor [post]
|
||||
func (s *Service) handleDoctor(c *gin.Context) {
|
||||
// updateInstallationCache performs a live check and updates the cache file.
|
||||
func (s *Service) updateInstallationCache() (*SystemInfo, error) {
|
||||
var allDetails []*InstallationDetails
|
||||
for _, availableMiner := range s.Manager.ListAvailableMiners() {
|
||||
var miner Miner
|
||||
|
|
@ -203,14 +162,54 @@ func (s *Service) handleDoctor(c *gin.Context) {
|
|||
default:
|
||||
continue
|
||||
}
|
||||
details, err := miner.CheckInstallation()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to check " + miner.GetName(), "details": err.Error()})
|
||||
return
|
||||
}
|
||||
details, _ := miner.CheckInstallation()
|
||||
allDetails = append(allDetails, details)
|
||||
}
|
||||
c.JSON(http.StatusOK, allDetails)
|
||||
|
||||
systemInfo := &SystemInfo{
|
||||
Timestamp: time.Now(),
|
||||
OS: runtime.GOOS,
|
||||
Architecture: runtime.GOARCH,
|
||||
GoVersion: runtime.Version(),
|
||||
AvailableCPUCores: runtime.NumCPU(),
|
||||
InstalledMinersInfo: allDetails,
|
||||
}
|
||||
|
||||
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, 0644); 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
|
||||
|
|
@ -274,19 +273,14 @@ func (s *Service) handleUpdateCheck(c *gin.Context) {
|
|||
// @Router /miners/{miner_type}/uninstall [delete]
|
||||
func (s *Service) handleUninstallMiner(c *gin.Context) {
|
||||
minerType := c.Param("miner_name")
|
||||
var miner Miner
|
||||
switch minerType {
|
||||
case "xmrig":
|
||||
miner = NewXMRigMiner()
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "unknown miner type"})
|
||||
return
|
||||
}
|
||||
if err := miner.Uninstall(); err != nil {
|
||||
if err := s.Manager.UninstallMiner(minerType); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": miner.GetName() + " uninstalled successfully."})
|
||||
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
|
||||
|
|
@ -337,6 +331,10 @@ func (s *Service) handleInstallMiner(c *gin.Context) {
|
|||
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()})
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
package mining
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
)
|
||||
|
||||
// XMRigMiner represents an XMRig miner, embedding the BaseMiner for common functionality.
|
||||
|
|
@ -38,6 +44,20 @@ func NewXMRigMiner() *XMRigMiner {
|
|||
}
|
||||
}
|
||||
|
||||
// getXMRigConfigPath returns the platform-specific path for the xmrig.json file.
|
||||
func getXMRigConfigPath() (string, error) {
|
||||
path, err := xdg.ConfigFile("lethean-desktop/xmrig.json")
|
||||
if err != nil {
|
||||
// Fallback for non-XDG environments or when XDG variables are not set
|
||||
homeDir, homeErr := os.UserHomeDir()
|
||||
if homeErr != nil {
|
||||
return "", homeErr
|
||||
}
|
||||
return filepath.Join(homeDir, ".config", "lethean-desktop", "xmrig.json"), nil
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// GetLatestVersion fetches the latest version of XMRig from the GitHub API.
|
||||
func (m *XMRigMiner) GetLatestVersion() (string, error) {
|
||||
resp, err := httpClient.Get("https://api.github.com/repos/xmrig/xmrig/releases/latest")
|
||||
|
|
@ -92,3 +112,55 @@ func (m *XMRigMiner) Install() error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Uninstall removes all files related to the XMRig miner, including its specific config file.
|
||||
func (m *XMRigMiner) Uninstall() error {
|
||||
// Remove the specific xmrig.json config file using the centralized helper
|
||||
configPath, err := getXMRigConfigPath()
|
||||
if err == nil {
|
||||
os.Remove(configPath) // Ignore error if it doesn't exist
|
||||
}
|
||||
|
||||
// Call the base uninstall method to remove the installation directory
|
||||
return m.BaseMiner.Uninstall()
|
||||
}
|
||||
|
||||
// CheckInstallation verifies if the XMRig miner is installed correctly.
|
||||
func (m *XMRigMiner) CheckInstallation() (*InstallationDetails, error) {
|
||||
binaryPath, err := m.findMinerBinary()
|
||||
if err != nil {
|
||||
return &InstallationDetails{IsInstalled: false}, err
|
||||
}
|
||||
|
||||
m.MinerBinary = binaryPath
|
||||
m.Path = filepath.Dir(binaryPath)
|
||||
|
||||
cmd := exec.Command(binaryPath, "--version")
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
if err := cmd.Run(); err != nil {
|
||||
m.Version = "Unknown (could not run executable)"
|
||||
} else {
|
||||
fields := strings.Fields(out.String())
|
||||
if len(fields) >= 2 {
|
||||
m.Version = fields[1]
|
||||
} else {
|
||||
m.Version = "Unknown (could not parse version)"
|
||||
}
|
||||
}
|
||||
|
||||
// Get the config path using the helper
|
||||
configPath, err := getXMRigConfigPath()
|
||||
if err != nil {
|
||||
// Log the error but don't fail CheckInstallation if config path can't be determined
|
||||
configPath = "Error: Could not determine config path"
|
||||
}
|
||||
|
||||
return &InstallationDetails{
|
||||
IsInstalled: true,
|
||||
MinerBinary: m.MinerBinary,
|
||||
Path: m.Path,
|
||||
Version: m.Version,
|
||||
ConfigPath: configPath, // Include the config path
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ import (
|
|||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
)
|
||||
|
||||
// Start launches the XMRig miner with the specified configuration.
|
||||
|
|
@ -40,7 +38,8 @@ func (m *XMRigMiner) Start(config *Config) error {
|
|||
return err
|
||||
}
|
||||
} else {
|
||||
configPath, err := xdg.ConfigFile("lethean-desktop/xmrig.json")
|
||||
// Use the centralized helper to get the config path
|
||||
configPath, err := getXMRigConfigPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not determine config file path: %w", err)
|
||||
}
|
||||
|
|
@ -105,13 +104,10 @@ func addCliArgs(config *Config, args *[]string) {
|
|||
|
||||
// createConfig creates a JSON configuration file for the XMRig miner.
|
||||
func (m *XMRigMiner) createConfig(config *Config) error {
|
||||
configPath, err := xdg.ConfigFile("lethean-desktop/xmrig.json")
|
||||
// Use the centralized helper to get the config path
|
||||
configPath, err := getXMRigConfigPath()
|
||||
if err != nil {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configPath = filepath.Join(homeDir, ".config", "lethean-desktop", "xmrig.json")
|
||||
return err
|
||||
}
|
||||
m.ConfigPath = configPath
|
||||
|
||||
|
|
|
|||
|
|
@ -32,9 +32,10 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.card-error {
|
||||
--base-color: var(--wa-color-red-200);
|
||||
border-color: var(--wa-color-red-400);
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.miner-list {
|
||||
|
|
@ -48,8 +49,8 @@
|
|||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: var(--wa-border-radius-medium);
|
||||
background-color: var(--wa-color-neutral-50);
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.miner-item {
|
||||
|
|
@ -74,10 +75,43 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
border-top: 1px solid var(--wa-color-neutral-200);
|
||||
border-top: 1px solid #e0e0e0;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.admin-panel {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
margin-top: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.admin-title {
|
||||
margin-top: 0;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.path-list ul {
|
||||
list-style: none;
|
||||
padding: 0.5rem;
|
||||
margin: 0;
|
||||
font-family: monospace;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.path-list li {
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.button-spinner {
|
||||
font-size: 1em; /* Make spinner same size as button text */
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
wa-button {
|
||||
min-width: auto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<div class="mining-dashboard">
|
||||
|
||||
<!-- Initial Loading State -->
|
||||
@if (systemInfo === null) {
|
||||
@if (systemInfo === null && !needsSetup) {
|
||||
<div class="centered-container">
|
||||
<wa-card class="card-overview">
|
||||
<div slot="header">Loading...</div>
|
||||
|
|
@ -10,30 +10,53 @@
|
|||
</div>
|
||||
}
|
||||
|
||||
<!-- API Not Available State -->
|
||||
@if (systemInfo !== null && !apiAvailable) {
|
||||
<!-- Setup Wizard: Shown when needsSetup is true -->
|
||||
@if (needsSetup) {
|
||||
<div class="centered-container">
|
||||
<wa-card class="card-overview">
|
||||
<div slot="header">
|
||||
<wa-icon name="cloud-off" style="font-size: 2rem;"></wa-icon>
|
||||
API Not Available
|
||||
<wa-icon name="wand-magic-sparkles" style="font-size: 2rem;"></wa-icon>
|
||||
Welcome! Let's Get Started
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ error }}</p>
|
||||
<p>Please ensure the mining API server is running and accessible.</p>
|
||||
<p>To begin, please install a miner from the list below.</p>
|
||||
|
||||
<h4>Available Miners</h4>
|
||||
<div class="miner-list">
|
||||
@for (miner of manageableMiners; track miner.name) {
|
||||
<div class="miner-item">
|
||||
<span>{{ miner.name }}</span>
|
||||
<wa-button
|
||||
variant="success"
|
||||
size="small"
|
||||
[disabled]="actionInProgress === 'install-' + miner.name"
|
||||
(click)="installMiner(miner.name)">
|
||||
@if (actionInProgress === 'install-' + miner.name) {
|
||||
<wa-spinner class="button-spinner"></wa-spinner>
|
||||
} @else {
|
||||
<wa-icon name="download" slot="prefix"></wa-icon>
|
||||
Install
|
||||
}
|
||||
</wa-button>
|
||||
</div>
|
||||
} @empty {
|
||||
<div class="miner-item">
|
||||
<span>Could not load available miners.</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div slot="footer">
|
||||
<wa-button (click)="checkSystemState()">
|
||||
<wa-icon name="arrow-clockwise" slot="prefix"></wa-icon>
|
||||
Retry
|
||||
Refresh Status
|
||||
</wa-button>
|
||||
</div>
|
||||
</wa-card>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Main Content -->
|
||||
@if (systemInfo !== null && apiAvailable) {
|
||||
<!-- Main Content: API is Available and initial load is complete -->
|
||||
@if (systemInfo !== null && apiAvailable && !needsSetup) {
|
||||
<div>
|
||||
@if (error) {
|
||||
<wa-card class="card-error">
|
||||
|
|
@ -47,33 +70,73 @@
|
|||
|
||||
<wa-card class="card-overview">
|
||||
<div slot="header" class="card-header">
|
||||
<div>
|
||||
<div class="header-title">
|
||||
<wa-icon name="cpu" style="font-size: 1.5rem;"></wa-icon>
|
||||
Mining Control
|
||||
</div>
|
||||
<wa-button variant="neutral" appearance="plain" (click)="toggleAdminPanel()">
|
||||
<wa-icon name="gear" label="Admin Panel"></wa-icon>
|
||||
</wa-button>
|
||||
</div>
|
||||
|
||||
@if (installedMiners.length === 0) {
|
||||
<div class="miner-list">
|
||||
<div class="miner-item">
|
||||
<span>No miners installed.</span>
|
||||
<!-- Admin Panel -->
|
||||
@if (showAdminPanel) {
|
||||
<div class="admin-panel">
|
||||
<h3 class="admin-title">Admin Panel</h3>
|
||||
|
||||
<h4>Manage Miners</h4>
|
||||
<div class="miner-list">
|
||||
@for (miner of manageableMiners; track miner.name) {
|
||||
<div class="miner-item">
|
||||
<span>{{ miner.name }}</span>
|
||||
@if (miner.is_installed) {
|
||||
<wa-button
|
||||
variant="danger"
|
||||
size="small"
|
||||
[disabled]="actionInProgress === 'uninstall-' + miner.name"
|
||||
(click)="uninstallMiner(miner.name)">
|
||||
@if (actionInProgress === 'uninstall-' + miner.name) {
|
||||
<wa-spinner class="button-spinner"></wa-spinner>
|
||||
} @else {
|
||||
<wa-icon name="trash" slot="prefix"></wa-icon>
|
||||
Uninstall
|
||||
}
|
||||
</wa-button>
|
||||
} @else {
|
||||
<wa-button
|
||||
variant="success"
|
||||
size="small"
|
||||
[disabled]="actionInProgress === 'install-' + miner.name"
|
||||
(click)="installMiner(miner.name)">
|
||||
@if (actionInProgress === 'install-' + miner.name) {
|
||||
<wa-spinner class="button-spinner"></wa-spinner>
|
||||
} @else {
|
||||
<wa-icon name="download" slot="prefix"></wa-icon>
|
||||
Install
|
||||
}
|
||||
</wa-button>
|
||||
}
|
||||
</div>
|
||||
} @empty {
|
||||
<div class="miner-item">
|
||||
<span>Could not load available miners.</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<h4>Antivirus Whitelist Paths</h4>
|
||||
<div class="path-list">
|
||||
<p>To prevent antivirus software from interfering, please add the following paths to your exclusion list:</p>
|
||||
<ul>
|
||||
@for (path of whitelistPaths; track path) {
|
||||
<li><code>{{ path }}</code></li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
@for (miner of availableMiners; track miner.name) {
|
||||
<div class="miner-item">
|
||||
<span>{{ miner.name }}</span>
|
||||
<wa-button variant="success" (click)="installMiner(miner.name)">
|
||||
<wa-icon name="download" slot="prefix"></wa-icon>
|
||||
Install
|
||||
</wa-button>
|
||||
</div>
|
||||
} @empty {
|
||||
<div class="miner-item">
|
||||
<span>Checking for available miners...</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Miner Control List -->
|
||||
@if (installedMiners.length > 0) {
|
||||
<div class="miner-list">
|
||||
@for (miner of installedMiners; track miner.path) {
|
||||
|
|
@ -88,17 +151,34 @@
|
|||
</span>
|
||||
|
||||
@if (isMinerRunning(miner)) {
|
||||
<wa-button variant="danger" (click)="stopMiner(miner)">
|
||||
<wa-icon name="stop-circle" slot="prefix"></wa-icon>
|
||||
Stop
|
||||
<wa-button
|
||||
variant="danger"
|
||||
[disabled]="actionInProgress === 'stop-' + miner.type"
|
||||
(click)="stopMiner(miner)">
|
||||
@if (actionInProgress === 'stop-' + miner.type) {
|
||||
<wa-spinner class="button-spinner"></wa-spinner>
|
||||
} @else {
|
||||
<wa-icon name="stop-circle" slot="prefix"></wa-icon>
|
||||
Stop
|
||||
}
|
||||
</wa-button>
|
||||
} @else {
|
||||
<div class="start-buttons">
|
||||
<wa-button variant="secondary" (click)="startMiner(miner, true)">
|
||||
<wa-icon name="play-circle" slot="prefix"></wa-icon>
|
||||
Start Last Config
|
||||
<wa-button
|
||||
variant="secondary"
|
||||
[disabled]="actionInProgress === 'start-' + miner.type"
|
||||
(click)="startMiner(miner, true)">
|
||||
@if (actionInProgress === 'start-' + miner.type && showStartOptionsFor !== miner.type) {
|
||||
<wa-spinner class="button-spinner"></wa-spinner>
|
||||
} @else {
|
||||
<wa-icon name="play-circle" slot="prefix"></wa-icon>
|
||||
Start Last Config
|
||||
}
|
||||
</wa-button>
|
||||
<wa-button variant="primary" (click)="toggleStartOptions(miner.type)">
|
||||
<wa-button
|
||||
variant="primary"
|
||||
[disabled]="actionInProgress === 'start-' + miner.type"
|
||||
(click)="toggleStartOptions(miner.type)">
|
||||
<wa-icon name="gear" slot="prefix"></wa-icon>
|
||||
New Config
|
||||
</wa-button>
|
||||
|
|
@ -110,9 +190,16 @@
|
|||
<div class="start-options">
|
||||
<wa-input label="Pool Address" [(ngModel)]="poolAddress" name="poolAddress"></wa-input>
|
||||
<wa-input label="Wallet Address" [(ngModel)]="walletAddress" name="walletAddress"></wa-input>
|
||||
<wa-button variant="success" (click)="startMiner(miner)">
|
||||
<wa-icon name="rocket-launch" slot="prefix"></wa-icon>
|
||||
Confirm & Start
|
||||
<wa-button
|
||||
variant="success"
|
||||
[disabled]="actionInProgress === 'start-' + miner.type"
|
||||
(click)="startMiner(miner)">
|
||||
@if (actionInProgress === 'start-' + miner.type) {
|
||||
<wa-spinner class="button-spinner"></wa-spinner>
|
||||
} @else {
|
||||
<wa-icon name="rocket-launch" slot="prefix"></wa-icon>
|
||||
Confirm & Start
|
||||
}
|
||||
</wa-button>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
import { HttpClient, HttpClientModule, HttpErrorResponse } from '@angular/common/http';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { of } from 'rxjs';
|
||||
import { of, forkJoin } from 'rxjs';
|
||||
import { switchMap, catchError, map } from 'rxjs/operators';
|
||||
|
||||
// Import Web Awesome components
|
||||
|
|
@ -20,6 +20,21 @@ import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
|||
import '@awesome.me/webawesome/dist/components/spinner/spinner.js';
|
||||
import '@awesome.me/webawesome/dist/components/input/input.js';
|
||||
|
||||
// Define interfaces for our data structures
|
||||
interface InstallationDetails {
|
||||
is_installed: boolean;
|
||||
version: string;
|
||||
path: string;
|
||||
miner_binary: string;
|
||||
config_path?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
interface AvailableMiner {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mde-mining-dashboard',
|
||||
standalone: true,
|
||||
|
|
@ -33,13 +48,17 @@ export class MiningDashboardElementComponent implements OnInit {
|
|||
apiBaseUrl: string = 'http://localhost:9090/api/v1/mining';
|
||||
|
||||
// State management
|
||||
needsSetup: boolean = false;
|
||||
apiAvailable: boolean = true;
|
||||
error: string | null = null;
|
||||
showAdminPanel: boolean = false;
|
||||
actionInProgress: string | null = null; // To track which miner action is running
|
||||
|
||||
systemInfo: any = null;
|
||||
availableMiners: any[] = [];
|
||||
manageableMiners: any[] = [];
|
||||
runningMiners: any[] = [];
|
||||
installedMiners: any[] = [];
|
||||
installedMiners: InstallationDetails[] = [];
|
||||
whitelistPaths: string[] = [];
|
||||
|
||||
// Form inputs
|
||||
poolAddress: string = 'pool.hashvault.pro:80';
|
||||
|
|
@ -55,10 +74,8 @@ export class MiningDashboardElementComponent implements OnInit {
|
|||
private handleError(err: HttpErrorResponse, defaultMessage: string) {
|
||||
console.error(err);
|
||||
if (err.error && err.error.error) {
|
||||
// Handles { "error": "..." } from the backend
|
||||
this.error = `${defaultMessage}: ${err.error.error}`;
|
||||
} else if (typeof err.error === 'string' && err.error.length < 200) {
|
||||
// Handles plain text errors
|
||||
this.error = `${defaultMessage}: ${err.error}`;
|
||||
} else {
|
||||
this.error = `${defaultMessage}. Please check the console for details.`;
|
||||
|
|
@ -67,43 +84,66 @@ export class MiningDashboardElementComponent implements OnInit {
|
|||
|
||||
checkSystemState() {
|
||||
this.error = null;
|
||||
this.http.get<any>(`${this.apiBaseUrl}/info`).pipe(
|
||||
switchMap(info => {
|
||||
forkJoin({
|
||||
available: this.http.get<AvailableMiner[]>(`${this.apiBaseUrl}/miners/available`),
|
||||
info: this.http.get<any>(`${this.apiBaseUrl}/info`)
|
||||
}).pipe(
|
||||
switchMap(({ available, info }) => {
|
||||
this.apiAvailable = true;
|
||||
this.systemInfo = info;
|
||||
|
||||
this.installedMiners = (info.installed_miners_info || [])
|
||||
.filter((m: any) => m.is_installed)
|
||||
.map((m: any) => ({ ...m, type: this.getMinerType(m) }));
|
||||
const trulyInstalledMiners = (info.installed_miners_info || []).filter((m: InstallationDetails) => m.is_installed);
|
||||
|
||||
if (this.installedMiners.length === 0) {
|
||||
this.fetchAvailableMiners();
|
||||
if (trulyInstalledMiners.length === 0) {
|
||||
this.needsSetup = true;
|
||||
this.manageableMiners = available.map(availMiner => ({ ...availMiner, is_installed: false }));
|
||||
this.installedMiners = [];
|
||||
this.runningMiners = [];
|
||||
return of(null);
|
||||
}
|
||||
|
||||
this.needsSetup = false;
|
||||
const installedMap = new Map<string, InstallationDetails>(
|
||||
(info.installed_miners_info || []).map((m: InstallationDetails) => [this.getMinerType(m), m])
|
||||
);
|
||||
|
||||
this.manageableMiners = available.map(availMiner => ({
|
||||
...availMiner,
|
||||
is_installed: installedMap.get(availMiner.name)?.is_installed ?? false,
|
||||
}));
|
||||
|
||||
this.installedMiners = trulyInstalledMiners.map((m: InstallationDetails) => ({ ...m, type: this.getMinerType(m) }));
|
||||
|
||||
this.updateWhitelistPaths();
|
||||
return this.fetchRunningMiners();
|
||||
}),
|
||||
catchError(err => {
|
||||
this.apiAvailable = false;
|
||||
this.error = 'Failed to connect to the mining API.';
|
||||
if (err.status === 500) {
|
||||
this.needsSetup = true;
|
||||
this.fetchAvailableMinersForWizard();
|
||||
} else {
|
||||
this.apiAvailable = false;
|
||||
this.error = 'Failed to connect to the mining API.';
|
||||
}
|
||||
this.systemInfo = {};
|
||||
this.installedMiners = [];
|
||||
this.runningMiners = [];
|
||||
console.error('API not available:', err);
|
||||
console.error('API not available or needs setup:', err);
|
||||
return of(null);
|
||||
})
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
fetchAvailableMiners(): void {
|
||||
this.http.get<any[]>(`${this.apiBaseUrl}/miners/available`).subscribe({
|
||||
next: miners => { this.availableMiners = miners; },
|
||||
error: err => { this.handleError(err, 'Could not fetch available miners'); }
|
||||
fetchAvailableMinersForWizard(): void {
|
||||
this.http.get<AvailableMiner[]>(`${this.apiBaseUrl}/miners/available`).subscribe({
|
||||
next: miners => { this.manageableMiners = miners.map(m => ({...m, is_installed: false})); },
|
||||
error: err => { this.handleError(err, 'Could not fetch available miners for setup'); }
|
||||
});
|
||||
}
|
||||
|
||||
fetchRunningMiners() {
|
||||
return this.http.get<any[]>(`${this.apiBaseUrl}/miners`).pipe(
|
||||
map(miners => { this.runningMiners = miners; }),
|
||||
map(miners => { this.runningMiners = miners; this.updateWhitelistPaths(); }),
|
||||
catchError(err => {
|
||||
this.handleError(err, 'Could not fetch running miners');
|
||||
this.runningMiners = [];
|
||||
|
|
@ -112,22 +152,55 @@ export class MiningDashboardElementComponent implements OnInit {
|
|||
);
|
||||
}
|
||||
|
||||
private performAction(action: any) {
|
||||
action.subscribe({
|
||||
private updateWhitelistPaths() {
|
||||
const paths = new Set<string>();
|
||||
this.installedMiners.forEach(miner => {
|
||||
if (miner.miner_binary) paths.add(miner.miner_binary);
|
||||
if (miner.config_path) paths.add(miner.config_path);
|
||||
});
|
||||
this.runningMiners.forEach(miner => {
|
||||
if (miner.configPath) paths.add(miner.configPath);
|
||||
});
|
||||
this.whitelistPaths = Array.from(paths);
|
||||
}
|
||||
|
||||
installMiner(minerType: string): void {
|
||||
this.actionInProgress = `install-${minerType}`;
|
||||
this.error = null;
|
||||
this.http.post(`${this.apiBaseUrl}/miners/${minerType}/install`, {}).subscribe({
|
||||
next: () => {
|
||||
setTimeout(() => this.checkSystemState(), 1000);
|
||||
setTimeout(() => {
|
||||
this.checkSystemState();
|
||||
this.actionInProgress = null;
|
||||
}, 1000);
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this.handleError(err, 'Action failed');
|
||||
this.handleError(err, `Failed to install ${minerType}`);
|
||||
this.actionInProgress = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
installMiner(minerType: string): void {
|
||||
this.performAction(this.http.post(`${this.apiBaseUrl}/miners/${minerType}/install`, {}));
|
||||
uninstallMiner(minerType: string): void {
|
||||
this.actionInProgress = `uninstall-${minerType}`;
|
||||
this.error = null;
|
||||
this.http.delete(`${this.apiBaseUrl}/miners/${minerType}/uninstall`).subscribe({
|
||||
next: () => {
|
||||
setTimeout(() => {
|
||||
this.checkSystemState();
|
||||
this.actionInProgress = null;
|
||||
}, 1000);
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this.handleError(err, `Failed to uninstall ${minerType}`);
|
||||
this.actionInProgress = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
startMiner(miner: any, useLastConfig: boolean = false): void {
|
||||
this.actionInProgress = `start-${miner.type}`;
|
||||
this.error = null;
|
||||
let config = {};
|
||||
if (!useLastConfig) {
|
||||
config = {
|
||||
|
|
@ -137,7 +210,18 @@ export class MiningDashboardElementComponent implements OnInit {
|
|||
hugePages: true,
|
||||
};
|
||||
}
|
||||
this.performAction(this.http.post(`${this.apiBaseUrl}/miners/${miner.type}`, config));
|
||||
this.http.post(`${this.apiBaseUrl}/miners/${miner.type}`, config).subscribe({
|
||||
next: () => {
|
||||
setTimeout(() => {
|
||||
this.checkSystemState();
|
||||
this.actionInProgress = null;
|
||||
}, 1000);
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this.handleError(err, `Failed to start ${miner.type}`);
|
||||
this.actionInProgress = null;
|
||||
}
|
||||
});
|
||||
this.showStartOptionsFor = null;
|
||||
}
|
||||
|
||||
|
|
@ -147,10 +231,28 @@ export class MiningDashboardElementComponent implements OnInit {
|
|||
this.error = "Cannot stop a miner that is not running.";
|
||||
return;
|
||||
}
|
||||
this.performAction(this.http.delete(`${this.apiBaseUrl}/miners/${runningInstance.name}`));
|
||||
this.actionInProgress = `stop-${miner.type}`;
|
||||
this.error = null;
|
||||
this.http.delete(`${this.apiBaseUrl}/miners/${runningInstance.name}`).subscribe({
|
||||
next: () => {
|
||||
setTimeout(() => {
|
||||
this.checkSystemState();
|
||||
this.actionInProgress = null;
|
||||
}, 1000);
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this.handleError(err, `Failed to stop ${runningInstance.name}`);
|
||||
this.actionInProgress = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleStartOptions(minerType: string): void {
|
||||
toggleAdminPanel(): void {
|
||||
this.showAdminPanel = !this.showAdminPanel;
|
||||
}
|
||||
|
||||
toggleStartOptions(minerType: string | undefined): void {
|
||||
if (!minerType) return;
|
||||
this.showStartOptionsFor = this.showStartOptionsFor === minerType ? null : minerType;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue