Feature: Receptor de webhooks interno para debug
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m3s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m3s
- Endpoint POST /api/debug/webhook-receiver para recibir webhooks - Almacenamiento en memoria de ultimos 100 eventos - Endpoint GET/DELETE para consultar/limpiar eventos - Nueva tab Webhooks en seccion Debug con polling cada 5s
This commit is contained in:
181
app/components/debug/WebhookReceiverSection.vue
Normal file
181
app/components/debug/WebhookReceiverSection.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<div class="space-y-6 p-4">
|
||||
<!-- Setup Info -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-medium text-[var(--wa-text)]">Receptor de Webhooks</h3>
|
||||
<p class="text-sm text-[var(--wa-text-muted)]">
|
||||
Configura un webhook con la siguiente URL para recibir eventos aqui:
|
||||
</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="flex-1 p-3 rounded bg-gray-900 text-green-400 text-sm font-mono">
|
||||
{{ receiverUrl }}
|
||||
</code>
|
||||
<button
|
||||
@click="copyToClipboard(receiverUrl)"
|
||||
class="px-3 py-3 rounded bg-gray-700 hover:bg-gray-600 text-gray-300"
|
||||
title="Copiar URL"
|
||||
>
|
||||
<UIcon name="i-lucide-copy" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-[var(--wa-border)]" />
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<UButton
|
||||
:loading="loading"
|
||||
variant="soft"
|
||||
@click="fetchEvents"
|
||||
>
|
||||
<UIcon name="i-lucide-refresh-cw" class="w-4 h-4 mr-2" />
|
||||
Actualizar
|
||||
</UButton>
|
||||
<UButton
|
||||
variant="soft"
|
||||
color="error"
|
||||
:disabled="events.length === 0"
|
||||
@click="clearEvents"
|
||||
>
|
||||
<UIcon name="i-lucide-trash-2" class="w-4 h-4 mr-2" />
|
||||
Limpiar
|
||||
</UButton>
|
||||
</div>
|
||||
<span class="text-sm text-[var(--wa-text-muted)]">
|
||||
{{ events.length }} eventos
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Events List -->
|
||||
<div v-if="events.length === 0" class="text-center py-8">
|
||||
<UIcon name="i-lucide-webhook" class="w-12 h-12 text-[var(--wa-text-muted)] mx-auto mb-3" />
|
||||
<p class="text-[var(--wa-text-muted)]">No hay eventos recibidos</p>
|
||||
<p class="text-sm text-[var(--wa-text-muted)] mt-1">
|
||||
Configura un webhook apuntando a la URL de arriba
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-3 max-h-[500px] overflow-y-auto">
|
||||
<div
|
||||
v-for="event in events"
|
||||
:key="event.id"
|
||||
class="p-3 rounded bg-gray-900 border border-gray-700"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-2 py-1 rounded bg-blue-900/50 text-blue-400 text-xs font-medium">
|
||||
{{ event.event }}
|
||||
</span>
|
||||
<span class="text-xs text-[var(--wa-text-muted)]">
|
||||
{{ formatTime(event.receivedAt) }}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
@click="copyToClipboard(JSON.stringify(event, null, 2))"
|
||||
class="px-2 py-1 rounded bg-gray-700 hover:bg-gray-600 text-gray-300"
|
||||
title="Copiar evento"
|
||||
>
|
||||
<UIcon name="i-lucide-copy" class="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<button
|
||||
@click="toggleExpand(event.id)"
|
||||
class="text-xs text-[var(--wa-text-muted)] hover:text-[var(--wa-text)] mb-1"
|
||||
>
|
||||
{{ expandedEvents.has(event.id) ? 'Colapsar' : 'Expandir' }} datos
|
||||
</button>
|
||||
<pre
|
||||
v-if="expandedEvents.has(event.id)"
|
||||
class="text-xs font-mono text-green-400 whitespace-pre-wrap overflow-x-auto max-h-60 mt-2"
|
||||
>{{ JSON.stringify(event.data, null, 2) }}</pre>
|
||||
<pre
|
||||
v-else
|
||||
class="text-xs font-mono text-green-400 truncate"
|
||||
>{{ JSON.stringify(event.data) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface WebhookEvent {
|
||||
id: string
|
||||
receivedAt: string
|
||||
event: string
|
||||
timestamp?: string
|
||||
data: any
|
||||
headers: Record<string, string | undefined>
|
||||
}
|
||||
|
||||
const events = ref<WebhookEvent[]>([])
|
||||
const loading = ref(false)
|
||||
const expandedEvents = ref(new Set<string>())
|
||||
|
||||
// Build receiver URL
|
||||
const receiverUrl = computed(() => {
|
||||
if (import.meta.client) {
|
||||
return `${window.location.origin}/api/debug/webhook-receiver`
|
||||
}
|
||||
return '/api/debug/webhook-receiver'
|
||||
})
|
||||
|
||||
const fetchEvents = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await $fetch<{ events: WebhookEvent[] }>('/api/debug/webhook-events')
|
||||
events.value = result.events
|
||||
} catch (error) {
|
||||
console.error('Error fetching events:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const clearEvents = async () => {
|
||||
try {
|
||||
await $fetch('/api/debug/webhook-events', { method: 'DELETE' })
|
||||
events.value = []
|
||||
expandedEvents.value.clear()
|
||||
} catch (error) {
|
||||
console.error('Error clearing events:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleExpand = (id: string) => {
|
||||
if (expandedEvents.value.has(id)) {
|
||||
expandedEvents.value.delete(id)
|
||||
} else {
|
||||
expandedEvents.value.add(id)
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (dateStr: string) => {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleTimeString('es-AR', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch events on mount and set up polling
|
||||
onMounted(() => {
|
||||
fetchEvents()
|
||||
|
||||
// Poll every 5 seconds
|
||||
const interval = setInterval(fetchEvents, 5000)
|
||||
onUnmounted(() => clearInterval(interval))
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user