From c69b904b033df83f16bb756fac6a1ff94c483ce8 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Thu, 13 Aug 2026 11:32:38 -0500 Subject: [PATCH] feat(umind): vincula tenants a clientes y agrega planes con precios MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Base para que el cliente administre uMind desde su portal y para el cobro por consumo. Todavía no cambia nada del comportamiento actual. - UmindTenant gana ClienteID (punteros, sin not null: los tenants creados antes del portal quedan sin asignar y el ALTER TABLE no falla sobre datos existentes) y PlanID. - GetUmindTenantsByClientes es fail-closed: sin clientes, no ve nada. - UmindPlan define el máximo de agentes y los precios por 1k tokens, por imagen OCR, por transcripción y la mensualidad, más un tope de consumo que solo avisa. CRUD para staff en /app/umind-planes. - El modelo va en AMBAS listas de AutoMigrate (main.go y migrations), no repetir el error de agregarlo solo en una. Co-Authored-By: Claude Sonnet 5 --- main.go | 1 + migrations/migrate.go | 26 ++- orchestrator/src/components/Sidebar.vue | 53 +++++- pkg/models/umind.go | 18 ++ pkg/models/umind_plan.go | 66 +++++++ public/orchestrator/assets/index-BfxLbsxs.js | 26 +++ public/orchestrator/assets/index-C6K_wyko.css | 1 + public/orchestrator/assets/index-D1KpwOlP.js | 26 --- public/orchestrator/assets/index-a_0kTstc.css | 1 - public/orchestrator/index.html | 4 +- resources/views/umind_planes.html | 177 ++++++++++++++++++ rest/controllers/umind_admin_controller.go | 6 + rest/controllers/umind_plan_controller.go | 115 ++++++++++++ rest/routes/user.go | 6 + 14 files changed, 493 insertions(+), 33 deletions(-) create mode 100644 pkg/models/umind_plan.go create mode 100644 public/orchestrator/assets/index-BfxLbsxs.js create mode 100644 public/orchestrator/assets/index-C6K_wyko.css delete mode 100644 public/orchestrator/assets/index-D1KpwOlP.js delete mode 100644 public/orchestrator/assets/index-a_0kTstc.css create mode 100644 resources/views/umind_planes.html create mode 100644 rest/controllers/umind_plan_controller.go diff --git a/main.go b/main.go index 11b4c05..fdf5c45 100755 --- a/main.go +++ b/main.go @@ -156,6 +156,7 @@ func main() { &models.UmindCanal{}, &models.UmindConexion{}, &models.UmindEventoLog{}, + &models.UmindPlan{}, // API Keys de /api/v2 (token + IP obligatoria + scopes) &models.ApiKey{}, // Integraciones: OCR y transcripción de audio (servicios propios) diff --git a/migrations/migrate.go b/migrations/migrate.go index a23132d..8915e3b 100755 --- a/migrations/migrate.go +++ b/migrations/migrate.go @@ -127,6 +127,7 @@ func Migrate() { &models.UmindCanal{}, &models.UmindConexion{}, &models.UmindEventoLog{}, + &models.UmindPlan{}, // API Keys de /api/v2 (token + IP obligatoria + scopes) &models.ApiKey{}, // Integraciones: OCR y transcripción de audio (servicios propios) @@ -1326,12 +1327,33 @@ func SeedUmind() { 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 { - asignarSubmodulosSiFaltan(&rol, []models.Submodules{sub}) - log.Printf("[SEED] Submódulo 'uMind' asignado al rol 'Administrador'") + 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.") } diff --git a/orchestrator/src/components/Sidebar.vue b/orchestrator/src/components/Sidebar.vue index afd6898..4a6c233 100644 --- a/orchestrator/src/components/Sidebar.vue +++ b/orchestrator/src/components/Sidebar.vue @@ -7,6 +7,8 @@ const route = useRoute() const router = useRouter() const tenants = ref([]) +const clientes = ref([]) +const planes = ref([]) const loading = ref(true) const error = ref('') const showForm = ref(false) @@ -18,7 +20,7 @@ const form = ref(vacio()) const tenantActivoId = computed(() => route.params.tenantId || route.params.id) function vacio() { - return { nombre: '', dominios_permitidos: '', activo: true } + return { nombre: '', dominios_permitidos: '', activo: true, cliente_id: null, plan_id: null } } async function cargar() { @@ -34,6 +36,22 @@ async function cargar() { } } +// Clientes y planes solo los necesita el staff para asignarlos; si el endpoint +// no está disponible (portal del cliente) el formulario sigue funcionando. +async function cargarAsignables() { + try { + const [c, p] = await Promise.all([ + api.get('/app/api/clientes/select'), + api.get('/app/umind-planes/list'), + ]) + clientes.value = c.registros || c.items || [] + planes.value = p.items || [] + } catch { + clientes.value = [] + planes.value = [] + } +} + function nuevoTenant() { editing.value = null form.value = vacio() @@ -46,6 +64,8 @@ function editarTenant(t) { nombre: t.nombre, dominios_permitidos: t.dominios_permitidos, activo: t.activo, + cliente_id: t.cliente_id ?? null, + plan_id: t.plan_id ?? null, } showForm.value = true } @@ -82,7 +102,10 @@ async function eliminar(t) { } defineExpose({ recargar: cargar }) -onMounted(cargar) +onMounted(() => { + cargar() + cargarAsignables() +})