aboutsummaryrefslogtreecommitdiff
path: root/frontend/src/lib/components/ActivityLog.svelte
blob: 4d7ff793e33762ae4b45dd4974313258555e1ec8 (plain)
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>