aboutsummaryrefslogtreecommitdiff
path: root/frontend/src/routes/status
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/routes/status')
-rw-r--r--frontend/src/routes/status/[id]/+page.svelte278
-rw-r--r--frontend/src/routes/status/[id]/+page.ts22
2 files changed, 300 insertions, 0 deletions
diff --git a/frontend/src/routes/status/[id]/+page.svelte b/frontend/src/routes/status/[id]/+page.svelte
new file mode 100644
index 0000000..afc59a2
--- /dev/null
+++ b/frontend/src/routes/status/[id]/+page.svelte
@@ -0,0 +1,278 @@
+<script lang="ts">
+ import type { PageData } from './$types';
+
+ let { data }: { data: PageData } = $props();
+
+ let svc = $derived(data.service);
+ let history = $derived(data.history ?? []);
+ let stats = $derived(data.stats);
+
+ let isUp = $derived(svc?.last_heartbeat?.is_up ?? null);
+ let expandedError: number | null = $state(null);
+
+ let chartPad = { t: 8, b: 20, l: 40, r: 12 };
+ let chartW = $derived(Math.max(history.length * 32, 300));
+ let chartH = 200;
+ let plotW = $derived(chartW - chartPad.l - chartPad.r);
+ let plotH = $derived(chartH - chartPad.t - chartPad.b);
+ let chartMax = $derived(Math.max(...history.map((h: any) => h.response_time_ms), 100));
+
+ let dots = $derived(
+ history.map((h: any, i: number) => {
+ const x = history.length === 1
+ ? chartPad.l + plotW / 2
+ : chartPad.l + (i / (history.length - 1)) * plotW;
+ const y = chartPad.t + plotH - ((h.response_time_ms - 0) / (chartMax || 1)) * plotH;
+ return { x, y, ...h };
+ })
+ );
+
+ let yTicks = $derived.by(() => {
+ if (chartMax <= 0) return [];
+ const n = 4;
+ return Array.from({ length: n + 1 }, (_, i) => {
+ const val = (chartMax / n) * i;
+ const y = chartPad.t + plotH - (val / chartMax) * plotH;
+ return { val: Math.round(val), y };
+ });
+ });
+</script>
+
+<svelte:head>
+ <title>{svc?.name ?? 'Status'} — YAUM</title>
+</svelte:head>
+
+<div class="mx-auto max-w-4xl">
+ {#if !svc}
+ <div class="rounded-xl border border-[var(--border-color)] p-12 text-center">
+ <div class="mx-auto mb-4 h-16 w-16 rounded-2xl bg-[var(--bg-secondary)] p-4">
+ <svg class="h-full w-full text-[var(--text-muted)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
+ </svg>
+ </div>
+ <p class="text-sm font-medium text-[var(--text-secondary)]">Serviço não encontrado</p>
+ <p class="mt-1 text-xs text-[var(--text-muted)]">
+ <a href="/" class="underline transition-colors hover:text-white">← Voltar ao início</a>
+ </p>
+ </div>
+ {:else}
+ <!-- Back link -->
+ <div class="mb-6">
+ <a href="/" class="text-xs text-[var(--text-muted)] underline transition-colors hover:text-white">
+ ← Voltar ao Dashboard
+ </a>
+ </div>
+
+ <!-- Hero status -->
+ <div
+ class="mb-8 overflow-hidden rounded-2xl border p-8 text-center transition-all"
+ style="border-color: {isUp === true ? 'rgba(34,240,106,0.3)' : isUp === false ? 'rgba(255,64,96,0.3)' : 'var(--border-color)'}; background-color: {isUp === true ? 'rgba(34,240,106,0.04)' : isUp === false ? 'rgba(255,64,96,0.04)' : 'var(--bg-card)'}"
+ >
+ {#if isUp === null}
+ <div class="mb-2 h-4 w-4 animate-pulse rounded-full bg-[var(--text-muted)] mx-auto"></div>
+ <p class="text-sm font-semibold uppercase tracking-widest text-[var(--text-muted)]">Aguardando verificação</p>
+ {:else}
+ <div
+ class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full"
+ style="background-color: {isUp ? 'rgba(34,240,106,0.15)' : 'rgba(255,64,96,0.15)'}"
+ >
+ {#if isUp}
+ <svg class="h-8 w-8" style="color: var(--green)" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
+ </svg>
+ {:else}
+ <svg class="h-8 w-8" style="color: var(--red)" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
+ </svg>
+ {/if}
+ </div>
+ <h1
+ class="text-3xl font-bold tracking-tight"
+ style="color: {isUp ? 'var(--green)' : 'var(--red)'}"
+ >
+ {isUp ? 'Operando Normalmente' : 'Fora do Ar'}
+ </h1>
+ {/if}
+
+ <div class="mt-4">
+ <h2 class="text-xl font-semibold text-white">{svc.name}</h2>
+ <p class="mt-1 font-mono text-sm text-[var(--text-secondary)]">{svc.url}</p>
+ </div>
+
+ {#if stats}
+ <div class="mt-6 inline-flex items-center gap-6 rounded-xl border border-[var(--border-color)] bg-[var(--bg-secondary)]/30 px-6 py-3">
+ <div class="text-center">
+ <p class="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">SLA 30 dias</p>
+ <p
+ class="text-xl font-bold tabular-nums"
+ style="color: {stats.uptime_30d >= 99 ? 'var(--green)' : stats.uptime_30d >= 95 ? '#facc15' : 'var(--red)'}"
+ >
+ {stats.uptime_30d.toFixed(2)}%
+ </p>
+ </div>
+ <div class="h-8 w-px bg-[var(--border-color)]"></div>
+ <div class="text-center">
+ <p class="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">Latência Média</p>
+ <p class="text-xl font-bold tabular-nums text-white">
+ {stats.avg_response_ms_30d.toFixed(0)}<span class="text-sm font-normal text-[var(--text-secondary)]">ms</span>
+ </p>
+ </div>
+ <div class="h-8 w-px bg-[var(--border-color)]"></div>
+ <div class="text-center">
+ <p class="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">Total de Checks</p>
+ <p class="text-xl font-bold tabular-nums text-white">{stats.total_checks_30d}</p>
+ </div>
+ </div>
+ {/if}
+ </div>
+
+ <div class="grid gap-6 lg:grid-cols-5">
+ <!-- SLA Breakdown -->
+ {#if stats}
+ <div class="rounded-xl border border-[var(--border-color)] bg-[var(--bg-card)] p-5 lg:col-span-2">
+ <h2 class="mb-4 text-xs font-semibold uppercase tracking-wider text-[var(--text-secondary)]">
+ Uptime
+ </h2>
+ <div class="space-y-2.5">
+ {#each [
+ { label: 'Últimas 24h', uptime: stats.uptime_24h, checks: stats.total_checks_24h, avg: stats.avg_response_ms_24h },
+ { label: 'Últimos 7 dias', uptime: stats.uptime_7d, checks: stats.total_checks_7d, avg: stats.avg_response_ms_7d },
+ { label: 'Últimos 30 dias', uptime: stats.uptime_30d, checks: stats.total_checks_30d, avg: stats.avg_response_ms_30d }
+ ] as item}
+ <div class="rounded-lg border border-[var(--border-color)] bg-[var(--bg-secondary)]/20 p-3">
+ <div class="mb-1 flex items-center justify-between">
+ <span class="text-[10px] font-medium uppercase tracking-wider text-[var(--text-muted)]">{item.label}</span>
+ <span class="font-mono text-[10px] text-[var(--text-muted)]">{item.checks} checks</span>
+ </div>
+ <div class="flex items-baseline gap-3">
+ <span
+ class="text-base font-bold tabular-nums"
+ style="color: {item.uptime >= 99 ? 'var(--green)' : item.uptime >= 95 ? '#facc15' : 'var(--red)'}"
+ >
+ {item.uptime.toFixed(2)}%
+ </span>
+ <span class="font-mono text-[10px] text-[var(--text-muted)]">{item.avg.toFixed(0)}ms méd.</span>
+ </div>
+ </div>
+ {/each}
+ </div>
+ </div>
+ {/if}
+
+ <!-- Latency chart -->
+ <div
+ class="rounded-xl border border-[var(--border-color)] bg-[var(--bg-card)] p-5"
+ class:lg:col-span-3={!!stats}
+ class:lg:col-span-5={!stats}
+ >
+ <h2 class="mb-4 text-xs font-semibold uppercase tracking-wider text-[var(--text-secondary)]">
+ Tempo de Resposta (ms)
+ </h2>
+ <div class="relative h-48">
+ {#if history.length === 0}
+ <div class="flex h-full items-center justify-center text-xs text-[var(--text-muted)]">
+ Sem dados ainda
+ </div>
+ {:else if history.length === 1}
+ <div class="flex h-full flex-col items-center justify-center gap-1">
+ <span
+ class="text-3xl font-bold tabular-nums"
+ style="color: {history[0].is_up ? 'var(--green)' : 'var(--red)'}"
+ >
+ {history[0].response_time_ms}
+ <span class="text-base font-normal text-[var(--text-secondary)]">ms</span>
+ </span>
+ <span class="text-xs text-[var(--text-muted)]">
+ {history[0].is_up ? 'Online' : 'Offline'} — {history[0].status_code || 'timeout'}
+ </span>
+ </div>
+ {:else}
+ <svg class="h-full w-full" viewBox="0 0 {chartW} {chartH}" preserveAspectRatio="none">
+ {#each yTicks as tick}
+ <line
+ x1={chartPad.l} y1={tick.y} x2={chartW - chartPad.r} y2={tick.y}
+ stroke="var(--border-color)" stroke-width="1" stroke-dasharray="3,3"
+ />
+ <text x={chartPad.l - 6} y={tick.y + 3} text-anchor="end" fill="var(--text-muted)" font-size="9">{tick.val}</text>
+ {/each}
+ <polyline
+ points={dots.map((d: any) => `${d.x},${d.y}`).join(' ')}
+ fill="none" stroke="var(--green)" stroke-width="2"
+ stroke-linecap="round" stroke-linejoin="round"
+ />
+ {#each dots as d}
+ <circle cx={d.x} cy={d.y} r="3.5" fill={d.is_up ? 'var(--green)' : 'var(--red)'} />
+ {/each}
+ </svg>
+ {/if}
+ </div>
+ <div class="mt-2 flex justify-between text-[10px] text-[var(--text-muted)]">
+ <span>{history[0]?.tested_at ? new Date(history[0].tested_at).toLocaleTimeString() : ''}</span>
+ <span>{history[history.length - 1]?.tested_at ? new Date(history[history.length - 1].tested_at).toLocaleTimeString() : ''}</span>
+ </div>
+ </div>
+ </div>
+
+ <!-- History table -->
+ <div class="mt-6 rounded-xl border border-[var(--border-color)] bg-[var(--bg-card)] p-5">
+ <h2 class="mb-4 text-xs font-semibold uppercase tracking-wider text-[var(--text-secondary)]">
+ Histórico de Verificações
+ </h2>
+ {#if history.length === 0}
+ <p class="py-8 text-center text-xs text-[var(--text-muted)]">Nenhuma verificação registrada</p>
+ {:else}
+ <div class="overflow-x-auto">
+ <table class="w-full text-left text-xs">
+ <thead>
+ <tr class="border-b border-[var(--border-color)]">
+ <th class="pb-2 pr-4 text-[10px] font-semibold uppercase tracking-wider text-[var(--text-muted)]">Horário</th>
+ <th class="pb-2 pr-4 text-[10px] font-semibold uppercase tracking-wider text-[var(--text-muted)]">Status</th>
+ <th class="pb-2 pr-4 text-[10px] font-semibold uppercase tracking-wider text-[var(--text-muted)]">Código</th>
+ <th class="pb-2 pr-4 text-[10px] font-semibold uppercase tracking-wider text-[var(--text-muted)]">Latência</th>
+ <th class="pb-2 text-[10px] font-semibold uppercase tracking-wider text-[var(--text-muted)]">Erro</th>
+ </tr>
+ </thead>
+ <tbody>
+ {#each [...history].reverse() as h}
+ <tr class="border-b border-[var(--border-color)]/50 transition-colors hover:bg-[var(--bg-secondary)]/20">
+ <td class="py-2 pr-4 font-mono text-[var(--text-muted)]">
+ {new Date(h.tested_at).toLocaleString('pt-BR')}
+ </td>
+ <td class="py-2 pr-4">
+ <span
+ class="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-semibold"
+ style="background-color: {h.is_up ? 'rgba(34,240,106,0.1)' : 'rgba(255,64,96,0.1)'}; color: {h.is_up ? 'var(--green)' : 'var(--red)'}"
+ >
+ {h.is_up ? 'UP' : 'DOWN'}
+ </span>
+ </td>
+ <td class="py-2 pr-4 font-mono text-[var(--text-secondary)]">
+ {h.status_code > 0 ? h.status_code : '—'}
+ </td>
+ <td class="py-2 pr-4 font-mono text-[var(--text-secondary)]">
+ {h.response_time_ms}ms
+ </td>
+ <td class="py-2 font-mono text-[var(--text-muted)]">
+ {#if h.error_message}
+ <button
+ onclick={() => (expandedError = expandedError === h.id ? null : h.id)}
+ class="text-[10px] text-[var(--red)] underline underline-offset-2 transition-colors hover:opacity-70"
+ >
+ {expandedError === h.id ? '▲ erro' : '▼ erro'}
+ </button>
+ {#if expandedError === h.id}
+ <pre class="mt-1 break-all text-[11px] leading-relaxed">{(h as any).error_message}</pre>
+ {/if}
+ {:else}
+ —
+ {/if}
+ </td>
+ </tr>
+ {/each}
+ </tbody>
+ </table>
+ </div>
+ {/if}
+ </div>
+ {/if}
+</div>
diff --git a/frontend/src/routes/status/[id]/+page.ts b/frontend/src/routes/status/[id]/+page.ts
new file mode 100644
index 0000000..4351aae
--- /dev/null
+++ b/frontend/src/routes/status/[id]/+page.ts
@@ -0,0 +1,22 @@
+import type { PageLoad } from './$types';
+
+export const load: PageLoad = async ({ params, fetch }) => {
+ const id = params.id;
+
+ const [servicesRes, historyRes, statsRes] = await Promise.all([
+ fetch(`http://localhost:8080/api/services`).catch(() => null),
+ fetch(`http://localhost:8080/api/services/${id}/history`).catch(() => null),
+ fetch(`http://localhost:8080/api/services/${id}/stats`).catch(() => null)
+ ]);
+
+ const services = servicesRes?.ok ? await servicesRes.json() : [];
+ const svc = services.find((s: any) => s.id === Number(id));
+ const history = historyRes?.ok ? await historyRes.json() : [];
+ const stats = statsRes?.ok ? await statsRes.json() : null;
+
+ return {
+ service: svc ?? null,
+ history,
+ stats
+ };
+};