"Avisame cuando esta API devuelva stock en cero." La vigilancia apunta a una HERRAMIENTA ya configurada del agente y nunca a una URL libre: así hereda entero el blindaje de LlamarHerramientaWebhook —DNS resuelto en el connect, IPs internas rechazadas, sin redirects, respuesta acotada— sin reimplementar una línea de eso. Hay un test que falla si alguien le agrega un campo url. El ahorro que hace viable la feature: se hashea la respuesta y, si no cambió, no hay nada que evaluar y no se gasta un token. Preguntarle a la IA en cada chequeo serían 1440 consultas diarias por cliente para responder algo que ya sabíamos. El test verifica que el corte por hash esté ANTES de la evaluación. Y se avisa al ENTRAR en condición, no en cada chequeo que la siga cumpliendo: una alerta que llega cada media hora deja de leerse a la segunda. Es la lección que el monitor de sitios ya había aprendido con las transiciones de estado. Solo un SI explícito dispara el aviso. Un modelo que devuelve una explicación, un error o cualquier otra cosa se toma como que no: un aviso que no llega molesta menos que uno que despierta a alguien a las 3 AM sin motivo. Si el envío del aviso falla, no se marca como avisado — se reintenta al siguiente ciclo. Y tras diez fallos seguidos la vigilancia se apaga sola: una API que dejó de existir no puede consultarse para siempre ni seguir gastando el plan de quien la configuró. Máximo 5 por agente, mínimo 5 minutos de intervalo. Solo canal interno: cada vigilancia es una llamada saliente recurrente y consultas a la IA, o sea plan del dueño gastado por quien la pida. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
255 lines
8.6 KiB
Go
Executable File
255 lines
8.6 KiB
Go
Executable File
package main
|
|
|
|
import (
|
|
"flag"
|
|
"log"
|
|
"os"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
|
"github.com/pyroscope-io/pyroscope/pkg/agent/profiler"
|
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
|
"github.com/sujit-baniya/fiber-boilerplate/config"
|
|
"github.com/sujit-baniya/fiber-boilerplate/migrations"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/auth"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
|
"github.com/sujit-baniya/fiber-boilerplate/rest/routes"
|
|
"github.com/sujit-baniya/fiber-boilerplate/utils"
|
|
)
|
|
|
|
func main() {
|
|
configFile := flag.String("config", "config.yml", "User Config file from user")
|
|
migrate := flag.Bool("migrate", false, "Update db structure")
|
|
flag.Parse()
|
|
|
|
// Cargar configuración
|
|
app.Load(*configFile)
|
|
|
|
// Configurar Pyroscope profiler
|
|
if app.Http.Profiler.Enabled {
|
|
_, _ = profiler.Start(profiler.Config{
|
|
ApplicationName: app.Http.Server.Name,
|
|
ServerAddress: app.Http.Profiler.Server,
|
|
})
|
|
}
|
|
|
|
// Actualizar versión del servidor
|
|
app.Http.Server.Version = app.Version
|
|
|
|
// Los archivos de /uploads dejan de ser públicos: solo se sirven a usuarios
|
|
// con sesión iniciada en el panel. Los clientes del portal siguen bajando sus
|
|
// documentos por los endpoints dedicados, que validan que les pertenezcan.
|
|
config.UploadsGuard = func(c *fiber.Ctx) bool {
|
|
return auth.IsLoggedIn(c)
|
|
}
|
|
|
|
// Habilitar CORS
|
|
app.Http.Server.App.Use(cors.New(cors.Config{
|
|
AllowOrigins: "*",
|
|
AllowMethods: "GET,POST,PUT,DELETE,OPTIONS",
|
|
AllowHeaders: "Content-Type, Authorization",
|
|
}))
|
|
|
|
if *migrate {
|
|
// Ejecutar migraciones
|
|
migrations.Migrate()
|
|
} else {
|
|
// Crear/actualizar tablas de Renovaciones e idempotente)
|
|
migrations.MigrateRenovaciones()
|
|
// Crear/actualizar tablas de Portal, Telegram y Proyectos
|
|
migrations.MigratePortal()
|
|
// Crear tabla de Landing Generator
|
|
migrations.MigrateLanding()
|
|
// Crear tablas de integraciones y pasarelas si no existen.
|
|
// AutoMigrate(a, b, c...) se detiene en el primer modelo que falle: todo lo
|
|
// que viene después en la lista se queda sin tabla, en silencio (fue la
|
|
// causa real de que plantillas_documento/tarifas desaparecieran de
|
|
// producción — un modelo antes en la lista falló y todo lo posterior no se
|
|
// llegó a crear). Migrando uno por uno, un modelo roto no bloquea al resto.
|
|
modelosBase := []interface{}{
|
|
// Tablas base del sistema (columnas nuevas)
|
|
&models.Users{},
|
|
&models.Roles{},
|
|
&models.Submodules{},
|
|
&models.QueryHistory{},
|
|
&models.OssApi{},
|
|
&models.QrVcard{},
|
|
&models.VcfVcard{},
|
|
&models.HostingerConfig{},
|
|
&models.CloudflareConfig{},
|
|
&models.BoldConfig{},
|
|
&models.BoldWebhookLog{},
|
|
&models.BoldCallbackLog{},
|
|
&models.DlocalPaymentLog{},
|
|
// Documentación por SaaS
|
|
&models.SaasProducto{},
|
|
&models.DocCategoria{},
|
|
&models.DocPagina{},
|
|
// Integraciones SaaS (dispatcher de pagos)
|
|
&models.SaasApiConfig{},
|
|
&models.SaasDispatchLog{},
|
|
// Notificaciones
|
|
&models.NotifEventoConfig{},
|
|
&models.SistemaNotificacion{},
|
|
&models.ServidorAlertaUmbral{},
|
|
// Shield
|
|
&models.ShieldConfig{},
|
|
// Partner
|
|
&models.PartnerRecurso{},
|
|
&models.PartnerComunicado{},
|
|
// Portal: recuperación de contraseña
|
|
&models.PortalPasswordResetToken{},
|
|
// Integración VCard API Admin
|
|
&models.VcardApiConfig{},
|
|
// Integración Coolify (multi-instancia)
|
|
&models.CoolifyConfig{},
|
|
// AI Config: campos del agente Telegram
|
|
&models.AiConfig{},
|
|
// Agente Telegram: historial y chats autorizados
|
|
&models.TelegramAgentHistory{},
|
|
&models.TelegramAgentAuth{},
|
|
// Servidores: nuevos campos de agente + tabla join de integraciones
|
|
&models.Servidor{},
|
|
&models.ConxDb{},
|
|
&models.ProvServidor{},
|
|
// SMTP
|
|
&models.SmtpConfig{},
|
|
// WebSMS (LabsMobile)
|
|
&models.WebSmsConfig{},
|
|
&models.WebSmsLog{},
|
|
&models.WebSmsWebhookLog{},
|
|
// Contabilidad
|
|
&models.Cuenta{},
|
|
&models.Entidad{},
|
|
&models.Transaccion{},
|
|
&models.CuentaCobro{},
|
|
&models.CuentaPagar{},
|
|
&models.ConsolidadoMensual{},
|
|
// Historial de métricas del agente
|
|
&models.ServidorMetricasHistory{},
|
|
// Monitor de disponibilidad de URLs
|
|
&models.UrlMonitor{},
|
|
&models.UrlMonitorLog{},
|
|
// Tablero de tareas
|
|
&models.Tarea{},
|
|
&models.TareaComentario{},
|
|
// Pagos externos: API de cobros para aplicaciones de terceros
|
|
&models.ServicioPagoExterno{},
|
|
&models.SolicitudPagoExterna{},
|
|
// Soporte: webhook de correo entrante
|
|
&models.SoporteWebhookConfig{},
|
|
// Automatización de cotizaciones, contratos, actas y cuentas de cobro con IA
|
|
&models.PlantillaDocumento{},
|
|
&models.Tarifa{},
|
|
&models.DocumentoGenerado{},
|
|
&models.Arquitectura{},
|
|
// Vinculación de Telegram para staff interno
|
|
&models.TelegramStaffToken{},
|
|
// uMind: chat con IA embebible por tenant (widget web + RAG)
|
|
&models.UmindTenant{},
|
|
&models.UmindAgente{},
|
|
&models.UmindDocumento{},
|
|
&models.UmindChunk{},
|
|
&models.UmindMensaje{},
|
|
&models.UmindHerramienta{},
|
|
&models.UmindCanal{},
|
|
&models.UmindAviso{},
|
|
&models.UmindArchivo{},
|
|
&models.UmindAccionPendiente{},
|
|
&models.UmindVigilancia{},
|
|
&models.UmindConexion{},
|
|
&models.UmindEventoLog{},
|
|
&models.UmindPlan{},
|
|
&models.UmindUso{},
|
|
// API Keys de /api/v2 (token + IP obligatoria + scopes)
|
|
&models.ApiKey{},
|
|
// Integraciones: OCR y transcripción de audio (servicios propios)
|
|
&models.OcrConfig{},
|
|
&models.WhisperAsrConfig{},
|
|
}
|
|
for _, m := range modelosBase {
|
|
if err := app.Http.Database.DB.AutoMigrate(m); err != nil {
|
|
log.Printf("[MIGRATE] Error migrando %T: %v", m, err)
|
|
}
|
|
}
|
|
// Seed automático (idempotente) de módulos del sistema
|
|
migrations.SeedRenovaciones()
|
|
migrations.SeedIntegraciones()
|
|
migrations.SeedPasarelas()
|
|
migrations.SeedSaas()
|
|
migrations.SeedServidores()
|
|
migrations.SeedAdministracion()
|
|
migrations.SeedPlantillasBase()
|
|
migrations.SeedTelegram()
|
|
migrations.SeedPortalClientes()
|
|
migrations.SeedNotifDefaults()
|
|
migrations.SeedShield()
|
|
migrations.SeedPartnerRecursos()
|
|
models.SeedContabilidad()
|
|
// SeedBalanceData carga transacciones contables reales de 2026. Ya no se
|
|
// ejecuta en cada arranque: si el contador editaba o borraba una de esas
|
|
// filas, el siguiente reinicio la recreaba y el balance quedaba duplicado.
|
|
// Para recargarla puntualmente: SEED_BALANCE=1 ./apiv2
|
|
if os.Getenv("SEED_BALANCE") == "1" {
|
|
log.Println("[SEED] SEED_BALANCE=1 — cargando datos de balance")
|
|
models.SeedBalanceData()
|
|
}
|
|
migrations.SeedContabilidadMenu()
|
|
migrations.SeedWebSms()
|
|
migrations.SeedUrlMonitor()
|
|
migrations.SeedTareas()
|
|
migrations.SeedPagosExternos()
|
|
migrations.SeedAutomatizacionIA()
|
|
migrations.SeedUmind()
|
|
migrations.SeedDocumentacion()
|
|
// Va antes de MigrarUmindAgentes: libera las columnas huérfanas que
|
|
// dejó el refactor multi-agente. Sin esto no se puede insertar nada en
|
|
// uMind (ver el comentario de la función).
|
|
migrations.LiberarColumnasHuerfanasUmind()
|
|
migrations.IndicesUnicosMessageID()
|
|
migrations.MigrarUmindAgentes()
|
|
migrations.SeedApiKeys()
|
|
if n, err := models.RepararEstadosTareaInvalidos(); err != nil {
|
|
log.Printf("[FIX] Error reparando estados de tareas: %v", err)
|
|
} else if n > 0 {
|
|
log.Printf("[FIX] %d tarea(s) con estado inválido reparadas (ahora visibles en el Kanban)", n)
|
|
}
|
|
// Cargar config SMTP desde BD (sobreescribe valores del .env/config.yml si hay registro activo)
|
|
if smtpCfg, err := models.GetSmtpConfig(); err == nil {
|
|
app.Http.Mail.Host = smtpCfg.Host
|
|
app.Http.Mail.Port = smtpCfg.Port
|
|
app.Http.Mail.Username = smtpCfg.Username
|
|
app.Http.Mail.Password = utils.Decrypt(smtpCfg.Password, app.Http.Server.Key)
|
|
app.Http.Mail.Encryption = smtpCfg.Encryption
|
|
app.Http.Mail.FromAddress = smtpCfg.FromAddress
|
|
app.Http.Mail.FromName = smtpCfg.FromName
|
|
app.Http.Mail.SetupMailer()
|
|
log.Println("[SMTP] Configuración cargada desde base de datos")
|
|
} else {
|
|
log.Println("[SMTP] Sin config en BD, usando valores del entorno")
|
|
}
|
|
// Iniciar cron de vencimientos
|
|
services.IniciarCron()
|
|
defer services.DetenerCron()
|
|
// Cargar rutas
|
|
routes.LoadRoutes(app.Http.Server.App)
|
|
|
|
// Con las rutas ya montadas se puede avisar de los ítems del menú que
|
|
// apuntan a una URL inexistente — el síntoma es un 404 que parece un
|
|
// problema de permisos.
|
|
rutasGET := map[string]bool{}
|
|
for _, capa := range app.Http.Server.Stack() {
|
|
for _, r := range capa {
|
|
if r.Method == "GET" {
|
|
rutasGET[r.Path] = true
|
|
}
|
|
}
|
|
}
|
|
migrations.VerificarUrlsDeSubmodulos(rutasGET)
|
|
|
|
app.Http.Route404()
|
|
log.Fatal(app.Http.Server.ServeWithGraceFullShutdown())
|
|
}
|
|
}
|