Docs: Script para scrapear documentacion de Baileys API
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m3s

- Script en scripts/scrape-baileys-docs.ts
- Genera docs/baileys-api-reference.md con 6433 lineas
- 79 secciones: interfaces, types, functions, variables, enums
- Referencia completa para desarrollo de mensajes
This commit is contained in:
2025-12-02 20:49:59 -06:00
parent ae8e4e37a7
commit 9f2f3ac510
19 changed files with 8117 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
<template>
<div class="space-y-6 p-4">
<!-- Fetch Blocklist -->
<div class="space-y-4">
<h3 class="text-lg font-medium text-[var(--wa-text)]">Obtener Lista de Bloqueados</h3>
<UButton
:loading="loadingFetch"
:disabled="!instanceId"
@click="fetchBlocklist"
>
Obtener Blocklist
</UButton>
</div>
<hr class="border-[var(--wa-border)]" />
<!-- Block/Unblock -->
<div class="space-y-4">
<h3 class="text-lg font-medium text-[var(--wa-text)]">Bloquear / Desbloquear</h3>
<div class="grid grid-cols-2 gap-4">
<UInput
v-model="jid"
placeholder="JID (ej: 5491155551234@s.whatsapp.net)"
class="col-span-2"
/>
<USelectMenu
v-model="action"
:items="actionOptions"
placeholder="Accion"
/>
</div>
<UButton
:loading="loadingUpdate"
:disabled="!instanceId || !jid || !action"
@click="updateBlockStatus"
>
Ejecutar
</UButton>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
instanceId: string | null
}>()
const emit = defineEmits<{
(e: 'response', data: any): void
}>()
const jid = ref('')
const action = ref<{ label: string; value: string } | null>(null)
const loadingFetch = ref(false)
const loadingUpdate = ref(false)
const actionOptions = [
{ label: 'Bloquear', value: 'block' },
{ label: 'Desbloquear', value: 'unblock' },
]
const fetchBlocklist = async () => {
loadingFetch.value = true
try {
const result = await $fetch(`/api/debug/blocklist?instanceId=${props.instanceId}`)
emit('response', result)
} catch (error: any) {
emit('response', { success: false, error: error.data?.message || error.message })
} finally {
loadingFetch.value = false
}
}
const updateBlockStatus = async () => {
if (!action.value) return
loadingUpdate.value = true
try {
const result = await $fetch('/api/debug/blocklist/update', {
method: 'POST',
body: {
instanceId: props.instanceId,
jid: jid.value,
action: action.value.value
}
})
emit('response', result)
} catch (error: any) {
emit('response', { success: false, error: error.data?.message || error.message })
} finally {
loadingUpdate.value = false
}
}
</script>

View File

