aboutsummaryrefslogtreecommitdiff
path: root/frontend/src/routes/+page.svelte
diff options
context:
space:
mode:
authorzwlucas <lucas.fariamo08@gmail.com>2026-05-29 18:57:37 +0000
committerzwlucas <lucas.fariamo08@gmail.com>2026-05-29 18:57:37 +0000
commit6cc0bfd1d79074df790272b8091b1f0226d14283 (patch)
treecab18b0f708cf3b0eaeeeb98e2cf81459365b33e /frontend/src/routes/+page.svelte
downloadyaum-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.svelte164
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}
+/>