From 1a96caeb8300c8fe1462bbfdf8750bc654877d2d Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Thu, 21 May 2026 23:49:37 -0500 Subject: [PATCH] ip --- migrations/migrate.go | 1 + pkg/models/notif_config.go | 56 +++++++++ pkg/services/cron_service.go | 108 +++++++++++++++++- resources/views/notif_config.html | 102 ++++++++++++++++- resources/views/servidor_dashboard.html | 43 +++++-- .../controllers/servidor_alerta_controller.go | 24 ++++ rest/routes/user.go | 3 + 7 files changed, 324 insertions(+), 13 deletions(-) create mode 100644 rest/controllers/servidor_alerta_controller.go diff --git a/migrations/migrate.go b/migrations/migrate.go index ea7e121..84bcf5f 100755 --- a/migrations/migrate.go +++ b/migrations/migrate.go @@ -87,6 +87,7 @@ func Migrate() { // Sistema de notificaciones por evento &models.NotifEventoConfig{}, &models.SistemaNotificacion{}, + &models.ServidorAlertaUmbral{}, // Submódulo Partner &models.PartnerRecurso{}, &models.PartnerComunicado{}, diff --git a/pkg/models/notif_config.go b/pkg/models/notif_config.go index daa9a5d..cc135e3 100644 --- a/pkg/models/notif_config.go +++ b/pkg/models/notif_config.go @@ -1,6 +1,8 @@ package models import ( + "time" + "github.com/sujit-baniya/fiber-boilerplate/app" "gorm.io/gorm" ) @@ -130,3 +132,57 @@ func MarcarNotifsFacturasLeidas(usuarioID uint) error { Where("tipo_usuario = 'portal_user' AND usuario_id = ? AND icono = '🧾'", usuarioID). Update("leida", true).Error } + +// ─── ServidorAlertaUmbral ───────────────────────────────────────────────────── +// Configuración global de umbrales para alertas de servidores. +// Solo existe un registro (singleton, ID = 1). + +type ServidorAlertaUmbral struct { + gorm.Model + Activo bool `json:"activo" gorm:"column:activo;default:true"` + MinutosSinPing int `json:"minutos_sin_ping" gorm:"column:minutos_sin_ping;default:10"` + UmbralCPU int `json:"umbral_cpu" gorm:"column:umbral_cpu;default:90"` + UmbralRAM int `json:"umbral_ram" gorm:"column:umbral_ram;default:90"` + UmbralDisco int `json:"umbral_disco" gorm:"column:umbral_disco;default:90"` + DiasAnteVencimiento int `json:"dias_ante_vencimiento" gorm:"column:dias_ante_vencimiento;default:7"` +} + +func (ServidorAlertaUmbral) TableName() string { return "servidor_alerta_umbral" } + +// GetServidorAlertaUmbral obtiene el singleton de umbrales; crea uno con defaults si no existe. +func GetServidorAlertaUmbral() *ServidorAlertaUmbral { + var cfg ServidorAlertaUmbral + db := app.Http.Database.DB + if err := db.First(&cfg).Error; err != nil { + cfg = ServidorAlertaUmbral{Activo: true, MinutosSinPing: 10, UmbralCPU: 90, UmbralRAM: 90, UmbralDisco: 90, DiasAnteVencimiento: 7} + db.Create(&cfg) + } + return &cfg +} + +// SaveServidorAlertaUmbral guarda el singleton. +func SaveServidorAlertaUmbral(cfg *ServidorAlertaUmbral) error { + db := app.Http.Database.DB + var existing ServidorAlertaUmbral + if err := db.First(&existing).Error; err != nil { + return db.Create(cfg).Error + } + return db.Model(&existing).Updates(map[string]interface{}{ + "activo": cfg.Activo, + "minutos_sin_ping": cfg.MinutosSinPing, + "umbral_cpu": cfg.UmbralCPU, + "umbral_ram": cfg.UmbralRAM, + "umbral_disco": cfg.UmbralDisco, + "dias_ante_vencimiento": cfg.DiasAnteVencimiento, + }).Error +} + +// YaExisteAlertaServidor evita spam: true si ya se creó una notificación con el +// mismo título para este servidor en las últimas `horas` horas. +func YaExisteAlertaServidor(titulo string, horas int) bool { + var count int64 + app.Http.Database.DB.Model(&SistemaNotificacion{}). + Where("titulo = ? AND created_at > ?", titulo, time.Now().Add(-time.Duration(horas)*time.Hour)). + Count(&count) + return count > 0 +} diff --git a/pkg/services/cron_service.go b/pkg/services/cron_service.go index a564acd..16ec560 100644 --- a/pkg/services/cron_service.go +++ b/pkg/services/cron_service.go @@ -1,8 +1,11 @@ package services import ( + "encoding/json" "fmt" "log" + "strconv" + "time" "github.com/robfig/cron/v3" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" @@ -32,8 +35,14 @@ func IniciarCron() { return } + // Salud de servidores — cada 5 minutos + if _, err := cronScheduler.AddFunc("*/5 * * * *", VerificarSaludServidores); err != nil { + log.Printf("[CRON] Error registrando tarea salud_servidores: %v", err) + return + } + cronScheduler.Start() - log.Println("[CRON] Scheduler iniciado — vencimientos próximos 8AM, ya vencidos 9AM, Bold polling cada 15min") + log.Println("[CRON] Scheduler iniciado — vencimientos próximos 8AM, ya vencidos 9AM, Bold polling cada 15min, salud servidores cada 5min") } // DetenerCron para graceful shutdown @@ -208,3 +217,100 @@ func VerificarPagosBoldPendientes() { log.Println("[BOLD-POLL] Verificación completada") } + +// ─── Salud de Servidores ───────────────────────────────────────────────────── + +// VerificarSaludServidores corre cada 5 min y genera notificaciones del sistema +// cuando detecta: agente caído, recurso al límite o VPS próximo a vencer. +func VerificarSaludServidores() { + cfg := models.GetServidorAlertaUmbral() + if !cfg.Activo { + return + } + + servidores, _, err := models.GetAllServidores(1000, 0, "") + if err != nil { + log.Printf("[CRON-SRV] Error obteniendo servidores: %v", err) + return + } + + for _, srv := range servidores { + // 1. Agente caído (sin ping en X minutos) + if srv.AgentToken != "" && srv.AgentLastSeen != nil { + minutos := time.Since(*srv.AgentLastSeen).Minutes() + if minutos > float64(cfg.MinutosSinPing) { + titulo := fmt.Sprintf("🔴 Sin señal: %s", srv.Nombre) + if !models.YaExisteAlertaServidor(titulo, 1) { + crearNotifServidor(titulo, + fmt.Sprintf("El agente no reporta hace %.0f minutos (umbral: %d min)", minutos, cfg.MinutosSinPing), + "servidor_caido") + } + } + } + + // 2. Recursos (CPU / RAM / Disco) al límite + if srv.MetricasJson != "" { + var m struct { + CPU struct{ Porcentaje float64 `json:"porcentaje"` } `json:"cpu"` + RAM struct{ Porcentaje float64 `json:"porcentaje"` } `json:"ram"` + Disco struct{ Porcentaje float64 `json:"porcentaje"` } `json:"disco"` + } + if jsonErr := json.Unmarshal([]byte(srv.MetricasJson), &m); jsonErr == nil { + checkRecurso(srv.Nombre, "CPU", m.CPU.Porcentaje, cfg.UmbralCPU) + checkRecurso(srv.Nombre, "RAM", m.RAM.Porcentaje, cfg.UmbralRAM) + checkRecurso(srv.Nombre, "Disco", m.Disco.Porcentaje, cfg.UmbralDisco) + } + } + + // 3. Vencimiento próximo + if srv.Vencimiento != "" && cfg.DiasAnteVencimiento > 0 { + vence, parseErr := time.Parse("2006-01-02", srv.Vencimiento) + if parseErr == nil { + dias := int(time.Until(vence).Hours() / 24) + if dias >= 0 && dias <= cfg.DiasAnteVencimiento { + titulo := fmt.Sprintf("📅 %s vence en %s", srv.Nombre, strconv.Itoa(dias)+" días") + if dias == 0 { + titulo = fmt.Sprintf("📅 %s vence HOY", srv.Nombre) + } + if !models.YaExisteAlertaServidor(titulo, 12) { + crearNotifServidor(titulo, + fmt.Sprintf("Fecha de vencimiento: %s. Renueva a tiempo.", srv.Vencimiento), + "servidor_vence_pronto") + } + } + } + } + } +} + +func checkRecurso(nombre, recurso string, valor float64, umbral int) { + if valor < float64(umbral) { + return + } + titulo := fmt.Sprintf("⚠️ %s alto en %s (%.0f%%)", recurso, nombre, valor) + if !models.YaExisteAlertaServidor(titulo, 1) { + crearNotifServidor(titulo, + fmt.Sprintf("%s al %.0f%% (umbral: %d%%)", recurso, valor, umbral), + "servidor_recurso_alto") + } +} + +func crearNotifServidor(titulo, cuerpo, evento string) { + // Verificar si la notificación del sistema está habilitada para este evento + evtCfg := models.GetNotifConfig(evento, "admin") + // Por defecto, si no hay config, crear notificación de sistema igualmente + if evtCfg == nil || evtCfg.CanalSistema { + _ = models.CreateSistemaNotif(&models.SistemaNotificacion{ + TipoUsuario: "admin", + Titulo: titulo, + Cuerpo: cuerpo, + Url: "/app/servidor-dashboard", + Icono: "🖥️", + }) + } + // Telegram (si está configurado para este evento) + if evtCfg != nil && evtCfg.CanalTelegram { + msg := fmt.Sprintf("%s\n%s", titulo, cuerpo) + sendTelegramAdmin(msg) + } +} diff --git a/resources/views/notif_config.html b/resources/views/notif_config.html index 5cbe9cd..87a6413 100644 --- a/resources/views/notif_config.html +++ b/resources/views/notif_config.html @@ -91,7 +91,81 @@

- + +
+
+

🖥️ Umbrales de alerta de servidores

+ +
+ +
+
+ + +
+ +

Alerta si el agente no reporta en X minutos.

+ +
+ + +
+ +

Alerta si la CPU supera este porcentaje.

+ +
+ + +
+ +

Alerta si la RAM supera este porcentaje.

+ +
+ + +
+ +

Alerta si el disco supera este porcentaje.

+ +
+ + +
+ +

Alerta X días antes de que venza el VPS.

+ +
+ + +
+ +
+ +
+
+ + +
+
+
+ +
this.umbralToast.visible = false, 3000); + }, + }; +} diff --git a/resources/views/servidor_dashboard.html b/resources/views/servidor_dashboard.html index 56cc4d2..dc2e0c0 100644 --- a/resources/views/servidor_dashboard.html +++ b/resources/views/servidor_dashboard.html @@ -46,10 +46,9 @@ ● En línea ● Fuera ○ Sin agente - - - + + +
@@ -129,13 +128,23 @@ - -
- - - - BD - + +
+ + +
+ + + + BD + +
@@ -608,6 +617,18 @@ return diffMs < 3 * 60 * 1000; // online si reportó en últimos 3 minutos }, + vencimientoInfo(fecha) { + if (!fecha) return null; + const hoy = new Date(); hoy.setHours(0,0,0,0); + const vence = new Date(fecha); vence.setHours(0,0,0,0); + const dias = Math.round((vence - hoy) / 86400000); + if (dias < 0) return { texto: 'Vencido', clase: 'bg-red-100 text-red-700' }; + if (dias === 0) return { texto: 'Vence hoy', clase: 'bg-red-100 text-red-700' }; + if (dias <= 7) return { texto: `Vence en ${dias}d`, clase: 'bg-red-100 text-red-700' }; + if (dias <= 30) return { texto: `Vence en ${dias}d`, clase: 'bg-amber-100 text-amber-700' }; + return { texto: fecha, clase: 'bg-slate-100 text-slate-500' }; + }, + tiempoDesde(isoStr) { if (!isoStr) return '—'; const diff = Math.floor((Date.now() - new Date(isoStr).getTime()) / 1000); diff --git a/rest/controllers/servidor_alerta_controller.go b/rest/controllers/servidor_alerta_controller.go new file mode 100644 index 0000000..98042f3 --- /dev/null +++ b/rest/controllers/servidor_alerta_controller.go @@ -0,0 +1,24 @@ +package controllers + +import ( + "github.com/gofiber/fiber/v2" + "github.com/sujit-baniya/fiber-boilerplate/pkg/models" +) + +// GetServidorAlertaConfig devuelve la configuración actual de umbrales de alerta. +func GetServidorAlertaConfig(c *fiber.Ctx) error { + cfg := models.GetServidorAlertaUmbral() + return c.JSON(fiber.Map{"ok": true, "data": cfg}) +} + +// SaveServidorAlertaConfig guarda la configuración de umbrales de alerta. +func SaveServidorAlertaConfig(c *fiber.Ctx) error { + var input models.ServidorAlertaUmbral + if err := c.BodyParser(&input); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"ok": false, "error": "JSON inválido"}) + } + if err := models.SaveServidorAlertaUmbral(&input); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"ok": false, "error": err.Error()}) + } + return c.JSON(fiber.Map{"ok": true}) +} diff --git a/rest/routes/user.go b/rest/routes/user.go index 46a4fc3..2464fb4 100755 --- a/rest/routes/user.go +++ b/rest/routes/user.go @@ -346,6 +346,9 @@ func UserRoutes(app fiber.Router) { protected.Get("/notif-config", middlewares.MenuMiddleware, controllers.NotifConfigIndex) protected.Get("/notif-config/data", controllers.GetNotifConfigs) protected.Post("/notif-config", controllers.SaveNotifConfig) + // Umbrales de alerta de servidores + protected.Get("/servidor-alerta-config", controllers.GetServidorAlertaConfig) + protected.Post("/servidor-alerta-config", controllers.SaveServidorAlertaConfig) // Partner Recursos (documentación y comunicados para partners) protected.Get("/partner-recursos", middlewares.MenuMiddleware, controllers.PartnerRecursosIndex)