1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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>
|