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

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;
```