From 6cc0bfd1d79074df790272b8091b1f0226d14283 Mon Sep 17 00:00:00 2001 From: zwlucas Date: Fri, 29 May 2026 15:57:37 -0300 Subject: feat: upload project Signed-off-by: zwlucas --- frontend/src/app.css | 50 +++ frontend/src/app.d.ts | 3 + frontend/src/app.html | 13 + frontend/src/hooks.server.js | 31 ++ frontend/src/lib/api.ts | 39 ++ frontend/src/lib/components/ActivityLog.svelte | 122 ++++++ frontend/src/lib/components/AddServiceModal.svelte | 224 +++++++++++ frontend/src/lib/components/ServiceCard.svelte | 90 +++++ frontend/src/lib/components/UptimeTimeline.svelte | 41 ++ frontend/src/lib/realtime.ts | 42 ++ frontend/src/lib/types.ts | 44 +++ frontend/src/routes/(admin)/admin/+page.server.js | 27 ++ frontend/src/routes/(admin)/admin/+page.svelte | 432 +++++++++++++++++++++ frontend/src/routes/+layout.svelte | 50 +++ frontend/src/routes/+page.svelte | 164 ++++++++ frontend/src/routes/+page.ts | 33 ++ frontend/src/routes/login/+page.server.js | 51 +++ frontend/src/routes/login/+page.svelte | 61 +++ frontend/src/routes/services/[id]/+page.svelte | 261 +++++++++++++ frontend/src/routes/status/[id]/+page.svelte | 278 +++++++++++++ frontend/src/routes/status/[id]/+page.ts | 22 ++ 21 files changed, 2078 insertions(+) create mode 100644 frontend/src/app.css create mode 100644 frontend/src/app.d.ts create mode 100644 frontend/src/app.html create mode 100644 frontend/src/hooks.server.js create mode 100644 frontend/src/lib/api.ts create mode 100644 frontend/src/lib/components/ActivityLog.svelte create mode 100644 frontend/src/lib/components/AddServiceModal.svelte create mode 100644 frontend/src/lib/components/ServiceCard.svelte create mode 100644 frontend/src/lib/components/UptimeTimeline.svelte create mode 100644 frontend/src/lib/realtime.ts create mode 100644 frontend/src/lib/types.ts create mode 100644 frontend/src/routes/(admin)/admin/+page.server.js create mode 100644 frontend/src/routes/(admin)/admin/+page.svelte create mode 100644 frontend/src/routes/+layout.svelte create mode 100644 frontend/src/routes/+page.svelte create mode 100644 frontend/src/routes/+page.ts create mode 100644 frontend/src/routes/login/+page.server.js create mode 100644 frontend/src/routes/login/+page.svelte create mode 100644 frontend/src/routes/services/[id]/+page.svelte create mode 100644 frontend/src/routes/status/[id]/+page.svelte create mode 100644 frontend/src/routes/status/[id]/+page.ts (limited to 'frontend/src') diff --git a/frontend/src/app.css b/frontend/src/app.css new file mode 100644 index 0000000..c1d9e47 --- /dev/null +++ b/frontend/src/app.css @@ -0,0 +1,50 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap'); + +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --bg-primary: #0f1117; + --bg-secondary: #1a1d27; + --bg-card: #1e2130; + --border-color: #2a2e3d; + --text-primary: #e8eaed; + --text-secondary: #9aa0b0; + --text-muted: #5c6278; + --green: #22f06a; + --red: #ff4060; + --amber: #ffb020; +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; + background-color: var(--bg-primary); + color: var(--text-primary); + font-family: 'Inter', system-ui, -apple-system, sans-serif; + min-height: 100vh; +} + +body { + background: + radial-gradient(ellipse 80% 60% at 50% -20%, rgba(34, 240, 106, 0.04) 0%, transparent 60%), + radial-gradient(ellipse 60% 50% at 80% 20%, rgba(255, 64, 96, 0.03) 0%, transparent 50%), + var(--bg-primary); +} + +::-webkit-scrollbar { + width: 6px; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 3px; +} diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts new file mode 100644 index 0000000..779a3d4 --- /dev/null +++ b/frontend/src/app.d.ts @@ -0,0 +1,3 @@ +/// + +declare namespace App {} diff --git a/frontend/src/app.html b/frontend/src/app.html new file mode 100644 index 0000000..6293dcd --- /dev/null +++ b/frontend/src/app.html @@ -0,0 +1,13 @@ + + + + + + + YAUM — Yet Another Uptime Monitor + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/frontend/src/hooks.server.js b/frontend/src/hooks.server.js new file mode 100644 index 0000000..39c89e9 --- /dev/null +++ b/frontend/src/hooks.server.js @@ -0,0 +1,31 @@ +import { redirect } from '@sveltejs/kit'; + +const API = 'http://localhost:8080/api'; + +const PUBLIC_PREFIXES = ['/_app', '/api']; + +export async function handle({ event, resolve }) { + const isStatic = PUBLIC_PREFIXES.some((p) => event.url.pathname.startsWith(p)); + const isLogin = event.url.pathname === '/login'; + const isPublic = event.url.pathname === '/' || event.url.pathname.startsWith('/status/'); + + if (isStatic || isLogin || isPublic) { + return resolve(event); + } + + if (event.url.pathname.startsWith('/admin')) { + const token = event.cookies.get('auth_token'); + if (!token) { + throw redirect(302, '/login'); + } + const res = await fetch(`${API}/auth/verify`, { + headers: { Authorization: `Bearer ${token}` } + }); + if (!res.ok) { + throw redirect(302, '/login'); + } + event.locals.token = token; + } + + return resolve(event); +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts new file mode 100644 index 0000000..0be9148 --- /dev/null +++ b/frontend/src/lib/api.ts @@ -0,0 +1,39 @@ +import type { Service, Heartbeat, ServiceStats } from './types'; + +const BASE = '/api'; + +export async function listServices(): Promise { + const res = await fetch(`${BASE}/services`); + if (!res.ok) throw new Error('falha ao listar serviços'); + return res.json(); +} + +export async function createService(data: { name: string; url: string }): Promise { + const res = await fetch(`${BASE}/services`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.error || 'falha ao criar serviço'); + } + return res.json(); +} + +export async function deleteService(id: number): Promise { + const res = await fetch(`${BASE}/services/${id}`, { method: 'DELETE' }); + if (!res.ok) throw new Error('falha ao remover serviço'); +} + +export async function getHistory(id: number): Promise { + const res = await fetch(`${BASE}/services/${id}/history`); + if (!res.ok) throw new Error('falha ao obter histórico'); + return res.json(); +} + +export async function getServiceStats(id: number): Promise { + const res = await fetch(`${BASE}/services/${id}/stats`); + if (!res.ok) throw new Error('falha ao obter estatísticas'); + return res.json(); +} diff --git a/frontend/src/lib/components/ActivityLog.svelte b/frontend/src/lib/components/ActivityLog.svelte new file mode 100644 index 0000000..4d7ff79 --- /dev/null +++ b/frontend/src/lib/components/ActivityLog.svelte @@ -0,0 +1,122 @@ + + +
+
+

