Feature: debug buttons + SSE realtime updates
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 51s

- 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
This commit is contained in:
2025-12-02 20:18:05 -06:00
parent 1b5317845d
commit 26f755926b
4 changed files with 267 additions and 25 deletions

View File

@@ -1,4 +1,5 @@
<template>
<div class="relative">
<div
class="flex items-center gap-3 p-3 cursor-pointer transition-colors hover:bg-[var(--wa-bg-light)]"
:class="{ 'bg-[var(--wa-bg-light)]': active }"
@@ -13,7 +14,17 @@
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between">
<p class="font-medium text-[var(--wa-text)] truncate">{{ chat.name }}</p>
<div class="flex items-center gap-1">
<span class="text-xs text-[var(--wa-text-muted)]">{{ formatTime(chat.lastMessageAt) }}</span>
<!-- Debug button -->
<button
@click.stop="showDebug = !showDebug"
class="text-xs text-[var(--wa-text-muted)] hover:text-[var(--wa-blue)] opacity-50 hover:opacity-100"
title="Debug info"
>
<UIcon name="i-lucide-bug" class="w-3 h-3" />
</button>
</div>
</div>
<div class="flex items-center justify-between">
<p class="text-sm text-[var(--wa-text-muted)] truncate">{{ chat.lastMessage }}</p>
@@ -26,6 +37,15 @@
</div>
</div>
</div>
<!-- Debug panel -->
<div
v-if="showDebug"
class="mx-2 mb-2 p-2 rounded bg-gray-900 border border-gray-700 text-xs font-mono overflow-x-auto"
>
<pre class="text-green-400 whitespace-pre-wrap">{{ JSON.stringify(chat, null, 2) }}</pre>
</div>
</div>
</template>
<script setup lang="ts">
@@ -49,6 +69,8 @@ defineEmits<{
click: []
}>()
const showDebug = ref(false)
const formatTime = (date: Date) => {
const d = new Date(date)
const now = new Date()

View File

@@ -1,7 +1,7 @@
<template>
<div
class="flex"
:class="message.fromMe ? 'justify-end' : 'justify-start'"
class="flex flex-col"
:class="message.fromMe ? 'items-end' : 'items-start'"
>
<div
class="max-w-[70%] rounded-lg px-3 py-2"
@@ -36,8 +36,24 @@
class="w-4 h-4"
:class="statusColor"
/>
<!-- Debug button -->
<button
@click="showDebug = !showDebug"
class="ml-1 text-xs text-[var(--wa-text-muted)] hover:text-[var(--wa-blue)] opacity-50 hover:opacity-100"
title="Debug info"
>
<UIcon name="i-lucide-bug" class="w-3 h-3" />
</button>
</div>
</div>
<!-- Debug panel -->
<div
v-if="showDebug"
class="max-w-[90%] mt-1 p-2 rounded bg-gray-900 border border-gray-700 text-xs font-mono overflow-x-auto"
>
<pre class="text-green-400 whitespace-pre-wrap">{{ JSON.stringify(message, null, 2) }}</pre>
</div>
</div>
</template>
@@ -59,6 +75,8 @@ interface Props {
const props = defineProps<Props>()
const showDebug = ref(false)
const formatTime = (date: Date) => {
return new Date(date).toLocaleTimeString('es-AR', {
hour: '2-digit',

View File

@@ -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<EventSource | null>('sse', () => null)
const isConnected = useState('sseConnected', () => false)
const lastEvent = useState<{ type: string; data: any; timestamp: Date } | null>('sseLastEvent', () => null)
// Event listeners
const listeners = useState<Map<string, Set<Function>>>('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 = <K extends keyof RealtimeEvents>(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 = <K extends keyof RealtimeEvents>(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
}
}

View File

@@ -6,6 +6,36 @@
<h1 class="text-2xl font-bold text-[var(--wa-text)]">Mensajes</h1>
<p class="text-[var(--wa-text-muted)]">Vista de conversaciones de todas las instancias</p>
</div>
<div class="flex items-center gap-2">
<!-- SSE Status -->
<span
class="flex items-center gap-1 text-xs px-2 py-1 rounded"
:class="isConnected ? 'bg-green-900/50 text-green-400' : 'bg-red-900/50 text-red-400'"
>
<span class="w-2 h-2 rounded-full" :class="isConnected ? 'bg-green-400' : 'bg-red-400'" />
{{ isConnected ? 'Realtime' : 'Offline' }}
</span>
<!-- Debug toggle -->
<button
@click="showDebugPanel = !showDebugPanel"
class="text-xs px-2 py-1 rounded bg-gray-700 hover:bg-gray-600 text-gray-300"
>
<UIcon name="i-lucide-bug" class="w-4 h-4" />
</button>
</div>
</div>
<!-- Debug Panel -->
<div
v-if="showDebugPanel"
class="p-4 rounded bg-gray-900 border border-gray-700 text-xs font-mono"
>
<div class="flex items-center justify-between mb-2">
<span class="text-gray-400">Last SSE Event:</span>
<span class="text-gray-500">{{ lastEvent?.timestamp?.toLocaleTimeString() || 'N/A' }}</span>
</div>
<pre v-if="lastEvent" class="text-green-400 whitespace-pre-wrap overflow-x-auto max-h-40">{{ JSON.stringify(lastEvent, null, 2) }}</pre>
<p v-else class="text-gray-500">No events received yet</p>
</div>
<!-- Instance Selector -->
@@ -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<any[]>([])
const messages = ref<any[]>([])
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
})
})
</script>