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/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 ++++ 7 files changed, 602 insertions(+) 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 (limited to 'frontend/src/lib') 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; +} -- cgit v1.2.3