Initial commit: AetherForge Linux (forge-mesh) v0.1.0-dev
Some checks failed
Test / test (push) Has been cancelled
Some checks failed
Test / test (push) Has been cancelled
This commit is contained in:
219
internal/fleet/store.go
Normal file
219
internal/fleet/store.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package fleet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"forge-mesh/internal/api/types"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Store persists fleet host and mining profile state.
|
||||
type Store struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewStore(db *sql.DB) *Store {
|
||||
return &Store{db: db}
|
||||
}
|
||||
|
||||
func (s *Store) UpsertHeartbeat(hb types.HeartbeatPayload) (*types.Host, error) {
|
||||
hostID := hb.HostID
|
||||
if hostID == "" {
|
||||
hostID = uuid.NewString()
|
||||
}
|
||||
|
||||
hps := hb.EffectiveHashrate()
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO hosts (
|
||||
id, hostname, fingerprint, status, hashrate, hashrate_hps,
|
||||
current_tier, tier_type, tier_state, last_seen_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, 'online', ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
hostname = excluded.hostname,
|
||||
fingerprint = COALESCE(NULLIF(excluded.fingerprint, ''), hosts.fingerprint),
|
||||
status = 'online',
|
||||
hashrate = excluded.hashrate,
|
||||
hashrate_hps = excluded.hashrate_hps,
|
||||
current_tier = excluded.current_tier,
|
||||
tier_type = excluded.tier_type,
|
||||
tier_state = excluded.tier_state,
|
||||
last_seen_at = excluded.last_seen_at,
|
||||
updated_at = excluded.updated_at
|
||||
`, hostID, hb.Hostname, hb.Fingerprint, hps, hps,
|
||||
hb.CurrentTier, hb.TierType, hb.TierState, now, now)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("upsert host: %w", err)
|
||||
}
|
||||
|
||||
return s.GetHost(hostID)
|
||||
}
|
||||
|
||||
func (s *Store) GetHost(id string) (*types.Host, error) {
|
||||
row := s.db.QueryRow(`
|
||||
SELECT id, hostname, fingerprint, phenotype, status, hashrate, hashrate_hps,
|
||||
current_tier, tier_type, tier_state, clearance_level,
|
||||
mining_profile_id, last_seen_at, created_at, updated_at
|
||||
FROM hosts WHERE id = ?
|
||||
`, id)
|
||||
|
||||
return scanHost(row)
|
||||
}
|
||||
|
||||
func (s *Store) ListHosts() ([]types.Host, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, hostname, fingerprint, phenotype, status, hashrate, hashrate_hps,
|
||||
current_tier, tier_type, tier_state, clearance_level,
|
||||
mining_profile_id, last_seen_at, created_at, updated_at
|
||||
FROM hosts ORDER BY updated_at DESC
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list hosts: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var hosts []types.Host
|
||||
for rows.Next() {
|
||||
h, err := scanHost(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hosts = append(hosts, *h)
|
||||
}
|
||||
return hosts, rows.Err()
|
||||
}
|
||||
|
||||
func scanHost(scanner interface {
|
||||
Scan(dest ...any) error
|
||||
}) (*types.Host, error) {
|
||||
var h types.Host
|
||||
var fp, pheno, tierType, tierState, mpID, lastSeen sql.NullString
|
||||
var createdAt, updatedAt string
|
||||
|
||||
err := scanner.Scan(
|
||||
&h.ID, &h.Hostname, &fp, &pheno, &h.Status, &h.Hashrate, &h.HashrateHps,
|
||||
&h.CurrentTier, &tierType, &tierState, &h.ClearanceLevel,
|
||||
&mpID, &lastSeen, &createdAt, &updatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan host: %w", err)
|
||||
}
|
||||
|
||||
if fp.Valid {
|
||||
h.Fingerprint = fp.String
|
||||
}
|
||||
if pheno.Valid {
|
||||
h.Phenotype = pheno.String
|
||||
}
|
||||
if tierType.Valid {
|
||||
h.TierType = tierType.String
|
||||
}
|
||||
if tierState.Valid {
|
||||
h.TierState = tierState.String
|
||||
}
|
||||
if mpID.Valid {
|
||||
h.MiningProfileID = &mpID.String
|
||||
}
|
||||
if lastSeen.Valid {
|
||||
t, _ := time.Parse(time.RFC3339, lastSeen.String)
|
||||
h.LastSeenAt = &t
|
||||
}
|
||||
h.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||
h.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
||||
return &h, nil
|
||||
}
|
||||
|
||||
func (s *Store) MarkOffline(id string) error {
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
_, err := s.db.Exec(`UPDATE hosts SET status = 'offline', updated_at = ? WHERE id = ?`, now, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) SaveMiningProfile(ctx context.Context, profile *types.MiningProfile) error {
|
||||
tiersJSON, err := json.Marshal(profile.Tiers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal tiers: %w", err)
|
||||
}
|
||||
|
||||
policy := 0
|
||||
if profile.PolicyFromServer {
|
||||
policy = 1
|
||||
}
|
||||
|
||||
if profile.CreatedAt.IsZero() {
|
||||
profile.CreatedAt = time.Now().UTC()
|
||||
}
|
||||
profile.UpdatedAt = time.Now().UTC()
|
||||
|
||||
_, err = s.db.ExecContext(ctx, `
|
||||
INSERT INTO mining_profiles (id, name, wallet_address, tiers_json, policy_from_server, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
wallet_address = excluded.wallet_address,
|
||||
tiers_json = excluded.tiers_json,
|
||||
policy_from_server = excluded.policy_from_server,
|
||||
updated_at = excluded.updated_at
|
||||
`, profile.ID, profile.Name, profile.WalletAddress, string(tiersJSON), policy,
|
||||
profile.CreatedAt.UTC().Format(time.RFC3339), profile.UpdatedAt.UTC().Format(time.RFC3339))
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) GetMiningProfile(ctx context.Context, id string) (*types.MiningProfile, error) {
|
||||
row := s.db.QueryRowContext(ctx, `
|
||||
SELECT id, name, wallet_address, tiers_json, policy_from_server, created_at, updated_at
|
||||
FROM mining_profiles WHERE id = ?
|
||||
`, id)
|
||||
|
||||
var p types.MiningProfile
|
||||
var tiersJSON, created, updated sql.NullString
|
||||
var policy int
|
||||
|
||||
if err := row.Scan(&p.ID, &p.Name, &p.WalletAddress, &tiersJSON, &policy, &created, &updated); err != nil {
|
||||
return nil, fmt.Errorf("get mining profile: %w", err)
|
||||
}
|
||||
if tiersJSON.Valid {
|
||||
_ = json.Unmarshal([]byte(tiersJSON.String), &p.Tiers)
|
||||
}
|
||||
p.PolicyFromServer = policy == 1
|
||||
if created.Valid {
|
||||
p.CreatedAt, _ = time.Parse(time.RFC3339, created.String)
|
||||
}
|
||||
if updated.Valid {
|
||||
p.UpdatedAt, _ = time.Parse(time.RFC3339, updated.String)
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func (s *Store) AssignMiningProfile(ctx context.Context, hostID, profileID string) error {
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
res, err := s.db.ExecContext(ctx, `
|
||||
UPDATE hosts SET mining_profile_id = ?, updated_at = ? WHERE id = ?
|
||||
`, profileID, now, hostID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
if n == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) GetHostMiningProfile(ctx context.Context, hostID string) (*types.MiningProfile, error) {
|
||||
host, err := s.GetHost(hostID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if host.MiningProfileID == nil || *host.MiningProfileID == "" {
|
||||
return nil, sql.ErrNoRows
|
||||
}
|
||||
return s.GetMiningProfile(ctx, *host.MiningProfileID)
|
||||
}
|
||||
Reference in New Issue
Block a user