@@ -0,0 +1,131 @@
<template>
<div class="space-y-6 p-4">
<!-- Chat Modify -->
<div class="space-y-4">
<h3 class="text-lg font-medium text-[var(--wa-text)]">Modificar Chat</h3>
<p class="text-sm text-[var(--wa-text-muted)]">
Modifica el estado de un chat (archivar, silenciar, marcar como leido, fijar).
</p>
<div class="grid grid-cols-2 gap-4">
<UInput
v-model="jid"
placeholder="JID del chat"
class="col-span-2"
/>
<USelectMenu
v-model="modificationType"
:items="modificationTypes"
placeholder="Tipo de modificacion"
/>
<template v-if="modificationType?.value === 'mute'">
<UInput
v-model.number="muteTimestamp"
type="number"
placeholder="Timestamp de expiracion (0 = unmute)"
/>
</template>
<template v-else>
<USelectMenu
v-model="boolValue"
:items="boolOptions"
placeholder="Valor"
/>
</template>
</div>
<div v-if="requiresLastMessages" class="space-y-2">
<p class="text-sm text-[var(--wa-text-muted)]">Last Messages (JSON array, requerido para archive/markRead):</p>
<UTextarea
v-model="lastMessagesJson"
placeholder='[{"key": {"remoteJid": "...", "id": "..."}, "messageTimestamp": 123456}]'
:rows="3"
class="font-mono text-sm"
/>
</div>
<UButton
:loading="loading"
:disabled="!instanceId || !jid || !modificationType"
@click="modifyChat"
>
Modificar Chat
</UButton>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
instanceId: string | null
}>()
const emit = defineEmits<{
(e: 'response', data: any): void
}>()
const jid = ref('')
const modificationType = ref<{ label: string; value: string } | null>(null)
const boolValue = ref<{ label: string; value: boolean } | null>(null)
const muteTimestamp = ref<number>(0)
const lastMessagesJson = ref('')
const loading = ref(false)
const modificationTypes = [
{ label: 'Archivar', value: 'archive' },
{ label: 'Silenciar', value: 'mute' },
{ label: 'Marcar Leido', value: 'markRead' },
{ label: 'Fijar', value: 'pin' },
]
const boolOptions = [
{ label: 'Activar (true)', value: true },
{ label: 'Desactivar (false)', value: false },
]
const requiresLastMessages = computed(() =>
modificationType.value?.value === 'archive' || modificationType.value?.value === 'markRead'
)
const modifyChat = async () => {
if (!modificationType.value) return
loading.value = true
try {
let value: boolean | number | null
let lastMessages: any[] = []
if (modificationType.value.value === 'mute') {
value = muteTimestamp.value || null
} else {
value = boolValue.value?.value ?? false
}
if (requiresLastMessages.value && lastMessagesJson.value.trim()) {
try {
lastMessages = JSON.parse(lastMessagesJson.value)
} catch {
emit('response', { success: false, error: 'Invalid JSON for lastMessages' })
loading.value = false
return
}
}
const result = await $fetch('/api/debug/chat/modify', {
method: 'POST',
body: {
instanceId: props.instanceId,
jid: jid.value,
type: modificationType.value.value,
value,
lastMessages
}
})
emit('response', result)
} catch (error: any) {
emit('response', { success: false, error: error.data?.message || error.message })
} finally {
loading.value = false
}
}
</script>

View File

