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:
@@ -4,15 +4,17 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/scheduler"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func (s *Server) baseData(page, title string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
data := map[string]interface{}{
|
||||
"Page": page,
|
||||
"Title": title,
|
||||
"CustomerName": s.cfg.Customer.Name,
|
||||
@@ -20,6 +22,10 @@ func (s *Server) baseData(page, title string) map[string]interface{} {
|
||||
"Version": s.version,
|
||||
"AuthEnabled": s.authEnabled(),
|
||||
}
|
||||
if s.alertManager != nil {
|
||||
data["Alerts"] = s.alertManager.GetAlerts()
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func (s *Server) dashboardHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
@@ -196,9 +202,41 @@ func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug s
|
||||
func (s *Server) monitoringHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
data := s.baseData("monitoring", "Rendszermonitor")
|
||||
data["SystemInfo"] = system.GetInfo(s.cfg.Paths.HDDPath, s.cpuCollector)
|
||||
|
||||
// On monitoring page, exclude the "pings-missing" alert since the detailed table is visible
|
||||
if s.alertManager != nil {
|
||||
data["Alerts"] = s.alertManager.GetAlerts("pings-missing")
|
||||
}
|
||||
|
||||
// Ping status section
|
||||
data["MonitoringEnabled"] = s.cfg.Monitoring.Enabled
|
||||
if s.cfg.Monitoring.Enabled {
|
||||
pings := []map[string]interface{}{
|
||||
{"Label": "Életjel (Heartbeat)", "Icon": "💓", "Configured": isPingConfigured(s.cfg.Monitoring.PingUUIDs.Heartbeat), "Schedule": "5 percenként"},
|
||||
{"Label": "Rendszer állapot", "Icon": "🖥️", "Configured": isPingConfigured(s.cfg.Monitoring.PingUUIDs.SystemHealth), "Schedule": "5 percenként"},
|
||||
{"Label": "Adatbázis mentés", "Icon": "🗄️", "Configured": isPingConfigured(s.cfg.Monitoring.PingUUIDs.DBDump), "Schedule": "Naponta " + s.cfg.Backup.DBDumpSchedule},
|
||||
{"Label": "Biztonsági mentés", "Icon": "💾", "Configured": isPingConfigured(s.cfg.Monitoring.PingUUIDs.Backup), "Schedule": "Naponta " + s.cfg.Backup.ResticSchedule},
|
||||
{"Label": "Mentés integritás", "Icon": "🔍", "Configured": isPingConfigured(s.cfg.Monitoring.PingUUIDs.BackupIntegrity), "Schedule": "Hetente (vasárnap)"},
|
||||
}
|
||||
allConfigured := true
|
||||
for _, p := range pings {
|
||||
if !p["Configured"].(bool) {
|
||||
allConfigured = false
|
||||
break
|
||||
}
|
||||
}
|
||||
data["PingStatus"] = pings
|
||||
data["AllPingsConfigured"] = allConfigured
|
||||
}
|
||||
|
||||
s.render(w, "monitoring", data)
|
||||
}
|
||||
|
||||
// isPingConfigured returns true if a healthcheck ping UUID is non-empty and not a placeholder.
|
||||
func isPingConfigured(uuid string) bool {
|
||||
return uuid != "" && !strings.HasPrefix(uuid, "CHANGEME")
|
||||
}
|
||||
|
||||
func (s *Server) backupsHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
data := s.baseData("backups", "Biztonsági mentés")
|
||||
|
||||
@@ -214,10 +252,8 @@ func (s *Server) backupsHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
s.render(w, "backups", data)
|
||||
}
|
||||
|
||||
func (s *Server) settingsHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
func (s *Server) settingsData() map[string]interface{} {
|
||||
data := s.baseData("settings", "Beállítások")
|
||||
|
||||
// System configuration (read-only display from controller.yaml)
|
||||
data["CustomerID"] = s.cfg.Customer.ID
|
||||
data["CustomerDomain"] = s.cfg.Customer.Domain
|
||||
data["GitRepoURL"] = s.cfg.Git.RepoURL
|
||||
@@ -228,8 +264,12 @@ func (s *Server) settingsHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
data["MonitoringEnabled"] = s.cfg.Monitoring.Enabled
|
||||
data["HealthchecksBase"] = s.cfg.Monitoring.HealthchecksBase
|
||||
data["HubEnabled"] = s.cfg.Hub.Enabled
|
||||
data["NotificationPrefs"] = s.settings.GetNotificationPrefs()
|
||||
return data
|
||||
}
|
||||
|
||||
s.render(w, "settings", data)
|
||||
func (s *Server) settingsHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
s.render(w, "settings", s.settingsData())
|
||||
}
|
||||
|
||||
func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -238,17 +278,7 @@ func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request)
|
||||
newPassword := r.FormValue("new_password")
|
||||
confirmPassword := r.FormValue("confirm_password")
|
||||
|
||||
data := s.baseData("settings", "Beállítások")
|
||||
data["CustomerID"] = s.cfg.Customer.ID
|
||||
data["CustomerDomain"] = s.cfg.Customer.Domain
|
||||
data["GitRepoURL"] = s.cfg.Git.RepoURL
|
||||
data["GitSyncInterval"] = s.cfg.Git.SyncInterval
|
||||
data["BackupEnabled"] = s.cfg.Backup.Enabled
|
||||
data["DBDumpSchedule"] = s.cfg.Backup.DBDumpSchedule
|
||||
data["ResticSchedule"] = s.cfg.Backup.ResticSchedule
|
||||
data["MonitoringEnabled"] = s.cfg.Monitoring.Enabled
|
||||
data["HealthchecksBase"] = s.cfg.Monitoring.HealthchecksBase
|
||||
data["HubEnabled"] = s.cfg.Hub.Enabled
|
||||
data := s.settingsData()
|
||||
|
||||
// Validate current password
|
||||
effectiveHash := s.effectivePasswordHash()
|
||||
@@ -298,3 +328,71 @@ func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request)
|
||||
flash := url.QueryEscape("Jelszó sikeresen módosítva. Kérjük, jelentkezzen be az új jelszóval.")
|
||||
http.Redirect(w, r, "/login?flash="+flash, http.StatusFound)
|
||||
}
|
||||
|
||||
func (s *Server) settingsNotificationsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
_ = r.ParseForm()
|
||||
|
||||
email := strings.TrimSpace(r.FormValue("notification_email"))
|
||||
cooldownStr := r.FormValue("cooldown_hours")
|
||||
cooldownHours := 6
|
||||
if cooldownStr != "" {
|
||||
if n, err := fmt.Sscanf(cooldownStr, "%d", &cooldownHours); n != 1 || err != nil {
|
||||
cooldownHours = 6
|
||||
}
|
||||
}
|
||||
if cooldownHours < 1 {
|
||||
cooldownHours = 1
|
||||
}
|
||||
if cooldownHours > 168 {
|
||||
cooldownHours = 168
|
||||
}
|
||||
|
||||
// Collect enabled events from checkboxes
|
||||
var enabledEvents []string
|
||||
for _, evt := range []string{"disk_warning", "backup_failed", "update_available", "security_update"} {
|
||||
if r.FormValue("event_"+evt) == "on" {
|
||||
enabledEvents = append(enabledEvents, evt)
|
||||
}
|
||||
}
|
||||
|
||||
prefs := &settings.NotificationPrefs{
|
||||
Email: email,
|
||||
EnabledEvents: enabledEvents,
|
||||
CooldownHours: cooldownHours,
|
||||
}
|
||||
|
||||
if err := s.settings.SetNotificationPrefs(prefs); err != nil {
|
||||
s.logger.Printf("[ERROR] Failed to save notification prefs: %v", err)
|
||||
data := s.settingsData()
|
||||
data["NotificationError"] = "Hiba a beállítások mentésekor"
|
||||
s.render(w, "settings", data)
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Printf("[INFO] Notification preferences updated: email=%s, events=%v", email, enabledEvents)
|
||||
|
||||
data := s.settingsData()
|
||||
data["NotificationSuccess"] = "Értesítési beállítások mentve."
|
||||
s.render(w, "settings", data)
|
||||
}
|
||||
|
||||
func (s *Server) settingsNotificationsTestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data := s.settingsData()
|
||||
|
||||
if s.notifier == nil {
|
||||
data["NotificationError"] = "Az értesítések nincsenek bekapcsolva"
|
||||
s.render(w, "settings", data)
|
||||
return
|
||||
}
|
||||
|
||||
err := s.notifier.SendTest()
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] Test notification failed: %v", err)
|
||||
data["NotificationError"] = fmt.Sprintf("Teszt email küldése sikertelen: %v", err)
|
||||
s.render(w, "settings", data)
|
||||
return
|
||||
}
|
||||
|
||||
data["NotificationSuccess"] = "Teszt email elküldve."
|
||||
s.render(w, "settings", data)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user