|
|
|
@ -75,43 +75,7 @@
|
|
|
|
} catch {}
|
|
|
|
} catch {}
|
|
|
|
}, 30000);
|
|
|
|
}, 30000);
|
|
|
|
|
|
|
|
|
|
|
|
// Client-side sparkline — seed with SSR data, update locally on SSE
|
|
|
|
// SSE: on each ping, update text fields and redraw sparkline in place
|
|
|
|
const _sparkData = {
|
|
|
|
|
|
|
|
<% it.monitors.forEach(function(m, i) {
|
|
|
|
|
|
|
|
const lats = (m.pings || []).filter(p => p.latency_ms != null).map(p => p.latency_ms);
|
|
|
|
|
|
|
|
%>'<%= m.id %>': [<%- lats.join(',') %>]<% if (i < it.monitors.length - 1) { %>,<% } %>
|
|
|
|
|
|
|
|
<% }) %>
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function drawSparkline(values, el) {
|
|
|
|
|
|
|
|
const W = 120, H = 32;
|
|
|
|
|
|
|
|
if (!values.length) { el.innerHTML = ''; return; }
|
|
|
|
|
|
|
|
const max = Math.max(...values, 1);
|
|
|
|
|
|
|
|
const min = Math.min(...values, 0);
|
|
|
|
|
|
|
|
const range = max - min || 1;
|
|
|
|
|
|
|
|
const step = W / Math.max(values.length - 1, 1);
|
|
|
|
|
|
|
|
const pts = values.map((v, i) => {
|
|
|
|
|
|
|
|
const x = i * step;
|
|
|
|
|
|
|
|
const y = H - ((v - min) / range) * (H - 4) - 2;
|
|
|
|
|
|
|
|
return `${x},${y}`;
|
|
|
|
|
|
|
|
}).join(' ');
|
|
|
|
|
|
|
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
|
|
|
|
|
|
svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
|
|
|
|
|
|
|
|
svg.setAttribute('width', W);
|
|
|
|
|
|
|
|
svg.setAttribute('height', H);
|
|
|
|
|
|
|
|
const pl = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
|
|
|
|
|
|
|
|
pl.setAttribute('points', pts);
|
|
|
|
|
|
|
|
pl.setAttribute('fill', 'none');
|
|
|
|
|
|
|
|
pl.setAttribute('stroke', '#60a5fa');
|
|
|
|
|
|
|
|
pl.setAttribute('stroke-width', '1.5');
|
|
|
|
|
|
|
|
pl.setAttribute('stroke-linecap', 'round');
|
|
|
|
|
|
|
|
pl.setAttribute('stroke-linejoin', 'round');
|
|
|
|
|
|
|
|
svg.appendChild(pl);
|
|
|
|
|
|
|
|
const old = el.querySelector('svg');
|
|
|
|
|
|
|
|
if (old) old.replaceWith(svg); else el.appendChild(svg);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// SSE: update cards in realtime
|
|
|
|
|
|
|
|
watchAccount((ping) => {
|
|
|
|
watchAccount((ping) => {
|
|
|
|
const card = document.querySelector(`[data-monitor-id="${ping.monitor_id}"]`);
|
|
|
|
const card = document.querySelector(`[data-monitor-id="${ping.monitor_id}"]`);
|
|
|
|
if (!card) return;
|
|
|
|
if (!card) return;
|
|
|
|
@ -124,13 +88,30 @@
|
|
|
|
if (ping.latency_ms != null) card.querySelector('.stat-latency').textContent = ping.latency_ms + 'ms';
|
|
|
|
if (ping.latency_ms != null) card.querySelector('.stat-latency').textContent = ping.latency_ms + 'ms';
|
|
|
|
card.querySelector('.stat-last').innerHTML = timeAgo(ping.checked_at);
|
|
|
|
card.querySelector('.stat-last').innerHTML = timeAgo(ping.checked_at);
|
|
|
|
|
|
|
|
|
|
|
|
// Sparkline — update buffer and redraw locally
|
|
|
|
// Sparkline — append point to existing polyline, drop oldest, no refetch
|
|
|
|
if (ping.latency_ms != null) {
|
|
|
|
if (ping.latency_ms != null) {
|
|
|
|
const buf = _sparkData[ping.monitor_id] = _sparkData[ping.monitor_id] || [];
|
|
|
|
|
|
|
|
buf.push(ping.latency_ms);
|
|
|
|
|
|
|
|
if (buf.length > 20) buf.shift();
|
|
|
|
|
|
|
|
const sparkEl = card.querySelector('.stat-sparkline');
|
|
|
|
const sparkEl = card.querySelector('.stat-sparkline');
|
|
|
|
if (sparkEl) drawSparkline(buf, sparkEl);
|
|
|
|
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>
|
|
|
|
</script>
|
|
|
|
|