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:
@@ -31,7 +31,7 @@ type Broadcaster func(AlertEvent)
|
||||
type Evaluator struct {
|
||||
db *db.Database
|
||||
thresholds func() Thresholds
|
||||
notify func() NotifyConfig
|
||||
settings func() Settings
|
||||
broadcast Broadcaster
|
||||
mu sync.Mutex
|
||||
baseline map[string]float64
|
||||
@@ -40,11 +40,11 @@ type Evaluator struct {
|
||||
cooldown time.Duration
|
||||
}
|
||||
|
||||
func NewEvaluator(database *db.Database, thresholds func() Thresholds, notify func() NotifyConfig, broadcast Broadcaster) *Evaluator {
|
||||
func NewEvaluator(database *db.Database, thresholds func() Thresholds, settings func() Settings, broadcast Broadcaster) *Evaluator {
|
||||
return &Evaluator{
|
||||
db: database,
|
||||
thresholds: thresholds,
|
||||
notify: notify,
|
||||
settings: settings,
|
||||
broadcast: broadcast,
|
||||
baseline: make(map[string]float64),
|
||||
lastFired: make(map[string]time.Time),
|
||||
@@ -180,12 +180,25 @@ func (e *Evaluator) fire(ev AlertEvent, cooldownKey string) {
|
||||
e.mu.Unlock()
|
||||
|
||||
log.Printf("[Alert] %s: %s", ev.Type, ev.Message)
|
||||
NotifyAll(e.notify(), "AetherForge "+ev.Type, ev.Message)
|
||||
s := e.settings()
|
||||
if s.EnabledForAlertType(ev.Type) {
|
||||
NotifyAll(s.NotifyConfig, "AetherForge "+ev.Type, ev.Message)
|
||||
}
|
||||
if e.broadcast != nil {
|
||||
e.broadcast(ev)
|
||||
}
|
||||
}
|
||||
|
||||
// GetNotifyConfig returns delivery credentials for channel probes.
|
||||
func (e *Evaluator) GetNotifyConfig() NotifyConfig {
|
||||
return e.settings().NotifyConfig
|
||||
}
|
||||
|
||||
// GetSettings returns full notification settings.
|
||||
func (e *Evaluator) GetSettings() Settings {
|
||||
return e.settings()
|
||||
}
|
||||
|
||||
func (e *Evaluator) ActiveAlerts() []AlertEvent {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
@@ -17,7 +17,7 @@ func TestEvaluatorOfflineAlert(t *testing.T) {
|
||||
var fired []AlertEvent
|
||||
e := &Evaluator{
|
||||
thresholds: func() Thresholds { return Thresholds{OfflineMinutes: 5} },
|
||||
notify: func() NotifyConfig { return NotifyConfig{} },
|
||||
settings: func() Settings { return NewSettings(NotifyConfig{}, EventToggles{AgentOffline: true}) },
|
||||
broadcast: func(ev AlertEvent) { fired = append(fired, ev) },
|
||||
baseline: make(map[string]float64),
|
||||
lastFired: make(map[string]time.Time),
|
||||
|
||||
54
server/internal/alerts/notifier.go
Normal file
54
server/internal/alerts/notifier.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package alerts
|
||||
|
||||
import "log"
|
||||
|
||||
// Notifier sends Telegram/email when Calibrate toggles allow it.
|
||||
type Notifier struct {
|
||||
settings func() Settings
|
||||
}
|
||||
|
||||
func NewNotifier(settings func() Settings) *Notifier {
|
||||
return &Notifier{settings: settings}
|
||||
}
|
||||
|
||||
func (n *Notifier) Emit(event string, title, body string) {
|
||||
if n == nil || n.settings == nil {
|
||||
return
|
||||
}
|
||||
s := n.settings()
|
||||
if !n.eventEnabled(s, event) {
|
||||
return
|
||||
}
|
||||
if s.TelegramBotToken == "" && s.TelegramChatID == "" && !s.EmailEnabled {
|
||||
return
|
||||
}
|
||||
log.Printf("[Notify] %s: %s", event, body)
|
||||
NotifyAll(s.NotifyConfig, title, body)
|
||||
}
|
||||
|
||||
func (n *Notifier) eventEnabled(s Settings, event string) bool {
|
||||
switch event {
|
||||
case EventAgentConnect:
|
||||
return s.Events.AgentConnect
|
||||
case EventAgentReconnect:
|
||||
return s.Events.AgentReconnect
|
||||
case EventBuildComplete:
|
||||
return s.Events.BuildComplete
|
||||
case "offline":
|
||||
return s.Events.AgentOffline
|
||||
case "hashrate_drop":
|
||||
return s.Events.HashrateDrop
|
||||
case "rejection_rate":
|
||||
return s.Events.RejectionRate
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// GetSettings exposes current settings (alert test handler).
|
||||
func (n *Notifier) GetSettings() Settings {
|
||||
if n == nil || n.settings == nil {
|
||||
return Settings{}
|
||||
}
|
||||
return n.settings()
|
||||
}
|
||||
25
server/internal/alerts/notifier_test.go
Normal file
25
server/internal/alerts/notifier_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package alerts
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNotifierRespectsToggles(t *testing.T) {
|
||||
n := NewNotifier(func() Settings {
|
||||
return NewSettings(
|
||||
NotifyConfig{TelegramBotToken: "t", TelegramChatID: "1"},
|
||||
EventToggles{AgentConnect: false, AgentReconnect: true},
|
||||
)
|
||||
})
|
||||
// disabled — no panic
|
||||
n.Emit(EventAgentConnect, "title", "body")
|
||||
n.Emit(EventAgentReconnect, "title", "body")
|
||||
}
|
||||
|
||||
func TestSettingsEnabledForAlertType(t *testing.T) {
|
||||
s := NewSettings(NotifyConfig{}, EventToggles{AgentOffline: false, HashrateDrop: true})
|
||||
if s.EnabledForAlertType("offline") {
|
||||
t.Fatal("expected offline disabled")
|
||||
}
|
||||
if !s.EnabledForAlertType("hashrate_drop") {
|
||||
t.Fatal("expected hashrate enabled")
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
@@ -27,11 +28,11 @@ func SendTelegram(cfg NotifyConfig, text string) error {
|
||||
return nil
|
||||
}
|
||||
url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", cfg.TelegramBotToken)
|
||||
body, _ := json.Marshal(map[string]string{
|
||||
payload, _ := json.Marshal(map[string]string{
|
||||
"chat_id": cfg.TelegramChatID,
|
||||
"text": text,
|
||||
})
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -42,8 +43,19 @@ func SendTelegram(cfg NotifyConfig, text string) error {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("telegram API status %d", resp.StatusCode)
|
||||
msg := strings.TrimSpace(string(body))
|
||||
if msg == "" {
|
||||
return fmt.Errorf("telegram API status %d", resp.StatusCode)
|
||||
}
|
||||
var apiErr struct {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if json.Unmarshal(body, &apiErr) == nil && apiErr.Description != "" {
|
||||
return fmt.Errorf("telegram: %s", apiErr.Description)
|
||||
}
|
||||
return fmt.Errorf("telegram API status %d: %s", resp.StatusCode, msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
41
server/internal/alerts/settings.go
Normal file
41
server/internal/alerts/settings.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package alerts
|
||||
|
||||
// Event toggles — all default true in server DefaultConfig.
|
||||
type EventToggles struct {
|
||||
AgentConnect bool
|
||||
AgentReconnect bool
|
||||
AgentOffline bool
|
||||
HashrateDrop bool
|
||||
RejectionRate bool
|
||||
BuildComplete bool
|
||||
}
|
||||
|
||||
// Settings combines delivery credentials with per-event toggles.
|
||||
type Settings struct {
|
||||
NotifyConfig
|
||||
Events EventToggles
|
||||
}
|
||||
|
||||
func NewSettings(nc NotifyConfig, ev EventToggles) Settings {
|
||||
return Settings{NotifyConfig: nc, Events: ev}
|
||||
}
|
||||
|
||||
// EnabledForAlertType maps evaluator alert types to toggles.
|
||||
func (s Settings) EnabledForAlertType(alertType string) bool {
|
||||
switch alertType {
|
||||
case "offline":
|
||||
return s.Events.AgentOffline
|
||||
case "hashrate_drop":
|
||||
return s.Events.HashrateDrop
|
||||
case "rejection_rate":
|
||||
return s.Events.RejectionRate
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
EventAgentConnect = "agent_connect"
|
||||
EventAgentReconnect = "agent_reconnect"
|
||||
EventBuildComplete = "build_complete"
|
||||
)
|
||||
Reference in New Issue
Block a user