@@ -0,0 +1,291 @@
<template>
<div class="space-y-6 p-4">
<!-- Get Metadata -->
<div class="space-y-4">
<h3 class="text-lg font-medium text-[var(--wa-text)]">Obtener Metadata del Grupo</h3>
<div class="flex gap-4">
<UInput
v-model="metadataJid"
placeholder="Group JID (ej: 123456789@g.us)"
class="flex-1"
/>
<UButton
:loading="loadingMetadata"
:disabled="!instanceId || !metadataJid"
@click="getMetadata"
>
Obtener
</UButton>
</div>
</div>
<hr class="border-[var(--wa-border)]" />
<!-- Get Invite Code -->
<div class="space-y-4">
<h3 class="text-lg font-medium text-[var(--wa-text)]">Obtener Codigo de Invitacion</h3>
<div class="flex gap-4">
<UInput
v-model="inviteJid"
placeholder="Group JID"
class="flex-1"
/>
<UButton
:loading="loadingInvite"
:disabled="!instanceId || !inviteJid"
@click="getInviteCode"
>
Obtener
</UButton>
</div>
</div>
<hr class="border-[var(--wa-border)]" />
<!-- Create Group -->
<div class="space-y-4">
<h3 class="text-lg font-medium text-[var(--wa-text)]">Crear Grupo</h3>
<div class="grid grid-cols-2 gap-4">
<UInput
v-model="groupName"
placeholder="Nombre del grupo"
/>
<UInput
v-model="groupParticipants"
placeholder="Participantes (separados por coma)"
/>
</div>
<UButton
:loading="loadingCreate"
:disabled="!instanceId || !groupName || !groupParticipants"
@click="createGroup"
>
Crear Grupo
</UButton>
</div>
<hr class="border-[var(--wa-border)]" />
<!-- Update Participants -->
<div class="space-y-4">
<h3 class="text-lg font-medium text-[var(--wa-text)]">Gestionar Participantes</h3>
<div class="grid grid-cols-2 gap-4">
<UInput
v-model="participantsJid"
placeholder="Group JID"
/>
<UInput
v-model="participantsList"
placeholder="Participantes (separados por coma)"
/>
<USelectMenu
v-model="participantAction"
:items="participantActions"
placeholder="Accion"
class="col-span-2"
/>
</div>
<UButton
:loading="loadingParticipants"
:disabled="!instanceId || !participantsJid || !participantsList || !participantAction"
@click="updateParticipants"
>
Ejecutar
</UButton>
</div>
<hr class="border-[var(--wa-border)]" />
<!-- Update Subject -->
<div class="space-y-4">
<h3 class="text-lg font-medium text-[var(--wa-text)]">Cambiar Nombre del Grupo</h3>
<div class="grid grid-cols-2 gap-4">
<UInput
v-model="subjectJid"
placeholder="Group JID"
/>
<UInput
v-model="newSubject"
placeholder="Nuevo nombre"
/>
</div>
<UButton
:loading="loadingSubject"
:disabled="!instanceId || !subjectJid || !newSubject"
@click="updateSubject"
>
Actualizar Nombre
</UButton>
</div>
<hr class="border-[var(--wa-border)]" />
<!-- Update Description -->
<div class="space-y-4">
<h3 class="text-lg font-medium text-[var(--wa-text)]">Cambiar Descripcion del Grupo</h3>
<div class="grid grid-cols-1 gap-4">
<UInput
v-model="descriptionJid"
placeholder="Group JID"
/>
<UTextarea
v-model="newDescription"
placeholder="Nueva descripcion"
:rows="3"
/>
</div>
<UButton
:loading="loadingDescription"
:disabled="!instanceId || !descriptionJid"
@click="updateDescription"
>
Actualizar Descripcion
</UButton>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
instanceId: string | null
}>()
const emit = defineEmits<{
(e: 'response', data: any): void
}>()
// Metadata
const metadataJid = ref('')
const loadingMetadata = ref(false)
// Invite
const inviteJid = ref('')
const loadingInvite = ref(false)
// Create
const groupName = ref('')
const groupParticipants = ref('')
const loadingCreate = ref(false)
// Participants
const participantsJid = ref('')
const participantsList = ref('')
const participantAction = ref<{ label: string; value: string } | null>(null)
const loadingParticipants = ref(false)
// Subject
const subjectJid = ref('')
const newSubject = ref('')
const loadingSubject = ref(false)
// Description
const descriptionJid = ref('')
const newDescription = ref('')
const loadingDescription = ref(false)
const participantActions = [
{ label: 'Agregar', value: 'add' },
{ label: 'Remover', value: 'remove' },
{ label: 'Promover a Admin', value: 'promote' },
{ label: 'Degradar a Miembro', value: 'demote' },
]
const getMetadata = async () => {
loadingMetadata.value = true
try {
const result = await $fetch('/api/debug/groups/metadata', {
method: 'POST',
body: { instanceId: props.instanceId, jid: metadataJid.value }
})
emit('response', result)
} catch (error: any) {
emit('response', { success: false, error: error.data?.message || error.message })
} finally {
loadingMetadata.value = false
}
}
const getInviteCode = async () => {
loadingInvite.value = true
try {
const result = await $fetch('/api/debug/groups/invite-code', {
method: 'POST',
body: { instanceId: props.instanceId, jid: inviteJid.value }
})
emit('response', result)
} catch (error: any) {
emit('response', { success: false, error: error.data?.message || error.message })
} finally {
loadingInvite.value = false
}
}
const createGroup = async () => {
loadingCreate.value = true
try {
const participants = groupParticipants.value.split(',').map(p => p.trim()).filter(Boolean)
const result = await $fetch('/api/debug/groups/create', {
method: 'POST',
body: { instanceId: props.instanceId, name: groupName.value, participants }
})
emit('response', result)
} catch (error: any) {
emit('response', { success: false, error: error.data?.message || error.message })
} finally {
loadingCreate.value = false
}
}
const updateParticipants = async () => {
if (!participantAction.value) return
loadingParticipants.value = true
try {
const participants = participantsList.value.split(',').map(p => p.trim()).filter(Boolean)
const result = await $fetch('/api/debug/groups/participants', {
method: 'POST',
body: {
instanceId: props.instanceId,
jid: participantsJid.value,
participants,
action: participantAction.value.value
}
})
emit('response', result)
} catch (error: any) {
emit('response', { success: false, error: error.data?.message || error.message })
} finally {
loadingParticipants.value = false
}
}
const updateSubject = async () => {
loadingSubject.value = true
try {
const result = await $fetch('/api/debug/groups/subject', {
method: 'POST',
body: { instanceId: props.instanceId, jid: subjectJid.value, subject: newSubject.value }
})
emit('response', result)
} catch (error: any) {
emit('response', { success: false, error: error.data?.message || error.message })
} finally {
loadingSubject.value = false
}
}
const updateDescription = async () => {
loadingDescription.value = true
try {
const result = await $fetch('/api/debug/groups/description', {
method: 'POST',
body: { instanceId: props.instanceId, jid: descriptionJid.value, description: newDescription.value }
})
emit('response', result)
} catch (error: any) {
emit('response', { success: false, error: error.data?.message || error.message })
} finally {
loadingDescription.value = false
}
}
</script>

