Add fleet ops dashboard, Calibrate enforcement, and dead-code cleanup.

Ship live alerts, pool status, AI monitor, remote agent commands, build manager, and uninstall flow; wire Calibrate settings (WS ping, pool traffic log, retention limits) at runtime and exclude server/data from git.
This commit is contained in:
drjones
2026-05-27 09:16:04 -07:00
parent 9d223b8137
commit df81eb7744
75 changed files with 8891 additions and 966 deletions

View File

@@ -7,10 +7,13 @@ import (
"net/http"
"os"
"path/filepath"
"time"
"crypto-miner-server/internal/alerts"
"crypto-miner-server/internal/api"
"crypto-miner-server/internal/builder"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/maintenance"
"crypto-miner-server/internal/pool"
)
@@ -26,6 +29,7 @@ func main() {
dirs := []string{
cfg.DataDir,
filepath.Join(cfg.DataDir, "builds"),
filepath.Join(cfg.DataDir, "preps"),
filepath.Join(cfg.DataDir, "logs"),
}
for _, dir := range dirs {
@@ -42,18 +46,18 @@ func main() {
defer database.Close()
log.Println("Database initialized")
// Initialize AI autonomy handler (Ollama)
aiHandler := api.NewAIHandler(database)
log.Println("AI handler initialized")
// Initialize WebSocket hub
wsHub := api.NewWSHub(database)
wsHub.SetDefaultAgentConfig(cfg.DefaultAgent)
wsHub.SetAIHandler(aiHandler)
aiHandler.SetEventBroadcaster(func(entry api.AIActivityEntry) {
wsHub.BroadcastAIActivity(entry)
})
log.Println("WebSocket hub initialized")
// Initialize config provider (wraps the config for the API handler)
configProvider := &serverConfigProvider{config: cfg}
// Initialize config handler
configHandler := api.NewConfigHandler(database, configProvider)
log.Println("Config handler initialized")
// Initialize builder handler
// The agent source is expected at ../agent relative to the server directory
agentSrcDir := findAgentSourceDir()
@@ -61,54 +65,96 @@ func main() {
builderHandler := builder.NewHandler(database, cfg.DataDir, agentSrcDir, projectRoot)
log.Printf("Builder handler initialized (agent source: %s)", agentSrcDir)
// Initialize Stratum pool proxy
poolCfg := &pool.Config{
defaultPoolCfg := pool.Config{
Host: cfg.Pool.Host,
Port: cfg.Pool.Port,
UseTLS: cfg.Pool.UseTLS,
Wallet: cfg.Wallet.Address,
Password: cfg.Pool.Password,
}
poolProxy := pool.NewProxy(poolCfg)
// Set pool proxy on WebSocket hub for share forwarding
wsHub.SetPoolProxy(poolProxy)
// Set up pool callbacks
poolProxy.SetCallbacks(
// onJob - new job from pool, broadcast to all agents
// Initialize Stratum pool manager (connections keyed by forged pool + wallet)
poolManager := pool.NewManager(
func(job *pool.Job) {
log.Printf("[Pool] New job received: ID=%s, Height=%d", job.ID, job.Height)
// Broadcast new job to all connected agents
payload, _ := json.Marshal(job)
wsHub.BroadcastToAgents(api.Message{
Type: "new_job",
Payload: payload,
})
},
// onShare - share submission result from pool
func(accepted bool, agentID string, jobID string) {
log.Printf("[Pool] Share result for agent %s (job: %s): accepted=%v", agentID, jobID, accepted)
},
// onError - pool connection error
func(err error) {
log.Printf("[Pool] Error: %v", err)
},
)
wsHub.SetPoolManager(poolManager, defaultPoolCfg)
// Start pool proxy connection (non-blocking, runs in background)
applyRuntimeConfig(cfg, wsHub, poolManager, builderHandler)
configProvider := &serverConfigProvider{
config: cfg,
onSaved: func(c *Config) {
applyRuntimeConfig(c, wsHub, poolManager, builderHandler)
},
}
configHandler := api.NewConfigHandler(database, configProvider)
log.Println("Config handler initialized")
maintenance.StartRetentionJobs(database, cfg.DataDir, cfg.Server.StatsRetentionHours, cfg.Server.BuildRetentionDays)
// Pre-connect default upstream pool from server config (Forge defaults seed from here)
go func() {
if err := poolProxy.Start(); err != nil {
log.Printf("[Pool] Failed to connect to pool (will retry): %v", err)
if _, err := poolManager.EnsurePool(&defaultPoolCfg); err != nil {
log.Printf("[Pool] Failed to connect default pool (will retry on agent auth): %v", err)
}
}()
// Fleet alert evaluator (thresholds from Calibrate → alerts config)
alertEvaluator := alerts.NewEvaluator(database, func() alerts.Thresholds {
return alerts.Thresholds{
OfflineMinutes: cfg.Alerts.OfflineThresholdMinutes,
HashrateDropPct: cfg.Alerts.HashrateDropThresholdPct,
RejectionRatePct: cfg.Alerts.RejectionRateThresholdPct,
}
}, alerts.NotifyConfig{
TelegramBotToken: cfg.Alerts.TelegramBotToken,
TelegramChatID: cfg.Alerts.TelegramChatID,
EmailEnabled: cfg.Alerts.EmailEnabled,
SMTPHost: cfg.Alerts.SMTPHost,
SMTPPort: cfg.Alerts.SMTPPort,
SMTPUser: cfg.Alerts.SMTPUser,
SMTPPassword: cfg.Alerts.SMTPPassword,
EmailTo: cfg.Alerts.EmailTo,
EmailFrom: cfg.Alerts.EmailFrom,
}, func(ev alerts.AlertEvent) {
wsHub.BroadcastFleetAlert(ev)
})
alertEvaluator.Start(30 * time.Second)
log.Println("Fleet alert evaluator started")
// Pool status broadcast to dashboard
go func() {
ticker := time.NewTicker(15 * time.Second)
defer ticker.Stop()
for range ticker.C {
wsHub.BroadcastPoolStatus(poolManager.ListStatus())
}
}()
fleetHandler := api.NewFleetHandler(database, wsHub, aiHandler, poolManager, alertEvaluator, defaultPoolCfg)
// Initialize blueprint handler (config presets)
blueprintHandler := api.NewBlueprintHandler(cfg.DataDir)
log.Println("Blueprint handler initialized")
// Find web root for frontend
webRoot := findWebRoot()
log.Printf("Web root: %s", webRoot)
// Initialize router
router := api.NewRouter(database, wsHub, configHandler, builderHandler, webRoot)
router := api.NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, webRoot, func() string {
return configProvider.PublicURL()
})
log.Println("Router initialized")
// Start server
@@ -121,9 +167,39 @@ func main() {
}
}
func applyRuntimeConfig(cfg *Config, wsHub *api.WSHub, poolManager *pool.Manager, builderHandler *builder.Handler) {
if wsHub != nil {
wsHub.SetPingInterval(cfg.Server.WebSocketPingSeconds)
wsHub.SetServerPolicy(api.ServerPolicy{
MaxAgents: cfg.Server.MaxAgents,
LogAgentConnections: cfg.Server.LogAgentConnections,
LogShareSubmissions: cfg.Server.LogShareSubmissions,
LogPoolTraffic: cfg.Server.LogPoolTraffic,
StrictWalletValidation: cfg.Server.StrictWalletValidation,
MaxBuildSizeMB: cfg.Server.MaxBuildSizeMB,
PoolReconnectSeconds: cfg.Server.PoolReconnectSeconds,
})
}
if poolManager != nil {
poolManager.SetReconnectDelay(cfg.Server.PoolReconnectSeconds)
poolManager.SetVerboseTraffic(cfg.Server.LogPoolTraffic)
}
if builderHandler != nil {
builderHandler.SetBuildPolicy(builder.BuildPolicy{
StrictWalletValidation: cfg.Server.StrictWalletValidation,
MaxBuildSizeMB: cfg.Server.MaxBuildSizeMB,
})
}
}
// serverConfigProvider wraps the Config to implement api.ConfigProvider interface
type serverConfigProvider struct {
config *Config
config *Config
onSaved func(*Config)
}
func (p *serverConfigProvider) PublicURL() string {
return p.config.Server.PublicURL
}
func (p *serverConfigProvider) GetConfigJSON() json.RawMessage {
@@ -145,6 +221,10 @@ func (p *serverConfigProvider) UpdateConfigFromJSON(data json.RawMessage) error
return fmt.Errorf("failed to save config: %w", err)
}
if p.onSaved != nil {
p.onSaved(p.config)
}
return nil
}
@@ -222,8 +302,8 @@ func findProjectRoot() string {
// findWebRoot locates the frontend build output directory
func findWebRoot() string {
candidates := []string{
"webroot", // Copied by run.bat
"web/dist", // Vite build output relative to server/
"webroot", // Copied by run.bat
"web/dist", // Vite build output relative to server/
filepath.Join("..", "server", "web", "dist"), // Relative to project root
filepath.Join("server", "web", "dist"), // From project root
}