Files
Lizandro GuarnizoandClaude Opus 5 fe96e5b60d Documentación en usted, y panel de LIA con el mismo diseño del dashboard
Toda la documentación pasa de voseo a tratamiento de usted, incluidos los
diagramas y los textos de la interfaz. Los prompts de ambos asistentes lo
piden explícitamente, para que las respuestas generadas también lo respeten.

El panel de LIA en la página de documentación adopta el diseño del dashboard
del turnero: mismo botón circular, mismo panel deslizante con encabezado en
degradado y logo LIA, mismos chips de atajo, burbujas y campo de entrada.
Los atajos se arman con los documentos que ese usuario puede ver.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 10:57:28 -05:00

6.2 KiB

Runbook de incidentes

Qué hacer cuando algo falla. Ordenado por lo que reporta el usuario, no por la causa.


«No veo un módulo que antes veía»

O el opuesto: «puedo editar algo que no debería».

Casi siempre es una de dos cosas: el usuario no volvió a iniciar sesión, o role y role_id quedaron desincronizados.

-- 1. ¿Las dos columnas coinciden?
SELECT u.id, u.username, u.role, u.role_id, r.slug AS rol_real
  FROM admin_users u LEFT JOIN roles r ON r.id = u.role_id
 WHERE u.username = 'usuario';

-- 2. ¿Qué le da ese rol?
SELECT module_slug, permission FROM role_modules WHERE role_id = <role_id>;

Si los datos están bien → que cierre sesión y vuelva a entrar. Los permisos se cargan al iniciar sesión, no en cada petición.

Si role y role_id no coinciden, actualice las dos:

UPDATE admin_users
   SET role = 'lab_recepcion',
       role_id = (SELECT id FROM roles WHERE slug = 'lab_recepcion')
 WHERE id = <id>;

Para dejar un módulo en solo lectura, lo que importa es permission = 'read'. Las columnas can_edit, can_create y demás no las lee el control de acceso.


«Un enlace que enviamos por WhatsApp está roto»

Primero, compruebe si el destino responde:

curl -s -o /dev/null -w "%{http_code}\n" "<la URL>"

Si devuelve 503 o no resuelve, el dominio está caído o cambió. Revise si el archivo existe en el dominio actual del sistema.

Las URL que enviamos viven en lugares distintos según el caso:

Enlace Dónde está definido
Botón de consentimiento En la plantilla de Meta, no en el código
Documento de términos terms_versions.documento_url y system_config.terms_message
Respaldo texto plano del consentimiento Se arma con el dominio del servidor

Si el enlace roto es el botón de una plantilla, cambiar el código no lo arregla. Hay que editar la plantilla en el WhatsApp Manager de Meta y esperar la reaprobación.

Para el documento de términos, hay que cambiar los dos lugares a la vez:

UPDATE terms_versions
   SET documento_url = REPLACE(documento_url, 'dominio.viejo', 'dominio.nuevo')
 WHERE documento_url LIKE '%dominio.viejo%';

UPDATE system_config
   SET config_value = REPLACE(config_value, 'dominio.viejo', 'dominio.nuevo')
 WHERE config_value LIKE '%dominio.viejo%';

«El mensaje salió por el número equivocado»

Las dos líneas comparten cuenta y token; lo único que cambia es el phone_number_id. Revise que el envío haya especificado el canal:

$wa = new WhatsAppService('turnero');   // no  new WhatsAppService()

«LIA responde cortado»

El asistente del dashboard del turnero tiene tope de salida. Si la respuesta se corta a media frase, ahora avisa con «respuesta cortada por longitud».

Qué revisar Dónde
Tope de tokens de salida modules/turnero/api/ai_chat.php, maxOutputTokens
Presupuesto consumido lab_config.lia_tokens_usados (tope: 1.000.000)
Clave configurada lab_config.gemini_api_key

Si el presupuesto se agotó, LIA se bloquea y pide contactar a soporte. Para reiniciar el contador:

UPDATE lab_config SET valor = '0' WHERE clave = 'lia_tokens_usados';

El contexto del día se manda completo en cada pregunta, así que el gasto por consulta es alto aunque la respuesta sea corta.


«El formulario de tomas prolongadas muestra secciones que no corresponden»

El formulario F-LAB-28 tiene secciones para todos los exámenes posibles y muestra solo las del examen del paciente. Si aparecen de más:

  1. Verifique que el documento se abra con &embed=1&compact=1 — sin esos parámetros no se aplica el filtrado.
  2. Revise _tomas_config dentro de datos_respuestas: ahí queda qué ciclos se configuraron.

«No aparece quién firmó una toma»

Las firmas de tomas prolongadas registran el profesional desde el 3 de agosto de 2026. Los documentos firmados antes no tienen ese dato y no es recuperable — no quedó traza en ninguna tabla de auditoría.

Para los nuevos, el nombre y la cédula se resuelven en el servidor desde la sesión de quien firma. Si aparece vacío en un documento reciente, compruebe que el usuario tenga cédula:

SELECT id, username, full_name, cedula FROM admin_users WHERE id = <id>;

Los enfermeros la toman de lab_enfermeras.numero_documento; el resto de admin_users.cedula.


«El bot no responde»

Revisar Cómo
¿Llegan los mensajes? Tabla webhook_logs
¿Está en horario? BusinessHoursService — fuera de horario responde distinto
¿La conversación quedó con un operador? Estado en conversations; el bot no interrumpe una atención humana
¿Aceptó los términos? users.terms_accepted_at; sin aceptar, el bot no avanza

«Un paciente quedó con una muestra pendiente»

Cuando el paciente vuelve, las muestras pendientes y rechazadas de visitas anteriores aparecen automáticamente en la estación de toma de muestras, con la etiqueta visita anterior y los exámenes de aquella orden.

Al recibirla queda registrado en qué turno se completó (turnero_muestras.recibida_en_turno_id), y ambos turnos quedan enlazados en el historial y en la bandeja. El turno original no se modifica: sigue finalizado como estaba.


Consultas útiles

-- Facturación real de hoy (solo turnos finalizados)
SELECT ROUND(SUM(ts.total_cobrado)) AS facturado, COUNT(*) AS turnos
  FROM turnero_turnos t
  JOIN turnero_solicitudes ts ON ts.turno_id = t.id
  JOIN turnero_sesiones s ON s.id = t.sesion_id
 WHERE s.fecha = CURDATE() AND t.estado = 'finalizado' AND ts.total_cobrado > 0;

-- Consentimientos sin firmar
SELECT tc.estado, COUNT(*) FROM turnero_consentimientos tc
  JOIN turnero_turnos t ON t.id = tc.turno_id
  JOIN turnero_sesiones s ON s.id = t.sesion_id
 WHERE s.fecha = CURDATE() GROUP BY tc.estado;

-- Muestras pendientes acumuladas por paciente
SELECT p.nombre_completo, COUNT(*) AS pendientes
  FROM turnero_muestras tm
  JOIN turnero_solicitudes ts ON ts.id = tm.solicitud_id
  JOIN lab_pacientes p ON p.id = ts.paciente_id
 WHERE tm.estado IN ('pendiente','rechazada')
 GROUP BY p.id ORDER BY pendientes DESC LIMIT 20;