"Avisame el 15 de marzo que vence la póliza de Acme, y todos los años" queda programado desde la conversación, visible y cancelable en el panel. Tres tools: programar, listar, cancelar. El aviso vuelve por donde se pidió — Telegram a ese chat, correo a esa casilla — o al dueño del espacio por correo y campanita. Nunca a una dirección que dicte la conversación: eso sería un cañón de spam con destinatario libre. Y las tools de aviso solo se le OFRECEN al modelo cuando la sesión es interna: el chat de prueba del panel (que corre autenticado) o un canal marcado como línea privada del dueño. En el widget público escribe cualquiera, y cualquiera no puede programarle recordatorios ni gastarle el plan a otro. El gate está en dos capas: la tool no se declara, y si igual la pide, el ejecutor la rechaza. Tres detalles que se pagan una sola vez: - El cron corre en memoria del proceso, sin lock distribuido: con dos instancias cada aviso saldría dos veces. El reclamo es un UPDATE condicional — la base ya es el árbitro, no hace falta traer otro. - Si el servidor estuvo caído, una repetición diaria se saltea los ciclos perdidos en vez de disparar diez avisos viejos de golpe. - Un fallo de SMTP devuelve el aviso a pendiente: una caída de correo no puede perder un vencimiento de póliza. Una fecha sin hora se entrega a las 9, no a medianoche, que es cuando nadie mira el teléfono. De paso: los archivos de los espacios uMind nunca se sirven por el estático de /uploads. El guard genérico solo sabe si hay sesión de panel, no de quién es el archivo — salen por su endpoint, que sí valida propiedad. Cerrado por construcción y no por acordarse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
103 lines
3.3 KiB
Go
103 lines
3.3 KiB
Go
package models
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type UrlMonitor struct {
|
|
gorm.Model
|
|
Nombre string `json:"nombre" gorm:"column:nombre;size:100"`
|
|
URL string `json:"url" gorm:"column:url;size:500"`
|
|
IntervaloMin int `json:"intervalo_min" gorm:"column:intervalo_min;default:5"`
|
|
TimeoutSeg int `json:"timeout_seg" gorm:"column:timeout_seg;default:10"`
|
|
Activo bool `json:"activo" gorm:"column:activo;default:true"`
|
|
// Estado actual
|
|
UltimoStatus int `json:"ultimo_status" gorm:"column:ultimo_status"`
|
|
UltimaLatMs int64 `json:"ultima_lat_ms" gorm:"column:ultima_lat_ms"`
|
|
UltimoCheckAt *time.Time `json:"ultimo_check_at" gorm:"column:ultimo_check_at"`
|
|
UltimoOk *bool `json:"ultimo_ok" gorm:"column:ultimo_ok"`
|
|
}
|
|
|
|
func (UrlMonitor) TableName() string { return "url_monitor" }
|
|
|
|
type UrlMonitorLog struct {
|
|
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
|
MonitorID uint `json:"monitor_id" gorm:"column:monitor_id;index"`
|
|
CreatedAt time.Time `json:"created_at" gorm:"column:created_at;index"`
|
|
StatusCode int `json:"status_code" gorm:"column:status_code"`
|
|
LatenciaMs int64 `json:"latencia_ms" gorm:"column:latencia_ms"`
|
|
Ok bool `json:"ok" gorm:"column:ok"`
|
|
Error string `json:"error" gorm:"column:error;type:text"`
|
|
}
|
|
|
|
func (UrlMonitorLog) TableName() string { return "url_monitor_log" }
|
|
|
|
func GetAllUrlMonitors() ([]UrlMonitor, error) {
|
|
var items []UrlMonitor
|
|
err := app.Http.Database.DB.Order("nombre ASC").Find(&items).Error
|
|
return items, err
|
|
}
|
|
|
|
func GetActiveUrlMonitors() ([]UrlMonitor, error) {
|
|
var items []UrlMonitor
|
|
err := app.Http.Database.DB.Where("activo = true").Find(&items).Error
|
|
return items, err
|
|
}
|
|
|
|
func GetUrlMonitorByID(id uint) (*UrlMonitor, error) {
|
|
var m UrlMonitor
|
|
err := app.Http.Database.DB.First(&m, id).Error
|
|
return &m, err
|
|
}
|
|
|
|
func CreateUrlMonitor(m *UrlMonitor) error {
|
|
return app.Http.Database.DB.Create(m).Error
|
|
}
|
|
|
|
func SaveUrlMonitor(m *UrlMonitor) error {
|
|
return app.Http.Database.DB.Save(m).Error
|
|
}
|
|
|
|
func DeleteUrlMonitor(id uint) error {
|
|
return app.Http.Database.DB.Delete(&UrlMonitor{}, id).Error
|
|
}
|
|
|
|
func UpdateUrlMonitorStatus(id uint, statusCode int, latMs int64, ok bool) error {
|
|
now := time.Now()
|
|
return app.Http.Database.DB.Model(&UrlMonitor{}).Where("id = ?", id).Updates(map[string]interface{}{
|
|
"ultimo_status": statusCode,
|
|
"ultima_lat_ms": latMs,
|
|
"ultimo_check_at": &now,
|
|
"ultimo_ok": ok,
|
|
}).Error
|
|
}
|
|
|
|
func InsertUrlMonitorLog(monitorID uint, statusCode int, latMs int64, ok bool, errStr string) {
|
|
app.Http.Database.DB.Create(&UrlMonitorLog{
|
|
MonitorID: monitorID,
|
|
CreatedAt: time.Now(),
|
|
StatusCode: statusCode,
|
|
LatenciaMs: latMs,
|
|
Ok: ok,
|
|
Error: errStr,
|
|
})
|
|
}
|
|
|
|
func GetUrlMonitorLogs(monitorID uint, horas int) ([]UrlMonitorLog, error) {
|
|
var items []UrlMonitorLog
|
|
desde := time.Now().Add(-time.Duration(horas) * time.Hour)
|
|
err := app.Http.Database.DB.
|
|
Where("monitor_id = ? AND created_at >= ?", monitorID, desde).
|
|
Order("created_at ASC").
|
|
Find(&items).Error
|
|
return items, err
|
|
}
|
|
|
|
func PurgarUrlMonitorLogs(dias int) error {
|
|
corte := time.Now().AddDate(0, 0, -dias)
|
|
return app.Http.Database.DB.Where("created_at < ?", corte).Delete(&UrlMonitorLog{}).Error
|
|
}
|