Runbooks
Procedimientos para cuando algo va mal en un entorno desplegado. Para la instalación y el despliegue normal, ver Operaciones y despliegue.
Un usuario dice que le salió un error
- Pídele el código de referencia de la pantalla de error, si lo vio, y búscalo:
select * from platform_error_events where id = '<código>'; - Si no lo tiene, busca por su tenant y la hora en
platform_error_eventso enplatform_error_groups_v. La fila trae la pantalla, la acción, el código de Postgres y la traza. - Detalle y consultas: Reporte de errores.
Un mensaje entrante no llegó
docker compose logs backend --tail 200 | grep -i webhook
Por orden:
- ¿Llegó el webhook al backend? Si no aparece nada, el problema está antes: proveedor, DNS o
nginx. Comprueba que el proveedor tiene registrada la URL correcta y que responde
curl -I https://api.xenpia.com/api. - ¿Se rechazó por firma? Meta firma sus peticiones. Un secreto mal configurado se ve como un rechazo sistemático de todos los webhooks.
- ¿El canal está activo para esa cuenta? Un canal desactivado en el panel descarta el mensaje.
- ¿Es un canal de los que encolan? X, correo, webchat y los de Meta encolan en
inbound, que no tiene consumidor en este repositorio. Si el mensaje entró y no pasó nada más, es esto. Ver Visión general.
El bot no responde
status_botde la conversación. Si alguien apagó el bot desde la bandeja, el bot está callado a propósito.- Palabras de cumplimiento. Si el mensaje disparó una palabra de parada o de escalación, el silencio es el comportamiento correcto.
- Límite de consumo del tenant.
- Errores del modelo. Con
DEBUG_LOGS=truese ven las excepciones que hoy se convierten en una frase amable de cara al usuario. Enciéndela un rato y vuelve a apagarla: imprime datos reales de conversación.
docker compose logs backend | grep '\[dbg:'
Redis caído
BullMQ, el límite de peticiones y la caché del bot dependen de Redis.
docker compose ps redis
docker compose logs redis --tail 50
docker compose restart redis
WhatsApp y Telegram siguen respondiendo sin Redis, porque procesan de forma síncrona en el webhook. Lo que se degrada son las colas y la caché, así que el bot responde más lento y más caro (cada consulta al catálogo vuelve a calcularse).
Una migración falló en el despliegue
El pipeline aplica las migraciones antes de desplegar, así que un fallo aquí deja el código sin desplegar pero la base a medias.
- Mira el log del job para ver qué migración y qué error.
- La causa habitual es una migración no idempotente que ya se aplicó parcialmente: un
create policysin sudrop policy if existsdelante. - Arréglala en una migración nueva. No edites una ya aplicada: en el otro entorno ya corrió y el hash no coincidirá.
El frontend muestra datos viejos tras cambiar el .env
Las variables NEXT_PUBLIC_* se congelan en el build. Reiniciar el contenedor no basta:
./deploy.sh --build
Un usuario no ve una pantalla
Recorre la cadena entera antes de tocar nada:
- ¿El módulo está activo para ese tenant?
- ¿La entrada de menú existe y está activa?
- ¿El permiso de esa entrada está asignado a su rol?
- ¿La función
menus_accessiblela devuelve para ese usuario?
Casi siempre es el paso 2: se creó la página y se olvidó dar de alta la entrada de menú. Ver Frontend.
Certificados a punto de caducar
Los dominios están tras Cloudflare. Revisa la sección de certificados de Operaciones y despliegue.
La documentación no refleja el último despliegue
Los sitios son estáticos: el HTML se congela al construir la imagen. Si docs.xenpia.com o
docstest.xenpia.com siguen mostrando lo de ayer, el contenedor está corriendo una imagen vieja.
docker compose ps docs devdocs # ¿están arriba?
docker compose up -d --build docs devdocs
Si tras reconstruir sigue igual, es caché de Cloudflare: purga la URL desde el panel. El HTML se
sirve con Cache-Control: no-cache precisamente para que esto no pase, así que si se repite,
revisa que no haya una regla de página en Cloudflare cacheando HTML.
Si el dominio ni siquiera resuelve o nginx responde 502, falta el DNS o el bloque de nginx en ese
servidor: los contenedores solo escuchan en 127.0.0.1:3840 / 3841.
Entrar a devdocs / devdocstest da 403 sin pedir identificación
El 403 lo devuelve el nginx del servidor, no Cloudflare: la petición no vino por el proxy. O el registro DNS dejó de estar proxied (nube gris en vez de naranja), o alguien entró por IP. Se comprueba en el panel de Cloudflare, y el detalle está en Operaciones y despliegue.
Si en cambio pide identificación y luego rechaza a alguien del equipo, es la política de Access:
Zero Trust → Access → Applications → devdocs.xenpia.com / devdocstest.xenpia.com, revisar la
lista de correos.
Guardar el plan de un tenant falla
El plan se guarda en Odoo (fuente de verdad) desde Tenants → Editar → Plan: el panel llama a
POST /api/billing/tenant-subscription del backend, que habla con el puente de Odoo
(/xemp/ai/v1/*). En pantalla el usuario solo ve "No se puede actualizar el plan ahora" (a
propósito no se menciona Odoo ni el detalle técnico). El código y el detalle real están en los logs:
[billing] setTenantSubscription … en el frontend y BillingController en el backend. Cada llamada a
Odoo corta a los 10 s (Odoo no respondió en 10 s) y la del panel al backend a los 45 s. Por código:
| Código | Qué pasa | Qué revisar |
|---|---|---|
not_configured | Falta ODOO_WEBHOOK_SECRET en el frontend, o ODOO_BASE_URL / ODOO_API_KEY en el backend | El .env que carga cada servicio |
unauthorized | El ODOO_WEBHOOK_SECRET del frontend y el del backend no coinciden | En local sin Docker, frontend/.env y backend/.env son archivos distintos |
backend_unreachable | El frontend no llega a Nest | BACKEND_INTERNAL_URL (en Docker, http://backend:3000) |
odoo_unreachable | Nest no llega a Odoo | ODOO_BASE_URL visto desde el contenedor del backend: localhost ahí es el propio contenedor |
odoo_error | Odoo respondió con un error | El detalle trae el mensaje de Odoo |
plan_not_found | El código de plan no existe en Odoo | Planes activos en Odoo y backfill POST /api/billing/sync |
sync_failed | Odoo aceptó, pero falló el espejo en Supabase | Logs del backend (BillingController) |
El odoo_error más habitual es column ... does not exist: se desplegó código de un addon de
Odoo con campos nuevos sin actualizar la base. Odoo carga el Python nuevo pero la tabla sigue
vieja, y cualquier llamada al puente que lea ese modelo revienta. Se arregla actualizando el
módulo en la base afectada:
docker compose exec -T odoo bash -c 'odoo -d <base> -u <modulo> --stop-after-init --http-port=8899 --max-cron-threads=0 --db_host="$HOST" --db_user="$USER" --db_password="$PASSWORD"'
Recoger información antes de escalar
- Rama y commit desplegado:
git log -1 --onelineen el servidor. docker compose psy los últimos 200 logs del servicio afectado.- Tenant, canal y hora aproximada.
- Si es de bots, un fragmento del log con
DEBUG_LOGSactivo.