View File

@@ -0,0 +1,87 @@
<template>
<div class="space-y-6 p-4">
<!-- Fetch Message History -->
<div class="space-y-4">
<h3 class="text-lg font-medium text-[var(--wa-text)]">Solicitar Historial de Mensajes</h3>
<p class="text-sm text-[var(--wa-text-muted)]">
Solicita mensajes adicionales del historial. Los resultados llegaran via el evento messaging-history.set.
</p>
<div class="grid grid-cols-2 gap-4">
<UInput
v-model.number="count"
type="number"
placeholder="Cantidad de mensajes"
:min="1"
:max="1000"
/>
<UInput
v-model.number="oldestMsgTimestamp"
type="number"
placeholder="Timestamp mas antiguo (opcional)"
/>
</div>
<div class="space-y-2">
<p class="text-sm text-[var(--wa-text-muted)]">Oldest Message Key (opcional, JSON):</p>
<UTextarea
v-model="oldestMsgKeyJson"
placeholder='{"remoteJid": "...", "id": "...", "fromMe": false}'
:rows="3"
class="font-mono text-sm"
/>
</div>
<UButton
:loading="loading"
:disabled="!instanceId || !count"
@click="fetchHistory"
>
Solicitar Historial
</UButton>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
instanceId: string | null
}>()
const emit = defineEmits<{
(e: 'response', data: any): void
}>()
const count = ref<number>(50)
const oldestMsgTimestamp = ref<number | null>(null)
const oldestMsgKeyJson = ref('')
const loading = ref(false)
const fetchHistory = async () => {
loading.value = true
try {
let oldestMsgKey = undefined
if (oldestMsgKeyJson.value.trim()) {
try {
oldestMsgKey = JSON.parse(oldestMsgKeyJson.value)
} catch {
emit('response', { success: false, error: 'Invalid JSON for oldestMsgKey' })
loading.value = false
return
}
}
const result = await $fetch('/api/debug/history/fetch', {
method: 'POST',
body: {
instanceId: props.instanceId,
count: count.value,
oldestMsgKey,
oldestMsgTimestamp: oldestMsgTimestamp.value || undefined
}
})
emit('response', result)
} catch (error: any) {
emit('response', { success: false, error: error.data?.message || error.message })
} finally {
loading.value = false
}
}
</script>

View File

@@ -0,0 +1,95 @@
<template>
<div class="space-y-6 p-4">
<!-- Update Media Message -->
<div class="space-y-4">
<h3 class="text-lg font-medium text-[var(--wa-text)]">Actualizar Mensaje de Media</h3>
<p class="text-sm text-[var(--wa-text-muted)]">
Actualiza un mensaje de media expirado. Proporciona el objeto proto.IWebMessageInfo completo.
</p>
<div class="space-y-2">
<p class="text-sm text-[var(--wa-text-muted)]">Mensaje (JSON proto.IWebMessageInfo):</p>
<UTextarea
v-model="messageJson"
placeholder='{
"key": {
"remoteJid": "...",
"fromMe": false,
"id": "..."
},
"message": {
"imageMessage": {
"url": "...",
"mimetype": "image/jpeg",
...
}
},
"messageTimestamp": 123456789
}'
:rows="12"
class="font-mono text-sm"
/>
</div>
<UButton
:loading="loading"
:disabled="!instanceId || !messageJson"
@click="updateMedia"
>
Actualizar Media
</UButton>
</div>
<hr class="border-[var(--wa-border)]" />
<!-- Help -->
<div class="space-y-2">
<h4 class="text-md font-medium text-[var(--wa-text)]">Ayuda</h4>
<p class="text-sm text-[var(--wa-text-muted)]">
Esta funcion se usa para recargar media de mensajes cuyo contenido ha expirado.
Necesitas proporcionar el mensaje original completo (puedes obtenerlo de la base de datos
en la columna raw_message de la tabla messages).
</p>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
instanceId: string | null
}>()
const emit = defineEmits<{
(e: 'response', data: any): void
}>()
const messageJson = ref('')
const loading = ref(false)
const updateMedia = async () => {
loading.value = true
try {
let message: any
try {
message = JSON.parse(messageJson.value)
} catch {
emit('response', { success: false, error: 'Invalid JSON for message' })
loading.value = false
return
}
const result = await $fetch('/api/debug/media/update', {
method: 'POST',
body: {
instanceId: props.instanceId,
message
}
})
emit('response', result)
} catch (error: any) {
emit('response', { success: false, error: error.data?.message || error.message })
} finally {
loading.value = false
}
}
</script>

