- GET_LOCK('bot_user_ID', 10) serializa el procesamiento por usuario
- Solo un proceso a la vez puede ejecutar processMessage para el mismo user
- Recarga datos frescos del usuario tras adquirir el lock
- El lock se libera siempre en el bloque finally, incluso si hay excepciones
- Resuelve definitivamente el doble envío de bienvenida + términos
- conversations.message_id ahora tiene UNIQUE index (aplicado en BD prod)
- saveMessage() usa INSERT IGNORE: si el mismo wamid llega dos veces en
paralelo solo una llamada procesa el mensaje y llama al bot
- La segunda llamada recibe rowCount=0 y hace continue sin pasar al bot
- Elimina race condition que enviaba bienvenida + términos simultáneamente
- sendTermsMessage: solo agrega PDF al final si la URL no está ya en el mensaje
- sendTermsMessage: marca welcome_sent_at=now() junto con terms_pending=1
para evitar race condition donde un segundo webhook concurrente cae en
isNewUser() y envía la bienvenida mientras los términos ya fueron enviados
El metodo registrarCambioEstado usaba estadoNuevo ('pendiente')
como valor de accion, pero pendiente no es un valor valido del
ENUM de lab_autorizaciones.accion.
Se pasa 'creada' explicitamente al crear una orden nueva, que si
es un valor valido del ENUM ('creada','en_revision','autorizada',
'rechazada','en_domicilio','completada','editada').
- index.php: NotificationManager simplificado a stub vacío.
Los elementos notification-bell / global-notification-toasts no existen en el
markup → el setInterval cada 7s solo generaba tráfico inútil a get_notifications.php
Las notificaciones ya se manejan vía SSE en conversations.php
- send_broadcast.php: exponer debug_wamids en respuesta (wamid, wa_status, wa_contact)
para saber exactamente qué devuelve WhatsApp por cada destinatario.
Agregar dryRun log del payload completo al error_log para diagnóstico de
mensajes que WhatsApp acepta (200+wamid) pero no entrega.
La API de WhatsApp NO devuelve {success:true} - devuelve
{messaging_product, messages:[{id:wamid.xxx}], contacts:[...]}.
La comprobación anterior siempre fallaba → sent_count=0, error_count=N.
Ahora verifica presencia de messages/contacts en la respuesta.
- Detecta header_type=IMAGE en get_template_details.php
- send_message (individual): muestra input de imagen si la plantilla lo requiere,
sube a upload_media.php y envía header_image_url al backend
- broadcast (masivo): igual, con upload previo al envío
- api/send_message.php: acepta header_image_url y lo pasa como headerParameters
[type:image, image:{link:URL}] a WhatsAppService::sendTemplateMessage()
- api/send_broadcast.php: igual para cada destinatario del masivo
- Preview en tiempo real de la imagen seleccionada en ambos formularios
- fix: portal.renderAgenda → portal._renderLista() (error JS al registrar pago)
- feat: modal nueva agenda añade tipo_cliente (particular/seguro), seguro_nombre, autorizacion, valor_domicilio, valor_copago
- feat: toggleSeguro() muestra/oculta campo EPS según tipo seleccionado
- fix: abrir() resetea todos los campos nuevos en cada apertura del modal
- datos de cobro se envían al guardar y se auto-asigna la enfermera creadora
- Modal expandido: tipo_cliente, seguro, autorizacion, valor_domicilio, valor_copago, copago_laboratorio, indicaciones, fecha, hora, tipo_servicio, notas
- Botón Editar en header del panel detalle (aparece al seleccionar un domicilio)
- editarDomicilio(id): carga y pre-rellena todos los campos del modal
- toggleSeguro(): muestra/oculta campo seguro_nombre según tipo_cliente
- Tras guardar, el detalle se refresca automáticamente
- database/07_add_pago_domicilio.sql: 6 columnas nuevas en lab_domicilios
(pago_estado ENUM, pago_modo, pago_monto, pago_fecha, pago_notas, pago_registrado_por)
- Domicilio::filtrarCampos(): permite guardar campos de pago vía actualizar()
- Enfermera::agenda(): incluye valor_domicilio, valor_copago, pago_estado, pago_modo, pago_monto, pago_notas
- api/lab/registrar_pago.php: endpoint POST para registrar/actualizar pago del domicilio
- enfermero_portal.php: bloque cobro en card, modal registrar pago (efectivo/transferencia/exento), objeto pagoModal JS
- lab_domicilios.php: badge pago ⏳/✅/🔵 en fila tabla, sección pago detallada en panel lateral
Si lab_domicilios.orden_id es NULL (domicilio creado antes del fix
o desde otro flujo), usa subquery para encontrar la orden más reciente
del mismo paciente que tenga local_file.
Así el enfermero siempre ve la imagen de la orden del paciente
independientemente de cómo se agendó el domicilio.
La BD guarda local_file con 'uploads/media/...' incluido.
El código le añadía otro 'uploads/media/' dando ruta doble.
- lab_ordenes.php: helper _resolveImg() detecta si el path
ya empieza con 'uploads/' y no añade el prefijo de nuevo
- enfermero_portal.php: nueva función resImg() con misma lógica,
usada en _cardHTML() al renderizar la imagen de la orden
Al abrir el modal desde una imagen en el chat:
- guardar() crea primero la OrdenMedica via crear_desde_whatsapp.php
y pasa el orden_id resultante a save_domicilio.php
- Enfermera::agenda() hace LEFT JOIN a lab_ordenes_medicas para traer
local_file de la orden asociada (as orden_local_file)
- enfermero_portal._cardHTML() muestra la imagen de la orden médica
con link para ampliar
La tarjeta ahora muestra: documento, fecha de nacimiento, EPS,
teléfono (clickeable), correo (clickeable), exámenes solicitados
y tipo de cliente (particular/seguro con nombre del seguro).
Cambios:
- Enfermera::agenda() amplía SELECT con email, fecha_nacimiento,
numero_documento, tipo_documento, eps, examenes_solicitados,
tipo_cliente, seguro_nombre, ciudad, indicaciones_dir, notas_admin
- my_agenda.php simplificado: ya no re-consulta lab_domicilios por
los campos que ahora vienen directo en la query
- enfermero_portal.php _cardHTML() muestra todos los campos nuevos
styles.css define .tab-content { display:none } para un sistema de tabs
propio. Bootstrap 5 agrega 'active' al .tab-pane, no al .tab-content,
por lo que el contenedor nunca se mostraba (display:none permanente).
Solución: style='display:block;padding:0' en el div .tab-content para
que Bootstrap controle la visibilidad de los panes internos.
Coolify usa /artifacts/.../docker-compose.prod.yml en el deploy.
Ese archivo tenía 'whatsapp-network' local bridge — el contenedor nunca
se conectaba a la red 'coolify' donde está el Redis → SERVFAIL en DNS.
Cambios:
- networks: whatsapp-network → coolify (external: true)
- container_name: removido (Coolify asigna el suyo)
- healthcheck: timeout 5s→10s, start_period 30s→60s
php.ini NO expande variables de entorno, estaba hardcodeado 'tcp://redis:6379'.
PHP intentaba conectar al host inexistente 'redis' en cada request →
timeout 10s en session_start() → todos los scripts tardaban 10-13s.
Solución:
- entrypoint.sh reescribe session.save_path con el host/password real al inicio
- php.ini actualizado con placeholder (sobreescrito por entrypoint de todas formas)
Versión anterior intentaba conectar a DB (g1286...) y Redis (zqmfom0...)
que colgaban esperando conexión → todos los workers PHP-FPM bloqueados →
nginx acepta TCP pero no responde HTTP → healthcheck timeout → unhealthy.
El contenedor Redis (zqmfom0vw6nj3ae66mkwxek1) vive en la red 'coolify'.
La app estaba solo en la red nj05x5... → DNS SERVFAIL → health.php tardaba
10-12s intentando conectar → healthcheck timeout → unhealthy → 'no available server'.
Solución: declarar red coolify como external y conectar el servicio app.
Fix inmediato aplicado manualmente con: docker network connect coolify <container>
06_master_sync.sql:
- Comentario tenía ';' (La columna role_id ya existe; la FK) →
el parser de migrate.php divide por ; y el texto 'la FK' quedaba
como sentencia SQL → error SQLSTATE[42000]
docker-compose.yml:
- Eliminar whatsapp-network (red bridge propia)
- Con 1 solo servicio no se necesita red interna
- Coolify añade automáticamente su red nj05x5... (donde está Traefik)
- La red bridge extra podría interferir con el routing de Traefik
health.php:
- DB fallo → 'warning' (no 'unhealthy', no cierra tráfico Traefik)
- Redis ya era warning desde commit anterior
docker-compose.yml:
- timeout: 5s → 10s (DB externa puede tardar más)
- start_period: 40s → 60s (tiempo suficiente para that migrations corran)
- fallback: si health.php falla, intenta / raíz antes de marcar unhealthy
- Eliminar container_name (Coolify asigna su propio nombre, conflicto evitado)
- Eliminar ./:/var/www/html (bind mount sobreescribía código del contenedor en prod)
- Eliminar ./logs y ./uploads bind mounts (reemplazados por named volumes)
- Eliminar php-socket volume (ya no se usa)
- Usar named volumes app-logs/app-uploads (persistencia correcta en Coolify)
1. ADD CONSTRAINT IF NOT EXISTS no es estándar en MariaDB < 10.5 —
eliminado el bloque FK de admin_users.role_id (la integridad la
gestiona la aplicación).
2. AFTER `pdf_path` fallaba porque la columna no existe en lab_form_envios —
eliminado el AFTER, la columna se agrega al final de la tabla.
Esto evita que la migración falle en cada deploy.
Redis es externo (Coolify managed) — esperar con redis-cli causaba
30 segundos de delay en cada arranque porque:
1. redis-cli no pasaba -a $REDIS_PASSWORD (auth fallaba)
2. Redis no es local, no tiene sentido hacer ping desde dentro del contenedor
La app conecta a Redis cuando necesita (RedisQueue, ConversationState, etc.)