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:
AetherForge
2026-06-03 20:32:59 -07:00
parent 03937edba7
commit d52479c9a6
139 changed files with 10611 additions and 369 deletions

View File

@@ -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,