View File

@@ -0,0 +1,124 @@
<template>
<div class="space-y-6 p-4">
<!-- Fetch Privacy Settings -->
<div class="space-y-4">
<h3 class="text-lg font-medium text-[var(--wa-text)]">Obtener Configuracion de Privacidad</h3>
<UButton
:loading="loadingFetch"
:disabled="!instanceId"
@click="fetchPrivacy"
>
Obtener Configuracion
</UButton>
</div>
<hr class="border-[var(--wa-border)]" />
<!-- Update Privacy Setting -->
<div class="space-y-4">
<h3 class="text-lg font-medium text-[var(--wa-text)]">Actualizar Configuracion</h3>
<div class="grid grid-cols-2 gap-4">
<USelectMenu
v-model="setting"
:items="settingOptions"
placeholder="Configuracion"
/>
<USelectMenu
v-model="value"
:items="valueOptions"
placeholder="Valor"
/>
</div>
<UButton
:loading="loadingUpdate"
:disabled="!instanceId || !setting || !value"
@click="updatePrivacy"
>
Actualizar
</UButton>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
instanceId: string | null
}>()
const emit = defineEmits<{
(e: 'response', data: any): void
}>()
const setting = ref<{ label: string; value: string } | null>(null)
const value = ref<{ label: string; value: string } | null>(null)
const loadingFetch = ref(false)
const loadingUpdate = ref(false)
const settingOptions = [
{ label: 'Last Seen', value: 'lastSeen' },
{ label: 'Online', value: 'online' },
{ label: 'Profile Picture', value: 'profilePicture' },
{ label: 'Status', value: 'status' },
{ label: 'Groups Add', value: 'groupsAdd' },
{ label: 'Read Receipts', value: 'readReceipts' },
]
const valueOptions = computed(() => {
if (setting.value?.value === 'online') {
return [
{ label: 'all', value: 'all' },
{ label: 'match_last_seen', value: 'match_last_seen' },
]
}
if (setting.value?.value === 'readReceipts') {
return [
{ label: 'all', value: 'all' },
{ label: 'none', value: 'none' },
]
}
return [
{ label: 'all', value: 'all' },
{ label: 'contacts', value: 'contacts' },
{ label: 'contact_blacklist', value: 'contact_blacklist' },
{ label: 'none', value: 'none' },
]
})
// Reset value when setting changes
watch(setting, () => {
value.value = null
})
const fetchPrivacy = async () => {
loadingFetch.value = true
try {
const result = await $fetch(`/api/debug/privacy?instanceId=${props.instanceId}`)
emit('response', result)
} catch (error: any) {
emit('response', { success: false, error: error.data?.message || error.message })
} finally {
loadingFetch.value = false
}
}
const updatePrivacy = async () => {
if (!setting.value || !value.value) return
loadingUpdate.value = true
try {
const result = await $fetch('/api/debug/privacy/update', {
method: 'POST',
body: {
instanceId: props.instanceId,
setting: setting.value.value,
value: value.value.value
}
})
emit('response', result)
} catch (error: any) {
emit('response', { success: false, error: error.data?.message || error.message })
} finally {
loadingUpdate.value = false
}
}
</script>

View File

@@ -85,6 +85,7 @@ const menuItems = [
{ to: '/', label: 'Instancias', icon: 'i-lucide-smartphone' },
{ to: '/messages', label: 'Mensajes', icon: 'i-lucide-message-square' },
{ to: '/webhooks', label: 'Webhooks', icon: 'i-lucide-webhook' },
{ to: '/debug', label: 'Debug', icon: 'i-lucide-bug' },
]
// Close sidebar on route change (mobile)