+ Atividade Recente +

+ {entries.length} eventos +
+
+ {#if entries.length === 0} +

Nenhuma atividade ainda

+ {:else} + {#each entries as entry, i (i)} +
+
+ {#if entry.kind === 'created'} + + + + + + {:else if entry.kind === 'deleted'} + + + + + + {:else if entry.kind === 'transition'} + + {entry.hb?.is_up ? '↑' : '↓'} + + {:else} + + {entry.hb?.is_up ? '✓' : '✗'} + + {/if} +
+
+
+ {entry.serviceName} + {timeAgo(entry.timestamp)} +
+

+ {#if entry.kind === 'created'} + Serviço adicionado + {:else if entry.kind === 'deleted'} + Serviço removido + {:else if entry.kind === 'transition'} + {entry.hb?.is_up ? 'Recuperado' : 'Falha'} — {entry.hb?.response_time_ms ?? '?'}ms + {:else} + {entry.hb?.is_up ? 'Online' : 'Offline'} — {entry.hb?.status_code} / {entry.hb?.response_time_ms}ms + {/if} +

+
+
+ {/each} + {/if} +
+
+ + diff --git a/frontend/src/lib/components/AddServiceModal.svelte b/frontend/src/lib/components/AddServiceModal.svelte new file mode 100644 index 0000000..de1ba13 --- /dev/null +++ b/frontend/src/lib/components/AddServiceModal.svelte @@ -0,0 +1,224 @@ + + + + +{#if show} + +
+
+
+

+ Novo Serviço +

+ +
+ +
+
+ + +
+ +
+ + +
+ +
+

+ Alertas (opcional) +

+
+ + +
+
+ + +
+
+ + +

+ Se informado, o monitor vai ler o HTML e marcar como DOWN se não encontrar essa palavra. +

+
+
+ + {#if error} +

{error}

+ {/if} + +
+ + +
+
+
+
+{/if} + + diff --git a/frontend/src/lib/components/ServiceCard.svelte b/frontend/src/lib/components/ServiceCard.svelte new file mode 100644 index 0000000..29305f2 --- /dev/null +++ b/frontend/src/lib/components/ServiceCard.svelte @@ -0,0 +1,90 @@ + + +{#if service} +
+
+ +
+
+
+

{service.name}

+

{service.url}

+
+ +
+ +
+ + + {isUp ? 'UP' : 'DOWN'} + + + {#if lastMs !== null} + + {lastMs}ms + + {/if} + + {#if uptime30d !== null} + + {uptime30d.toFixed(2)}% + + {/if} +
+ + + + + Detalhes → + +
+
+{/if} diff --git a/frontend/src/lib/components/UptimeTimeline.svelte b/frontend/src/lib/components/UptimeTimeline.svelte new file mode 100644 index 0000000..6cb47df --- /dev/null +++ b/frontend/src/lib/components/UptimeTimeline.svelte @@ -0,0 +1,41 @@ + + +
+
+ {#each blocks as block, i} +
+ {/each} +
+
+ {heartbeats.length > 0 ? '-30 min' : ''} + {heartbeats.length} checks + agora +
+
diff --git a/frontend/src/lib/realtime.ts b/frontend/src/lib/realtime.ts new file mode 100644 index 0000000..fb9c75c --- /dev/null +++ b/frontend/src/lib/realtime.ts @@ -0,0 +1,42 @@ +import type { Heartbeat } from '$lib/types'; + +type Listener = (hb: Heartbeat) => void; + +const listeners = new Set(); +let es: EventSource | null = null; + +function connect() { + if (es) return; + + es = new EventSource('/api/stream'); + + es.addEventListener('connected', () => { + console.log('[sse] conectado'); + }); + + es.addEventListener('heartbeat', (event: MessageEvent) => { + try { + const hb: Heartbeat = JSON.parse(event.data); + listeners.forEach((fn) => fn(hb)); + } catch { + // ignore parse errors + } + }); + + es.onerror = () => { + console.warn('[sse] erro — reconectando...'); + }; +} + +export function subscribeHeartbeats(fn: Listener) { + listeners.add(fn); + connect(); + + return () => { + listeners.delete(fn); + if (listeners.size === 0 && es) { + es.close(); + es = null; + } + }; +} diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts new file mode 100644 index 0000000..f80c933 --- /dev/null +++ b/frontend/src/lib/types.ts @@ -0,0 +1,44 @@ +export interface Service { + id: number; + name: string; + url: string; + group_name: string; + interval_seconds: number; + is_active: boolean; + discord_webhook_url?: string; + alert_email?: string; + keyword_to_find?: string; + created_at: string; + last_heartbeat?: Heartbeat | null; +} + +export interface Maintenance { + id: number; + title: string; + start_time: string; + end_time: string; + is_active: boolean; +} + +export interface Heartbeat { + id: number; + service_id: number; + status_code: number; + response_time_ms: number; + is_up: boolean; + error_message?: string; + tested_at: string; +} + +export interface ServiceStats { + service_id: number; + uptime_24h: number; + uptime_7d: number; + uptime_30d: number; + avg_response_ms_24h: number; + avg_response_ms_7d: number; + avg_response_ms_30d: number; + total_checks_24h: number; + total_checks_7d: number; + total_checks_30d: number; +} diff --git a/frontend/src/routes/(admin)/admin/+page.server.js b/frontend/src/routes/(admin)/admin/+page.server.js new file mode 100644 index 0000000..bdf498b --- /dev/null +++ b/frontend/src/routes/(admin)/admin/+page.server.js @@ -0,0 +1,27 @@ +const API = 'http://localhost:8080/api'; + +export async function load({ locals }) { + const token = locals.token; + + const headers = token ? { Authorization: `Bearer ${token}` } : {}; + + const res = await fetch(`${API}/services`, { headers }); + if (!res.ok) { + return { services: [], error: 'Falha ao carregar serviços', token: token ?? '' }; + } + const services = await res.json(); + + const servicesWithStats = await Promise.all( + services.map(async (svc) => { + try { + const statsRes = await fetch(`${API}/services/${svc.id}/stats`, { headers }); + if (statsRes.ok) { + svc.stats = await statsRes.json(); + } + } catch { /* ignore */ } + return svc; + }) + ); + + return { services: servicesWithStats, error: null, token: token ?? '' }; +} diff --git a/frontend/src/routes/(admin)/admin/+page.svelte b/frontend/src/routes/(admin)/admin/+page.svelte new file mode 100644 index 0000000..24b56db --- /dev/null +++ b/frontend/src/routes/(admin)/admin/+page.svelte @@ -0,0 +1,432 @@ + + + + Admin — YAUM + + +
+
+

Painel de Controle

+

Gerenciamento de serviços monitorados

+
+ + ← Dashboard + +
+ +
+ +
+ +{#if data.error} +
+

{data.error}

+
+{:else if services.length === 0} +
+

Nenhum serviço cadastrado

+
+{:else} + {#each [...new Set(services.map((s) => s.group_name || 'Geral'))].sort() as group} +
+

{group}

+
+ + + + + + + + + + + + + {#each services.filter((s) => (s.group_name || 'Geral') === group) as svc (svc.id)} + + + + + + + + + {/each} + +
IDNomeStatusSLA 30dAções
{svc.id}{svc.name} + + {svc.last_heartbeat?.is_up ?? true ? 'UP' : 'DOWN'} + + + {svc.stats ? svc.stats.uptime_30d.toFixed(2) + '%' : '—'} + +
+ + + + +
+
+
+
+ {/each} +{/if} + + +{#if showForm} + +
+
e.stopPropagation()} + > +
+

+ {editingSvc ? 'Editar Serviço' : 'Novo Serviço'} +

+ +
+ +
+ {#if editingSvc} + + {/if} + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+

Opcionais

+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + {#if formError} +

{formError}

+ {/if} + +
+ + +
+
+
+
+{/if} + + diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte new file mode 100644 index 0000000..9ed74ce --- /dev/null +++ b/frontend/src/routes/+layout.svelte @@ -0,0 +1,50 @@ + + +
+ {#if maintenance} +
+
+ + + +

{maintenance.title}

+
+
+ {/if} + +
+
+
+ + YAUM + + +
+ +
+
+ +
+ {@render children()} +
+
diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte new file mode 100644 index 0000000..511460a --- /dev/null +++ b/frontend/src/routes/+page.svelte @@ -0,0 +1,164 @@ + + + + YAUM — Yet Another Uptime Monitor + + +
+

Dashboard

+

Status dos serviços monitorados

+
+ +{#if error} +
+ + + +

{error}

+ +
+{:else if services.length === 0} +
+
+ + + +
+

Nenhum serviço sendo monitorado

+

Adicione o primeiro serviço para começar

+ +
+{:else} + {#each [...new Set(services.map((s) => s.group_name || 'Geral'))].sort() as group} +
+

{group}

+
+ {#each services.filter((s) => (s.group_name || 'Geral') === group) as svc (svc.id)} + + {/each} +
+
+ {/each} + +
+ +
+{/if} + + (showModal = false)} + oncreated={handleCreated} +/> diff --git a/frontend/src/routes/+page.ts b/frontend/src/routes/+page.ts new file mode 100644 index 0000000..ba70169 --- /dev/null +++ b/frontend/src/routes/+page.ts @@ -0,0 +1,33 @@ +import type { Service, Heartbeat } from '$lib/types'; + +export interface EnhancedService extends Service { + heartbeats: Heartbeat[]; +} + +export async function load({ fetch }) { + let services: EnhancedService[] = []; + let error = ''; + + try { + const res = await fetch('/api/services'); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const list: Service[] = await res.json(); + + services = await Promise.all( + list.map(async (svc) => { + try { + const hres = await fetch(`/api/services/${svc.id}/history`); + if (!hres.ok) throw new Error(`HTTP ${hres.status}`); + const heartbeats: Heartbeat[] = await hres.json(); + return { ...svc, heartbeats }; + } catch { + return { ...svc, heartbeats: [] }; + } + }) + ); + } catch (e) { + error = 'Não foi possível conectar ao servidor'; + } + + return { services, error }; +} diff --git a/frontend/src/routes/login/+page.server.js b/frontend/src/routes/login/+page.server.js new file mode 100644 index 0000000..4272ef7 --- /dev/null +++ b/frontend/src/routes/login/+page.server.js @@ -0,0 +1,51 @@ +import { redirect } from '@sveltejs/kit'; + +const API = 'http://localhost:8080/api'; + +export async function load({ cookies }) { + const token = cookies.get('auth_token'); + if (token) { + const res = await fetch(`${API}/auth/verify`, { + headers: { Authorization: `Bearer ${token}` } + }); + if (res.ok) { + throw redirect(302, '/admin'); + } + } + return {}; +} + +export const actions = { + default: async ({ request, cookies }) => { + const data = await request.formData(); + const username = data.get('username'); + const password = data.get('password'); + + if (!username || !password) { + return { error: 'Preencha todos os campos' }; + } + + const res = await fetch(`${API}/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }) + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({})); + return { error: err.error || 'Credenciais inválidas' }; + } + + const { token } = await res.json(); + + cookies.set('auth_token', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/', + maxAge: 7 * 24 * 60 * 60 + }); + + throw redirect(302, '/admin'); + } +}; diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte new file mode 100644 index 0000000..a472c89 --- /dev/null +++ b/frontend/src/routes/login/+page.svelte @@ -0,0 +1,61 @@ + + + + Login — YAUM + + +
+
+
+

YAUM

+

Yet Another Uptime Monitor

+
+ +
+
+
+ + +
+ +
+ + +
+ + {#if form?.error} +

{form.error}

+ {/if} + + +
+
+ +

+ ← Voltar ao Dashboard +

+
+
diff --git a/frontend/src/routes/services/[id]/+page.svelte b/frontend/src/routes/services/[id]/+page.svelte new file mode 100644 index 0000000..36ea105 --- /dev/null +++ b/frontend/src/routes/services/[id]/+page.svelte @@ -0,0 +1,261 @@ + + + + {svc?.name ?? 'Detalhes'} — YAUM + + +{#if loading} +
+
+
+{:else if !svc} +
+

Serviço não encontrado

+
+{:else} + + +
+

{svc.name}

+

{svc.url}

+
+ +
+ + {#if stats} +
+

+ Uptime +

+
+ {#each [ + { label: '24h', uptime: stats.uptime_24h, checks: stats.total_checks_24h, avg: stats.avg_response_ms_24h }, + { label: '7 dias', uptime: stats.uptime_7d, checks: stats.total_checks_7d, avg: stats.avg_response_ms_7d }, + { label: '30 dias', uptime: stats.uptime_30d, checks: stats.total_checks_30d, avg: stats.avg_response_ms_30d } + ] as item} +
+
+ + {item.label} + + + {item.checks} checks + +
+
+ + {item.uptime.toFixed(2)}% + + + {item.avg.toFixed(0)}ms méd. + +
+
+ {/each} +
+
+ {/if} + + +
+

+ Tempo de Resposta (ms) +

+
+ {#if history.length === 0} +
+ Sem dados ainda +
+ {:else if history.length === 1} +
+ + {history[0].response_time_ms} + ms + + + {history[0].is_up ? 'Online' : 'Offline'} — + {history[0].status_code || 'timeout'} + +
+ {:else} + + {#each yTicks as tick} + + {tick.val} + {/each} + + `${d.x},${d.y}`).join(' ')} + fill="none" + stroke="var(--green)" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + /> + + {#each dots as d} + + {/each} + + {/if} +
+
+ {history[0]?.tested_at ? new Date(history[0].tested_at).toLocaleTimeString() : ''} + {history[history.length - 1]?.tested_at + ? new Date(history[history.length - 1].tested_at).toLocaleTimeString() + : ''} +
+
+ + +
+

+ Últimas Verificações +

+
+ {#each history.slice().reverse() as h} +
+
+
+ + + {#if h.status_code > 0} + {h.status_code} + {:else} + TIMEOUT + {/if} + +
+
+ {h.response_time_ms}ms + {#if h.error_message} + + {/if} +
+
+ {#if expandedError === h.id && h.error_message} +
+
{h.error_message}
+
+ {/if} +
+ {/each} +
+
+
+{/if} 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 @@ + + + + {svc?.name ?? 'Status'} — YAUM + + +
+ {#if !svc} +
+
+ + + +
+

Serviço não encontrado

+

+ ← Voltar ao início +

+
+ {:else} + + + + +
+ {#if isUp === null} +
+

Aguardando verificação

+ {:else} +
+ {#if isUp} + + + + {:else} + + + + {/if} +
+

+ {isUp ? 'Operando Normalmente' : 'Fora do Ar'} +

+ {/if} + +
+

{svc.name}

+

{svc.url}

+
+ + {#if stats} +
+
+

SLA 30 dias

+

+ {stats.uptime_30d.toFixed(2)}% +

+
+
+
+

Latência Média

+

+ {stats.avg_response_ms_30d.toFixed(0)}ms +

+
+
+
+

Total de Checks

+

{stats.total_checks_30d}

+
+
+ {/if} +
+ +
+ + {#if stats} +
+

+ Uptime +

+
+ {#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} +
+
+ {item.label} + {item.checks} checks +
+
+ + {item.uptime.toFixed(2)}% + + {item.avg.toFixed(0)}ms méd. +
+
+ {/each} +
+
+ {/if} + + +
+

+ Tempo de Resposta (ms) +

+
+ {#if history.length === 0} +
+ Sem dados ainda +
+ {:else if history.length === 1} +
+ + {history[0].response_time_ms} + ms + + + {history[0].is_up ? 'Online' : 'Offline'} — {history[0].status_code || 'timeout'} + +
+ {:else} + + {#each yTicks as tick} + + {tick.val} + {/each} + `${d.x},${d.y}`).join(' ')} + fill="none" stroke="var(--green)" stroke-width="2" + stroke-linecap="round" stroke-linejoin="round" + /> + {#each dots as d} + + {/each} + + {/if} +
+
+ {history[0]?.tested_at ? new Date(history[0].tested_at).toLocaleTimeString() : ''} + {history[history.length - 1]?.tested_at ? new Date(history[history.length - 1].tested_at).toLocaleTimeString() : ''} +
+
+
+ + +
+

+ Histórico de Verificações +

+ {#if history.length === 0} +

Nenhuma verificação registrada

+ {:else} +
+ + + + + + + + + + + + {#each [...history].reverse() as h} + + + + + + + + {/each} + +
HorárioStatusCódigoLatênciaErro
+ {new Date(h.tested_at).toLocaleString('pt-BR')} + + + {h.is_up ? 'UP' : 'DOWN'} + + + {h.status_code > 0 ? h.status_code : '—'} + + {h.response_time_ms}ms + + {#if h.error_message} + + {#if expandedError === h.id} +
{(h as any).error_message}
+ {/if} + {:else} + — + {/if} +
+
+ {/if} +
+ {/if} +
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 + }; +}; -- cgit v1.2.3