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

@@ -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()

View File

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

View 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()
}

View 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")
}
}

View File

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

View 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"
)