From 44423adb3252856b2d59ff0e4dece74caf2ae57d Mon Sep 17 00:00:00 2001
From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com>
Date: Thu, 29 Jan 2026 11:40:04 -0500
Subject: [PATCH] up
---
api/send_media_message_debug.log | 8 ++
api/sse_events.php | 2 +-
conversations.php | 191 +++++++++++++++++++------------
docker/nginx/default.conf | 21 +++-
docker/nginx/nginx.conf | 6 +-
docker/nginx/ssl.conf | 5 +-
manifest.json | 15 ---
service-worker.js | 43 -------
8 files changed, 146 insertions(+), 145 deletions(-)
delete mode 100644 manifest.json
delete mode 100644 service-worker.js
diff --git a/api/send_media_message_debug.log b/api/send_media_message_debug.log
index 91c0f73..19861fb 100644
--- a/api/send_media_message_debug.log
+++ b/api/send_media_message_debug.log
@@ -231,3 +231,11 @@
[2026-01-28 14:04:21] Send media by id response: {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSREYzMUY2RUMwQTgxNERENTUwAA=="}]}
[2026-01-28 14:04:21] WhatsApp response (successful): {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSREYzMUY2RUMwQTgxNERENTUwAA=="}]}
[2026-01-28 14:04:21] Saving to DB - content: record_1769627059068.webm, media_url: 1602247974444552, local_file: uploads/media_697a5db44d08f0.23171792.ogg, local_thumb: null
+[2026-01-29 11:39:39] Raw input: {"recipient":"573168950803","media_url":"http://localhost:8080/uploads/media_697b8d4b88a6c2.66006027.ogg","media_type":"audio","caption":null,"filename":"record_1769704778262.webm","is_voice":true}
+[2026-01-29 11:39:39] Input decoded: {"recipient":"573168950803","media_url":"http:\/\/localhost:8080\/uploads\/media_697b8d4b88a6c2.66006027.ogg","media_type":"audio","caption":null,"filename":"record_1769704778262.webm","is_voice":true}
+[2026-01-29 11:39:39] is_voice: true
+[2026-01-29 11:39:40] Upload result: {"id":"906365935662402"}
+[2026-01-29 11:39:40] Media ID obtained: 906365935662402
+[2026-01-29 11:39:41] Send media by id response: {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSNzAyNEVCRkRGNUYwM0IzQURGAA=="}]}
+[2026-01-29 11:39:41] WhatsApp response (successful): {"messaging_product":"whatsapp","contacts":[{"input":"573168950803","wa_id":"573168950803"}],"messages":[{"id":"wamid.HBgMNTczMTY4OTUwODAzFQIAERgSNzAyNEVCRkRGNUYwM0IzQURGAA=="}]}
+[2026-01-29 11:39:41] Saving to DB - content: record_1769704778262.webm, media_url: 906365935662402, local_file: uploads/media_697b8d4b88a6c2.66006027.ogg, local_thumb: null
diff --git a/api/sse_events.php b/api/sse_events.php
index 6897f53..aa5324f 100644
--- a/api/sse_events.php
+++ b/api/sse_events.php
@@ -95,7 +95,7 @@ try {
$lastEventId = $_SERVER['HTTP_LAST_EVENT_ID'] ?? null;
$lastCheck = time();
$connectionStart = time();
- $maxConnectionTime = 300; // 5 minutos máximo
+ $maxConnectionTime = 1800; // 30 minutos máximo (aumentado para evitar desconexiones frecuentes)
// Loop principal - revisar eventos cada 2 segundos
while (true) {
diff --git a/conversations.php b/conversations.php
index 2c10bfa..217df2c 100644
--- a/conversations.php
+++ b/conversations.php
@@ -826,10 +826,6 @@ if (!isset($_SESSION['user_id'])) {
.unread-badge { margin-left: 8px; font-size: 12px; }
-
-
-
-
@@ -839,7 +835,7 @@ if (!isset($_SESSION['user_id'])) {
💬 Conversaciones
- v1.2.1-
+ v1.2.2-
@@ -910,8 +906,6 @@ if (!isset($_SESSION['user_id'])) {
-
-
@@ -1218,23 +1212,35 @@ if (!isset($_SESSION['user_id'])) {
if (this.currentUserId && String(this.currentUserId) === String(userId)) {
console.log('♻️ Mensaje para conversación ACTIVA');
- // SOLUCIÓN: Siempre usar staged messages + indicador
- // Esto evita el error que borra la pantalla al intentar recargar automáticamente
- if (!this._stagedMessages) this._stagedMessages = [];
- if (!this._stagedMessageIds) this._stagedMessageIds = new Set();
+ // Verificar si el usuario está cerca del final del chat
+ const container = document.getElementById('chat-conversations');
+ const nearBottom = container ? ((container.scrollHeight - container.scrollTop - container.clientHeight) < 150) : true;
- // Crear key única para evitar duplicados
- const msgKey = data.message_id || data.id || `${data.created_at}_${data.content?.substring(0,20)}`;
-
- if (!this._stagedMessageIds.has(msgKey)) {
- this._stagedMessageIds.add(msgKey);
- this._stagedMessages.push(data);
- console.log('💾 Mensaje guardado en staging. Total:', this._stagedMessages.length);
-
- // Mostrar indicador de nuevos mensajes
- this.showNewMessagesIndicator(this._stagedMessages.length);
+ if (nearBottom && !this._userScrolling) {
+ // Usuario está en el final: recargar mensajes automáticamente
+ console.log('🔄 Usuario cerca del final, recargando mensajes...');
+ this.loadMessages(this.currentUserId, true, true).catch(err => {
+ console.error('Error recargando mensajes:', err);
+ });
} else {
- console.log('⏭️ Mensaje duplicado, omitiendo');
+ // Usuario está leyendo historial: usar staged messages
+ console.log('📜 Usuario leyendo historial, guardando en staging...');
+ if (!this._stagedMessages) this._stagedMessages = [];
+ if (!this._stagedMessageIds) this._stagedMessageIds = new Set();
+
+ // Crear key única para evitar duplicados
+ const msgKey = data.message_id || data.id || `${data.created_at}_${data.content?.substring(0,20)}`;
+
+ if (!this._stagedMessageIds.has(msgKey)) {
+ this._stagedMessageIds.add(msgKey);
+ this._stagedMessages.push(data);
+ console.log('💾 Mensaje guardado en staging. Total:', this._stagedMessages.length);
+
+ // Mostrar indicador de nuevos mensajes
+ this.showNewMessagesIndicator(this._stagedMessages.length);
+ } else {
+ console.log('⏭️ Mensaje duplicado, omitiendo');
+ }
}
} else {
console.log('📋 Mensaje para OTRA conversación, solo actualizando lista');
@@ -1380,7 +1386,7 @@ if (!isset($_SESSION['user_id'])) {
*/
updateConversationInList(data) {
try {
- console.log('📝 Actualizando conversación en lista:', data);
+ console.log('📝 updateConversationInList INICIADO:', data);
// Asegurar que conversations esté inicializado
if (!this.conversations || !Array.isArray(this.conversations)) {
@@ -1396,6 +1402,8 @@ if (!isset($_SESSION['user_id'])) {
return;
}
+ console.log('👤 User ID extraído:', userId);
+
// Si no viene el mensaje, recargar la lista completa para obtener datos actualizados
if (!data.message && !data.content && !data.text && !data.last_message) {
console.log('⚠️ Evento SSE sin contenido de mensaje, recargando lista completa...');
@@ -1412,11 +1420,14 @@ if (!isset($_SESSION['user_id'])) {
const message = data.message || data.content || data.text || data.last_message || 'Nuevo mensaje';
const timestamp = data.timestamp || data.created_at || new Date().toISOString();
+ console.log('💬 Mensaje extraído:', message);
+ console.log('⏰ Timestamp:', timestamp);
+
// Buscar la conversación en el array
const existingIndex = this.conversations.findIndex(c => String(c.user_id) === String(userId));
if (existingIndex !== -1) {
- console.log('✅ Conversación encontrada, actualizando...');
+ console.log('✅ Conversación encontrada en índice:', existingIndex);
// Actualizar conversación existente
const conv = this.conversations[existingIndex];
conv.last_message = message;
@@ -1425,14 +1436,17 @@ if (!isset($_SESSION['user_id'])) {
// Solo incrementar unread_count si no es la conversación activa
if (String(this.currentUserId) !== String(userId)) {
conv.unread_count = (conv.unread_count || 0) + 1;
+ console.log('📬 unread_count incrementado a:', conv.unread_count);
} else {
// Si es la conversación activa, mantener unread_count en 0
conv.unread_count = 0;
+ console.log('✅ Conversación activa, unread_count = 0');
}
// Mover al inicio de la lista
this.conversations.splice(existingIndex, 1);
this.conversations.unshift(conv);
+ console.log('⬆️ Conversación movida al inicio');
} else {
console.log('➕ Conversación no existe, agregando nueva...');
// Nueva conversación, agregar al inicio
@@ -1444,22 +1458,25 @@ if (!isset($_SESSION['user_id'])) {
last_time: timestamp,
unread_count: String(this.currentUserId) !== String(userId) ? 1 : 0
});
+ console.log('✅ Nueva conversación agregada');
}
// Re-renderizar solo la lista de conversaciones
console.log('🔄 Re-renderizando lista de conversaciones...');
+ console.log('📊 Total conversaciones:', this.conversations.length);
console.log('🔍 Filtro activo:', this.conversationFilter);
- console.log('🔍 this.conversations:', this.conversations);
if (typeof this.renderConversations === 'function') {
console.log('✅ Llamando a renderConversations()...');
this.renderConversations();
+ console.log('✅ renderConversations() ejecutado correctamente');
} else {
console.error('❌ renderConversations no es una función!');
}
} catch (error) {
- console.error('❌ Error actualizando conversación en lista:', error);
+ console.error('❌ Error updateConversationInList:', error);
+ console.error('Stack trace:', error.stack);
}
}
@@ -2110,49 +2127,6 @@ if (!isset($_SESSION['user_id'])) {
};
applyInitialMobile();
window.addEventListener('resize', applyInitialMobile);
-
- // PWA support (install prompt + SW)
- let _deferredPWA = null;
- const _pwaBtnConv = document.getElementById('pwa-install-btn');
- window.addEventListener('beforeinstallprompt', (e) => {
- e.preventDefault();
- _deferredPWA = e;
- if (_pwaBtnConv) { _pwaBtnConv.style.display = 'inline-block'; _pwaBtnConv.classList.remove('d-none'); }
- });
- if (_pwaBtnConv) {
- _pwaBtnConv.addEventListener('click', async () => {
- if (!_deferredPWA) return;
- _deferredPWA.prompt();
- const choice = await _deferredPWA.userChoice;
- console.debug('PWA install (conv):', choice);
- if (choice && choice.outcome === 'accepted') _pwaBtnConv.style.display = 'none';
- _deferredPWA = null;
- });
- }
- window.addEventListener('appinstalled', () => console.debug('PWA installed (conversations)'));
- if ('serviceWorker' in navigator) {
- (async () => {
- try {
- // Check if the service-worker script is actually reachable to avoid noisy 404 warnings
- const resp = await fetch('/service-worker.js', { method: 'GET', cache: 'no-store' });
- if (resp && resp.ok) {
- navigator.serviceWorker.register('/service-worker.js')
- .then(() => console.debug('SW reg (conv)'))
- .catch(e => console.warn('SW reg failed', e));
- } else {
- console.debug('Service worker not registered: script not found (status ' + (resp && resp.status) + ')');
- // Remove manifest link if it's missing to avoid extra 404s
- try {
- const link = document.querySelector('link[rel="manifest"]');
- if (link && (!resp || resp.status === 404)) link.parentNode && link.parentNode.removeChild(link);
- } catch(e) {}
- }
- } catch (e) {
- // Network or fetch failed — don't spam console with errors
- console.debug('Service worker check failed:', e);
- }
- })();
- }
}
const stopRecBtn = document.getElementById('stop-recording');
@@ -2524,7 +2498,9 @@ if (!isset($_SESSION['user_id'])) {
renderConversations() {
- console.log('🎨 Renderizando conversaciones:', this.conversations.length);
+ console.log('🎨 renderConversations INICIADO');
+ console.log('📊 Total conversaciones:', this.conversations?.length || 0);
+
const container = document.getElementById('conversation-list');
if (!container) {
@@ -2631,12 +2607,20 @@ if (!isset($_SESSION['user_id'])) {
const wasNearTop = prevScrollTop < 60;
console.log('🔄 Actualizando innerHTML del container...');
+ console.log('📏 Scroll antes - Top:', prevScrollTop, 'Height:', prevScrollHeight);
+
container.innerHTML = html;
- console.log('✅ DOM actualizado con', this.conversations.length, 'conversaciones');
+
+ console.log('✅ DOM actualizado');
console.log('📊 Elementos en DOM:', container.children.length);
-
+
+ // Forzar repaint del DOM
+ void container.offsetHeight;
+
const newScrollHeight = container.scrollHeight;
const scrollDelta = newScrollHeight - prevScrollHeight;
+
+ console.log('📏 Scroll después - Height:', newScrollHeight, 'Delta:', scrollDelta);
if (wasNearTop) {
// keep at top
@@ -2645,10 +2629,15 @@ if (!isset($_SESSION['user_id'])) {
// preserve visual offset (avoid jumping) whether the user is scrolling or not
container.scrollTop = Math.max(0, prevScrollTop + scrollDelta);
}
+
+ console.log('✅ renderConversations COMPLETADO');
} catch (e) {
// fallback to naive replace if anything failed
console.warn('renderConversations: scroll preservation failed', e);
container.innerHTML = html;
+ // Forzar repaint
+ void container.offsetHeight;
+ console.log('⚠️ renderConversations COMPLETADO con fallback');
}
}
@@ -4046,9 +4035,40 @@ if (!isset($_SESSION['user_id'])) {
this.startRecording = async function() {
try {
+ // Verificar que el navegador soporta MediaDevices
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
+ throw new Error('Tu navegador no soporta grabación de audio');
+ }
+
// Suspend periodic reload while recording
this._suspendAutoRefresh = true;
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+
+ // Intentar obtener acceso al micrófono con diferentes configuraciones
+ let stream = null;
+ try {
+ // Intento 1: Configuración básica sin especificar dispositivo
+ stream = await navigator.mediaDevices.getUserMedia({
+ audio: {
+ echoCancellation: true,
+ noiseSuppression: true,
+ autoGainControl: true
+ }
+ });
+ } catch (err1) {
+ console.warn('Intento 1 falló, probando configuración más simple:', err1.message);
+ try {
+ // Intento 2: Configuración minimalista
+ stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+ } catch (err2) {
+ console.error('Todos los intentos fallaron');
+ throw err2;
+ }
+ }
+
+ if (!stream) {
+ throw new Error('No se pudo obtener acceso al micrófono');
+ }
+
this._mediaRecorder = new MediaRecorder(stream);
const chunks = [];
this._mediaRecorder.ondataavailable = (e) => { if (e.data && e.data.size) chunks.push(e.data); };
@@ -4114,8 +4134,29 @@ if (!isset($_SESSION['user_id'])) {
update();
this._recordingInterval = setInterval(update, 1000);
} catch (err) {
- console.error('startRecording error', err);
- alert('No se pudo acceder al micrófono: ' + (err.message||err));
+ console.error('startRecording error', err.name, err.message);
+
+ // Mensajes de error más específicos
+ let errorMsg = 'No se pudo acceder al micrófono';
+
+ if (err.name === 'NotFoundError') {
+ errorMsg = 'No se encontró ningún micrófono. Conecta un micrófono e intenta de nuevo.';
+ } else if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
+ errorMsg = 'Permiso denegado. Permite el acceso al micrófono en la configuración del navegador.';
+ } else if (err.name === 'NotReadableError') {
+ errorMsg = 'El micrófono está siendo usado por otra aplicación.';
+ } else if (err.message) {
+ errorMsg += ': ' + err.message;
+ }
+
+ alert(errorMsg);
+
+ // Limpiar estado
+ this._suspendAutoRefresh = false;
+ const ind = document.getElementById('recording-indicator');
+ if (ind) ind.style.display = 'none';
+ const micEl = document.getElementById('mic-btn');
+ if (micEl) micEl.classList.remove('recording', 'recording-cancel');
}
};
diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf
index ebbfca1..3ca4fca 100644
--- a/docker/nginx/default.conf
+++ b/docker/nginx/default.conf
@@ -5,6 +5,11 @@ server {
root /var/www/html;
index index.php index.html;
+ # 🚫 CACHE DESACTIVADO GLOBALMENTE
+ add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
+ add_header Pragma "no-cache" always;
+ add_header Expires "0" always;
+
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
@@ -79,10 +84,12 @@ server {
access_log off;
}
- # Static assets caching
+ # Static assets - SIN CACHE para desarrollo
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
- expires 30d;
- add_header Cache-Control "public, immutable";
+ # CACHE DESACTIVADO
+ add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0" always;
+ add_header Pragma "no-cache" always;
+ expires off;
access_log off;
}
@@ -105,10 +112,12 @@ server {
alias /var/www/html/uploads/;
autoindex off;
- # Security: solo permitir ciertos tipos de archivo
+ # Security: solo permitir ciertos tipos de archivo - SIN CACHE
location ~* \.(jpg|jpeg|png|gif|pdf|doc|docx|xls|xlsx|mp4|mp3|webp)$ {
- expires 7d;
- add_header Cache-Control "public";
+ # CACHE DESACTIVADO
+ add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0" always;
+ add_header Pragma "no-cache" always;
+ expires off;
}
# Denegar ejecución de PHP en uploads
diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf
index 2f70637..2d0d30e 100644
--- a/docker/nginx/nginx.conf
+++ b/docker/nginx/nginx.conf
@@ -51,9 +51,9 @@ http {
client_header_timeout 12;
send_timeout 10;
- # FastCGI cache (opcional)
- fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=whatsapp_cache:10m
- max_size=100m inactive=60m use_temp_path=off;
+ # FastCGI cache DESACTIVADO para desarrollo
+ # fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=whatsapp_cache:10m
+ # max_size=100m inactive=60m use_temp_path=off;
# Include virtual hosts
include /etc/nginx/http.d/*.conf;
diff --git a/docker/nginx/ssl.conf b/docker/nginx/ssl.conf
index 2403e84..bcf181e 100644
--- a/docker/nginx/ssl.conf
+++ b/docker/nginx/ssl.conf
@@ -1,7 +1,8 @@
# Configuración SSL para bot.u-s.app
server {
- listen 443 ssl http2;
- listen [::]:443 ssl http2;
+ listen 443 ssl;
+ listen [::]:443 ssl;
+ http2 on;
server_name bot.u-s.app localhost;
root /var/www/html;
index index.php index.html;
diff --git a/manifest.json b/manifest.json
deleted file mode 100644
index dec063b..0000000
--- a/manifest.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "name": "WhatsApp Bot Manager",
- "short_name": "WA Bot",
- "start_url": "/",
- "display": "standalone",
- "background_color": "#ffffff",
- "theme_color": "#10b981",
- "icons": [
- {
- "src": "/assets/icons/icon-192.png",
- "sizes": "192x192",
- "type": "image/png"
- }
- ]
-}
diff --git a/service-worker.js b/service-worker.js
deleted file mode 100644
index 0dd1a59..0000000
--- a/service-worker.js
+++ /dev/null
@@ -1,43 +0,0 @@
-// Service worker - Force no-cache for API requests
-const CACHE_VERSION = 'v1.0.1';
-
-self.addEventListener('install', (event) => {
- // activate immediately
- self.skipWaiting();
-});
-
-self.addEventListener('activate', (event) => {
- event.waitUntil(
- caches.keys().then(keys => {
- // Clear all caches on activation
- return Promise.all(keys.map(key => caches.delete(key)));
- }).then(() => self.clients.claim())
- );
-});
-
-self.addEventListener('fetch', (event) => {
- const url = new URL(event.request.url);
-
- // For API requests, always go to network with no-cache
- if (url.pathname.startsWith('/api/') || url.pathname.includes('/api/')) {
- event.respondWith(
- fetch(event.request, {
- cache: 'no-store',
- headers: {
- ...Object.fromEntries(event.request.headers),
- 'Cache-Control': 'no-cache, no-store, must-revalidate',
- 'Pragma': 'no-cache'
- }
- }).catch(err => {
- console.error('SW fetch error:', err);
- return new Response(JSON.stringify({ error: 'Network error' }), {
- status: 503,
- headers: { 'Content-Type': 'application/json' }
- });
- })
- );
- return;
- }
-
- // For other requests, use default network behavior (no caching)
-});