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) -});