feat: SSE live ping stream for monitors
This commit is contained in:
@@ -267,7 +267,35 @@
|
||||
});
|
||||
|
||||
load();
|
||||
setInterval(load, 30000);
|
||||
setInterval(load, 60000); // fallback poll, less frequent now that SSE handles updates
|
||||
|
||||
// SSE: live ping updates
|
||||
const id = window.location.pathname.split('/').pop();
|
||||
watchMonitor(id, (ping) => {
|
||||
// Update stat bar
|
||||
document.getElementById('stat-last').innerHTML = timeAgo(ping.checked_at);
|
||||
if (ping.latency_ms) document.getElementById('stat-latency').textContent = ping.latency_ms + 'ms';
|
||||
|
||||
// Prepend new row to ping table
|
||||
const tbody = document.getElementById('pings-table');
|
||||
if (tbody) {
|
||||
const upBadge = ping.up
|
||||
? '<span class="text-green-400">Up</span>'
|
||||
: '<span class="text-red-400">Down</span>';
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'border-b border-gray-800 fade-in-row';
|
||||
tr.innerHTML = `
|
||||
<td class="px-4 py-2">${upBadge}</td>
|
||||
<td class="px-4 py-2 text-gray-300">${ping.status_code ?? '—'}</td>
|
||||
<td class="px-4 py-2 text-gray-300">${ping.latency_ms ? ping.latency_ms + 'ms' : '—'}</td>
|
||||
<td class="px-4 py-2 text-gray-500">${timeAgo(ping.checked_at)}</td>
|
||||
<td class="px-4 py-2 text-red-400 text-xs">${ping.error ?? ''}</td>
|
||||
`;
|
||||
tbody.prepend(tr);
|
||||
// Trim to keep table manageable
|
||||
while (tbody.children.length > 100) tbody.removeChild(tbody.lastChild);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<%~ include('./partials/foot') %>
|
||||
|
||||
@@ -59,10 +59,10 @@
|
||||
const avgLatency = latencies.length ? Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length) : null;
|
||||
|
||||
return `
|
||||
<a href="/dashboard/monitors/${m.id}" class="block bg-gray-900 hover:bg-gray-800/80 border border-gray-800 rounded-xl p-4 transition-colors group">
|
||||
<a href="/dashboard/monitors/${m.id}" data-monitor-id="${m.id}" class="block bg-gray-900 hover:bg-gray-800/80 border border-gray-800 rounded-xl p-4 transition-colors group">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
${statusBadge(lastPing?.up)}
|
||||
<span class="status-dot w-2.5 h-2.5 rounded-full ${lastPing == null ? 'bg-gray-600' : lastPing.up ? 'bg-green-500' : 'bg-red-500'}"></span>
|
||||
<div class="min-w-0">
|
||||
<div class="font-medium text-gray-100 group-hover:text-white truncate">${escapeHtml(m.name)}</div>
|
||||
<div class="text-xs text-gray-500 truncate">${escapeHtml(m.url)}</div>
|
||||
@@ -71,8 +71,8 @@
|
||||
<div class="flex items-center gap-6 shrink-0 ml-4">
|
||||
<div class="hidden sm:block">${sparkline(latencies)}</div>
|
||||
<div class="text-right">
|
||||
<div class="text-sm text-gray-300">${avgLatency != null ? avgLatency + 'ms' : '—'}</div>
|
||||
<div class="text-xs text-gray-500">${lastPing ? timeAgo(lastPing.checked_at) : 'no pings'}</div>
|
||||
<div class="text-sm text-gray-300 stat-latency">${avgLatency != null ? avgLatency + 'ms' : '—'}</div>
|
||||
<div class="text-xs text-gray-500 stat-last">${lastPing ? timeAgo(lastPing.checked_at) : 'no pings'}</div>
|
||||
</div>
|
||||
<div class="text-xs px-2 py-1 rounded ${m.enabled ? 'bg-gray-800 text-gray-400' : 'bg-yellow-900/30 text-yellow-500'}">${m.enabled ? m.interval_s + 's' : 'paused'}</div>
|
||||
</div>
|
||||
@@ -87,6 +87,31 @@
|
||||
|
||||
load();
|
||||
setInterval(load, 30000);
|
||||
|
||||
// SSE: subscribe to all monitors after load so cards update in real time
|
||||
const sseConnections = [];
|
||||
async function subscribeAll() {
|
||||
sseConnections.forEach(es => es.close());
|
||||
sseConnections.length = 0;
|
||||
const monitors = await api('/monitors/');
|
||||
monitors.forEach(m => {
|
||||
const es = watchMonitor(m.id, (ping) => {
|
||||
// Update the card's last ping info without full reload
|
||||
const card = document.querySelector(`[data-monitor-id="${m.id}"]`);
|
||||
if (!card) return;
|
||||
const statusDot = card.querySelector('.status-dot');
|
||||
const latencyEl = card.querySelector('.stat-latency');
|
||||
const lastEl = card.querySelector('.stat-last');
|
||||
if (statusDot) {
|
||||
statusDot.className = `status-dot w-2.5 h-2.5 rounded-full ${ping.up ? 'bg-green-500' : 'bg-red-500'}`;
|
||||
}
|
||||
if (latencyEl && ping.latency_ms) latencyEl.textContent = `${ping.latency_ms}ms`;
|
||||
if (lastEl) lastEl.innerHTML = timeAgo(ping.checked_at);
|
||||
});
|
||||
if (es) sseConnections.push(es);
|
||||
});
|
||||
}
|
||||
subscribeAll();
|
||||
</script>
|
||||
|
||||
<%~ include('./partials/foot') %>
|
||||
|
||||
Reference in New Issue
Block a user