package web import ( "fmt" "log" "strings" "sync" "gitea.dooplex.hu/admin/felhom-controller/internal/backup" "gitea.dooplex.hu/admin/felhom-controller/internal/config" "gitea.dooplex.hu/admin/felhom-controller/internal/monitor" ) // Alert represents a persistent dashboard alert banner. type Alert struct { ID string // unique identifier for filtering Level string // "error", "warning", "info" Message string // Hungarian display text Link string // optional link to relevant page LinkText string // link display text PageOnly []string // if non-empty, only show on these pages (e.g., ["dashboard", "monitoring"]) } // AlertManager generates and stores dashboard alerts from health check results. // Alerts are state-based (not event-based) — they reflect current system state // and are regenerated after each health check cycle. type AlertManager struct { mu sync.RWMutex alerts []Alert logger *log.Logger } // NewAlertManager creates a new AlertManager. func NewAlertManager(logger *log.Logger) *AlertManager { return &AlertManager{ logger: logger, } } // Refresh regenerates alerts from the latest health check report and config state. // Called after each health check cycle (every 5 minutes). func (am *AlertManager) Refresh(report *monitor.HealthReport, cfg *config.Config, backupMgr *backup.Manager) { var alerts []Alert // From health check issues (critical) for _, issue := range report.Issues { alerts = append(alerts, Alert{ ID: "health-" + simpleHash(issue), Level: "error", Message: issue, Link: "/monitoring", LinkText: "Rendszermonitor", }) } // From health check warnings for _, w := range report.Warnings { alert := Alert{ ID: "health-" + simpleHash(w), Level: "warning", Message: w, Link: "/monitoring", LinkText: "Rendszermonitor", } // Disk-related warnings only relevant on dashboard and monitoring pages if strings.Contains(w, "meghajtón") || strings.Contains(w, "adattároló") || strings.Contains(w, "meghajtó") { alert.ID = "disk-not-separate" alert.PageOnly = []string{"dashboard", "monitoring"} } alerts = append(alerts, alert) } // Missing ping UUIDs if cfg.Monitoring.Enabled { missing := countMissingPings(cfg) if missing > 0 { alerts = append(alerts, Alert{ ID: "pings-missing", Level: "warning", Message: fmt.Sprintf("%d monitoring ellenőrzés nincs beállítva", missing), Link: "/monitoring", LinkText: "Rendszermonitor", }) } } // Backup disabled if !cfg.Backup.Enabled { alerts = append(alerts, Alert{ ID: "backup-disabled", Level: "warning", Message: "A biztonsági mentés nincs bekapcsolva", Link: "/settings", LinkText: "Beállítások", }) } // Sort: errors first, then warnings, then info sortAlerts(alerts) am.mu.Lock() am.alerts = alerts am.mu.Unlock() am.logger.Printf("[DEBUG] AlertManager refreshed: %d alerts (%d error, %d warning)", len(alerts), countLevel(alerts, "error"), countLevel(alerts, "warning")) } // GetAlerts returns a copy of the current alerts, optionally excluding specific IDs. func (am *AlertManager) GetAlerts(excludeIDs ...string) []Alert { am.mu.RLock() defer am.mu.RUnlock() if len(am.alerts) == 0 { return nil } exclude := make(map[string]bool, len(excludeIDs)) for _, id := range excludeIDs { exclude[id] = true } var result []Alert for _, a := range am.alerts { if exclude[a.ID] { continue } result = append(result, a) } // Cap at 5 visible alerts if len(result) > 5 { overflow := len(result) - 5 result = result[:5] result = append(result, Alert{ ID: "overflow", Level: "info", Message: fmt.Sprintf("+ %d további figyelmeztetés", overflow), Link: "/monitoring", }) } return result } // countMissingPings counts how many ping UUIDs are not configured. func countMissingPings(cfg *config.Config) int { count := 0 uuids := []string{ cfg.Monitoring.PingUUIDs.Heartbeat, cfg.Monitoring.PingUUIDs.SystemHealth, cfg.Monitoring.PingUUIDs.DBDump, cfg.Monitoring.PingUUIDs.Backup, cfg.Monitoring.PingUUIDs.BackupIntegrity, } for _, uuid := range uuids { if !isPingConfigured(uuid) { count++ } } return count } // simpleHash returns a short deterministic hash for deduplication. func simpleHash(s string) string { h := uint32(0) for _, c := range s { h = h*31 + uint32(c) } return fmt.Sprintf("%08x", h) } // sortAlerts sorts alerts by severity: error > warning > info. func sortAlerts(alerts []Alert) { levelOrder := map[string]int{"error": 0, "warning": 1, "info": 2} for i := 1; i < len(alerts); i++ { for j := i; j > 0 && levelOrder[alerts[j].Level] < levelOrder[alerts[j-1].Level]; j-- { alerts[j], alerts[j-1] = alerts[j-1], alerts[j] } } } func countLevel(alerts []Alert, level string) int { n := 0 for _, a := range alerts { if a.Level == level { n++ } } return n }