This commit is contained in:
Lizandro Guarnizo
2026-01-29 11:40:04 -05:00
parent 1e936be593
commit 44423adb32
8 changed files with 146 additions and 145 deletions
+8
View File
@@ -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
+1 -1
View File
@@ -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) {
+116 -75
View File
@@ -826,10 +826,6 @@ if (!isset($_SESSION['user_id'])) {
.unread-badge { margin-left: 8px; font-size: 12px; }
</style>
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#10b981">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
</head>
<body>
<div class="chat-container">
@@ -839,7 +835,7 @@ if (!isset($_SESSION['user_id'])) {
<div>
<h5 class="mb-0">💬 Conversaciones
<span class="badge bg-warning text-dark ms-2" style="font-size: 9px; padding: 2px 6px; animation: pulse 2s infinite;">
v1.2.1-<?php echo substr(time(), -4); ?>
v1.2.2-<?php echo substr(time(), -4); ?>
</span>
</h5>
</div>
@@ -910,8 +906,6 @@ if (!isset($_SESSION['user_id'])) {
</div>
<!-- Botón actualizar mensajes -->
<button class="btn btn-sm btn-outline-light" id="refresh-messages-btn" title="Actualizar mensajes"><i class="fas fa-sync-alt"></i></button>
<!-- PWA install button for conversations view -->
<button id="pwa-install-btn" class="btn btn-sm btn-outline-primary d-none" style="display:none;">Instalar</button>
<button class="btn btn-sm btn-outline-secondary" id="mark-unread-btn" title="Marcar conversación como no leída" style="display:none;">Marcar no leído</button>
<button class="btn btn-sm btn-outline-danger" id="delete-conversation-btn" title="Eliminar conversación" style="display:none;"><i class="fas fa-trash"></i></button>
</div>
@@ -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');
}
};
+15 -6
View File
@@ -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
+3 -3
View File
@@ -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;
+3 -2
View File
@@ -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;
-15
View File
@@ -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"
}
]
}
-43
View File
@@ -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)
});