El SPA pasa a llamarse uMind Studio y vive en /studio (el nombre
"Orquestador" no le decía nada al cliente final, que ahora es quien lo usa).
Diseño:
- Los colores viven una sola vez como variables CSS + clases semánticas
(.card, .input, .btn-*, .badge-*). Antes cada elemento repetía el par
claro/oscuro a mano en cientos de lugares y cambiar un tono era buscar
y reemplazar.
- darkMode pasa de 'media' a 'class' con toggle propio persistido: el
usuario elige, no el sistema operativo. Se aplica antes de montar la
app para que no parpadee.
- Componentes compartidos (UiModal, UiBadge, UiEmptyState) donde antes
había markup duplicado inline.
Nueva pantalla de consumo (/tenants/:id/uso): filtros por fecha con
atajos, total del período, pendiente de facturar, barra contra el tope
del plan, desglose por tipo y gráfico por día. El gráfico son divs con
altura porcentual — no vale traer una librería de charts para esto.
Rename:
- /studio y /portal/studio; /orchestrator redirige 301 conservando la
ruta interna, así los enlaces guardados siguen funcionando. Los assets
del bundle quedan exentos (el `base` de Vite sigue en /orchestrator/):
redirigirlos rompería el SPA.
- El submódulo sembrado migra el mismo registro buscando por las tres
URLs históricas (/app/umind → /orchestrator → /studio) en vez de
dejar entradas duplicadas en el menú.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Acá es donde uMind deja de ser una herramienta interna: el cliente entra
a /portal/studio con su sesión de portal y gestiona lo suyo.
- UmindScopePortal/UmindScopeStaff es el ÚNICO punto donde se decide el
alcance. El del cliente sale de GetClienteIDsForPortalUser, el mismo
que ya autoriza el resto del portal. nil = staff sin restricción,
slice vacío = no ve nada; una ruta sin scope también cae en "no ve
nada" para que olvidarse el middleware falle visible y no abra todo.
- Un solo set de handlers montado bajo /app/umind y /portal/umind
(RegistrarRutasUmind). Duplicarlos sería duplicar las chances de
olvidar un chequeo.
- Guarda de acceso en TODOS los handlers, incluidos los sub-recursos que
llegan por :id (documento, tool, canal, conexión): hay que cargarlos
para saber de quién son, si no un cliente podría borrar el canal de
otro adivinando el id. Responden 404, no 403: un 403 confirmaría que
el recurso existe.
- Cierra un bug preexistente: las lecturas GET /app/umind/* no tenían
SoloAdmin ni pasaban por MenuMiddleware, así que cualquier usuario de
staff podía leer los tenants de todos los clientes.
- Límite de agentes por plan (409 con mensaje claro). Un tenant sin plan
no tiene límite: cortarles de golpe sería peor que dejarlos como estaban.
- /umind/ai-configs reemplaza con alcance a /app/api/ai-config/select,
que devolvía TODAS las configs del sistema.
- El SPA deduce por la URL si es staff o cliente (base del router,
prefijo de API y URL de login) y oculta lo que es solo de staff.
- Test de aislamiento entre clientes: 7 casos, incluido que un scope
vacío no se confunda con staff.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Backend:
- UmindConexion: cuenta de correo conectada por tenant vía OAuth2
(golang.org/x/oauth2, promovida de indirecta a directa), tokens cifrados
en reposo con el mismo AES-GCM+APP_KEY que ya usan tools/canales.
- Flujo completo: /app/umind/conexiones/conectar redirige a Google/Microsoft,
/callback/:proveedor intercambia el code (state autoverificable por HMAC,
sin tabla de estados pendientes), refresh on-demand antes de cada uso.
- Dos tools nuevas para el agente (enviar_correo/leer_bandeja) que aparecen
solo si el tenant tiene una conexión activa, vía Gmail API / Microsoft
Graph directo (sin el SDK pesado de Google).
- Requiere que el dueño del proyecto cree las apps OAuth en Google Cloud
Console / Azure y cargue GOOGLE_OAUTH_CLIENT_ID/SECRET y
MS_OAUTH_CLIENT_ID/SECRET — sin eso los botones de conectar fallan con un
mensaje claro, no en silencio.
Frontend: rediseño del orquestador — layout de sidebar fijo (reemplaza el
navbar + lista de página completa), modo oscuro vía prefers-color-scheme,
tabs en pill, y la nueva tab "Conexiones".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SPA nueva en /orchestrator (Vue 3 + Vite, servida por el mismo binario Go
bajo /orchestrator para que la cookie de sesión funcione sin tocar CORS),
reemplaza al panel Alpine.js como punto de entrada del menú.
Backend, todo aditivo sobre el motor de uMind ya existente:
- UmindHerramienta: tools custom por tenant que llaman un webhook HTTP,
integradas al loop de function-calling existente. Cliente HTTP con
guardas SSRF (bloqueo de IPs privadas/loopback/link-local resuelto en el
momento de conectar, no antes, para cerrar la ventana de DNS rebinding)
que no existían en el proyecto.
- UmindCanal: Telegram y WhatsApp Business Cloud API como canales
adicionales del mismo agente que ya atiende el widget web, ambos
reusando ProcessWidgetMessage. WhatsApp valida X-Hub-Signature-256.
Credenciales cifradas en reposo con el mismo AES-GCM+APP_KEY que ya usa
el proyecto para la contraseña SMTP (primer uso para secretos de uMind).
- Se conecta middlewares.Limit() (rate limiter que existía pero no se
usaba en ningún lado) al widget público y a los webhooks nuevos.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>