Phase 2: monitoring warnings, dashboard alerts & notification system
- Monitoring page: "Távoli monitoring" section showing healthcheck ping UUID configuration status (configured/not configured) for each of the 5 pings - Alert manager: persistent dashboard banners on all pages generated from health check results, missing pings, and backup status - Notification system: controller-side notifier sends events to hub relay, with cooldown tracking and event-type filtering - Notification preferences UI: email, event checkboxes, cooldown settings on the settings page with test email functionality - Settings refactored: shared settingsData() helper, NotificationPrefs struct with getter/setter and defaults New files: - controller/internal/web/alerts.go (AlertManager) - controller/internal/notify/notifier.go (hub notification client) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
alerts = append(alerts, Alert{
|
||||
ID: "health-" + simpleHash(w),
|
||||
Level: "warning",
|
||||
Message: w,
|
||||
Link: "/monitoring",
|
||||
LinkText: "Rendszermonitor",
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user