@innertia-solutions/innertia-nuxt 0.1.53 → 0.1.55

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.
@@ -0,0 +1,102 @@
1
+ <script setup>
2
+ // Dashboard de base de datos con valor: gauges (cache hit, conexiones), cards
3
+ // (deadlocks, rollbacks), y tablas (queries lentas, índices sin usar, tablas con
4
+ // más dead tuples). Cada sección pide su endpoint (lazy) y refresca cada ~10s.
5
+ const api = useApi()
6
+ const REFETCH = 10_000
7
+
8
+ const overview = useQuery({ queryKey: ['obs-db', 'overview'], queryFn: () => api.get('platform/observability/database/overview'), refetchInterval: REFETCH })
9
+ const connections = useQuery({ queryKey: ['obs-db', 'connections'], queryFn: () => api.get('platform/observability/database/connections'), refetchInterval: REFETCH })
10
+ const queries = useQuery({ queryKey: ['obs-db', 'queries'], queryFn: () => api.get('platform/observability/database/queries'), refetchInterval: REFETCH })
11
+ const indexes = useQuery({ queryKey: ['obs-db', 'indexes'], queryFn: () => api.get('platform/observability/database/indexes'), refetchInterval: REFETCH })
12
+ const tables = useQuery({ queryKey: ['obs-db', 'tables'], queryFn: () => api.get('platform/observability/database/tables'), refetchInterval: REFETCH })
13
+
14
+ const connPercent = computed(() => {
15
+ const d = connections.data.value
16
+ if (!d || d.total == null || !d.max) return null
17
+ return Math.round((d.total / d.max) * 100)
18
+ })
19
+
20
+ function fmtBytes(bytes) {
21
+ if (bytes == null) return '—'
22
+ const u = ['B', 'KB', 'MB', 'GB', 'TB']; let n = Number(bytes), i = 0
23
+ while (n >= 1024 && i < u.length - 1) { n /= 1024; i++ }
24
+ return `${n.toFixed(1)} ${u[i]}`
25
+ }
26
+ function fmtMs(ms) { return ms == null ? '—' : `${Number(ms).toLocaleString('es-CL')} ms` }
27
+ </script>
28
+
29
+ <template>
30
+ <div class="space-y-6">
31
+ <!-- Gauges + cards de salud -->
32
+ <div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
33
+ <Observability.Gauge label="Cache hit" :percent="overview.data.value?.cache_hit_ratio ?? null" />
34
+ <Observability.Gauge label="Conexiones" :percent="connPercent" />
35
+ <Observability.MetricCard label="Deadlocks" :value="overview.data.value?.deadlocks ?? '—'"
36
+ :tone="(overview.data.value?.deadlocks ?? 0) > 0 ? 'bad' : 'good'" />
37
+ <Observability.MetricCard label="Rollbacks" :value="overview.data.value?.rollbacks ?? '—'"
38
+ :sublabel="`Tamaño BD: ${fmtBytes(overview.data.value?.database_bytes)}`" />
39
+ </div>
40
+
41
+ <!-- Conexiones por estado -->
42
+ <div v-if="connections.data.value" class="flex flex-wrap gap-2">
43
+ <span v-for="(n, state) in connections.data.value.by_state" :key="state"
44
+ class="rounded-full border border-card-line bg-card px-2.5 py-1 text-xs text-muted-foreground">
45
+ {{ state }}: <span class="font-semibold text-foreground">{{ n }}</span>
46
+ </span>
47
+ <span class="rounded-full border border-card-line bg-card px-2.5 py-1 text-xs text-muted-foreground">
48
+ total: <span class="font-semibold text-foreground">{{ connections.data.value.total }}</span> / {{ connections.data.value.max ?? '—' }}
49
+ </span>
50
+ </div>
51
+
52
+ <!-- Queries lentas -->
53
+ <section>
54
+ <h3 class="mb-2 text-sm font-semibold text-foreground">Queries más lentas</h3>
55
+ <div v-if="queries.data.value && !queries.data.value.available"
56
+ 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">
57
+ {{ queries.data.value.reason }}
58
+ </div>
59
+ <table v-else class="w-full text-sm">
60
+ <thead><tr class="text-left text-muted-foreground">
61
+ <th class="py-1">Query</th><th class="py-1">Llamadas</th><th class="py-1">Media</th><th class="py-1">Total</th>
62
+ </tr></thead>
63
+ <tbody>
64
+ <tr v-for="q in queries.data.value?.top ?? []" :key="q.id" class="border-t border-card-line">
65
+ <td class="py-1 font-mono text-xs">{{ q.query }}</td>
66
+ <td class="py-1">{{ Number(q.calls).toLocaleString('es-CL') }}</td>
67
+ <td class="py-1">{{ fmtMs(q.mean_ms) }}</td>
68
+ <td class="py-1">{{ fmtMs(q.total_ms) }}</td>
69
+ </tr>
70
+ </tbody>
71
+ </table>
72
+ </section>
73
+
74
+ <!-- Índices sin usar + tablas con dead tuples -->
75
+ <div class="grid gap-6 lg:grid-cols-2">
76
+ <section>
77
+ <h3 class="mb-2 text-sm font-semibold text-foreground">Índices sin usar</h3>
78
+ <table class="w-full text-sm">
79
+ <thead><tr class="text-left text-muted-foreground"><th class="py-1">Índice</th><th class="py-1">Tabla</th><th class="py-1">Tamaño</th></tr></thead>
80
+ <tbody>
81
+ <tr v-for="ix in indexes.data.value?.unused ?? []" :key="ix.index" class="border-t border-card-line">
82
+ <td class="py-1 font-mono text-xs">{{ ix.index }}</td><td class="py-1">{{ ix.table }}</td><td class="py-1">{{ fmtBytes(ix.bytes) }}</td>
83
+ </tr>
84
+ <tr v-if="!(indexes.data.value?.unused?.length)"><td colspan="3" class="py-2 text-xs text-muted-foreground">Sin índices sin usar 🎉</td></tr>
85
+ </tbody>
86
+ </table>
87
+ </section>
88
+ <section>
89
+ <h3 class="mb-2 text-sm font-semibold text-foreground">Tablas con más dead tuples</h3>
90
+ <table class="w-full text-sm">
91
+ <thead><tr class="text-left text-muted-foreground"><th class="py-1">Tabla</th><th class="py-1">Muertas</th><th class="py-1">Bloat</th><th class="py-1">Tamaño</th></tr></thead>
92
+ <tbody>
93
+ <tr v-for="t in tables.data.value?.tables ?? []" :key="t.name" class="border-t border-card-line">
94
+ <td class="py-1">{{ t.name }}</td><td class="py-1">{{ Number(t.dead).toLocaleString('es-CL') }}</td>
95
+ <td class="py-1">{{ t.bloat_pct }}%</td><td class="py-1">{{ fmtBytes(t.bytes) }}</td>
96
+ </tr>
97
+ </tbody>
98
+ </table>
99
+ </section>
100
+ </div>
101
+ </div>
102
+ </template>
@@ -0,0 +1,44 @@
1
+ <script setup>
2
+ const api = useApi()
3
+ const toast = useToast()
4
+ const reloadKey = ref(0)
5
+
6
+ const columns = [
7
+ { key: 'last_seen', label: 'Última vez', sortable: true },
8
+ { key: 'title', label: 'Error' },
9
+ { key: 'exception_class', label: 'Clase' },
10
+ { key: 'times_seen', label: 'Veces' },
11
+ { key: 'status', label: 'Estado' },
12
+ { key: 'actions', label: '' },
13
+ ]
14
+
15
+ const statusLabels = { open: 'Abierto', resolved: 'Resuelto', ignored: 'Ignorado' }
16
+
17
+ async function setStatus(row, status) {
18
+ try {
19
+ await api.post(`platform/observability/errors/${row.id}/status`, { status })
20
+ toast.success('Estado actualizado.')
21
+ reloadKey.value++
22
+ } catch (e) {
23
+ toast.error(e?.data?.message ?? 'No se pudo cambiar el estado.')
24
+ }
25
+ }
26
+ </script>
27
+
28
+ <template>
29
+ <Table.Standard
30
+ :key="reloadKey"
31
+ name="observability-errors"
32
+ endpoint="platform/observability/errors"
33
+ :columns="columns"
34
+ search-placeholder="Buscar error…"
35
+ >
36
+ <template #status="{ value }">{{ statusLabels[value] ?? value }}</template>
37
+ <template #actions="{ row }">
38
+ <div class="flex gap-2">
39
+ <button class="text-xs text-muted-foreground hover:text-foreground" @click="setStatus(row, 'resolved')">Resolver</button>
40
+ <button class="text-xs text-muted-foreground hover:text-foreground" @click="setStatus(row, 'ignored')">Ignorar</button>
41
+ </div>
42
+ </template>
43
+ </Table.Standard>
44
+ </template>
@@ -0,0 +1,21 @@
1
+ <script setup>
2
+ const props = defineProps({
3
+ label: { type: String, required: true },
4
+ percent: { type: Number, default: null }, // 0..100 o null
5
+ })
6
+ const series = computed(() => [props.percent == null ? 0 : Math.max(0, Math.min(100, props.percent))])
7
+ const options = computed(() => ({
8
+ labels: [props.label],
9
+ plotOptions: { radialBar: { dataLabels: { value: { formatter: (v) => `${Math.round(v)}%` } } } },
10
+ }))
11
+ </script>
12
+
13
+ <template>
14
+ <div class="rounded-card border border-card-line bg-card p-2">
15
+ <Chart v-if="percent != null" type="radialBar" :series="series" :options="options" :height="180" />
16
+ <div v-else class="flex h-[180px] flex-col items-center justify-center text-muted-foreground">
17
+ <span class="text-sm">{{ label }}</span>
18
+ <span class="text-xs">sin datos</span>
19
+ </div>
20
+ </div>
21
+ </template>
@@ -0,0 +1,28 @@
1
+ <script setup>
2
+ const props = defineProps({
3
+ channel: { type: String, default: 'operational' },
4
+ })
5
+
6
+ const endpoint = computed(() => `platform/observability/logs?channel=${props.channel}`)
7
+
8
+ const columns = [
9
+ { key: 'occurred_at', label: 'Fecha', sortable: true },
10
+ { key: 'level', label: 'Nivel' },
11
+ { key: 'message', label: 'Mensaje' },
12
+ ]
13
+
14
+ const levelOf = (row) => row?.payload?.level ?? '—'
15
+ const messageOf = (row) => row?.payload?.message ?? '—'
16
+ </script>
17
+
18
+ <template>
19
+ <Table.Standard
20
+ :name="`observability-logs-${channel}`"
21
+ :endpoint="endpoint"
22
+ :columns="columns"
23
+ search-placeholder="Buscar en logs…"
24
+ >
25
+ <template #level="{ row }">{{ levelOf(row) }}</template>
26
+ <template #message="{ row }">{{ messageOf(row) }}</template>
27
+ </Table.Standard>
28
+ </template>
@@ -0,0 +1,22 @@
1
+ <script setup>
2
+ defineProps({
3
+ label: { type: String, required: true },
4
+ value: { type: [String, Number], default: '—' },
5
+ sublabel: { type: String, default: '' },
6
+ tone: { type: String, default: 'neutral' }, // neutral | good | warn | bad
7
+ })
8
+ const TONE = {
9
+ neutral: 'text-foreground',
10
+ good: 'text-emerald-600 dark:text-emerald-400',
11
+ warn: 'text-amber-600 dark:text-amber-400',
12
+ bad: 'text-red-600 dark:text-red-400',
13
+ }
14
+ </script>
15
+
16
+ <template>
17
+ <div class="rounded-card border border-card-line bg-card p-4">
18
+ <p class="text-xs text-muted-foreground">{{ label }}</p>
19
+ <p class="mt-1 text-2xl font-bold" :class="TONE[tone] ?? TONE.neutral">{{ value }}</p>
20
+ <p v-if="sublabel" class="mt-0.5 text-xs text-muted-foreground">{{ sublabel }}</p>
21
+ </div>
22
+ </template>
@@ -0,0 +1,32 @@
1
+ <script setup>
2
+ // Panel drop-in de observabilidad: sub-nav interna. Fase 1: BD (Dashboard nuevo),
3
+ // Errores/Logs (componentes existentes), Archivos (placeholder Fase 3).
4
+ const TABS = [
5
+ { key: 'database', label: 'Base de datos' },
6
+ { key: 'errors', label: 'Errores' },
7
+ { key: 'logs', label: 'Logs' },
8
+ { key: 'files', label: 'Archivos' },
9
+ ]
10
+ const active = ref('database')
11
+ </script>
12
+
13
+ <template>
14
+ <div class="space-y-5">
15
+ <nav class="flex gap-1 border-b border-card-line">
16
+ <button v-for="t in TABS" :key="t.key" type="button" @click="active = t.key"
17
+ class="px-3 py-2 text-sm font-medium transition-colors -mb-px border-b-2"
18
+ :class="active === t.key ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground'">
19
+ {{ t.label }}
20
+ </button>
21
+ </nav>
22
+
23
+ <div v-show="active === 'database'"><Observability.Dashboard /></div>
24
+ <div v-show="active === 'errors'"><Observability.Errors /></div>
25
+ <div v-show="active === 'logs'"><Observability.Logs channel="operational" /></div>
26
+ <div v-show="active === 'files'">
27
+ <p class="rounded-card border border-dashed border-card-line p-8 text-center text-sm text-muted-foreground">
28
+ Visor de archivos de log — próximamente (Fase 3).
29
+ </p>
30
+ </div>
31
+ </div>
32
+ </template>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@innertia-solutions/innertia-nuxt",
3
- "version": "0.1.53",
3
+ "version": "0.1.55",
4
4
  "description": "Innertia Solutions — Nuxt layer unificada: core, auth, multitenancy, theme y app contexts",
5
5
  "keywords": [
6
6
  "nuxt",