package migrations import ( "fmt" "log" "strings" "time" "github.com/sujit-baniya/fiber-boilerplate/app" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" "gorm.io/driver/sqlite" "gorm.io/gorm" ) // Session structure for managing sessions type Session struct { ID uint `gorm:"primaryKey"` SessionID string `gorm:"unique;not null"` Data string `gorm:"type:text"` CreatedAt time.Time UpdatedAt time.Time ExpireAt time.Time } // Migrate performs the database migrations func Migrate() { log.Println("Initiating migration...") // Migrate the main application models. // Un solo 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. Migrando uno por // uno, un modelo roto no bloquea al resto (mismo fix que en main.go). modelosPrincipales := []interface{}{ &models.Roles{}, &models.Users{}, &models.Modules{}, &models.Submodules{}, &models.Inicio{}, &models.TipoServidor{}, &models.TipoDb{}, &models.Servidor{}, &models.ConxDb{}, &models.ConxSsh{}, &models.ProvServidor{}, &models.QrVcard{}, &models.OssApi{}, &models.VcfVcard{}, &models.DlocalApi{}, // Módulo de Renovaciones &models.Servicio{}, &models.Cliente{}, &models.PlantillaCorreo{}, &models.NotificacionRegla{}, &models.Contrato{}, &models.NotificacionLog{}, &models.SmtpConfig{}, // Integraciones externas &models.HostingerConfig{}, &models.CloudflareConfig{}, // Query Runner &models.QueryHistory{}, // Pasarelas de pago &models.BoldConfig{}, &models.BoldWebhookLog{}, &models.BoldCallbackLog{}, &models.DlocalPaymentLog{}, &models.PaypalConfig{}, // Documentación por SaaS &models.SaasProducto{}, &models.DocCategoria{}, &models.DocPagina{}, // Integraciones SaaS (dispatcher de pagos) &models.SaasApiConfig{}, &models.SaasDispatchLog{}, &models.SaasWebhookInLog{}, // Documentos de clientes &models.ClienteDocumento{}, // Telegram &models.TelegramConfig{}, &models.TelegramLog{}, // Portal de Clientes &models.PortalUser{}, &models.PortalAcceso{}, &models.Proyecto{}, &models.ProyectoFase{}, &models.ProyectoAvance{}, &models.ProyectoEntregable{}, &models.ProyectoDocumento{}, &models.ProyectoTicket{}, &models.TicketMensaje{}, &models.Factura{}, // Sistema de notificaciones por evento &models.NotifEventoConfig{}, &models.SistemaNotificacion{}, &models.ServidorAlertaUmbral{}, // Submódulo Partner &models.PartnerRecurso{}, &models.PartnerComunicado{}, // Integración VCard API Admin (Laravel) &models.VcardApiConfig{}, // Integración Coolify &models.CoolifyConfig{}, // 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{}, // 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{}, &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 modelosPrincipales { if err := app.Http.Database.DB.Migrator().AutoMigrate(m); err != nil { log.Printf("[MIGRATE] Error migrando %T: %v", m, err) } } // Open (or create) a SQLite database for sessions db, err := gorm.Open(sqlite.Open("session.db"), &gorm.Config{}) if err != nil { log.Fatalf("Error connecting to the database: %v", err) return } // Migrate the Session structure if err := db.AutoMigrate(&Session{}); err != nil { log.Fatalf("Error migrating session database: %v", err) return } fmt.Println("Session database created and table migrated successfully.") // Insertar módulo y submódulos de Renovaciones SeedRenovaciones() // Insertar módulo y submódulos de Integraciones externas SeedIntegraciones() // Insertar submódulo "Pasarelas de Pago" en el módulo Integraciones SeedPasarelas() // Insertar submódulos de SaaS (Productos + Integraciones) en el módulo Integraciones SeedSaas() // Insertar submódulo "Statuspage" en el módulo Integraciones SeedStatuspage() // Crear plantillas y reglas mínimas si no existen (bienvenida, pago_recibido) SeedPlantillasBase() // Agregar submódulo "Telegram" al módulo Integraciones SeedTelegram() // Crear módulo y submódulos de Portal de Clientes SeedPortalClientes() // Agregar submódulo "Partner Recursos" al módulo Portal de Clientes SeedPartnerRecursos() // Crear módulo y submódulos de Automatización IA (cotizaciones, contratos, asistente) SeedAutomatizacionIA() log.Println("Migration Completed...") } // asignarSubmodulosSiFaltan agrega submódulos a un rol solo si el rol todavía no // tiene NINGUNO de ellos. // // Antes los seeds hacían Append incondicional en cada arranque: si un admin le // quitaba un módulo a un rol desde el panel, el siguiente despliegue se lo // devolvía y la gestión de permisos era, en la práctica, imposible de sostener. func asignarSubmodulosSiFaltan(rol *models.Roles, subs []models.Submodules) { if rol == nil || len(subs) == 0 { return } db := app.Http.Database.DB var ids []uint for _, s := range subs { if s.ID > 0 { ids = append(ids, s.ID) } } if len(ids) == 0 { return } var yaAsignados int64 db.Table("roles_submodules"). Where("roles_id = ? AND submodules_id IN ?", rol.ID, ids). Count(&yaAsignados) if yaAsignados > 0 { // El rol ya conoce este grupo de submódulos: lo que tenga hoy es una // decisión del administrador y no se toca. return } if err := db.Model(rol).Association("Submodules").Append(&subs); err != nil { log.Printf("[SEED] Error asignando submódulos al rol %s: %v", rol.Name, err) } } // SeedStatuspage agrega el submódulo "Statuspage" (Atlassian) al módulo "Integraciones" // y lo asigna a todos los roles. Es idempotente. func SeedStatuspage() { db := app.Http.Database.DB // Obtener el módulo "Integraciones" (debe existir luego de SeedIntegraciones) var modulo models.Modules if err := db.Where("title = ?", "Integraciones").First(&modulo).Error; err != nil { log.Printf("[SEED] Módulo 'Integraciones' no encontrado para SeedStatuspage: %v", err) return } entries := []struct{ title, desc, url string }{ // La página real es la pública /status (rest/routes/publicas.go): // /app/statuspage nunca existió y el ítem daba 404 al entrar. {"Statuspage", "Monitoreo de estado de servicios vía Atlassian Statuspage", "/status"}, } // La URL vieja apuntaba a /app/statuspage, que no existe: se corrige el // registro ya creado en vez de dejar un ítem de menú que da 404. db.Model(&models.Submodules{}).Where("url = ?", "/app/statuspage").Update("url", "/status") var insertados []models.Submodules for _, e := range entries { var sub models.Submodules if err := db.Where("url = ?", e.url).First(&sub).Error; err != nil { sub = models.Submodules{ Title: e.title, Description: e.desc, Url: e.url, ModuleId: modulo.ID, ModifiedAt: time.Now(), } if err := db.Create(&sub).Error; err != nil { log.Printf("[SEED] Error creando submódulo '%s': %v", e.title, err) continue } log.Printf("[SEED] Submódulo '%s' creado (ID %d)", e.title, sub.ID) } else { log.Printf("[SEED] Submódulo '%s' ya existe (ID %d)", sub.Title, sub.ID) } insertados = append(insertados, sub) } // Solo se asigna al rol Administrador: los demás roles pueden estar // restringidos a propósito y no deben recibir módulos nuevos por su cuenta // — hay que habilitarlos manualmente desde /app/roles si corresponde. var rolAdmin models.Roles if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil { log.Printf("[SEED] Rol 'Administrador' no encontrado: %v", err) return } asignarSubmodulosSiFaltan(&rolAdmin, insertados) log.Println("[SEED] Seed de Statuspage completado.") } // SeedPlantillasBase crea una plantilla mínima y la regla de pago_recibido si no existen. // Es idempotente — no sobreescribe registros existentes. func SeedPlantillasBase() { db := app.Http.Database.DB plantillasBase := []struct { nombre string asunto string tipo string cuerpo string }{ { nombre: "Pago recibido (base)", asunto: "Pago recibido — gracias por tu pago", tipo: "pago", cuerpo: `

Pago recibido ✔

Hola {{.ClienteNombre}},

Hemos recibido y confirmado tu pago. Tu servicio ha sido renovado correctamente.

{{range .Servicios}} {{end}}
{{.Nombre}} {{.Moneda}} {{.Precio}} Vence: {{.FechaVenc}}

Si tienes alguna pregunta, responde a este correo.

`, }, { nombre: "Bienvenida (base)", asunto: "Bienvenido — tu servicio está activo", tipo: "bienvenida", cuerpo: `

¡Bienvenido!

Hola {{.ClienteNombre}},

Tu servicio ha sido activado. A continuación el detalle:

{{range .Servicios}} {{end}}
{{.Nombre}} {{.Moneda}} {{.Precio}} Vence: {{.FechaVenc}}

Gracias por elegirnos.

`, }, } for _, p := range plantillasBase { var existing models.PlantillaCorreo if err := db.Where("nombre = ?", p.nombre).First(&existing).Error; err != nil { nueva := models.PlantillaCorreo{ Nombre: p.nombre, Asunto: p.asunto, Tipo: p.tipo, CuerpoHTML: p.cuerpo, } if err := db.Create(&nueva).Error; err != nil { log.Printf("[SEED] Error creando plantilla '%s': %v", p.nombre, err) continue } log.Printf("[SEED] Plantilla '%s' creada (ID %d)", p.nombre, nueva.ID) } else { log.Printf("[SEED] Plantilla '%s' ya existe (ID %d)", existing.Nombre, existing.ID) } } // Crear regla pago_recibido si no existe var reglaExistente models.NotificacionRegla if err := db.Where("tipo_evento = ?", "pago_recibido").First(®laExistente).Error; err != nil { // Buscar la plantilla de pago var plantilla models.PlantillaCorreo if err2 := db.Where("tipo = ?", "pago").First(&plantilla).Error; err2 != nil { log.Printf("[SEED] No se encontró plantilla de tipo 'pago' para crear regla pago_recibido") return } regla := models.NotificacionRegla{ Nombre: "Confirmación de pago", TipoEvento: "pago_recibido", PlantillaID: plantilla.ID, Activo: true, AplicaA: "todos", PasarelaEnlace: "ninguna", } if err := db.Create(®la).Error; err != nil { log.Printf("[SEED] Error creando regla pago_recibido: %v", err) } else { log.Printf("[SEED] Regla 'pago_recibido' creada (ID %d) con plantilla ID %d", regla.ID, plantilla.ID) } } else { log.Printf("[SEED] Regla 'pago_recibido' ya existe (ID %d)", reglaExistente.ID) } log.Println("[SEED] SeedPlantillasBase completado.") } // MigrateRenovaciones crea/actualiza las tablas del módulo de Renovaciones. // Es idempotente: GORM AutoMigrate solo añade columnas/tablas nuevas, nunca las borra. func MigrateRenovaciones() { db := app.Http.Database.DB if err := db.AutoMigrate( &models.Servicio{}, &models.Cliente{}, &models.PlantillaCorreo{}, &models.NotificacionRegla{}, &models.Contrato{}, // incluye columnas enlace_pago y enlace_pago_link_id &models.NotificacionLog{}, &models.SmtpConfig{}, ); err != nil { log.Printf("[MIGRATE] Error en MigrateRenovaciones: %v", err) } else { log.Println("[MIGRATE] Tablas de Renovaciones OK") } } // SeedRenovaciones inserta el módulo "Renovaciones" y sus submódulos si no existen, // y los asigna a todos los roles presentes en la BD. func SeedRenovaciones() { db := app.Http.Database.DB // 1. Crear/obtener módulo var modulo models.Modules result := db.Where("title = ?", "Renovaciones").First(&modulo) if result.Error != nil { modulo = models.Modules{ Title: "Renovaciones", Description: "Gestión de contratos, clientes y notificaciones de vencimiento", ModifiedAt: time.Now(), } if err := db.Create(&modulo).Error; err != nil { log.Printf("[SEED] Error creando módulo Renovaciones: %v", err) return } log.Printf("[SEED] Módulo 'Renovaciones' creado con ID %d", modulo.ID) } else { log.Printf("[SEED] Módulo 'Renovaciones' ya existe (ID %d)", modulo.ID) } // 2. Definir submódulos entries := []struct{ title, desc, url string }{ {"Contratos", "Gestión de contratos y vencimientos", "/app/contratos"}, {"Clientes", "Clientes y datos de contacto", "/app/clientes"}, {"Servicios", "Catálogo de servicios", "/app/servicios"}, {"Plantillas Correo", "Editor de plantillas de correo", "/app/plantillas-correo"}, {"Reglas de Aviso", "Reglas de notificación automática", "/app/reglas-notificacion"}, {"Historial Notif.", "Historial de correos enviados", "/app/historial-notificaciones"}, {"Config. SMTP", "Configuración del servidor de correo", "/app/smtp-config"}, } var insertados []models.Submodules for _, e := range entries { var sub models.Submodules if err := db.Where("url = ?", e.url).First(&sub).Error; err != nil { sub = models.Submodules{ Title: e.title, Description: e.desc, Url: e.url, ModuleId: modulo.ID, ModifiedAt: time.Now(), } if err := db.Create(&sub).Error; err != nil { log.Printf("[SEED] Error creando submódulo '%s': %v", e.title, err) continue } log.Printf("[SEED] Submódulo '%s' creado (ID %d)", e.title, sub.ID) } else { // Asegurar que el submódulo apunte al módulo correcto (Renovaciones) if sub.ModuleId != modulo.ID { if err := db.Model(&sub).Update("module_id", modulo.ID).Error; err != nil { log.Printf("[SEED] Error actualizando module_id de '%s': %v", sub.Title, err) } else { log.Printf("[SEED] Submódulo '%s' reasignado a módulo Renovaciones", sub.Title) } } else { log.Printf("[SEED] Submódulo '%s' ya existe (ID %d)", sub.Title, sub.ID) } } insertados = append(insertados, sub) } // 3. Asignar solo al rol Administrador var rolAdmin models.Roles if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil { log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err) } else { asignarSubmodulosSiFaltan(&rolAdmin, insertados) log.Printf("[SEED] Submódulos de Renovaciones asignados al rol 'Administrador'") } log.Println("[SEED] Seed de Renovaciones completado.") } // SeedAutomatizacionIA inserta el módulo "Automatización IA" y sus submódulos // (Asistente, Plantillas de Documento, Tarifas) si no existen, y los asigna al // rol Administrador. Es idempotente. func SeedAutomatizacionIA() { db := app.Http.Database.DB // 1. Crear/obtener módulo var modulo models.Modules result := db.Where("title = ?", "Automatización IA").First(&modulo) if result.Error != nil { modulo = models.Modules{ Title: "Automatización IA", Description: "Cotizaciones, contratos, actas y cuentas de cobro generados por IA", ModifiedAt: time.Now(), } if err := db.Create(&modulo).Error; err != nil { log.Printf("[SEED] Error creando módulo Automatización IA: %v", err) return } log.Printf("[SEED] Módulo 'Automatización IA' creado con ID %d", modulo.ID) } else { log.Printf("[SEED] Módulo 'Automatización IA' ya existe (ID %d)", modulo.ID) } // 2. Definir submódulos entries := []struct{ title, desc, url string }{ {"Asistente", "Chat propio: mismo motor y tools que el bot de Telegram", "/app/asistente"}, {"Chats autorizados", "Quién puede hablarle al bot de Telegram y ejecutar acciones", "/app/agente/chats-autorizados"}, {"Plantillas de Documento", "Fuente de verdad de cotización, contrato, acta y cuenta de cobro", "/app/automatizacion/plantillas"}, {"Tarifas", "Valor por hora, licencias, VMs y márgenes usados al cotizar", "/app/automatizacion/tarifas"}, } var insertados []models.Submodules for _, e := range entries { var sub models.Submodules if err := db.Where("url = ?", e.url).First(&sub).Error; err != nil { sub = models.Submodules{ Title: e.title, Description: e.desc, Url: e.url, ModuleId: modulo.ID, ModifiedAt: time.Now(), } if err := db.Create(&sub).Error; err != nil { log.Printf("[SEED] Error creando submódulo '%s': %v", e.title, err) continue } log.Printf("[SEED] Submódulo '%s' creado (ID %d)", e.title, sub.ID) } else { if sub.ModuleId != modulo.ID { if err := db.Model(&sub).Update("module_id", modulo.ID).Error; err != nil { log.Printf("[SEED] Error actualizando module_id de '%s': %v", sub.Title, err) } else { log.Printf("[SEED] Submódulo '%s' reasignado a módulo Automatización IA", sub.Title) } } else { log.Printf("[SEED] Submódulo '%s' ya existe (ID %d)", sub.Title, sub.ID) } } insertados = append(insertados, sub) } // 3. Asignar solo al rol Administrador var rolAdmin models.Roles if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil { log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err) } else { asignarSubmodulosSiFaltan(&rolAdmin, insertados) log.Printf("[SEED] Submódulos de Automatización IA asignados al rol 'Administrador'") } log.Println("[SEED] Seed de Automatización IA completado.") } // SeedIntegraciones crea el módulo "Integraciones" con los submódulos // de Hostinger y Cloudflare, y los asigna a todos los roles existentes. // Es idempotente: si el registro ya existe (por URL) lo reutiliza sin duplicar. func SeedIntegraciones() { db := app.Http.Database.DB // 1. Crear/obtener módulo var modulo models.Modules if err := db.Where("title = ?", "Integraciones").First(&modulo).Error; err != nil { modulo = models.Modules{ Title: "Integraciones", Description: "Conexión con servicios externos: Hostinger, Cloudflare y más", ModifiedAt: time.Now(), } if err := db.Create(&modulo).Error; err != nil { log.Printf("[SEED] Error creando módulo Integraciones: %v", err) return } log.Printf("[SEED] Módulo 'Integraciones' creado con ID %d", modulo.ID) } else { log.Printf("[SEED] Módulo 'Integraciones' ya existe (ID %d)", modulo.ID) } // 2. Definir submódulos entries := []struct{ title, desc, url string }{ {"Hostinger", "Panel de VPS, dominios, hosting y DNS de Hostinger", "/app/hostinger"}, {"Cloudflare", "Gestión de zonas, DNS, SSL y firewall en Cloudflare", "/app/cloudflare"}, {"VCard API", "Integración con el sistema VCard externo (Laravel + Sanctum): usuarios, membresías, vcards, pagos y más", "/app/vcard-api"}, {"OCR", "Extracción de texto de imágenes (comprobantes, capturas) vía servicio OCR propio", "/app/ocr"}, {"Whisper ASR", "Transcripción de audio vía servicio propio de reconocimiento de voz (self-hosted)", "/app/whisper-asr"}, } var insertados []models.Submodules for _, e := range entries { var sub models.Submodules if err := db.Where("url = ?", e.url).First(&sub).Error; err != nil { sub = models.Submodules{ Title: e.title, Description: e.desc, Url: e.url, ModuleId: modulo.ID, ModifiedAt: time.Now(), } if err := db.Create(&sub).Error; err != nil { log.Printf("[SEED] Error creando submódulo '%s': %v", e.title, err) continue } log.Printf("[SEED] Submódulo '%s' creado (ID %d)", e.title, sub.ID) } else { if sub.ModuleId != modulo.ID { if err := db.Model(&sub).Update("module_id", modulo.ID).Error; err != nil { log.Printf("[SEED] Error actualizando module_id de '%s': %v", sub.Title, err) } else { log.Printf("[SEED] Submódulo '%s' reasignado a módulo Integraciones", sub.Title) } } else { log.Printf("[SEED] Submódulo '%s' ya existe (ID %d)", sub.Title, sub.ID) } } insertados = append(insertados, sub) } // 3. Asignar a todos los roles existentes // Solo se asigna al rol Administrador: los demás roles pueden estar // restringidos a propósito y no deben recibir módulos nuevos por su cuenta // — hay que habilitarlos manualmente desde /app/roles si corresponde. var rolAdmin models.Roles if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil { log.Printf("[SEED] Rol 'Administrador' no encontrado: %v", err) return } asignarSubmodulosSiFaltan(&rolAdmin, insertados) log.Println("[SEED] Seed de Integraciones completado.") } // SeedPasarelas agrega el submódulo "Pasarelas de Pago" al módulo "Integraciones" // y lo asigna a todos los roles. Es idempotente. func SeedPasarelas() { db := app.Http.Database.DB // Obtener el módulo "Integraciones" (debe existir luego de SeedIntegraciones) var modulo models.Modules if err := db.Where("title = ?", "Integraciones").First(&modulo).Error; err != nil { log.Printf("[SEED] Módulo 'Integraciones' no encontrado para SeedPasarelas: %v", err) return } entries := []struct{ title, desc, url string }{ {"Pasarelas de Pago", "Configuración de Bold, dLocal y otras pasarelas", "/app/pasarelas-pago"}, } var insertados []models.Submodules for _, e := range entries { var sub models.Submodules if err := db.Where("url = ?", e.url).First(&sub).Error; err != nil { sub = models.Submodules{ Title: e.title, Description: e.desc, Url: e.url, ModuleId: modulo.ID, ModifiedAt: time.Now(), } if err := db.Create(&sub).Error; err != nil { log.Printf("[SEED] Error creando submódulo '%s': %v", e.title, err) continue } log.Printf("[SEED] Submódulo '%s' creado (ID %d)", e.title, sub.ID) } else { log.Printf("[SEED] Submódulo '%s' ya existe (ID %d)", sub.Title, sub.ID) } insertados = append(insertados, sub) } // Solo se asigna al rol Administrador: los demás roles pueden estar // restringidos a propósito y no deben recibir módulos nuevos por su cuenta // — hay que habilitarlos manualmente desde /app/roles si corresponde. var rolAdmin models.Roles if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil { log.Printf("[SEED] Rol 'Administrador' no encontrado: %v", err) return } asignarSubmodulosSiFaltan(&rolAdmin, insertados) log.Println("[SEED] Seed de Pasarelas completado.") } // SeedServidores crea el módulo "Servidores" con los submódulos de gestión // de servidores, conexiones SSH/DB, tipos y el Query Runner. Es idempotente. func SeedServidores() { db := app.Http.Database.DB // 1. Crear/obtener módulo var modulo models.Modules if err := db.Where("title = ?", "Servidores").First(&modulo).Error; err != nil { modulo = models.Modules{ Title: "Servidores", Description: "Gestión de servidores, conexiones de base de datos y Query Runner", ModifiedAt: time.Now(), } if err := db.Create(&modulo).Error; err != nil { log.Printf("[SEED] Error creando módulo Servidores: %v", err) return } log.Printf("[SEED] Módulo 'Servidores' creado con ID %d", modulo.ID) } else { log.Printf("[SEED] Módulo 'Servidores' ya existe (ID %d)", modulo.ID) } // 2. Definir submódulos entries := []struct{ title, desc, url string }{ {"Servidores", "Lista y gestión de servidores registrados", "/app/servidor"}, {"Conexión DB", "Conexiones de base de datos a servidores", "/app/conexion_db"}, {"Conexión SSH", "Conexiones SSH a servidores", "/app/conexion_ssh"}, {"Tipo DB", "Catálogo de tipos de base de datos", "/app/tipo_db"}, {"Tipo Servidor", "Catálogo de tipos de servidor", "/app/tipo_servidor"}, {"Proveedor Servidor", "Catálogo de proveedores de servidor", "/app/prov_servidor"}, {"Query Runner", "Editor SQL con historial y exportación", "/app/query-runner"}, } var insertados []models.Submodules for _, e := range entries { var sub models.Submodules if err := db.Where("url = ?", e.url).First(&sub).Error; err != nil { sub = models.Submodules{ Title: e.title, Description: e.desc, Url: e.url, ModuleId: modulo.ID, ModifiedAt: time.Now(), } if err := db.Create(&sub).Error; err != nil { log.Printf("[SEED] Error creando submódulo '%s': %v", e.title, err) continue } log.Printf("[SEED] Submódulo '%s' creado (ID %d)", e.title, sub.ID) } else { if sub.ModuleId != modulo.ID { db.Model(&sub).Update("module_id", modulo.ID) log.Printf("[SEED] Submódulo '%s' reasignado a módulo Servidores", sub.Title) } else { log.Printf("[SEED] Submódulo '%s' ya existe (ID %d)", sub.Title, sub.ID) } } insertados = append(insertados, sub) } // 3. Asignar a todos los roles existentes // Solo se asigna al rol Administrador: los demás roles pueden estar // restringidos a propósito y no deben recibir módulos nuevos por su cuenta // — hay que habilitarlos manualmente desde /app/roles si corresponde. var rolAdmin models.Roles if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil { log.Printf("[SEED] Rol 'Administrador' no encontrado: %v", err) return } asignarSubmodulosSiFaltan(&rolAdmin, insertados) log.Println("[SEED] Seed de Servidores completado.") } // SeedAdministracion crea el módulo "Administración" con sus submódulos // (Roles, Módulos, Submódulos, Usuarios) y los asigna a todos los roles. Es idempotente. func SeedAdministracion() { db := app.Http.Database.DB var modulo models.Modules if err := db.Where("title = ?", "Administración").First(&modulo).Error; err != nil { modulo = models.Modules{ Title: "Administración", Description: "Gestión de usuarios, roles, módulos y permisos del sistema", ModifiedAt: time.Now(), } if err := db.Create(&modulo).Error; err != nil { log.Printf("[SEED] Error creando módulo Administración: %v", err) return } log.Printf("[SEED] Módulo 'Administración' creado con ID %d", modulo.ID) } else { log.Printf("[SEED] Módulo 'Administración' ya existe (ID %d)", modulo.ID) } entries := []struct{ title, desc, url string }{ {"Roles", "Gestión de roles y permisos", "/app/roles"}, {"Módulos", "Gestión de módulos del menú lateral", "/app/modules"}, {"Submódulos", "Gestión de submódulos y URLs", "/app/submodules"}, {"Usuarios", "Gestión de usuarios del sistema", "/app/users"}, } var insertados []models.Submodules for _, e := range entries { var sub models.Submodules if err := db.Where("url = ?", e.url).First(&sub).Error; err != nil { sub = models.Submodules{ Title: e.title, Description: e.desc, Url: e.url, ModuleId: modulo.ID, ModifiedAt: time.Now(), } if err := db.Create(&sub).Error; err != nil { log.Printf("[SEED] Error creando submódulo '%s': %v", e.title, err) continue } log.Printf("[SEED] Submódulo '%s' creado (ID %d)", e.title, sub.ID) } else { if sub.ModuleId != modulo.ID { db.Model(&sub).Update("module_id", modulo.ID) log.Printf("[SEED] Submódulo '%s' reasignado a módulo Administración", sub.Title) } else { log.Printf("[SEED] Submódulo '%s' ya existe (ID %d)", sub.Title, sub.ID) } } insertados = append(insertados, sub) } // Solo se asigna al rol Administrador: los demás roles pueden estar // restringidos a propósito y no deben recibir módulos nuevos por su cuenta // — hay que habilitarlos manualmente desde /app/roles si corresponde. var rolAdmin models.Roles if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil { log.Printf("[SEED] Rol 'Administrador' no encontrado: %v", err) return } asignarSubmodulosSiFaltan(&rolAdmin, insertados) log.Println("[SEED] Seed de Administración completado.") } // SeedSaas agrega los submódulos de gestión de productos SaaS e integraciones // de pago al módulo "Integraciones" ya existente. Es idempotente. func SeedSaas() { db := app.Http.Database.DB // Reutilizar el módulo "Integraciones" (creado por SeedIntegraciones) var modulo models.Modules if err := db.Where("title = ?", "Integraciones").First(&modulo).Error; err != nil { log.Printf("[SEED] Módulo 'Integraciones' no encontrado para SeedSaas: %v", err) return } entries := []struct{ title, desc, url string }{ {"Productos SaaS", "Gestión de productos SaaS y su documentación asociada", "/app/saas"}, {"Integraciones SaaS", "Callbacks de pago hacia APIs externas y logs de despacho", "/app/saas-api"}, } var insertados []models.Submodules for _, e := range entries { var sub models.Submodules if err := db.Where("url = ?", e.url).First(&sub).Error; err != nil { sub = models.Submodules{ Title: e.title, Description: e.desc, Url: e.url, ModuleId: modulo.ID, ModifiedAt: time.Now(), } if err := db.Create(&sub).Error; err != nil { log.Printf("[SEED] Error creando submódulo '%s': %v", e.title, err) continue } log.Printf("[SEED] Submódulo '%s' creado (ID %d)", e.title, sub.ID) } else { log.Printf("[SEED] Submódulo '%s' ya existe (ID %d)", sub.Title, sub.ID) } insertados = append(insertados, sub) } // Solo se asigna al rol Administrador: los demás roles pueden estar // restringidos a propósito y no deben recibir módulos nuevos por su cuenta // — hay que habilitarlos manualmente desde /app/roles si corresponde. var rolAdmin models.Roles if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil { log.Printf("[SEED] Rol 'Administrador' no encontrado: %v", err) return } asignarSubmodulosSiFaltan(&rolAdmin, insertados) log.Println("[SEED] Seed de SaaS completado.") } // MigratePortal ejecuta AutoMigrate para los modelos del portal de clientes. func MigratePortal() { db := app.Http.Database.DB if err := db.AutoMigrate( &models.Proyecto{}, &models.ProyectoFase{}, &models.ProyectoAvance{}, &models.ProyectoEntregable{}, &models.ProyectoDocumento{}, &models.ProyectoTicket{}, &models.TicketMensaje{}, &models.PortalUser{}, &models.PortalAcceso{}, &models.TelegramPortalToken{}, &models.Factura{}, ); err != nil { log.Printf("[MIGRATE] Error en MigratePortal: %v", err) } else { log.Println("[MIGRATE] MigratePortal completado.") } } // MigrateLanding crea/actualiza la tabla de sesiones del Landing Generator. func MigrateLanding() { db := app.Http.Database.DB if err := db.AutoMigrate(&models.LandingSession{}, &models.AiConfig{}); err != nil { log.Printf("[MIGRATE] Error en MigrateLanding: %v", err) } else { log.Println("[MIGRATE] Tablas landing_sessions y ai_configs OK") } } // SeedTelegram agrega el submódulo de Telegram al módulo "Integraciones". Es idempotente. func SeedTelegram() { db := app.Http.Database.DB var modulo models.Modules if err := db.Where("title = ?", "Integraciones").First(&modulo).Error; err != nil { log.Printf("[SEED] Módulo 'Integraciones' no encontrado para SeedTelegram: %v", err) return } url := "/app/telegram" var sub models.Submodules if err := db.Where("url = ?", url).First(&sub).Error; err != nil { sub = models.Submodules{ Title: "Telegram", Description: "Configuración de bots de Telegram para notificaciones", Url: url, ModuleId: modulo.ID, ModifiedAt: time.Now(), } if err2 := db.Create(&sub).Error; err2 != nil { log.Printf("[SEED] Error creando submódulo Telegram: %v", err2) return } log.Printf("[SEED] Submódulo 'Telegram' creado (ID %d)", sub.ID) } else { log.Printf("[SEED] Submódulo 'Telegram' ya existe (ID %d)", sub.ID) } // Solo se asigna al rol Administrador: los demás roles pueden estar // restringidos a propósito y no deben recibir módulos nuevos por su cuenta // — hay que habilitarlos manualmente desde /app/roles si corresponde. var rolAdmin models.Roles if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil { log.Printf("[SEED] Rol 'Administrador' no encontrado: %v", err) return } asignarSubmodulosSiFaltan(&rolAdmin, []models.Submodules{sub}) log.Println("[SEED] Seed de Telegram completado.") } // SeedPortalClientes agrega el módulo "Portal de Clientes" con sus submódulos. Es idempotente. func SeedPortalClientes() { db := app.Http.Database.DB var modulo models.Modules if err := db.Where("title = ?", "Portal de Clientes").First(&modulo).Error; err != nil { modulo = models.Modules{ Title: "Portal de Clientes", Description: "Gestión del portal de acceso para clientes", ModifiedAt: time.Now(), } if err2 := db.Create(&modulo).Error; err2 != nil { log.Printf("[SEED] Error creando módulo 'Portal de Clientes': %v", err2) return } log.Printf("[SEED] Módulo 'Portal de Clientes' creado (ID %d)", modulo.ID) } else { log.Printf("[SEED] Módulo 'Portal de Clientes' ya existe (ID %d)", modulo.ID) } entries := []struct{ title, desc, url string }{ {"Proyectos", "Gestión de proyectos y roadmap para clientes", "/app/proyectos"}, {"Facturas", "Gestión de facturas y cobros del portal", "/app/facturas"}, {"Portal Usuarios", "Gestión de usuarios del portal de clientes", "/app/portal-usuarios"}, } var insertados []models.Submodules for _, e := range entries { var sub models.Submodules if err := db.Where("url = ?", e.url).First(&sub).Error; err != nil { sub = models.Submodules{ Title: e.title, Description: e.desc, Url: e.url, ModuleId: modulo.ID, ModifiedAt: time.Now(), } if err2 := db.Create(&sub).Error; err2 != nil { log.Printf("[SEED] Error creando submódulo '%s': %v", e.title, err2) continue } log.Printf("[SEED] Submódulo '%s' creado (ID %d)", e.title, sub.ID) } else { log.Printf("[SEED] Submódulo '%s' ya existe (ID %d)", sub.Title, sub.ID) } insertados = append(insertados, sub) } // Solo se asigna al rol Administrador: los demás roles pueden estar // restringidos a propósito y no deben recibir módulos nuevos por su cuenta // — hay que habilitarlos manualmente desde /app/roles si corresponde. var rolAdmin models.Roles if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil { log.Printf("[SEED] Rol 'Administrador' no encontrado: %v", err) return } asignarSubmodulosSiFaltan(&rolAdmin, insertados) log.Println("[SEED] Seed de Portal de Clientes completado.") } // SeedNotifDefaults inserta configuraciones de notificación por defecto (idempotente). // Solo crea registros que no existen; no sobreescribe cambios del admin. func SeedNotifDefaults() { db := app.Http.Database.DB defaults := []models.NotifEventoConfig{ // Tickets {Evento: "ticket_nuevo", Destinatario: "admin", CanalEmail: true, CanalTelegram: false, CanalSistema: true, Descripcion: "Admin recibe cuando el cliente abre un ticket"}, {Evento: "ticket_respuesta_cliente", Destinatario: "admin", CanalEmail: true, CanalTelegram: false, CanalSistema: true, Descripcion: "Admin recibe cuando el cliente responde un ticket"}, {Evento: "ticket_respuesta_admin", Destinatario: "portal_user", CanalEmail: true, CanalTelegram: false, CanalSistema: true, Descripcion: "Portal user recibe cuando el admin responde"}, {Evento: "factura_subida", Destinatario: "portal_user", CanalEmail: true, CanalTelegram: false, CanalSistema: true, Descripcion: "Portal user recibe cuando se sube una factura"}, // Servidores {Evento: "servidor_caido", Destinatario: "admin", CanalEmail: false, CanalTelegram: true, CanalSistema: true, Descripcion: "Admin recibe cuando un servidor deja de reportar"}, {Evento: "servidor_vence_pronto", Destinatario: "admin", CanalEmail: true, CanalTelegram: true, CanalSistema: true, Descripcion: "Admin recibe cuando un VPS está próximo a vencer"}, {Evento: "servidor_recurso_alto", Destinatario: "admin", CanalEmail: false, CanalTelegram: true, CanalSistema: true, Descripcion: "Admin recibe cuando CPU/RAM/Disco supera el umbral"}, // Tareas {Evento: "tarea_asignada", Destinatario: "admin", CanalEmail: true, CanalTelegram: true, CanalSistema: true, Descripcion: "Notifica cuando se asigna una tarea a un usuario"}, {Evento: "tarea_estado", Destinatario: "admin", CanalEmail: false, CanalTelegram: false, CanalSistema: true, Descripcion: "Notifica cuando cambia el estado de una tarea"}, {Evento: "tarea_comentario", Destinatario: "admin", CanalEmail: false, CanalTelegram: false, CanalSistema: true, Descripcion: "Notifica cuando hay un nuevo comentario en una tarea"}, } created := 0 for _, d := range defaults { var existing models.NotifEventoConfig if err := db.Where("evento = ? AND destinatario = ?", d.Evento, d.Destinatario).First(&existing).Error; err != nil { db.Create(&d) created++ } } log.Printf("[SEED] SeedNotifDefaults: %d nuevas configuraciones creadas.", created) // Corrección puntual (una sola vez): tarea_asignada/admin se sembró originalmente // con canal_telegram=false, dejando la notificación muerta desde su creación. // Si nadie tocó el registro desde entonces, se activa por defecto. var taRow models.NotifEventoConfig if err := db.Where("evento = ? AND destinatario = ?", "tarea_asignada", "admin").First(&taRow).Error; err == nil { if !taRow.CanalTelegram && taRow.UpdatedAt.Equal(taRow.CreatedAt) { db.Model(&taRow).Update("canal_telegram", true) log.Println("[SEED] tarea_asignada/admin: canal_telegram activado (corrección de default)") } } } // SeedShield es un stub idempotente para la configuración de Shield. func SeedShield() { log.Println("[SEED] SeedShield: sin entradas por defecto definidas aún.") } // SeedPartnerRecursos agrega el submódulo "Partner Recursos" al módulo "Portal de Clientes". Es idempotente. func SeedPartnerRecursos() { db := app.Http.Database.DB var modulo models.Modules if err := db.Where("title = ?", "Portal de Clientes").First(&modulo).Error; err != nil { log.Printf("[SEED] Módulo 'Portal de Clientes' no encontrado para SeedPartnerRecursos: %v", err) return } url := "/app/partner-recursos" var sub models.Submodules if err := db.Where("url = ?", url).First(&sub).Error; err != nil { sub = models.Submodules{ Title: "Partner Recursos", Description: "Documentación y comunicados exclusivos para partners", Url: url, ModuleId: modulo.ID, ModifiedAt: time.Now(), } if err2 := db.Create(&sub).Error; err2 != nil { log.Printf("[SEED] Error creando submódulo Partner Recursos: %v", err2) return } log.Printf("[SEED] Submódulo 'Partner Recursos' creado (ID %d)", sub.ID) } else { log.Printf("[SEED] Submódulo 'Partner Recursos' ya existe (ID %d)", sub.ID) } var rolAdmin models.Roles if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil { log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err) } else { asignarSubmodulosSiFaltan(&rolAdmin, []models.Submodules{sub}) log.Printf("[SEED] Submódulo 'Partner Recursos' asignado al rol 'Administrador'") } log.Println("[SEED] Seed de Partner Recursos completado.") } // SeedContabilidadMenu crea el módulo "Contabilidad" y sus submódulos en el menú lateral. func SeedContabilidadMenu() { db := app.Http.Database.DB var modulo models.Modules result := db.Where("title = ?", "Contabilidad").First(&modulo) if result.Error != nil { modulo = models.Modules{ Title: "Contabilidad", Description: "Gestión contable: ingresos, egresos, cuentas por cobrar/pagar", ModifiedAt: time.Now(), } if err := db.Create(&modulo).Error; err != nil { log.Printf("[SEED] Error creando módulo Contabilidad: %v", err) return } log.Printf("[SEED] Módulo 'Contabilidad' creado con ID %d", modulo.ID) } else { log.Printf("[SEED] Módulo 'Contabilidad' ya existe (ID %d)", modulo.ID) } entries := []struct{ title, desc, url string }{ {"Dashboard", "Resumen mensual", "/app/contabilidad"}, {"Transacciones", "Ingresos y egresos", "/app/contabilidad/transacciones"}, {"Categorías", "Plan de cuentas", "/app/contabilidad/cuentas"}, {"Entidades", "Clientes y proveedores", "/app/contabilidad/entidades"}, {"Cuentas por cobrar", "Pendientes de cobro", "/app/contabilidad/cuentas-cobro"}, {"Cuentas por pagar", "Pendientes de pago", "/app/contabilidad/cuentas-pagar"}, } var insertados []models.Submodules for _, e := range entries { var sub models.Submodules if err := db.Where("url = ?", e.url).First(&sub).Error; err != nil { sub = models.Submodules{ Title: e.title, Description: e.desc, Url: e.url, ModuleId: modulo.ID, ModifiedAt: time.Now(), } if err := db.Create(&sub).Error; err != nil { log.Printf("[SEED] Error creando submódulo '%s': %v", e.title, err) continue } } else if sub.ModuleId != modulo.ID { db.Model(&sub).Update("module_id", modulo.ID) } insertados = append(insertados, sub) } var rolAdmin models.Roles if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil { log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err) } else { asignarSubmodulosSiFaltan(&rolAdmin, insertados) log.Printf("[SEED] Submódulos de Contabilidad asignados al rol 'Administrador'") } log.Println("[SEED] Seed de Contabilidad completado.") } // SeedWebSms crea el submódulo de WebSMS bajo "Integraciones". func SeedWebSms() { db := app.Http.Database.DB var modulo models.Modules if err := db.Where("title = ?", "Integraciones").First(&modulo).Error; err != nil { log.Println("[SEED] Módulo 'Integraciones' no encontrado, se creará") modulo = models.Modules{ Title: "Integraciones", Description: "Conexión con servicios externos", ModifiedAt: time.Now(), } db.Create(&modulo) } url := "/app/websms" var sub models.Submodules if err := db.Where("url = ?", url).First(&sub).Error; err != nil { sub = models.Submodules{ Title: "WebSMS", Description: "Notificaciones SMS vía LabsMobile", Url: url, ModuleId: modulo.ID, ModifiedAt: time.Now(), } if err := db.Create(&sub).Error; err != nil { log.Printf("[SEED] Error creando submódulo WebSMS: %v", err) return } log.Printf("[SEED] Submódulo 'WebSMS' creado") } else if sub.ModuleId != modulo.ID { db.Model(&sub).Update("module_id", modulo.ID) } var rolAdmin models.Roles if err := db.Where("name = ?", "Administrador").First(&rolAdmin).Error; err != nil { log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err) } else { asignarSubmodulosSiFaltan(&rolAdmin, []models.Submodules{sub}) log.Printf("[SEED] Submódulo 'WebSMS' asignado al rol 'Administrador'") } log.Println("[SEED] Seed de WebSMS completado.") } // SeedUrlMonitor agrega el submódulo "Monitor de URLs" al módulo "Servidores". Es idempotente. func SeedUrlMonitor() { db := app.Http.Database.DB var modulo models.Modules if err := db.Where("title = ?", "Servidores").First(&modulo).Error; err != nil { log.Println("[SEED] Módulo 'Servidores' no encontrado, se omite SeedUrlMonitor") return } url := "/app/url-monitor" var sub models.Submodules if err := db.Where("url = ?", url).First(&sub).Error; err != nil { sub = models.Submodules{ Title: "Monitor de URLs", Description: "Monitoreo de disponibilidad y latencia de sitios y APIs", Url: url, ModuleId: modulo.ID, ModifiedAt: time.Now(), } if err := db.Create(&sub).Error; err != nil { log.Printf("[SEED] Error creando submódulo 'Monitor de URLs': %v", err) return } log.Printf("[SEED] Submódulo 'Monitor de URLs' creado") } else if sub.ModuleId != modulo.ID { db.Model(&sub).Update("module_id", modulo.ID) } var rol models.Roles if err := db.Where("name = ?", "Administrador").First(&rol).Error; err != nil { log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err) } else { asignarSubmodulosSiFaltan(&rol, []models.Submodules{sub}) log.Printf("[SEED] Submódulo 'Monitor de URLs' asignado al rol 'Administrador'") } log.Println("[SEED] Seed de Monitor de URLs completado.") } // SeedTareas agrega el submódulo "Tareas" al módulo "Administración" solo para el rol Administrador. Es idempotente. func SeedTareas() { db := app.Http.Database.DB var modulo models.Modules if err := db.Where("title = ?", "Administración").First(&modulo).Error; err != nil { log.Println("[SEED] Módulo 'Administración' no encontrado, se omite SeedTareas") return } url := "/app/tareas" var sub models.Submodules if err := db.Where("url = ?", url).First(&sub).Error; err != nil { sub = models.Submodules{ Title: "Tareas", Description: "Tablero kanban de tareas internas", Url: url, ModuleId: modulo.ID, ModifiedAt: time.Now(), } if err := db.Create(&sub).Error; err != nil { log.Printf("[SEED] Error creando submódulo 'Tareas': %v", err) return } log.Printf("[SEED] Submódulo 'Tareas' creado") } else if sub.ModuleId != modulo.ID { db.Model(&sub).Update("module_id", modulo.ID) } var rol models.Roles if err := db.Where("name = ?", "Administrador").First(&rol).Error; err != nil { log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err) } else { asignarSubmodulosSiFaltan(&rol, []models.Submodules{sub}) log.Printf("[SEED] Submódulo 'Tareas' asignado al rol 'Administrador'") } log.Println("[SEED] Seed de Tareas completado.") } // SeedPagosExternos registra el submódulo del panel de administración de // servicios de pago externos (emisión de tokens de API para terceros). func SeedPagosExternos() { db := app.Http.Database.DB var modulo models.Modules if err := db.Where("title = ?", "Administración").First(&modulo).Error; err != nil { log.Println("[SEED] Módulo 'Administración' no encontrado, se omite SeedPagosExternos") return } url := "/app/pagos-externos" var sub models.Submodules if err := db.Where("url = ?", url).First(&sub).Error; err != nil { sub = models.Submodules{ Title: "Pagos externos", Description: "Tokens de API para que apps de terceros pidan cobros por Bold/dLocal/PayPal", Url: url, ModuleId: modulo.ID, ModifiedAt: time.Now(), } if err := db.Create(&sub).Error; err != nil { log.Printf("[SEED] Error creando submódulo 'Pagos externos': %v", err) return } log.Printf("[SEED] Submódulo 'Pagos externos' creado") } else if sub.ModuleId != modulo.ID { db.Model(&sub).Update("module_id", modulo.ID) } var rol models.Roles if err := db.Where("name = ?", "Administrador").First(&rol).Error; err != nil { log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err) } else { asignarSubmodulosSiFaltan(&rol, []models.Submodules{sub}) log.Printf("[SEED] Submódulo 'Pagos externos' asignado al rol 'Administrador'") } log.Println("[SEED] Seed de Pagos externos completado.") } // SeedUmind registra el submódulo del panel de administración de uMind // (tenants, base de conocimiento y conversaciones del widget embebible). func SeedUmind() { db := app.Http.Database.DB var modulo models.Modules if err := db.Where("title = ?", "Automatización IA").First(&modulo).Error; err != nil { log.Println("[SEED] Módulo 'Automatización IA' no encontrado, se omite SeedUmind") return } // El panel viejo (Alpine, /app/umind) sigue existiendo y respondiendo, pero // el menú apunta al SPA. La URL pasó por /app/umind → /orchestrator → // /studio: se busca por cualquiera de las tres y se migra el mismo // registro, en vez de dejar submódulos duplicados en el menú. url := "/studio" var sub models.Submodules if err := db.Where("url IN ?", []string{url, "/orchestrator", "/app/umind"}).First(&sub).Error; err != nil { sub = models.Submodules{ Title: "uMind Studio", Description: "Creación y gestión de agentes de IA: conocimiento, canales, herramientas y consumo", Url: url, ModuleId: modulo.ID, ModifiedAt: time.Now(), } if err := db.Create(&sub).Error; err != nil { log.Printf("[SEED] Error creando submódulo 'uMind': %v", err) return } log.Printf("[SEED] Submódulo 'uMind' creado") } else { updates := map[string]interface{}{} if sub.ModuleId != modulo.ID { updates["module_id"] = modulo.ID } if sub.Url != url { updates["url"] = url } if len(updates) > 0 { db.Model(&sub).Updates(updates) } } // Planes de uMind: límite de agentes y precios por consumo. Solo staff. var subPlanes models.Submodules if err := db.Where("url = ?", "/app/umind-planes").First(&subPlanes).Error; err != nil { subPlanes = models.Submodules{ Title: "Planes uMind", Description: "Límite de agentes y precios por consumo (IA, OCR, transcripción, mensualidad)", Url: "/app/umind-planes", ModuleId: modulo.ID, ModifiedAt: time.Now(), } if err := db.Create(&subPlanes).Error; err != nil { log.Printf("[SEED] Error creando submódulo 'Planes uMind': %v", err) } else { log.Printf("[SEED] Submódulo 'Planes uMind' creado") } } var rol models.Roles if err := db.Where("name = ?", "Administrador").First(&rol).Error; err != nil { log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err) } else { subs := []models.Submodules{sub} if subPlanes.ID > 0 { subs = append(subs, subPlanes) } asignarSubmodulosSiFaltan(&rol, subs) log.Printf("[SEED] Submódulos de uMind asignados al rol 'Administrador'") } log.Println("[SEED] Seed de uMind completado.") } // SeedApiKeys registra el submódulo del panel de administración de API Keys // (credenciales scoped para /api/v2, alternativa al ADMIN_API_KEY maestro). func SeedApiKeys() { db := app.Http.Database.DB var modulo models.Modules if err := db.Where("title = ?", "Administración").First(&modulo).Error; err != nil { log.Println("[SEED] Módulo 'Administración' no encontrado, se omite SeedApiKeys") return } url := "/app/api-keys" var sub models.Submodules if err := db.Where("url = ?", url).First(&sub).Error; err != nil { sub = models.Submodules{ Title: "API Keys", Description: "Credenciales scoped para /api/v2 (token + IP + alcance), alternativa a la llave maestra", Url: url, ModuleId: modulo.ID, ModifiedAt: time.Now(), } if err := db.Create(&sub).Error; err != nil { log.Printf("[SEED] Error creando submódulo 'API Keys': %v", err) return } log.Printf("[SEED] Submódulo 'API Keys' creado") } else if sub.ModuleId != modulo.ID { db.Model(&sub).Update("module_id", modulo.ID) } var rol models.Roles if err := db.Where("name = ?", "Administrador").First(&rol).Error; err != nil { log.Printf("[SEED] Rol 'Administrador' no encontrado, se omite asignación: %v", err) } else { asignarSubmodulosSiFaltan(&rol, []models.Submodules{sub}) log.Printf("[SEED] Submódulo 'API Keys' asignado al rol 'Administrador'") } log.Println("[SEED] Seed de API Keys completado.") } // MigrarUmindAgentes crea un agente "Principal" por cada UmindTenant que // todavía no tenga ninguno, heredando lo que antes vivía en el tenant // (site_key, config de IA, tono, mensaje de bienvenida, color) y mueve los // datos que ya tenía (documentos, chunks, tools, canales, conexiones, // mensajes) de tenant_id a agente_id. Idempotente: un tenant que ya tiene // al menos un agente se salta — así corre sola en cada arranque sin // duplicar nada, mismo criterio que los Seed* de este archivo. func MigrarUmindAgentes() { db := app.Http.Database.DB var tenants []models.UmindTenant if err := db.Find(&tenants).Error; err != nil { log.Printf("[MIGRACION] Error leyendo umind_tenants: %v", err) return } tablasConAgenteID := []string{ "umind_documentos", "umind_chunks", "umind_herramientas", "umind_canales", "umind_conexiones", "umind_mensajes", } for _, t := range tenants { var yaTieneAgente int64 if err := db.Model(&models.UmindAgente{}).Where("tenant_id = ?", t.ID).Count(&yaTieneAgente).Error; err != nil { log.Printf("[MIGRACION] Error contando agentes del tenant %d: %v", t.ID, err) continue } if yaTieneAgente > 0 { continue } // Las columnas viejas (site_key, ai_config_id, tono, // mensaje_bienvenida, color) ya no están en el struct Go UmindTenant, // pero AutoMigrate nunca las borró de la tabla — se leen directo. var viejo struct { SiteKey string AiConfigID *uint Tono string MensajeBienvenida string Color string } if err := db.Table("umind_tenants"). Select("site_key, ai_config_id, tono, mensaje_bienvenida, color"). Where("id = ?", t.ID).Scan(&viejo).Error; err != nil { log.Printf("[MIGRACION] Error leyendo datos viejos del tenant %d: %v", t.ID, err) continue } agente := models.UmindAgente{ TenantID: t.ID, Nombre: "Principal", SiteKey: viejo.SiteKey, AiConfigID: viejo.AiConfigID, Tono: viejo.Tono, MensajeBienvenida: viejo.MensajeBienvenida, Color: viejo.Color, Activo: true, } if agente.SiteKey == "" { key, err := models.GenerarSiteKey() if err != nil { log.Printf("[MIGRACION] Error generando site_key para el agente del tenant %d: %v", t.ID, err) continue } agente.SiteKey = key } if agente.Color == "" { agente.Color = "#8eb02f" } if err := db.Create(&agente).Error; err != nil { log.Printf("[MIGRACION] Error creando agente Principal del tenant %d: %v", t.ID, err) continue } for _, tabla := range tablasConAgenteID { sql := fmt.Sprintf("UPDATE %s SET agente_id = ? WHERE tenant_id = ? AND (agente_id IS NULL OR agente_id = 0)", tabla) if err := db.Exec(sql, agente.ID, t.ID).Error; err != nil { log.Printf("[MIGRACION] Error moviendo %s del tenant %d al agente %d: %v", tabla, t.ID, agente.ID, err) } } log.Printf("[MIGRACION] Tenant %d (%s): agente 'Principal' creado (id=%d), datos migrados", t.ID, t.Nombre, agente.ID) } } // LiberarColumnasHuerfanasUmind quita el NOT NULL de las columnas que dejaron // de mapearse cuando uMind pasó a multi-agente. // // GORM agrega columnas pero nunca las borra ni les cambia las restricciones. // Al sacar SiteKey de UmindTenant y renombrar TenantID→AgenteID en seis // tablas, las columnas viejas quedaron en la base CON su NOT NULL original. // El INSERT nuevo ya no las incluye, así que Postgres lo rechaza: // // null value in column "site_key" violates not-null constraint (23502) // // Sin esto no se puede crear un tenant, ni un documento, ni un chunk, ni // guardar un mensaje: es decir, uMind queda inutilizable después de // desplegar el refactor. // // No se hace DROP COLUMN a propósito: los datos viejos siguen ahí por si hay // que reconciliar algo. Solo se libera la restricción. // ColumnasHuerfanasUmind es la lista que recorre LiberarColumnasHuerfanasUmind. // Está afuera de la función para poder verificar en un test que cada nombre de // tabla coincide con el TableName() real del modelo: un typo acá haría que la // migración no encuentre la columna y no haga nada, en silencio. var ColumnasHuerfanasUmind = []struct{ Tabla, Columna string }{ {"umind_tenants", "site_key"}, {"umind_documentos", "tenant_id"}, {"umind_chunks", "tenant_id"}, {"umind_mensajes", "tenant_id"}, {"umind_herramientas", "tenant_id"}, {"umind_canales", "tenant_id"}, {"umind_conexiones", "tenant_id"}, } func LiberarColumnasHuerfanasUmind() { db := app.Http.Database.DB for _, h := range ColumnasHuerfanasUmind { // Se consulta information_schema en vez de intentar el ALTER a ciegas: // en una instalación nueva la columna no existe y el error sería ruido // en cada arranque. var nullable string err := db.Raw(` SELECT is_nullable FROM information_schema.columns WHERE table_name = ? AND column_name = ? `, h.Tabla, h.Columna).Scan(&nullable).Error if err != nil || nullable == "" || nullable == "YES" { continue } sql := fmt.Sprintf("ALTER TABLE %s ALTER COLUMN %s DROP NOT NULL", h.Tabla, h.Columna) if err := db.Exec(sql).Error; err != nil { log.Printf("[MIGRACION] no se pudo liberar %s.%s: %v", h.Tabla, h.Columna, err) continue } log.Printf("[MIGRACION] %s.%s ya no es NOT NULL (columna huérfana del refactor multi-agente)", h.Tabla, h.Columna) } } // VerificarUrlsDeSubmodulos avisa en el arranque de los ítems del menú cuya // URL no corresponde a ninguna ruta registrada. // // Un submódulo es una URL escrita a mano (en un seed o desde /app/submodules). // Si no coincide con una ruta real, el ítem igual aparece en el menú y se le // puede asignar permiso a un rol — y recién al hacer clic aparece un 404, sin // nada que explique por qué. Esto lo convierte en una línea de log al // arrancar, en vez de un misterio en producción. // // Solo informa: no borra ni modifica nada, porque una URL "rota" puede ser una // ruta servida por otro lado. func VerificarUrlsDeSubmodulos(rutasGET map[string]bool) { var subs []models.Submodules if err := app.Http.Database.DB.Find(&subs).Error; err != nil { log.Printf("[MENU] no se pudieron revisar las URLs de los submódulos: %v", err) return } rotos := 0 for _, s := range subs { url := strings.TrimSpace(s.Url) if url == "" { log.Printf("[MENU] el submódulo %q (ID %d) no tiene URL", s.Title, s.ID) rotos++ continue } if !rutasGET[url] { log.Printf("[MENU] el submódulo %q (ID %d) apunta a %q, que no es ninguna ruta: va a dar 404", s.Title, s.ID, url) rotos++ } } if rotos == 0 { log.Printf("[MENU] %d submódulos revisados, todas las URLs resuelven", len(subs)) } else { log.Printf("[MENU] %d de %d submódulos apuntan a una URL inexistente (ver líneas anteriores)", rotos, len(subs)) } } // SeedDocumentacion registra los submódulos de Documentación. // // Existían las rutas y las vistas pero nunca el seed, así que había que crear // el ítem del menú a mano desde /app/submodules — y una URL mal tipeada ahí es // indistinguible de un permiso mal asignado: en los dos casos se ve un 404. func SeedDocumentacion() { db := app.Http.Database.DB var modulo models.Modules if err := db.Where("title = ?", "Documentación").First(&modulo).Error; err != nil { modulo = models.Modules{ Title: "Documentación", Description: "Páginas y categorías de documentación interna", ModifiedAt: time.Now(), } if err := db.Create(&modulo).Error; err != nil { log.Printf("[SEED] Error creando módulo Documentación: %v", err) return } } entries := []struct{ title, desc, url string }{ {"Doc: Páginas", "Páginas de documentación", "/app/doc/paginas"}, {"Doc: Categorías", "Categorías de documentación", "/app/doc/categorias"}, } for _, e := range entries { var sub models.Submodules if err := db.Where("url = ?", e.url).First(&sub).Error; err != nil { sub = models.Submodules{ Title: e.title, Description: e.desc, Url: e.url, ModuleId: modulo.ID, ModifiedAt: time.Now(), } if err := db.Create(&sub).Error; err != nil { log.Printf("[SEED] Error creando submódulo '%s': %v", e.title, err) continue } log.Printf("[SEED] Submódulo '%s' creado (ID %d)", e.title, sub.ID) } } } // IndicesUnicosMessageID crea índices únicos parciales sobre el Message-Id de // los correos ya procesados. Es lo que hace que un mismo correo no pueda abrir // dos tickets pase lo que pase: si el buzón vuelve a entregarlo, si el flag de // leído no se guardó, o si dos instancias de la app lo leen a la vez, la base // rechaza el segundo. // // Parcial (WHERE message_id <> ”) porque los tickets del portal no tienen // Message-Id y todos comparten la cadena vacía. func IndicesUnicosMessageID() { db := app.Http.Database.DB indices := []struct{ nombre, tabla string }{ {"idx_tickets_message_id_unico", "proyecto_tickets"}, {"idx_ticket_mensajes_message_id_unico", "ticket_mensajes"}, } for _, ix := range indices { var existe bool if err := db.Raw( `SELECT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = ?)`, ix.nombre, ).Scan(&existe).Error; err != nil || existe { continue } // Si ya hay duplicados de antes, el índice no se puede crear. Se avisa y // se sigue: el chequeo previo en código igual filtra la mayoría. sql := fmt.Sprintf( `CREATE UNIQUE INDEX %s ON %s (message_id) WHERE message_id <> '' AND deleted_at IS NULL`, ix.nombre, ix.tabla) if err := db.Exec(sql).Error; err != nil { log.Printf("[MIGRATE] No se pudo crear %s (¿hay correos duplicados de antes?): %v", ix.nombre, err) continue } log.Printf("[MIGRATE] Índice %s creado.", ix.nombre) } }