feat: log de auditoría técnica por agente

Nueva tabla umind_eventos_log + tab "Auditoría" en el panel: errores y
eventos que hasta ahora solo se podían ver pidiendo los logs del servidor
(fallos al llamar al AI, respuestas sin texto usable, tools/webhooks que
fallan, tokens de correo que no se pueden refrescar, firmas de WhatsApp
inválidas, errores procesando mensajes de Telegram/WhatsApp) quedan
registrados por agente, visibles directo en el panel con el JSON crudo
expandible para diagnosticar sin acceso al servidor.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-13 09:58:59 -05:00
co-authored by Claude Sonnet 5
parent 5febbd24d5
commit 997e3cc790
13 changed files with 157 additions and 31 deletions
+43 -2
View File
@@ -273,11 +273,33 @@ const tabs = [
['conexiones', 'Conexiones'],
['chat', 'Chat de prueba'],
['conversaciones', 'Conversaciones'],
['auditoria', 'Auditoría'],
]
// ─── Auditoría ────────────────────────────────────────────────────────────────
const eventos = ref([])
async function cargarEventos() {
const r = await api.get(`/app/umind/eventos?agente_id=${props.agenteId}`)
eventos.value = r.items || []
}
const nivelColor = computed(() => (nivel) => ({
error: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-400',
warn: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-400',
}[nivel] || 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400'))
function formatearFecha(iso) {
try {
return new Date(iso).toLocaleString()
} catch {
return iso
}
}
onMounted(async () => {
try {
await Promise.all([cargarAgente(), cargarDocumentos(), cargarSesiones(), cargarTools(), cargarCanales(), cargarConexiones()])
await Promise.all([cargarAgente(), cargarDocumentos(), cargarSesiones(), cargarTools(), cargarCanales(), cargarConexiones(), cargarEventos()])
} catch (e) {
error.value = e.message
}
@@ -583,7 +605,7 @@ onMounted(async () => {
</div>
<!-- Conversaciones -->
<div v-else class="grid grid-cols-3 gap-4">
<div v-else-if="tab === 'conversaciones'" class="grid grid-cols-3 gap-4">
<div class="col-span-1 bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 divide-y divide-gray-100 dark:divide-gray-800 max-h-[28rem] overflow-y-auto">
<div v-if="sesiones.length === 0" class="p-4 text-sm text-gray-500 dark:text-gray-400">Sin conversaciones.</div>
<button
@@ -609,5 +631,24 @@ onMounted(async () => {
</div>
</div>
</div>
<!-- Auditoría -->
<div v-else>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-4">
Errores y eventos técnicos de este agente fallos al llamar al AI, a tools, a correo o a los canales. Últimos 100.
</p>
<div class="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 divide-y divide-gray-100 dark:divide-gray-800 max-h-[32rem] overflow-y-auto">
<div v-if="eventos.length === 0" class="p-6 text-sm text-gray-500 dark:text-gray-400">Sin eventos registrados buena señal.</div>
<details v-for="e in eventos" :key="e.ID" class="p-3">
<summary class="cursor-pointer flex items-center gap-2 text-sm">
<span class="px-1.5 py-0.5 rounded text-xs shrink-0" :class="nivelColor(e.nivel)">{{ e.nivel }}</span>
<span class="text-gray-400 dark:text-gray-500 text-xs shrink-0">{{ e.origen }}</span>
<span class="text-gray-800 dark:text-gray-200 truncate">{{ e.mensaje }}</span>
<span class="text-gray-400 dark:text-gray-500 text-xs ml-auto shrink-0">{{ formatearFecha(e.CreatedAt) }}</span>
</summary>
<pre v-if="e.detalle" class="mt-2 bg-gray-50 dark:bg-gray-950 border border-gray-200 dark:border-gray-800 rounded-lg p-2 text-xs text-gray-600 dark:text-gray-400 overflow-x-auto whitespace-pre-wrap">{{ e.detalle }}</pre>
</details>
</div>
</div>
</div>
</template>