Files
drjones 3678b199d0
Some checks failed
Test / test (push) Has been cancelled
Initial commit: AetherForge Linux (forge-mesh) v0.1.0-dev
2026-07-04 09:31:23 +00:00

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")