pingql/apps/web/src/views/home.ejs

120 lines
6.0 KiB
Plaintext

<%~ include('./partials/head', { title: 'Monitors' }) %>
<%~ include('./partials/nav', { nav: 'monitors' }) %>
<%
const upCount = it.monitors.filter(m => m.last_ping && m.last_ping.up === true).length;
const downCount = it.monitors.filter(m => m.last_ping && m.last_ping.up === false).length;
%>
<!-- Content -->
<main class="max-w-7xl mx-auto px-8 py-8">
<div class="flex items-center justify-between mb-6">
<h2 class="text-lg font-semibold text-gray-200">Monitors</h2>
<div class="flex items-center gap-4">
<div id="summary" class="text-sm text-gray-500"><% if (it.monitors.length > 0) { %><span class="text-green-400"><%= upCount %> up</span> · <span class="<%= downCount > 0 ? 'text-red-400' : 'text-gray-500' %>"><%= downCount %> down</span> · <%= it.monitors.length %> total<% } %></div>
<a href="/dashboard/monitors/new" class="bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors">+ New</a>
</div>
</div>
<div id="monitors-list" class="space-y-3">
<% if (it.monitors.length === 0) { %>
<div id="empty-state" class="text-center py-16">
<p class="text-gray-500 mb-4">No monitors yet</p>
<a href="/dashboard/monitors/new" class="bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium px-6 py-3 rounded-lg transition-colors inline-block">Create your first monitor</a>
</div>
<% } else { %>
<% it.monitors.forEach(function(m) {
const latencies = (m.pings || []).filter(p => p.latency_ms != null);
const avgLatency = latencies.length ? Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length) : null;
%>
<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">
<span class="status-dot w-2.5 h-2.5 rounded-full <%= m.last_ping == null ? 'bg-gray-600' : (m.last_ping.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"><%= m.name %></div>
<div class="text-xs text-gray-500 truncate"><%= m.url %></div>
</div>
</div>
<div class="flex items-center gap-6 shrink-0 ml-4">
<div class="hidden sm:block stat-sparkline" style="width:120px;height:32px"><%~ it.sparklineSSR(latencies) %></div>
<div class="text-right" style="width:72px">
<div class="text-sm text-gray-300 stat-latency"><%= avgLatency != null ? avgLatency + 'ms' : '—' %></div>
<div class="text-xs text-gray-500 stat-last"><%~ m.last_ping ? it.timeAgoSSR(m.last_ping.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>
</div>
</a>
<% }) %>
<% } %>
</div>
</main>
<script>
// Reload on tab focus if stale
let lastLoad = Date.now();
document.addEventListener('visibilitychange', () => {
if (!document.hidden && Date.now() - lastLoad > 60000) { location.reload(); }
});
// Known monitor IDs for change detection
const _knownIds = [<% it.monitors.forEach(function(m, i) { %>'<%= m.id %>'<% if (i < it.monitors.length - 1) { %>,<% } %><% }) %>];
// Poll every 30s to detect monitor list changes
setInterval(async () => {
try {
const res = await fetch('/dashboard/home/data', { credentials: 'same-origin' });
if (!res.ok) return;
const data = await res.json();
const ids = data.monitorIds || [];
if (ids.length !== _knownIds.length || ids.some(id => !_knownIds.includes(id))) {
location.reload();
}
} catch {}
}, 30000);
// SSE: on each ping, update text fields and redraw sparkline in place
watchAccount((ping) => {
const card = document.querySelector(`[data-monitor-id="${ping.monitor_id}"]`);
if (!card) return;
// Status dot
const dot = card.querySelector('.status-dot');
if (dot) dot.className = `status-dot w-2.5 h-2.5 rounded-full ${ping.up ? 'bg-green-500' : 'bg-red-500'}`;
// Last latency + last ping time
if (ping.latency_ms != null) card.querySelector('.stat-latency').textContent = ping.latency_ms + 'ms';
card.querySelector('.stat-last').innerHTML = timeAgo(ping.checked_at);
// Sparkline — append point to existing polyline, drop oldest, no refetch
if (ping.latency_ms != null) {
const sparkEl = card.querySelector('.stat-sparkline');
const polyline = sparkEl?.querySelector('polyline');
if (polyline) {
const pts = polyline.getAttribute('points').trim().split(' ').filter(Boolean);
const W = 120, H = 32;
// Parse existing points
let coords = pts.map(p => p.split(',').map(Number));
// Extract just y-values (latencies are encoded in y relative to scale)
// Easier: maintain a data attr on the svg with the raw values
const svg = sparkEl.querySelector('svg');
let vals = svg?.dataset.vals ? svg.dataset.vals.split(',').map(Number) : [];
vals.push(ping.latency_ms);
if (vals.length > 20) vals.shift();
if (svg) svg.dataset.vals = vals.join(',');
// Redraw polyline in place
const max = Math.max(...vals, 1);
const min = Math.min(...vals, 0);
const range = max - min || 1;
const step = W / Math.max(vals.length - 1, 1);
const newPts = vals.map((v, i) => `${i * step},${H - ((v - min) / range) * (H - 4) - 2}`).join(' ');
polyline.setAttribute('points', newPts);
}
}
});
</script>
<%~ include('./partials/foot') %>