Filtro (opcional, apagado por defecto): antes de abrir ticket, la IA lee el
correo y decide si es soporte o ruido —newsletters, notificaciones de
plataformas, facturas de proveedores, spam—. Falla abierto: si la IA no está
configurada o se cae, el ticket se abre igual. Ignorar a un cliente es peor que
tener un ticket de más. Hay un campo de contexto del negocio para los casos
raros de cada uno.
Los correos masivos se descartan por sus propios encabezados (List-Unsubscribe,
Precedence, Auto-Submitted) sin gastar una llamada de IA.
Y el motivo por el que el mismo correo se leía y se contestaba de nuevo: la
deduplicación era solo un SELECT previo, que no sirve si el flag de leído no
llegó a guardarse o si dos instancias leen el buzón a la vez. Ahora hay un
índice único parcial sobre message_id: la base rechaza el segundo, y ese rechazo
se trata como "ya estaba", no como error — sin ticket repetido y sin segunda
auto-respuesta.
Si algún correo no se pudo marcar como leído, "Revisar buzón ahora" lo dice en
vez de dejarlo solo en el log: es la señal de que van a volver a leerse.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Auditoría del sistema de permisos a partir del 404 en /app/doc/paginas.
1. La comparación de permisos usaba solo el último segmento de la URL
(lo que va después del último '/'). Rompía en las dos direcciones: con
permiso sobre /app/doc/categorias se entraba a cualquier otra ruta
terminada en "categorias", y a la vez el permiso parecía no aplicarse
donde sí correspondía porque el segmento coincidía por casualidad.
Ahora se compara la ruta completa, cortando en el separador para que
/app/doc/paginas no habilite /app/doc/paginas-privadas.
2. El submódulo de Documentación nunca se sembró, aunque las rutas y las
vistas existen desde mayo. Había que crear el ítem del menú a mano, y
una URL mal tipeada ahí se ve exactamente igual que un permiso mal
asignado: un 404. Ahora se siembra con la URL correcta.
3. El submódulo "Statuspage" apuntaba a /app/statuspage, que no existe en
ninguna parte — la página real es la pública /status. Se corrige el
seed y también el registro ya creado en la base.
4. Chequeo en el arranque que recorre los submódulos de la base y avisa
cuáles apuntan a una URL sin ruta. Cubre los creados a mano, que es
justo donde el compilador y los tests no llegan.
5. Test que cruza las URLs sembradas contra las rutas registradas: fue el
que encontró lo de statuspage.
Queda pendiente y es más grande: de 454 rutas protegidas solo 54 pasan por
MenuMiddleware. El permiso gatea la página HTML pero no los endpoints de
datos — cualquier usuario autenticado puede llamar a
GET /app/doc/loadpaginas o DELETE /app/doc/paginas/:id sin tener el
submódulo asignado. Se reporta antes de tocarlo porque cerrarlo de golpe
puede dejar gente afuera.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Error en producción al crear un tenant:
null value in column "site_key" of relation "umind_tenants"
violates not-null constraint (SQLSTATE 23502)
Es un bug que introduje yo. Al pasar uMind a multi-agente saqué SiteKey de
UmindTenant y renombré TenantID→AgenteID en seis tablas. GORM agrega
columnas pero nunca las borra ni les cambia las restricciones, así que las
viejas quedaron en la base CON su NOT NULL original — y el INSERT nuevo ya
no las incluye.
Al mirarlo, el alcance era mayor que el error reportado: no es solo
site_key. Las seis tablas renombradas tienen su tenant_id huérfano también
NOT NULL, así que fallaba insertar documentos, chunks, mensajes, tools,
canales y conexiones. En la práctica uMind quedaba inutilizable después de
desplegar el refactor: ni crear un tenant, ni ingestar conocimiento, ni
guardar un mensaje del chat.
La migración corre en cada arranque, antes de MigrarUmindAgentes, y
consulta information_schema para no intentar el ALTER a ciegas en una
instalación nueva donde la columna no existe.
No se hace DROP COLUMN a propósito: los datos viejos quedan por si hay que
reconciliar algo. Solo se libera la restricción.
El test compara los nombres de tabla contra el TableName() real de cada
modelo. Un typo ahí haría que la migración no encuentre la columna y siga
de largo: el bug seguiría vivo y el arranque se vería sano.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Es la base del cobro por uso: hasta ahora no había ninguna medición de
consumo en todo el repo.
- UmindUso registra cada evento facturable con el costo YA calculado al
precio vigente del plan. Congelarlo evita que subir un precio revalúe
consumo pasado, que haría indefendible una factura ante un reclamo.
- callAI devuelve los tokens que reportó el proveedor (campo usage, igual
en todos los OpenAI-compatibles; input+output en Anthropic). Se mide
cada ronda de tool-calling, no solo la última: todas gastan tokens.
- ExtraerTextoOCR y TranscribirAudioSelfHosted reciben agenteID; 0 = no
medir, que es lo que pasan los botones "Probar" del panel de staff.
- Aviso al superar el tope del plan, una vez por mes y sin cortar el
servicio. El flag de "ya avisé" es en memoria a propósito.
- GET /app/umind/uso con filtros de fecha: resumen por tipo + detalle.
- Test del cálculo de costo por tipo, incluida fracción de 1k tokens y
tenant sin plan.
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>
Nuevos submódulos en Integraciones para conectar el servicio propio de
OCR (extracción de texto de imágenes) y el de transcripción de audio
self-hosted (whisper-asr-webservice, Basic Auth), con panel de
configuración y prueba en vivo, siguiendo el patrón ya usado por
WebSMS/Hostinger.
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>
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>
Alternativa al ADMIN_API_KEY único de entorno, que sigue funcionando
como llave maestra para no romper integraciones existentes.
- Modelo ApiKey: token hasheado, IP/CIDR obligatoria (fail-closed sin
IP), scopes habilitados, último uso (fecha + IP).
- AdminApiAuth() acepta ahora tanto la llave maestra como una ApiKey;
nuevo middleware RequireScope(scope) para gatear grupos de rutas.
- Fase 1: scopes aplicados a lo más sensible — oss (archivos),
query_runner (SQL arbitrario), usuarios (usuarios/roles/módulos),
pasarelas (credenciales de pago). El resto de /api/v2 sigue con
la llave maestra hasta una fase 2.
- Panel /app/api-keys: crear/editar/revocar, token visible solo al
crear, scopes por checkbox, IP obligatoria.
Multi-tenant dentro de soft_usite, reutilizando la infraestructura ya
existente (AiConfig, motor de function-calling del agente de Telegram)
en vez de un servicio nuevo aparte:
- UmindTenant: sitio/cliente con dominios permitidos, config de IA para
el chat y personalidad/tono.
- Ingesta: crawler simple (mismo dominio, N páginas) + chunking +
embeddings (config global con módulo "umind_embeddings", pensada
para OpenAI ya que Claude no ofrece embeddings) guardados como JSON,
con búsqueda por similitud coseno en memoria (sin pgvector todavía).
- Agente acotado: única herramienta buscar_conocimiento, sin acceso a
nada interno — si no encuentra la respuesta, lo dice en vez de
inventar.
- Widget público (/widget/umind.js + /widget/:site_key/*), autenticado
por site_key + validación de dominio (Origin/Referer), no por
secreto, ya que la key viaja en el HTML público del sitio instalado.
- Panel /app/umind: tenants, estado de ingesta, historial de
conversaciones por sesión.
- AutoMigrate migraba ~50 modelos en un solo llamado que se detiene en
el primer error, dejando todo lo posterior (plantillas_documento,
tarifas, documentos_generados, arquitecturas, telegram_staff_tokens)
sin tabla, en silencio. Ahora se migra modelo por modelo: uno roto
ya no bloquea al resto.
- /app/contabilidad/entidades pedía datos a la misma URL de la página
(devuelve HTML) en vez de /list (JSON) — el listado nunca traía nada.
- Mensaje de bienvenida del asistente: estaba generado por la IA cada
vez con calidad variable; ahora es determinístico para /ayuda y
/start, con redacción revisada, y el system prompt pide el mismo
formato cuando el usuario solo saluda.
Permite emitir credenciales (token hasheado + IP/CIDR opcional) desde
/app/pagos-externos para que aplicaciones de terceros pidan cobros a
través de Bold/dLocal/PayPal sin acceso a nada más del sistema:
- POST /api/v1/pagos-externos/solicitar genera el link de cobro real
usando solo las pasarelas habilitadas para ese servicio.
- Los webhooks existentes de Bold/dLocal/PayPal (firma obligatoria,
idempotentes) ahora también resuelven referencias "extpay-…" sin
tocar el flujo de contratos ("contrato-{id}").
- Al confirmarse el pago se notifica por webhook firmado (HMAC) y/o
Telegram, configurable por servicio.
- CRUD de servicios protegido con SoloAdmin; token y callback_secret
solo se muestran una vez, en DB se guardan hasheados.
- Los seeds que creaban módulos nuevos asignaban esos submódulos a TODOS los
roles en cada arranque (idempotente contra re-agregar lo ya quitado, pero
igual tocaba roles personalizados/restringidos por primera vez apenas
existían). Ahora solo se asignan automáticamente al rol "Administrador";
cualquier rol restringido que crees para un empleado ya no recibe módulos
nuevos sin que tú se los habilites a propósito desde /app/roles.
- Se revierte el bloqueo "solo administrador" que había puesto en
/query-runner/run y /run-batch: bloqueaba también a usuarios con el
submódulo Query Runner correctamente asignado a su rol. En su lugar se
agrega la validación real que faltaba — RunQuery/RunBatchQuery no
verificaban que el conx_db_id recibido perteneciera al rol del usuario
(solo el listado de conexiones del selector estaba filtrado); ahora si no
es admin, se verifica contra Role.ConxDBs antes de ejecutar cualquier SQL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Seguridad (crítico):
- Los webhooks de Bold y dLocal solo validaban la firma si el atacante la
enviaba: sin cabecera se aceptaba cualquier payload. Ahora es obligatoria.
- GET /pago-exitoso marcaba contratos como pagados leyendo un query param del
navegador. Ahora solo muestra estado; la confirmación la hace la verificación
contra la API de la pasarela o el webhook firmado.
- /uploads se servía como estático público: se descargaban RUTs, facturas y
entregables sabiendo la ruta. Ahora exige sesión.
- Los secretos JWT no se podían sobreescribir por entorno (faltaba el tag env:)
y su valor estaba en el repo, permitiendo firmarse una sesión de admin. Ahora
son configurables y el arranque se detiene si siguen con el valor publicado.
- .env y session.db salen del control de versiones.
- Query Runner, gestión de usuarios/roles/módulos y seeds quedan restringidos a
administradores; antes bastaba con tener sesión.
Pasarelas de pago:
- dLocal generaba enlaces que nunca se reconciliaban: mandaba el ID numérico en
vez de "contrato-N", la URL de retorno apuntaba a la API de dLocal y nunca se
enviaba notification_url, así que su webhook jamás se disparaba.
- PayPal solo tenía pantalla de configuración. Se implementa el servicio
completo (OAuth, orden, captura, verificación de webhook) y queda
seleccionable como pasarela.
- La moneda estaba fija en COP: un contrato en USD generaba un cobro por esa
cifra en pesos.
Contratos:
- pago_confirmado nunca volvía a false, así que el segundo ciclo de renovación
no se cobraba aunque el cliente pagara. Se reinicia al generar enlace nuevo.
- Los contratos vencidos nunca cambiaban de estado y recibían correo a diario
de forma indefinida; ahora se cierran tras 30 días de gracia.
Otros:
- Coolify: coolifyCall ignoraba el status HTTP y reportaba errores como éxito.
El agente pasa de 10 a cobertura completa (servicios, bases de datos,
variables de entorno, proyectos, equipos y recursos de servidor).
- SeedBalanceData ya no corre en cada arranque (recreaba transacciones
borradas); ahora se invoca con SEED_BALANCE=1.
- Los seeds dejan de devolver permisos revocados en cada despliegue.
- Timeouts en las llamadas HTTP a Telegram y dLocal que podían colgarse.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Antes la whitelist de telegram_agent_auth solo se podía tocar con curl/Postman
contra /api/v2/agent/auth. Se agrega /app/agente/chats-autorizados con un
CRUD simple, más un botón "buscar mensajes recientes" (UpdatesRecientesDelBot,
vía getUpdates) para descubrir el chat_id de alguien que le acaba de escribir
al bot sin tener que pedírselo por otro medio. Entrada nueva en el menú lateral
del módulo Automatización IA.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- soporte: el webhook de correo entrante era público sin ninguna validación;
ahora exige una API key (query ?key= o header) comparada en tiempo constante.
Además evita tickets duplicados por reintentos del proveedor (dedup por
Message-Id) y enhebra respuestas del mismo remitente en vez de abrir un
ticket nuevo por cada correo.
- contabilidad: marcar una cuenta por cobrar/pagar como pagada ahora crea y
vincula la Transaccion correspondiente (antes el dashboard de ingresos/
egresos nunca reflejaba esos pagos). Se corrige además que actualizar una
cuenta por cobrar borraba su transaccion_id en cada PUT.
- tareas: se activa por defecto el canal Telegram para tarea_asignada (estaba
apagado desde el seed original) y se agrega un flujo real de vinculación de
Telegram para el staff interno (código temporal + verificación), igual al
que ya existía para los usuarios del portal — sin esto el chat_id de cada
usuario había que pegarlo a mano y la notificación nunca llegaba.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implementa las 4 fases de la especificación de automatización: módulo de
plantillas/tarifas editable por el equipo, generación de PDF (HTML+JS vía
Chrome headless) para cotizaciones/contratos/arquitecturas/cuentas de cobro,
chat propio en el dashboard reutilizando el mismo motor y tools del bot de
Telegram, y nuevas tools del agente para crear estos documentos end-to-end.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- SeedNotifDefaults: poblar 10 eventos por defecto (tickets, servidores, tareas)
- notif_dispatch: NotificarTareaAsignada/Estado/Comentario ahora respetan
canal_email, canal_telegram y canal_sistema de la config de notificaciones
- mail_service: añadir SendTareaAsignadaEmail y SendTareaNotifAdmin
- notif_config.html: agregar eventos tarea_asignada, tarea_estado, tarea_comentario
al panel de configuración para que el admin pueda activar/desactivar canales
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Modelos Tarea y TareaComentario con GORM, migración automática
- Estados: por_hacer → en_progreso → revisión → hecho; prioridades: baja/media/alta/urgente
- Drag & drop entre columnas via SortableJS (PUT /tarea/:id/estado)
- CRUD completo: crear, editar, eliminar; asignación a usuario + fecha límite
- Panel lateral de detalle: historial de comentarios, adjuntar archivos (uploads/tareas/:id)
- Telegram: notifica asignación, cambio de estado y comentarios nuevos via sendTelegramAdmin
- Seed automático bajo módulo "Administración", solo rol Administrador
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SeedUrlMonitor() agrega /app/url-monitor al módulo Servidores idempotentemente
- Se llama en el arranque junto al resto de seeds
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Modelos UrlMonitor y UrlMonitorLog con purga automática a 7 días
- Cron cada minuto ejecuta chequeos HEAD→GET con fallback, alerta Telegram en cambio de estado
- CRUD completo + chequeo manual + historial de logs
- Migración automática de tablas
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New table servidor_metricas_history (cpu%, ram%, swap%, disco%, tcp, load1)
- Each heartbeat inserts a compact row (~60 bytes)
- GET /app/servidor/:id/metricas-history?horas=24 returns the data
- Daily cron at 3AM purges rows older than 7 days
- ~1.2 MB per server per 7 days at 30s interval
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Implemented VCard API controller with methods to manage configuration (save, get).
- Added proxy functions to handle API requests for users, vcards, memberships, payments, transactions, logs, and miniwebs.
- Included error handling and response formatting for API interactions.