diff options
| author | zwlucas <lucas.fariamo08@gmail.com> | 2026-05-29 18:57:37 +0000 |
|---|---|---|
| committer | zwlucas <lucas.fariamo08@gmail.com> | 2026-05-29 18:57:37 +0000 |
| commit | 6cc0bfd1d79074df790272b8091b1f0226d14283 (patch) | |
| tree | cab18b0f708cf3b0eaeeeb98e2cf81459365b33e /frontend/src/routes/+page.svelte | |
| download | yaum-6cc0bfd1d79074df790272b8091b1f0226d14283.tar.gz yaum-6cc0bfd1d79074df790272b8091b1f0226d14283.zip | |
feat: upload project
Signed-off-by: zwlucas <lucas.fariamo08@gmail.com>
Diffstat (limited to 'frontend/src/routes/+page.svelte')
| -rw-r--r-- | frontend/src/routes/+page.svelte | 164 |
1 files changed, 164 insertions, 0 deletions
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 @@ +<script lang="ts"> + import { onMount } from 'svelte'; + import { deleteService } from '$lib/api'; + import ServiceCard from '$lib/components/ServiceCard.svelte'; + import AddServiceModal from '$lib/components/AddServiceModal.svelte'; + import ActivityLog from '$lib/components/ActivityLog.svelte'; + import { subscribeHeartbeats } from '$lib/realtime'; + import type { Service, Heartbeat } from '$lib/types'; + + let { data } = $props<{ + data: { services: (Service & { heartbeats: Heartbeat[] })[]; error: string }; + }>(); + + let initialized = $state(false); + let services = $state<(Service & { heartbeats: Heartbeat[] })[]>([]); + let error = $state(''); + let showModal = $state(false); + + $effect(() => { + if (!initialized && data) { + services = [...data.services]; + error = data.error; + initialized = true; + } + }); + + interface LogEntry { + kind: 'check' | 'created' | 'deleted' | 'transition'; + serviceName: string; + serviceUrl: string; + timestamp: Date; + hb?: Heartbeat; + wasUp?: boolean; + } + + let logEntries = $state<LogEntry[]>([]); + + function pushEntry(entry: LogEntry) { + logEntries = [...logEntries, entry]; + if (logEntries.length > 200) { + logEntries = logEntries.slice(-150); + } + } + + function handleCreated(svc: Service) { + services = [{ ...svc, heartbeats: [] }, ...services]; + pushEntry({ + kind: 'created', + serviceName: svc.name, + serviceUrl: svc.url, + timestamp: new Date() + }); + } + + async function handleDeleted(id: number) { + const svc = services.find((s) => s.id === id); + try { + await deleteService(id); + services = services.filter((s) => s.id !== id); + if (svc) { + pushEntry({ + kind: 'deleted', + serviceName: svc.name, + serviceUrl: svc.url, + timestamp: new Date() + }); + } + } catch { + // silent + } + } + + onMount(() => { + const unsub = subscribeHeartbeats((hb) => { + let wasUp: boolean | undefined; + services = services.map((s) => { + if (s.id !== hb.service_id) return s; + if (s.last_heartbeat) { + wasUp = s.last_heartbeat.is_up; + } + return { + ...s, + last_heartbeat: hb, + heartbeats: [...s.heartbeats, hb].slice(-30) + }; + }); + + const svc = services.find((s) => s.id === hb.service_id); + if (!svc) return; + + const kind = wasUp !== undefined && wasUp !== hb.is_up ? 'transition' : 'check'; + pushEntry({ + kind, + serviceName: svc.name, + serviceUrl: svc.url, + timestamp: new Date(), + hb, + wasUp + }); + }); + return unsub; + }); +</script> + +<svelte:head> + <title>YAUM — Yet Another Uptime Monitor</title> +</svelte:head> + +<div class="mb-8"> + <h1 class="text-2xl font-semibold tracking-tight text-white">Dashboard</h1> + <p class="mt-1 text-sm text-[var(--text-muted)]">Status dos serviços monitorados</p> +</div> + +{#if error} + <div class="rounded-2xl border border-[var(--red)]/20 bg-[var(--red)]/5 p-8 text-center"> + <svg class="mx-auto mb-3 h-8 w-8 text-[var(--red)]/60" 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> + <p class="text-sm text-[var(--red)]">{error}</p> + <button + onclick={() => location.reload()} + class="mt-4 text-xs font-medium text-[var(--text-muted)] underline underline-offset-2 transition-colors hover:text-white" + > + Tentar novamente + </button> + </div> +{:else if services.length === 0} + <div class="rounded-2xl border border-dashed border-[var(--border-color)] p-16 text-center"> + <div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-[var(--bg-card)]"> + <svg class="h-6 w-6 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="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" /> + </svg> + </div> + <p class="text-sm font-medium text-[var(--text-secondary)]">Nenhum serviço sendo monitorado</p> + <p class="mt-1 text-xs text-[var(--text-muted)]">Adicione o primeiro serviço para começar</p> + <button + onclick={() => (showModal = true)} + class="mt-6 rounded-xl border border-[var(--green)]/30 bg-[var(--green)]/5 px-5 py-2 text-sm font-medium text-[var(--green)] transition-all hover:bg-[var(--green)]/10" + > + + Adicionar Serviço + </button> + </div> +{:else} + {#each [...new Set(services.map((s) => s.group_name || 'Geral'))].sort() as group} + <div class="mb-8"> + <h2 class="mb-4 text-xs font-semibold uppercase tracking-wider text-[var(--text-secondary)]">{group}</h2> + <div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3"> + {#each services.filter((s) => (s.group_name || 'Geral') === group) as svc (svc.id)} + <ServiceCard service={svc} heartbeats={svc.heartbeats} ondeleted={handleDeleted} /> + {/each} + </div> + </div> + {/each} + + <div class="mt-8"> + <ActivityLog entries={logEntries} /> + </div> +{/if} + +<AddServiceModal + show={showModal} + onclose={() => (showModal = false)} + oncreated={handleCreated} +/> |