En /app/contabilidad/cuentas-cobro la petición respondía bien pero la tabla
quedaba en blanco, sin nada que dijera qué había pasado.
Probando la vista real con axios simulado quedó claro que el problema no
era el renderizado: con datos pinta bien, y que falle /clientes/select
tampoco la rompe. Lo que faltaba era distinguir el caso vacío.
Dos arreglos, uno en cada punta:
- Las 8 consultas de contabilidad declaraban `var items []T`, que sin filas
se serializa como null y no como []. La vista hacía x-for sobre null y no
pintaba nada; peor, con null se colaba además una fila basura.
- Ninguna de las 5 tablas tenía estado vacío. Ahora dicen "No hay ...
registradas", así que cero filas se lee como cero filas.
El x-for además queda blindado con (items || []) para no depender de que
todos los endpoints devuelvan siempre una lista.
Verificado con la vista real en tres escenarios: con datos pinta la fila,
vacío y null muestran el mensaje y ninguna fila de más.
Si la pantalla sigue sin traer registros después de esto, ya no es la
vista: es que la consulta no encuentra filas, y el mensaje lo va a decir.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Con ~40 submódulos repartidos en una docena de módulos, encontrar una
pantalla era ir abriendo y cerrando secciones.
El buscador filtra por título de submódulo y también por nombre de módulo
(quien escribe "integraciones" espera ver todo lo que cuelga de ahí). La
comparación normaliza acentos y mayúsculas: nadie escribe "Documentación"
con tilde cuando busca. Los módulos sin coincidencias se ocultan, Enter
entra al primer resultado y Esc limpia.
Dos decisiones que salieron de verlo fallar:
- La lógica va en un <script> y no dentro de x-data="{...}". El atributo se
corta con la primera comilla doble, así que un comentario con comillas
rompía la barra entera de forma silenciosa.
- El filtrado es una pasada imperativa sobre el DOM en vez de una condición
por elemento. Las condiciones dependían de una lista armada al iniciar y
no se re-evaluaban al escribir: el menú se filtraba a medias. Recorrer
unas decenas de nodos por tecleo no se nota y no deja lugar a esas
sutilezas.
Tampoco se interpola el título dentro de las expresiones: va en data-, que
Go sí escapa bien. Verificado con títulos que traen comillas y ampersand,
que con la versión anterior rompían el binding.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
La integración VCard vivía solo en /api/v1, que autentica por cookie de
sesión: obliga a manejar cookie jar, no permite allowlist por IP y no se
puede revocar sin tocar la contraseña del usuario (ver
docs/api-v1-contrato.md, punto 7).
Ahora los mismos controladores están también bajo /api/v2/vcard/* con
Bearer + IP + scope, que es lo que administra la pantalla /app/api-keys.
El scope "vcard" acota la llave a estos 12 endpoints: sin él, esa
integración tendría acceso a los otros ~300 de v2.
/api/v1 se mantiene intacto — esto es un camino nuevo, no un reemplazo
forzado, así que lo que ya está instalado sigue andando mientras migran.
Se agregan dos chequeos porque el compilador no ve ninguno de los dos
errores: que las 12 rutas queden registradas con su método y path (un
typo se descubriría recién con un 404 del lado del integrador), y que
todas figuren en el spec de /api/v2, que se mantiene a mano y se
desincroniza en silencio.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cuatro cosas que ya estaban a medio construir y no se usaban como canal.
1. El badge del widget ahora es un enlace con UTM. Cada cliente ya te
estaba dando exposición en su sitio y no se capitalizaba. El host sale
de la URL del propio script, así funciona igual en cualquier entorno.
2. Reporte del mes por agente en Excel: conversaciones atendidas, con qué
las abrió el visitante, y el consumo desglosado. Es lo que el cliente
necesita para justificar el gasto puertas adentro — un panel al que hay
que entrar no sirve para eso, un archivo que se reenvía sí. Reusa el
generador de xlsx del cronograma.
3. Aviso automático de agentes sin base de conocimiento (cron diario). Un
agente sin fuentes responde de memoria e inventa datos, el cliente
concluye que el producto no sirve y se va. Es el punto donde más gente
se cae y se detecta solo. Se avisa UNA vez por agente, usando el log de
auditoría como registro de envío para no necesitar tabla nueva ni
convertir el recordatorio en spam.
4. Tarjeta de uMind en el dashboard del portal: atajo si el cliente ya lo
tiene, oferta si no. Es el punto de contacto más barato que hay — ya
entró, ya confía, y el cobro se suma a la factura que ya recibe. El
mensaje lidera con notas de voz y fotos, que es lo más difícil de
copiar de lo que tenemos.
La consulta de conversaciones agrupa en SQL en vez de traerse el historial
entero para agrupar en Go.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Auditoría de /portal/dashboard. Cuatro problemas, todos en el mismo bucle:
1. El avance se veía un render atrasado. ActualizarProgresoProyecto corría
DESPUÉS de cargar los proyectos: escribía el valor nuevo en la base pero
los structs ya cargados seguían con el viejo, que es lo que se renderiza.
El usuario veía el cálculo de la visita anterior.
2. N+1 con escrituras en un GET: dos COUNT y un UPDATE por proyecto. Con 10
proyectos, 30 consultas y 10 escrituras por cada carga del dashboard —
incluyendo las de cualquier bot que pase. Ahora es UNA consulta agrupada
y ninguna escritura; los caminos que tocan una fase ya mantienen la
columna al día, así que recalcular en el GET no aportaba nada.
3. Orden aleatorio de los grupos: se recorría un map de Go, así que un
partner veía sus clientes en distinto orden en cada recarga.
4. isPartner se decidía con u.Rol mientras el alcance se decidía con
Role.EsPortalPartner. Desincronizados, un usuario veía proyectos de
varios clientes sin agrupar, o la vista agrupada vacía. Ahora hay una
sola definición (PortalUser.EsPartner) con test de los dos sentidos.
Además, el error de carga se descartaba con `_` y el usuario terminaba
viendo "no tenés proyectos", indistinguible de una caída de la base.
Sin hallazgos de seguridad: el chequeo de acceso por cliente está en todas
las rutas, la sesión revalida Activo en cada request, y las plantillas no
usan x-html ni template.HTML, así que Go escapa todo.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
El cliente ya veía el roadmap en pantalla pero no tenía cómo pasárselo a
alguien que no entra al portal.
El .xlsx se genera con archive/zip + encoding/xml de la stdlib en vez de
sumar una dependencia de Excel para una sola pantalla. Un CSV renombrado
no servía: en Excel en español el separador y los acentos se rompen, y
esto es un archivo que el cliente le entrega a un tercero.
Detalles que hacen que Excel lo acepte y se lea bien: inline strings (sin
tabla sharedStrings, así ningún índice puede apuntar a la cadena
equivocada), encabezado en negrita y congelado, anchos de columna según
el contenido, y nombre de hoja saneado (Excel rechaza el archivo si pasa
de 31 caracteres o trae : \ / ? * [ ]).
El endpoint repite el chequeo de acceso de PortalProyecto: sin eso,
cualquiera con sesión de portal se bajaba el cronograma de otro cliente
adivinando el slug.
Verificado abriendo el archivo generado con openpyxl: 6 partes, CRC OK,
XML bien formado en todas, acentos y CJK intactos, negrita y panel
congelado donde corresponde.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
En pantallas bajas (o con formularios largos como el de portal-usuarios)
el panel del modal se salía de la ventana y no había forma de llegar a
Guardar ni a Cancelar.
La regla va en el layout y no en cada vista: son ~214 modales repartidos
en las plantillas y arreglarlos uno por uno garantizaba olvidarse varios.
El selector toma solo los overlays centrados y deja afuera los 4 paneles
laterales, que ya manejan su propio scroll.
El alto se mide en % y no en vh/dvh: el overlay es inset-0, o sea que ya
es exactamente la caja visible. Medido en Edge headless a 600/900/700/420
de alto: el panel queda siempre en ventana-2rem, el tope nunca se recorta,
y Guardar se alcanza scrolleando. Con ventana alta el panel conserva su
alto natural y no scrollea.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
La ruta /portal/studio existía pero no había ningún enlace: el cliente
tenía que escribir la URL a mano, o sea que en la práctica no podía
entrar.
El enlace solo aparece si ese cliente tiene al menos un espacio de uMind
asignado — se consulta el mismo endpoint con alcance que usa el SPA, así
ningún cliente sin uMind ve un enlace que lo llevaría a una pantalla
vacía.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
El servicio devolvía la transcripción correcta y la reportábamos como
error: "respuesta inesperada del servicio de transcripción: <la
transcripción completa>". La causa es que mandábamos response_format=json,
que es la convención de la API de OpenAI — whisper-asr-webservice usa el
query param `output`, así que ignoró el campo y respondió en su formato
por defecto (txt), y el parser exigía JSON.
- textoDeRespuestaWhisper acepta las dos formas (JSON {"text":...} o texto
pelado) en vez de adivinar qué variante corre del otro lado. Con test:
8 casos, incluidos JSON sin campo text y un texto que empieza con "{".
- Whisper y OCR: la caja de resultado ahora muestra el conteo de
caracteres, scrollea si es largo, y tiene Copiar y Descargar .txt (Blob
+ <a download>, el texto ya está en el navegador).
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>
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.
- 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).
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.
- Nueva config "whisper" reutilizando /app/ai-config (credenciales/endpoint
validables) y servicio de transcripción compatible con OpenAI/Groq.
- El webhook de Telegram ahora detecta notas de voz/audio, las transcribe
y procesa el texto resultante como si el usuario lo hubiera escrito.
- Fix: subir un archivo adjunto en un comentario de tarea sin escribir
texto era rechazado por el backend ("contenido requerido") aunque el
frontend sí lo permitía.
- El agente creaba tareas con estado='pendiente' (default), pero el tablero
Kanban del dashboard solo reconoce por_hacer/en_progreso/revision/hecho. La
tarea se guardaba bien (por eso llegaba el correo de notificación) pero no
aparecía en ninguna columna. Se corrigen los enums y el default de las tools
crear_tarea/actualizar_estado_tarea, y se agrega una reparación única al
arranque que corrige las tareas ya creadas con el estado inválido.
- adjuntar_factura siempre asumía factura de VENTA (ligada a un cliente). Se
agrega adjuntar_factura_compra: cuando un proveedor le factura a U-SITE (no
al revés), busca/crea la Entidad proveedor y registra una cuenta por pagar
con el documento adjunto (se agregan campos archivo/original_name/tipo_mime
a CuentaPagar, que no los tenía). El prompt del sistema instruye a Claude a
decidir la dirección leyendo quién emite y quién recibe el documento.
De paso: endpoint de descarga del soporte de la factura de compra, expuesto
también en el dashboard (/app/contabilidad/cuentas-pagar).
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>
loadData() no tenía try/catch: si la petición fallaba (deploy sin el código
nuevo, tabla inexistente, etc.) el estado "loading" nunca volvía a false y la
pantalla quedaba trabada en el overlay de carga sin ningún mensaje de error.
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>
UpdateCuentaPagar actualizaba TODAS las columnas incluyendo entidad_id=0,
valor=0, descripcion='' cuando solo se enviaba estado+fecha_pago.
Se agrega MarcarCuentaPagarPagada que solo toca estado y fecha_pago,
con su endpoint POST /cuentas-pagar/:id/pagar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Agrega endpoint POST /api/contratos/:id/marcar-pagado y botón (✓)
en la tabla de contratos para confirmar el pago sin depender del
webhook de Bold. Llama a MarcarContratoPagado (pago_confirmado=true,
limpia enlace, renueva fecha vencimiento) y envía correo de confirmación.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SortableJS mueve el nodo DOM físicamente antes de que Alpine re-renderice,
causando conflicto: la tarjeta desaparecía o volvía a su columna original.
Se revierte el movimiento DOM de SortableJS en onEnd y se deja que Alpine
re-renderice solo al actualizar t.estado; luego se re-inicia Sortable.
Se agrega cursor grab/grabbing como indicador visual de arrastre.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- syncContratoJoin derivaba "contrato_servidore_id" en vez de "servidor_id"
al recortar el último carácter del nombre de tabla; ahora usa TrimPrefix+TrimSuffix
para derivar correctamente servidor_id, conx_db_id, ai_config_id
- Modal contratos: filteredConxDbList filtra BDs por servidor(es) seleccionado(s)
en vez de mostrar todas las BDs sin contexto
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- login.html: corregir link roto /password/reset → /request-password-reset
- request-password-reset.html: reescribir — pide email, AJAX, diseño de la app
- RequestPasswordResetPost: buscar por email (GetUserByEmail), no revela si
el correo existe o no (misma respuesta siempre — seguridad)
- password-reset.html: reescribir — eliminar campo nombre_usuario redundante,
validación de contraseñas en cliente, diseño consistente con el resto del app
- PasswordResetPost: usar c.Locals("email") del token (ya validado por
middleware) en vez de nombre_usuario del form
- password_reset.go middleware: ampliar expiración de token de 5 min a 1 hora
Co-Authored-By: Claude Sonnet 4.6 <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>
- Eliminar envío automático de confirmación al crear usuario
- Nuevo botón en cada fila: enviar credenciales (usuario + link establecer contraseña)
- POST /app/users/:id/credenciales → SendUserCredencialesEmail
- Email incluye username, link al login y link para establecer contraseña
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>
- Tabla con badge de estado (en línea/caído/sin datos), latencia coloreada y último chequeo relativo
- Stats rápidos: total, en línea, caídos, latencia promedio
- Modal crear/editar con toggle activo, intervalo y timeout configurables
- Verificar ahora por fila con spinner y recarga automática a los 4s
- Panel lateral con gráfico de barras Chart.js (verde=OK, rojo=error) y tabla de últimos 50 registros
- Rangos 1h/6h/24h/7d, autorefresh cada minuto
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Endpoint GET /ai-config/:id/test que llama /api/tags (Ollama) o /v1/models (OpenAI-compat/Anthropic)
- Botón "Probar" en cada fila de la tabla con modal de resultado JSON
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Agrega provider 'ollama' al panel de AiConfig con badge azul y hint de URL
- ia_controller: reemplaza webhook n8n hardcodeado por llamada directa a Ollama vía AiConfig (módulo 'ia')
- query_runner: agrega soporte explícito de ollama (model fallback gemma3:1b, Basic Auth para URL pública)
- HTML: agrega módulo 'IA / vCard' en el selector de módulos
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
html/template rejects escaped quotes in attribute values. Code examples
are now built in getCodeExample() and rendered via x-text on the <pre>.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows endpoint, auth, request/response formats and code tabs
for cURL, JavaScript, PHP and Python. API Key is auto-injected
from the page state so examples are ready to copy-paste.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Uses Chart.js to render line charts from servidor_metricas_history.
Chart loads when opening the server detail modal and downsamples
to max 300 points for performance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Filter by RAM first (cheap), then call CPUPercent on just the 10 winners.
Reduces /proc reads from hundreds to 10 per interval.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CPUPercent() on every process reads /proc/<pid>/stat for all processes,
causing ~19% CPU spike on servers with many containers. Sorting by RAM
is a single /proc read per process and much cheaper.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>