diff options
Diffstat (limited to 'frontend/src/lib/components/ActivityLog.svelte')
| -rw-r--r-- | frontend/src/lib/components/ActivityLog.svelte | 122 |
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> |