From 6cc0bfd1d79074df790272b8091b1f0226d14283 Mon Sep 17 00:00:00 2001
From: zwlucas <lucas.fariamo08@gmail.com>
Date: Fri, 29 May 2026 15:57:37 -0300
Subject: feat: upload project

Signed-off-by: zwlucas <lucas.fariamo08@gmail.com>
---
 frontend/src/routes/+page.svelte | 164 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 164 insertions(+)
 create mode 100644 frontend/src/routes/+page.svelte

(limited to 'frontend/src/routes/+page.svelte')

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}
+/>
-- 
cgit v1.2.3

