@innertia-solutions/innertia-nuxt 0.1.57 → 0.1.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -34,6 +34,9 @@ const STATUS = { open: 'Abierto', resolved: 'Resuelto', ignored: 'Ignorado' }
|
|
|
34
34
|
<div class="flex flex-wrap items-center gap-2">
|
|
35
35
|
<span v-if="data.is_critical" class="rounded-full bg-red-100 text-red-700 dark:bg-red-500/20 dark:text-red-300 px-2 py-0.5 text-xs font-semibold">Crítico</span>
|
|
36
36
|
<span class="rounded-full bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300 px-2 py-0.5 text-xs">{{ STATUS[data.case.status] ?? data.case.status }}</span>
|
|
37
|
+
<span v-if="data.case.origin" class="rounded-full bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300 px-2 py-0.5 text-xs">{{ ({http:'HTTP',queue:'Worker',schedule:'Cron',console:'Consola'})[data.case.origin] ?? data.case.origin }}</span>
|
|
38
|
+
<span v-if="data.case.category" class="rounded-full bg-indigo-100 text-indigo-700 dark:bg-indigo-500/20 dark:text-indigo-300 px-2 py-0.5 text-xs">{{ ({database:'BD',http:'HTTP',auth:'Auth',validation:'Validación',runtime:'Runtime'})[data.case.category] ?? data.case.category }}</span>
|
|
39
|
+
<span v-for="(v, k) in (data.case.tags ?? {})" :key="k" class="rounded-full bg-primary/10 text-primary px-2 py-0.5 text-xs">{{ k }}: {{ v }}</span>
|
|
37
40
|
<span class="text-xs text-muted-foreground">{{ data.case.times_seen }} ocurrencias</span>
|
|
38
41
|
</div>
|
|
39
42
|
<h2 class="mt-2 text-lg font-semibold text-foreground">{{ data.case.title }}</h2>
|
|
@@ -66,10 +69,23 @@ const STATUS = { open: 'Abierto', resolved: 'Resuelto', ignored: 'Ignorado' }
|
|
|
66
69
|
<h3 class="mb-2 text-sm font-semibold text-foreground">Contexto del request</h3>
|
|
67
70
|
<dl class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
|
|
68
71
|
<div><dt class="text-muted-foreground inline">Ruta:</dt> <dd class="inline text-foreground">{{ snap.request.method }} {{ snap.request.route ?? snap.request.url }}</dd></div>
|
|
69
|
-
<div><dt class="text-muted-foreground inline">
|
|
72
|
+
<div><dt class="text-muted-foreground inline">Status:</dt> <dd class="inline text-foreground">{{ data.trace?.status ?? '—' }}</dd></div>
|
|
70
73
|
<div><dt class="text-muted-foreground inline">Usuario:</dt> <dd class="inline text-foreground">{{ snap.request.user_id ?? '—' }}</dd></div>
|
|
71
74
|
<div><dt class="text-muted-foreground inline">IP:</dt> <dd class="inline text-foreground">{{ snap.request.ip ?? '—' }}</dd></div>
|
|
75
|
+
<div class="col-span-2"><dt class="text-muted-foreground inline">User-Agent:</dt> <dd class="inline text-foreground break-all">{{ snap.request.user_agent ?? '—' }}</dd></div>
|
|
72
76
|
</dl>
|
|
77
|
+
<details v-if="snap.request.query && Object.keys(snap.request.query).length" class="mt-2 text-xs">
|
|
78
|
+
<summary class="cursor-pointer text-muted-foreground">Query params</summary>
|
|
79
|
+
<pre class="mt-1 overflow-x-auto rounded bg-slate-50 dark:bg-slate-900/40 p-2 text-[11px]">{{ JSON.stringify(snap.request.query, null, 2) }}</pre>
|
|
80
|
+
</details>
|
|
81
|
+
<details v-if="snap.request.body && Object.keys(snap.request.body).length" class="mt-2 text-xs">
|
|
82
|
+
<summary class="cursor-pointer text-muted-foreground">Body</summary>
|
|
83
|
+
<pre class="mt-1 overflow-x-auto rounded bg-slate-50 dark:bg-slate-900/40 p-2 text-[11px]">{{ JSON.stringify(snap.request.body, null, 2) }}</pre>
|
|
84
|
+
</details>
|
|
85
|
+
<details v-if="snap.request.headers && Object.keys(snap.request.headers).length" class="mt-2 text-xs">
|
|
86
|
+
<summary class="cursor-pointer text-muted-foreground">Headers</summary>
|
|
87
|
+
<pre class="mt-1 overflow-x-auto rounded bg-slate-50 dark:bg-slate-900/40 p-2 text-[11px]">{{ JSON.stringify(snap.request.headers, null, 2) }}</pre>
|
|
88
|
+
</details>
|
|
73
89
|
</section>
|
|
74
90
|
|
|
75
91
|
<section v-if="snap.stack?.length">
|
|
@@ -14,6 +14,17 @@ const columns = [
|
|
|
14
14
|
{ key: 'title', label: 'Error' },
|
|
15
15
|
{ key: 'exception_class', label: 'Clase' },
|
|
16
16
|
{ key: 'times_seen', label: 'Veces', sortable: true },
|
|
17
|
+
{ key: 'origin', label: 'Origen', filterable: true, filterType: 'select',
|
|
18
|
+
filterOptions: [
|
|
19
|
+
{ value: 'http', label: 'HTTP' }, { value: 'queue', label: 'Worker' },
|
|
20
|
+
{ value: 'schedule', label: 'Cron' }, { value: 'console', label: 'Consola' },
|
|
21
|
+
] },
|
|
22
|
+
{ key: 'category', label: 'Tipo', filterable: true, filterType: 'select',
|
|
23
|
+
filterOptions: [
|
|
24
|
+
{ value: 'database', label: 'Base de datos' }, { value: 'http', label: 'HTTP' },
|
|
25
|
+
{ value: 'auth', label: 'Auth' }, { value: 'validation', label: 'Validación' },
|
|
26
|
+
{ value: 'runtime', label: 'Runtime' },
|
|
27
|
+
] },
|
|
17
28
|
{ key: 'status', label: 'Estado', filterable: true, filterType: 'select',
|
|
18
29
|
filterOptions: [
|
|
19
30
|
{ value: 'open', label: 'Abierto' },
|
|
@@ -57,6 +68,15 @@ watch(() => props.touchedAt, () => { reloadKey.value++ })
|
|
|
57
68
|
<span class="text-foreground">{{ row.title }}</span>
|
|
58
69
|
</div>
|
|
59
70
|
</template>
|
|
71
|
+
<template #origin="{ value }">
|
|
72
|
+
<span class="inline-flex items-center rounded-full bg-slate-100 dark:bg-slate-700 px-2 py-0.5 text-xs text-slate-600 dark:text-slate-300">{{ ({http:'HTTP',queue:'Worker',schedule:'Cron',console:'Consola'})[value] ?? value ?? '—' }}</span>
|
|
73
|
+
</template>
|
|
74
|
+
<template #category="{ value }">
|
|
75
|
+
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs"
|
|
76
|
+
:class="value === 'database' ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-500/20 dark:text-indigo-300' : value === 'auth' ? 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-300' : 'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300'">
|
|
77
|
+
{{ ({database:'BD',http:'HTTP',auth:'Auth',validation:'Validación',runtime:'Runtime'})[value] ?? value ?? '—' }}
|
|
78
|
+
</span>
|
|
79
|
+
</template>
|
|
60
80
|
<template #status="{ value }">{{ statusLabels[value] ?? value }}</template>
|
|
61
81
|
<template #actions="{ row }">
|
|
62
82
|
<div class="flex gap-2">
|
|
@@ -10,6 +10,7 @@ const TABS = [
|
|
|
10
10
|
{ key: 'overview', label: 'Resumen' },
|
|
11
11
|
{ key: 'database', label: 'Base de datos' },
|
|
12
12
|
{ key: 'errors', label: 'Errores' },
|
|
13
|
+
{ key: 'queues', label: 'Colas' },
|
|
13
14
|
{ key: 'logs', label: 'Logs' },
|
|
14
15
|
{ key: 'files', label: 'Archivos' },
|
|
15
16
|
]
|
|
@@ -34,6 +35,7 @@ onBeforeUnmount(() => rt.unsubscribe('observability'))
|
|
|
34
35
|
<div v-show="active === 'overview'"><Observability.Overview :touched-at="touchedAt" /></div>
|
|
35
36
|
<div v-show="active === 'database'"><Observability.Dashboard /></div>
|
|
36
37
|
<div v-show="active === 'errors'"><Observability.Errors :touched-at="touchedAt" /></div>
|
|
38
|
+
<div v-show="active === 'queues'"><Observability.Queues /></div>
|
|
37
39
|
<div v-show="active === 'logs'"><Observability.Logs channel="operational" /></div>
|
|
38
40
|
<div v-show="active === 'files'">
|
|
39
41
|
<p class="rounded-card border border-dashed border-card-line p-8 text-center text-sm text-muted-foreground">
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
// Colas & workers (v1): profundidad de cola + gestión de failed_jobs
|
|
3
|
+
// (reintentar/borrar por job + masivas). Refresca cada ~15s.
|
|
4
|
+
const api = useApi()
|
|
5
|
+
const toast = useToast()
|
|
6
|
+
const { confirm } = useModal()
|
|
7
|
+
const qc = useQueryClient()
|
|
8
|
+
const REFETCH = 15_000
|
|
9
|
+
|
|
10
|
+
const overview = useQuery({ queryKey: ['obs-queues', 'overview'], queryFn: () => api.get('platform/observability/queues/overview'), refetchInterval: REFETCH })
|
|
11
|
+
const failed = useQuery({ queryKey: ['obs-queues', 'failed'], queryFn: () => api.get('platform/observability/queues/failed'), refetchInterval: REFETCH })
|
|
12
|
+
|
|
13
|
+
function reload() { qc.invalidateQueries({ queryKey: ['obs-queues'] }) }
|
|
14
|
+
|
|
15
|
+
async function retry(id) {
|
|
16
|
+
try { await api.post(`platform/observability/queues/failed/${id}/retry`); toast.success('Job reintentado.'); reload() }
|
|
17
|
+
catch { toast.error('No se pudo reintentar.') }
|
|
18
|
+
}
|
|
19
|
+
async function remove(id) {
|
|
20
|
+
if (! await confirm({ severity: 'danger', title: 'Borrar job fallido', message: '¿Eliminar este job fallido?', confirmText: 'Borrar' })) return
|
|
21
|
+
try { await api.delete(`platform/observability/queues/failed/${id}`); toast.success('Borrado.'); reload() }
|
|
22
|
+
catch { toast.error('No se pudo borrar.') }
|
|
23
|
+
}
|
|
24
|
+
async function retryAll() {
|
|
25
|
+
if (! await confirm({ title: 'Reintentar todos', message: '¿Reintentar todos los jobs fallidos?', confirmText: 'Reintentar' })) return
|
|
26
|
+
try { await api.post('platform/observability/queues/failed/retry-all'); toast.success('Reintentados.'); reload() }
|
|
27
|
+
catch { toast.error('Error al reintentar.') }
|
|
28
|
+
}
|
|
29
|
+
async function flush() {
|
|
30
|
+
if (! await confirm({ severity: 'danger', title: 'Vaciar fallidos', message: '¿Eliminar TODOS los jobs fallidos? No se puede deshacer.', confirmText: 'Vaciar' })) return
|
|
31
|
+
try { await api.post('platform/observability/queues/failed/flush'); toast.success('Vaciado.'); reload() }
|
|
32
|
+
catch { toast.error('Error al vaciar.') }
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<div class="space-y-6">
|
|
38
|
+
<div class="grid grid-cols-2 gap-4 lg:grid-cols-3">
|
|
39
|
+
<Observability.MetricCard label="Pendientes" :value="overview.data.value?.depth?.total ?? '—'"
|
|
40
|
+
:sublabel="`${overview.data.value?.depth?.queues?.length ?? 0} colas`" />
|
|
41
|
+
<Observability.MetricCard label="Fallidos" :value="overview.data.value?.failed ?? 0"
|
|
42
|
+
:tone="(overview.data.value?.failed ?? 0) > 0 ? 'bad' : 'good'" />
|
|
43
|
+
<Observability.MetricCard label="Driver de cola" :value="overview.data.value?.depth?.driver ?? '—'" />
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<section>
|
|
47
|
+
<h3 class="mb-2 text-sm font-semibold text-foreground">Profundidad por cola</h3>
|
|
48
|
+
<div v-if="overview.data.value?.depth && !overview.data.value.depth.available"
|
|
49
|
+
class="rounded-card border border-amber-200 bg-amber-50 p-3 text-xs text-amber-700 dark:border-amber-800/50 dark:bg-amber-900/20 dark:text-amber-400">
|
|
50
|
+
{{ overview.data.value.depth.reason }}
|
|
51
|
+
</div>
|
|
52
|
+
<table v-else class="w-full text-sm">
|
|
53
|
+
<thead><tr class="text-left text-muted-foreground"><th class="py-1">Cola</th><th class="py-1">Pendientes</th></tr></thead>
|
|
54
|
+
<tbody>
|
|
55
|
+
<tr v-for="q in overview.data.value?.depth?.queues ?? []" :key="q.queue" class="border-t border-card-line">
|
|
56
|
+
<td class="py-1 font-mono text-xs">{{ q.queue }}</td><td class="py-1">{{ q.count }}</td>
|
|
57
|
+
</tr>
|
|
58
|
+
<tr v-if="!(overview.data.value?.depth?.queues?.length)"><td colspan="2" class="py-2 text-xs text-muted-foreground">Sin pendientes 🎉</td></tr>
|
|
59
|
+
</tbody>
|
|
60
|
+
</table>
|
|
61
|
+
</section>
|
|
62
|
+
|
|
63
|
+
<section>
|
|
64
|
+
<div class="mb-2 flex items-center justify-between gap-2">
|
|
65
|
+
<h3 class="text-sm font-semibold text-foreground">Jobs fallidos</h3>
|
|
66
|
+
<div v-if="failed.data.value?.jobs?.length" class="flex gap-2">
|
|
67
|
+
<button class="text-xs text-primary hover:underline" @click="retryAll">Reintentar todos</button>
|
|
68
|
+
<button class="text-xs text-red-500 hover:underline" @click="flush">Vaciar</button>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
<table class="w-full text-sm">
|
|
72
|
+
<thead><tr class="text-left text-muted-foreground">
|
|
73
|
+
<th class="py-1 pr-4">Job</th><th class="py-1 pr-4">Cola</th><th class="py-1 pr-4">Excepción</th><th class="py-1 pr-4">Fecha</th><th class="py-1"></th>
|
|
74
|
+
</tr></thead>
|
|
75
|
+
<tbody>
|
|
76
|
+
<tr v-for="j in failed.data.value?.jobs ?? []" :key="j.id" class="border-t border-card-line align-top">
|
|
77
|
+
<td class="py-1 pr-4 font-mono text-xs">{{ j.name }}</td>
|
|
78
|
+
<td class="py-1 pr-4 text-xs">{{ j.queue }}</td>
|
|
79
|
+
<td class="py-1 pr-4 text-xs text-muted-foreground truncate max-w-sm">{{ j.exception }}</td>
|
|
80
|
+
<td class="py-1 pr-4 text-xs whitespace-nowrap">{{ j.failed_at }}</td>
|
|
81
|
+
<td class="py-1 whitespace-nowrap">
|
|
82
|
+
<button class="text-xs text-primary hover:underline mr-2" @click="retry(j.id)">Reintentar</button>
|
|
83
|
+
<button class="text-xs text-red-500 hover:underline" @click="remove(j.id)">Borrar</button>
|
|
84
|
+
</td>
|
|
85
|
+
</tr>
|
|
86
|
+
<tr v-if="!(failed.data.value?.jobs?.length)"><td colspan="5" class="py-2 text-xs text-muted-foreground">Sin jobs fallidos 🎉</td></tr>
|
|
87
|
+
</tbody>
|
|
88
|
+
</table>
|
|
89
|
+
</section>
|
|
90
|
+
</div>
|
|
91
|
+
</template>
|