Un tenant (negocio/sitio, dueño de los dominios permitidos) puede tener varios UmindAgente independientes (ej. "Ventas", "Soporte"), cada uno con su propia config de IA, tono, base de conocimiento, tools, canales y conexión de correo. El site_key también pasa a ser por agente, así cada uno tiene su propio <script> de widget embebible y su propio color. Backend: - Nuevo modelo UmindAgente (pkg/models/umind_agente.go), con SiteKey, AiConfigID, Tono, MensajeBienvenida y Color — campos que antes vivían en UmindTenant y se sacan de ahí (las columnas viejas quedan huérfanas sin usar, no se hace DROP COLUMN). - UmindDocumento, UmindChunk, UmindHerramienta, UmindCanal, UmindConexion y UmindMensaje pasan de TenantID a AgenteID. El campo se agrega sin "not null" para no romper el ALTER TABLE en Postgres sobre tablas que ya tienen filas (ej. emetropolitana). - migrations.MigrarUmindAgentes(): idempotente, crea un agente "Principal" por cada tenant existente heredando lo que ya tenía configurado, y mueve sus datos de tenant_id a agente_id. Corre en cada arranque normal, mismo criterio que los Seed* — nada se rompe para los tenants ya en producción. - Motor del agente, widget, canales (Telegram/WhatsApp) y OAuth de correo ahora operan sobre UmindAgente; el tenant solo se consulta para el chequeo de dominio permitido y el nombre del negocio que ve el visitante. Frontend: nueva jerarquía de navegación tenant → lista de agentes (TenantAgentes.vue) → detalle de un agente (AgenteDetail.vue, antes TenantDetail.vue) con las mismas 6 tabs de siempre, ahora por agente. El modal de tenant en el sidebar se achica a nombre/dominios/activo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
225 lines
7.6 KiB
Go
Executable File
225 lines
7.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.UmindConexion{},
|
|
// API Keys de /api/v2 (token + IP obligatoria + scopes)
|
|
&models.ApiKey{},
|
|
}
|
|
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.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)
|
|
app.Http.Route404()
|
|
log.Fatal(app.Http.Server.ServeWithGraceFullShutdown())
|
|
}
|
|
}
|