182 lines
4.3 KiB
Go
182 lines
4.3 KiB
Go
package fleet
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"forge-mesh/internal/api/types"
|
|
"forge-mesh/internal/policy"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// FleetSummary aggregates fleet stats for the dashboard API.
|
|
type FleetSummary struct {
|
|
Hosts []FleetHostCard `json:"hosts"`
|
|
TotalHashrate float64 `json:"totalHashrate"`
|
|
OnlineCount int `json:"onlineCount"`
|
|
}
|
|
|
|
// FleetHostCard is the dashboard-facing host shape (matches React types).
|
|
type FleetHostCard struct {
|
|
ID string `json:"id"`
|
|
Hostname string `json:"hostname"`
|
|
IP string `json:"ip"`
|
|
Arch string `json:"arch"`
|
|
Hashrate float64 `json:"hashrate"`
|
|
Tier int `json:"tier"`
|
|
TierName string `json:"tierName"`
|
|
TierState string `json:"tierState"`
|
|
Algo string `json:"algo"`
|
|
UptimeSec int `json:"uptimeSec"`
|
|
LastSeen string `json:"lastSeen"`
|
|
Clearance int `json:"clearance"`
|
|
Online bool `json:"online"`
|
|
}
|
|
|
|
// ToFleetCard converts a DB host to a dashboard card.
|
|
func ToFleetCard(h *types.Host) FleetHostCard {
|
|
if h == nil {
|
|
return FleetHostCard{}
|
|
}
|
|
|
|
hps := h.HashrateHps
|
|
if hps == 0 {
|
|
hps = h.Hashrate
|
|
}
|
|
|
|
online := h.Status == "online" || h.Status == "mining" || h.Status == "paused"
|
|
tier := h.CurrentTier
|
|
if tier == 0 {
|
|
tier = 2
|
|
}
|
|
tierName := policy.TierDisplayName(h.TierType)
|
|
if tierName == "" || tierName == h.TierType {
|
|
tierName = "Bundled xmrig"
|
|
}
|
|
tierState := h.TierState
|
|
if tierState == "" {
|
|
switch h.Status {
|
|
case "mining":
|
|
tierState = "active"
|
|
case "probing":
|
|
tierState = "probing"
|
|
case "paused":
|
|
tierState = "paused"
|
|
case "offline":
|
|
tierState = "idle"
|
|
online = false
|
|
default:
|
|
tierState = "idle"
|
|
}
|
|
}
|
|
|
|
arch := h.Phenotype
|
|
if arch == "" {
|
|
arch = "linux/amd64"
|
|
}
|
|
|
|
lastSeen := ""
|
|
if h.LastSeenAt != nil {
|
|
lastSeen = h.LastSeenAt.UTC().Format(time.RFC3339)
|
|
}
|
|
|
|
return FleetHostCard{
|
|
ID: h.ID,
|
|
Hostname: h.Hostname,
|
|
IP: coalesceIP(h.Fingerprint),
|
|
Arch: arch,
|
|
Hashrate: hps,
|
|
Tier: tier,
|
|
TierName: tierName,
|
|
TierState: tierState,
|
|
Algo: "rx/0",
|
|
UptimeSec: 0,
|
|
LastSeen: lastSeen,
|
|
Clearance: h.ClearanceLevel,
|
|
Online: online,
|
|
}
|
|
}
|
|
|
|
func coalesceIP(fp string) string {
|
|
if fp == "" {
|
|
return "—"
|
|
}
|
|
return fp
|
|
}
|
|
|
|
// BuildFleetSummary builds the GET /api/v1/fleet response.
|
|
func (s *Store) BuildFleetSummary() (FleetSummary, error) {
|
|
hosts, err := s.ListHosts()
|
|
if err != nil {
|
|
return FleetSummary{}, err
|
|
}
|
|
|
|
summary := FleetSummary{Hosts: make([]FleetHostCard, 0, len(hosts))}
|
|
for i := range hosts {
|
|
card := ToFleetCard(&hosts[i])
|
|
summary.Hosts = append(summary.Hosts, card)
|
|
if card.Online {
|
|
summary.OnlineCount++
|
|
summary.TotalHashrate += card.Hashrate
|
|
}
|
|
}
|
|
return summary, nil
|
|
}
|
|
|
|
// SeedDemoHost inserts a demo host when fleet is empty.
|
|
func (s *Store) SeedDemoHost() error {
|
|
hosts, err := s.ListHosts()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(hosts) > 0 {
|
|
return nil
|
|
}
|
|
id := uuid.NewString()
|
|
now := time.Now().UTC().Format(time.RFC3339)
|
|
_, err = s.db.Exec(`
|
|
INSERT INTO hosts (id, hostname, fingerprint, phenotype, status, hashrate, hashrate_hps,
|
|
current_tier, tier_type, tier_state, clearance_level, last_seen_at, created_at, updated_at)
|
|
VALUES (?, 'forge-node-alpha', '10.0.1.12', 'linux/amd64', 'mining', 18200000, 18200000,
|
|
2, 'xmrig', 'active', 2, ?, ?, ?)`,
|
|
id, now, now, now)
|
|
return err
|
|
}
|
|
|
|
// TouchHost enrolls or refreshes a host from the register API.
|
|
func (s *Store) TouchHost(hostname, fingerprint, arch string) (*types.Host, error) {
|
|
phenotype := ""
|
|
if arch != "" {
|
|
phenotype = "linux/" + arch
|
|
}
|
|
hb := types.HeartbeatPayload{
|
|
Hostname: hostname,
|
|
Fingerprint: fingerprint,
|
|
Arch: arch,
|
|
TierState: "idle",
|
|
}
|
|
hb.SetHashrateFields(0)
|
|
host, err := s.UpsertHeartbeat(hb)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if phenotype != "" {
|
|
now := time.Now().UTC().Format(time.RFC3339)
|
|
_, _ = s.db.Exec(`UPDATE hosts SET phenotype = ?, updated_at = ? WHERE id = ?`,
|
|
phenotype, now, host.ID)
|
|
return s.GetHost(host.ID)
|
|
}
|
|
return host, nil
|
|
}
|
|
|
|
// SetHostStatus updates host status.
|
|
func (s *Store) SetHostStatus(id, status string) error {
|
|
now := time.Now().UTC().Format(time.RFC3339)
|
|
_, err := s.db.Exec(`UPDATE hosts SET status = ?, updated_at = ? WHERE id = ?`, status, now, id)
|
|
return err
|
|
}
|
|
|
|
// ErrNotFound indicates a missing host.
|
|
var ErrNotFound = fmt.Errorf("host not found")
|