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>
168 lines
6.2 KiB
Markdown
168 lines
6.2 KiB
Markdown
# 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.
|
|
|
|
```sql
|
|
-- 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:
|
|
|
|
```sql
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```sql
|
|
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:
|
|
|
|
```php
|
|
$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:
|
|
|
|
```sql
|
|
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:
|
|
|
|
```sql
|
|
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
|
|
|
|
```sql
|
|
-- 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;
|
|
```
|