Feature: debug buttons + SSE realtime updates
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 51s
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:
@@ -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()
|
||||
|
||||
@@ -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',
|
||||
|
||||
130
app/composables/useRealtime.ts
Normal file
130
app/composables/useRealtime.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user