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>
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 <noreply@anthropic.com>
Cada canal (Telegram/WhatsApp) de un agente ahora puede activar, de forma
independiente, que los audios entrantes se transcriban con el Whisper ASR
propio y que a las imágenes entrantes se les extraiga texto con el
servicio OCR propio, antes de pasarle el mensaje al agente. Antes esos
mensajes se ignoraban en silencio.
- UmindCanal gana usar_whisper_audio/usar_ocr_imagenes (default off).
- WhatsApp: se descarga el media vía Graph API (resolución de URL + fetch
con el mismo access_token del canal) y se enruta a Whisper/OCR según type.
- Telegram: se descarga el archivo vía getFile + CDN de archivos del bot,
mismo enrutamiento para voice/audio/photo.
- Panel: checkboxes en el alta de canal y toggles inline por canal ya
creado, en la tab Canales del agente.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Nueva tabla umind_eventos_log + tab "Auditoría" en el panel: errores y
eventos que hasta ahora solo se podían ver pidiendo los logs del servidor
(fallos al llamar al AI, respuestas sin texto usable, tools/webhooks que
fallan, tokens de correo que no se pueden refrescar, firmas de WhatsApp
inválidas, errores procesando mensajes de Telegram/WhatsApp) quedan
registrados por agente, visibles directo en el panel con el JSON crudo
expandible para diagnosticar sin acceso al servidor.
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>
El bubble era un emoji 💬 sobre un color fijo hardcodeado en el JS. Ahora:
- Ícono nuevo (spark/sparkle, el motivo visual estándar de "asistente IA"
hoy) en vez del emoji, con anillo de pulso al cargar para llamar la
atención sin ser invasivo.
- Color configurable por tenant (UmindTenant.Color, validado como hex en el
server) — se aplica en tiempo real vía /widget/:site_key/init, así que
cambiarlo desde el panel no requiere recopiar el <script>.
- Panel con animación de entrada, botón de cerrar explícito, subtítulo
"Asistente con IA · uMind" y badge de marca — antes solo tenía el nombre
del tenant y ningún indicio de qué lo potencia.
- El widget ahora se inicializa al cargar el script (no al primer click), así
el color/nombre reales ya están listos antes de que el visitante abra el chat.
Selector de color agregado al form de tenant en el orquestador (input
type=color + hex).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
El widget web no aparecía en ningún lado como "canal" (esa tab solo
mostraba Telegram/WhatsApp), así que no había forma de encontrar el
<script> a pegar en el sitio sin que se lo pidieran a soporte. Se agrega
una card fija "Web (widget)" arriba de la lista de canales, con el snippet
ya armado con el site_key real del tenant y un botón de copiar.
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>
El único link a /tenants/:id era el nombre del tenant en texto plano sin
ningún indicio visual de que fuera clickeable — después de crear un tenant
no había forma obvia de llegar a configurarlo (base de conocimiento, tools,
canales). Ahora crear un tenant navega directo a su detalle, y toda la fila
de la lista es clickeable con un "Configurar →" explícito.
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>
- GetSchemaRelations: trae todas las llaves foráneas de la base con
consulta específica por motor (Postgres/MySQL/MSSQL/SQLite). No
aplica a Redis/Mongo (schemaless).
- Endpoint GET /query-runner/relations, con el mismo chequeo de
autorización por conexión que ya usa RunQuery — de paso se lo
agrego también a GetDatabases/GetTables, que no lo tenían.
- UI: botón "Ver relaciones" (diagrama completo) y un ícono por tabla
en el árbol lateral (relaciones solo de esa tabla), renderizado con
Mermaid.js servido localmente (public/js/mermaid.min.js, sin CDN).