aboutsummaryrefslogtreecommitdiff
path: root/frontend/src/lib/components/ActivityLog.svelte
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/lib/components/ActivityLog.svelte')
-rw-r--r--frontend/src/lib/components/ActivityLog.svelte122
1 files changed, 122 insertions, 0 deletions
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 @@
+<script lang="ts">
+ import type { Heartbeat } from '$lib/types';
+
+ interface LogEntry {
+ kind: 'check' | 'created' | 'deleted' | 'transition';
+ serviceName: string;
+ serviceUrl: string;
+ timestamp: Date;
+ hb?: Heartbeat;
+ wasUp?: boolean;
+ }
+
+ let {
+ entries = []
+ }: {
+ entries: LogEntry[];
+ } = $props();
+
+ let el: HTMLDivElement;
+ let autoScroll = $state(true);
+
+ function handleScroll() {
+ if (!el) return;
+ const dist = el.scrollHeight - el.scrollTop - el.clientHeight;
+ autoScroll = dist < 40;
+ }
+
+ $effect(() => {
+ if (entries.length && autoScroll && el) {
+ el.scrollTop = el.scrollHeight;
+ }
+ });
+
+ function timeAgo(date: Date): string {
+ const sec = Math.floor((Date.now() - date.getTime()) / 1000);
+ if (sec < 10) return 'agora';
+ if (sec < 60) return `${sec}s`;
+ const min = Math.floor(sec / 60);
+ if (min === 1) return '1min';
+ if (min < 60) return `${min}min`;
+ return date.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
+ }
+</script>
+
+<div class="rounded-2xl border border-[var(--border-color)] bg-[var(--bg-card)]">
+ <div class="flex items-center justify-between border-b border-[var(--border-color)] px-5 py-3">
+ <h2 class="text-xs font-semibold uppercase tracking-wider text-[var(--text-secondary)]">
+ Atividade Recente
+ </h2>
+ <span class="text-[10px] tabular-nums text-[var(--text-muted)]">{entries.length} eventos</span>
+ </div>
+ <div
+ bind:this={el}
+ onscroll={handleScroll}
+ class="scrollbar-thin space-y-0.5 overflow-y-auto px-3 py-2"
+ style="max-height: 320px"
+ >
+ {#if entries.length === 0}
+ <p class="py-8 text-center text-xs text-[var(--text-muted)]">Nenhuma atividade ainda</p>
+ {:else}
+ {#each entries as entry, i (i)}
+ <div
+ class="flex items-start gap-3 rounded-xl px-3 py-2.5 transition-colors hover:bg-[var(--border-color)]/30"
+ >
+ <div class="mt-0.5 shrink-0">
+ {#if entry.kind === 'created'}
+ <span class="flex h-5 w-5 items-center justify-center rounded-md bg-[var(--green)]/10 text-[10px] text-[var(--green)]">
+ <svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
+ </svg>
+ </span>
+ {:else if entry.kind === 'deleted'}
+ <span class="flex h-5 w-5 items-center justify-center rounded-md bg-[var(--red)]/10 text-[10px] text-[var(--red)]">
+ <svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
+ </svg>
+ </span>
+ {:else if entry.kind === 'transition'}
+ <span
+ class="flex h-5 w-5 items-center justify-center rounded-md text-[10px]"
+ style="background-color: {entry.hb?.is_up ? 'rgba(34,240,106,0.1)' : 'rgba(255,64,96,0.1)'}; color: {entry.hb?.is_up ? 'var(--green)' : 'var(--red)'}"
+ >
+ {entry.hb?.is_up ? '↑' : '↓'}
+ </span>
+ {:else}
+ <span
+ class="flex h-5 w-5 items-center justify-center rounded-md text-[10px]"
+ style="background-color: {entry.hb?.is_up ? 'rgba(34,240,106,0.1)' : 'rgba(255,64,96,0.1)'}; color: {entry.hb?.is_up ? 'var(--green)' : 'var(--red)'}"
+ >
+ {entry.hb?.is_up ? '✓' : '✗'}
+ </span>
+ {/if}
+ </div>
+ <div class="min-w-0 flex-1">
+ <div class="flex items-baseline gap-2">
+ <span class="truncate text-xs font-medium text-white">{entry.serviceName}</span>
+ <span class="shrink-0 text-[10px] text-[var(--text-muted)]">{timeAgo(entry.timestamp)}</span>
+ </div>
+ <p class="truncate text-[10px] text-[var(--text-muted)]">
+ {#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}
+ </p>
+ </div>
+ </div>
+ {/each}
+ {/if}
+ </div>
+</div>
+
+<style>
+ .scrollbar-thin {
+ scrollbar-width: thin;
+ scrollbar-color: var(--border-color) transparent;
+ }
+</style>