From 26f755926b43b9b50c29383b9abb0b6a34565b80 Mon Sep 17 00:00:00 2001 From: josedario87 Date: Tue, 2 Dec 2025 20:18:05 -0600 Subject: [PATCH] Feature: debug buttons + SSE realtime updates - Agregar boton debug en MessageBubble para ver objeto completo - Agregar boton debug en ChatItem para ver objeto completo - Crear useRealtime composable para conectar a SSE - Agregar indicador de estado SSE en mensajes - Agregar panel debug para ver ultimo evento SSE - Auto-recargar chats/mensajes cuando llegan nuevos mensajes --- app/components/messages/ChatItem.vue | 68 +++++++---- app/components/messages/MessageBubble.vue | 22 +++- app/composables/useRealtime.ts | 130 ++++++++++++++++++++++ app/pages/messages/index.vue | 72 ++++++++++++ 4 files changed, 267 insertions(+), 25 deletions(-) create mode 100644 app/composables/useRealtime.ts diff --git a/app/components/messages/ChatItem.vue b/app/components/messages/ChatItem.vue index b05a8a8..553c296 100644 --- a/app/components/messages/ChatItem.vue +++ b/app/components/messages/ChatItem.vue @@ -1,30 +1,50 @@ @@ -49,6 +69,8 @@ defineEmits<{ click: [] }>() +const showDebug = ref(false) + const formatTime = (date: Date) => { const d = new Date(date) const now = new Date() diff --git a/app/components/messages/MessageBubble.vue b/app/components/messages/MessageBubble.vue index 097d113..25c643f 100644 --- a/app/components/messages/MessageBubble.vue +++ b/app/components/messages/MessageBubble.vue @@ -1,7 +1,7 @@ @@ -59,6 +75,8 @@ interface Props { const props = defineProps() +const showDebug = ref(false) + const formatTime = (date: Date) => { return new Date(date).toLocaleTimeString('es-AR', { hour: '2-digit', diff --git a/app/composables/useRealtime.ts b/app/composables/useRealtime.ts new file mode 100644 index 0000000..24ebac2 --- /dev/null +++ b/app/composables/useRealtime.ts @@ -0,0 +1,130 @@ +/** + * Composable for real-time updates via Server-Sent Events + */ + +export interface RealtimeEvents { + 'message.received': (data: { instanceId: string; message: any }) => void + 'message.sent': (data: { instanceId: string; message: any }) => void + 'message.status': (data: { instanceId: string; messageId: string; status: string }) => void + 'instance.status': (data: { instanceId: string; status: string; phoneNumber?: string }) => void + 'instance.qr': (data: { instanceId: string; qr: string; qrDataUrl: string }) => void +} + +export const useRealtime = () => { + const eventSource = useState('sse', () => null) + const isConnected = useState('sseConnected', () => false) + const lastEvent = useState<{ type: string; data: any; timestamp: Date } | null>('sseLastEvent', () => null) + + // Event listeners + const listeners = useState>>('sseListeners', () => new Map()) + + const connect = () => { + // Only run on client + if (import.meta.server) return + + // Already connected + if (eventSource.value?.readyState === EventSource.OPEN) return + + console.log('[SSE] Connecting to /api/events/stream...') + + const es = new EventSource('/api/events/stream') + + es.onopen = () => { + console.log('[SSE] Connected') + isConnected.value = true + } + + es.onerror = (error) => { + console.error('[SSE] Error:', error) + isConnected.value = false + + // Reconnect after 5 seconds + setTimeout(() => { + if (eventSource.value?.readyState === EventSource.CLOSED) { + connect() + } + }, 5000) + } + + // Listen for specific event types + const eventTypes = [ + 'message.received', + 'message.sent', + 'message.status', + 'instance.status', + 'instance.qr', + 'instance.pairing' + ] + + eventTypes.forEach(eventType => { + es.addEventListener(eventType, (e: MessageEvent) => { + try { + const data = JSON.parse(e.data) + console.log(`[SSE] ${eventType}:`, data) + + // Update last event for debugging + lastEvent.value = { type: eventType, data, timestamp: new Date() } + + // Call registered listeners + const eventListeners = listeners.value.get(eventType) + if (eventListeners) { + eventListeners.forEach(listener => listener(data)) + } + } catch (err) { + console.error(`[SSE] Error parsing ${eventType}:`, err) + } + }) + }) + + // Generic message handler for debugging + es.onmessage = (e) => { + console.log('[SSE] Generic message:', e.data) + } + + eventSource.value = es + } + + const disconnect = () => { + if (eventSource.value) { + eventSource.value.close() + eventSource.value = null + isConnected.value = false + console.log('[SSE] Disconnected') + } + } + + const on = (event: K, handler: RealtimeEvents[K]) => { + if (!listeners.value.has(event)) { + listeners.value.set(event, new Set()) + } + listeners.value.get(event)!.add(handler as Function) + + // Return cleanup function + return () => { + listeners.value.get(event)?.delete(handler as Function) + } + } + + const off = (event: K, handler: RealtimeEvents[K]) => { + listeners.value.get(event)?.delete(handler as Function) + } + + // Auto-connect on mount, disconnect on unmount + onMounted(() => { + connect() + }) + + onUnmounted(() => { + // Don't disconnect on unmount as other components may still need it + // disconnect() + }) + + return { + isConnected, + lastEvent, + connect, + disconnect, + on, + off + } +} diff --git a/app/pages/messages/index.vue b/app/pages/messages/index.vue index 8f229c4..0b85c59 100644 --- a/app/pages/messages/index.vue +++ b/app/pages/messages/index.vue @@ -6,6 +6,36 @@

Mensajes

+
+ + + + {{ isConnected ? 'Realtime' : 'Offline' }} + + + +
+ + + +
+
+ Last SSE Event: + {{ lastEvent?.timestamp?.toLocaleTimeString() || 'N/A' }} +
+
{{ JSON.stringify(lastEvent, null, 2) }}
+

No events received yet

@@ -94,6 +124,7 @@ definePageMeta({ }) const { instances, fetchInstances } = useInstances() +const { isConnected, lastEvent, on } = useRealtime() const selectedInstance = ref<{ label: string; value: string } | null>(null) const searchQuery = ref('') @@ -102,6 +133,7 @@ const chats = ref([]) const messages = ref([]) const loadingChats = ref(false) const loadingMessages = ref(false) +const showDebugPanel = ref(false) // Instance options for selector const instanceOptions = computed(() => @@ -176,8 +208,48 @@ const handleSendMessage = async (content: string) => { } } +// Reload chats for current instance +const reloadChats = async () => { + if (!selectedInstance.value?.value) return + try { + chats.value = await $fetch(`/api/messages/${selectedInstance.value.value}/chats`) + } catch (e) { + console.error('Error reloading chats:', e) + } +} + +// Reload messages for current chat +const reloadMessages = async () => { + if (!selectedInstance.value?.value || !selectedChat.value) return + try { + messages.value = await $fetch(`/api/messages/${selectedInstance.value.value}/${selectedChat.value.id}`) + } catch (e) { + console.error('Error reloading messages:', e) + } +} + // Load instances on mount onMounted(() => { fetchInstances() + + // Listen for real-time message events + on('message.received', (data) => { + console.log('[Messages] New message received:', data) + + // If it's for the current instance, reload chats + if (data.instanceId === selectedInstance.value?.value) { + reloadChats() + + // If it's for the current chat, reload messages + if (selectedChat.value?.jid === data.message?.key?.remoteJid) { + reloadMessages() + } + } + }) + + on('message.sent', (data) => { + console.log('[Messages] Message sent:', data) + // Already handled by the send function + }) })