141
app/pages/debug/index.vue Normal file
View File

@@ -0,0 +1,141 @@
<template>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-[var(--wa-text)]">Debug</h1>
<p class="text-[var(--wa-text-muted)]">Herramientas de depuracion para Baileys</p>
</div>
</div>
<!-- Warning Banner -->
<div class="p-4 rounded-lg bg-yellow-900/20 border border-yellow-600/50">
<div class="flex items-center gap-2 text-yellow-400">
<UIcon name="i-lucide-alert-triangle" class="w-5 h-5" />
<span class="font-medium">Modo Debug</span>
</div>
<p class="text-sm text-yellow-300/80 mt-1">
Estas funciones ejecutan comandos raw de Baileys. Usar con precaucion.
</p>
</div>
<!-- Instance Selector -->
<div class="flex items-center gap-4">
<USelectMenu
v-model="selectedInstance"
:items="instanceOptions"
placeholder="Seleccionar instancia conectada"
class="w-64"
/>
<span v-if="!selectedInstance" class="text-sm text-[var(--wa-text-muted)]">
Selecciona una instancia para usar las herramientas de debug
</span>
</div>
<!-- Tabs -->
<div v-if="selectedInstance" class="instance-card">
<UTabs :items="tabs" class="w-full">
<template #blocklist>
<DebugBlocklistSection
:instance-id="selectedInstance?.value"
@response="handleResponse"
/>
</template>
<template #privacy>
<DebugPrivacySection
:instance-id="selectedInstance?.value"
@response="handleResponse"
/>
</template>
<template #groups>
<DebugGroupsSection
:instance-id="selectedInstance?.value"
@response="handleResponse"
/>
</template>
<template #history>
<DebugHistorySection
:instance-id="selectedInstance?.value"
@response="handleResponse"
/>
</template>
<template #chat>
<DebugChatSection
:instance-id="selectedInstance?.value"
@response="handleResponse"
/>
</template>
<template #media>
<DebugMediaSection
:instance-id="selectedInstance?.value"
@response="handleResponse"
/>
</template>
</UTabs>
</div>
<!-- Response Viewer -->
<div v-if="lastResponse" class="instance-card p-4">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-[var(--wa-text)]">Ultima Respuesta</span>
<div class="flex items-center gap-2">
<span class="text-xs text-[var(--wa-text-muted)]">{{ lastResponseTime }}</span>
<UButton size="xs" variant="ghost" color="neutral" @click="lastResponse = null">
Limpiar
</UButton>
</div>
</div>
<pre
class="text-xs font-mono p-4 rounded overflow-auto max-h-96"
:class="lastResponse.success ? 'text-green-400 bg-green-900/20' : 'text-red-400 bg-red-900/20'"
>{{ JSON.stringify(lastResponse, null, 2) }}</pre>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'dashboard',
title: 'Debug',
icon: 'i-lucide-bug'
})
const { instances, fetchInstances } = useInstances()
const selectedInstance = ref<{ label: string; value: string } | null>(null)
const lastResponse = ref<any>(null)
const lastResponseTime = ref<string>('')
// Instance options for selector (only connected instances)
const instanceOptions = computed(() =>
instances.value
.filter(i => i.status === 'connected')
.map(i => ({ label: i.name, value: i.id }))
)
// Auto-select first connected instance
watch(instanceOptions, (opts) => {
if (opts.length > 0 && !selectedInstance.value) {
selectedInstance.value = opts[0]
}
}, { immediate: true })
const tabs = [
{ label: 'Blocklist', value: 'blocklist', icon: 'i-lucide-ban' },
{ label: 'Privacy', value: 'privacy', icon: 'i-lucide-shield' },
{ label: 'Groups', value: 'groups', icon: 'i-lucide-users' },
{ label: 'History', value: 'history', icon: 'i-lucide-history' },
{ label: 'Chat', value: 'chat', icon: 'i-lucide-message-circle' },
{ label: 'Media', value: 'media', icon: 'i-lucide-image' },
]
const handleResponse = (response: any) => {
lastResponse.value = response
lastResponseTime.value = new Date().toLocaleTimeString()
}
// Load instances on mount
onMounted(() => {
fetchInstances()
})
</script>