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:
@@ -8,6 +8,8 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -35,6 +37,7 @@ type FleetHandler struct {
|
||||
pools *pool.Manager
|
||||
alerts *alerts.Evaluator
|
||||
defaultPool pool.Config
|
||||
dataDir string
|
||||
|
||||
// XMR price cache — per-handler so multiple routers in one process stay isolated.
|
||||
xmrPriceMu sync.Mutex
|
||||
@@ -52,7 +55,7 @@ type poolEarningsCache struct {
|
||||
|
||||
const earningsCacheTTL = 5 * time.Minute
|
||||
|
||||
func NewFleetHandler(database *db.Database, ws *WSHub, ai *AIHandler, pools *pool.Manager, evaluator *alerts.Evaluator, defaultPool pool.Config) *FleetHandler {
|
||||
func NewFleetHandler(database *db.Database, ws *WSHub, ai *AIHandler, pools *pool.Manager, evaluator *alerts.Evaluator, defaultPool pool.Config, dataDir string) *FleetHandler {
|
||||
return &FleetHandler{
|
||||
db: database,
|
||||
ws: ws,
|
||||
@@ -60,6 +63,7 @@ func NewFleetHandler(database *db.Database, ws *WSHub, ai *AIHandler, pools *poo
|
||||
pools: pools,
|
||||
alerts: evaluator,
|
||||
defaultPool: defaultPool,
|
||||
dataDir: dataDir,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +75,53 @@ func (f *FleetHandler) GetAlerts(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, f.alerts.ActiveAlerts())
|
||||
}
|
||||
|
||||
// PostAlertTest fires a test notification on every configured channel and
|
||||
// returns per-channel results without raising a real fleet alert.
|
||||
func (f *FleetHandler) PostAlertTest(w http.ResponseWriter, r *http.Request) {
|
||||
const testMsg = "AetherForge test notification — alerts are configured correctly"
|
||||
|
||||
type channelResult struct {
|
||||
Sent bool `json:"sent"`
|
||||
Error *string `json:"error"`
|
||||
}
|
||||
|
||||
result := map[string]channelResult{}
|
||||
|
||||
if f.alerts == nil {
|
||||
errStr := "alert evaluator not configured"
|
||||
result["telegram"] = channelResult{Sent: false, Error: &errStr}
|
||||
result["smtp"] = channelResult{Sent: false, Error: &errStr}
|
||||
writeJSON(w, result)
|
||||
return
|
||||
}
|
||||
|
||||
cfg := f.alerts.GetNotifyConfig()
|
||||
|
||||
// Telegram
|
||||
if cfg.TelegramBotToken == "" || cfg.TelegramChatID == "" {
|
||||
errStr := "not configured"
|
||||
result["telegram"] = channelResult{Sent: false, Error: &errStr}
|
||||
} else if err := alerts.SendTelegram(cfg, testMsg); err != nil {
|
||||
errStr := err.Error()
|
||||
result["telegram"] = channelResult{Sent: false, Error: &errStr}
|
||||
} else {
|
||||
result["telegram"] = channelResult{Sent: true}
|
||||
}
|
||||
|
||||
// SMTP
|
||||
if !cfg.EmailEnabled || cfg.SMTPHost == "" || cfg.EmailTo == "" {
|
||||
errStr := "not configured"
|
||||
result["smtp"] = channelResult{Sent: false, Error: &errStr}
|
||||
} else if err := alerts.SendEmail(cfg, "AetherForge Alert Test", testMsg); err != nil {
|
||||
errStr := err.Error()
|
||||
result["smtp"] = channelResult{Sent: false, Error: &errStr}
|
||||
} else {
|
||||
result["smtp"] = channelResult{Sent: true}
|
||||
}
|
||||
|
||||
writeJSON(w, result)
|
||||
}
|
||||
|
||||
func (f *FleetHandler) GetPoolStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if f.pools == nil {
|
||||
writeJSON(w, []pool.PoolStatus{})
|
||||
@@ -254,6 +305,23 @@ func (f *FleetHandler) GetAgentLog(w http.ResponseWriter, r *http.Request) {
|
||||
// The dashboard will receive the log content via the commandResults queue.
|
||||
_ = f.ws.SendAgentCommand(id, "get_log", map[string]interface{}{"tail_lines": 300})
|
||||
}
|
||||
|
||||
if r.URL.Query().Get("download") == "1" {
|
||||
// If the in-memory buffer is empty, try the persisted log file on disk.
|
||||
if content == "" && f.dataDir != "" {
|
||||
logPath := filepath.Join(f.dataDir, "logs", id+".log")
|
||||
if data, err := os.ReadFile(logPath); err == nil {
|
||||
content = string(data)
|
||||
}
|
||||
}
|
||||
date := time.Now().UTC().Format("2006-01-02")
|
||||
filename := fmt.Sprintf("agent-%s-%s.log", id, date)
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="`+filename+`"`)
|
||||
fmt.Fprint(w, content)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"agent_id": id,
|
||||
"content": content,
|
||||
|
||||
Reference in New Issue
Block a user