feat: add tooltip to status history blips
This commit is contained in:
parent
6f0499d34b
commit
4149a4753e
|
|
@ -15,10 +15,12 @@
|
||||||
for (const p of pings.slice(0, 120).reverse()) {
|
for (const p of pings.slice(0, 120).reverse()) {
|
||||||
const rid = p.run_id || p.checked_at;
|
const rid = p.run_id || p.checked_at;
|
||||||
if (!runMap[rid]) {
|
if (!runMap[rid]) {
|
||||||
runMap[rid] = { run_id: rid, up: 0, down: 0, checked_at: p.checked_at, latency_ms: p.latency_ms };
|
runMap[rid] = { run_id: rid, up: 0, down: 0, total: 0, checked_at: p.checked_at, regions: [] };
|
||||||
barRuns.push(runMap[rid]);
|
barRuns.push(runMap[rid]);
|
||||||
}
|
}
|
||||||
|
runMap[rid].total++;
|
||||||
if (p.up) runMap[rid].up++; else runMap[rid].down++;
|
if (p.up) runMap[rid].up++; else runMap[rid].down++;
|
||||||
|
runMap[rid].regions.push({ region: p.region || '', up: p.up, latency_ms: p.latency_ms });
|
||||||
}
|
}
|
||||||
// Keep last 60 runs
|
// Keep last 60 runs
|
||||||
const barPings = barRuns.slice(-60);
|
const barPings = barRuns.slice(-60);
|
||||||
|
|
@ -90,18 +92,20 @@
|
||||||
<!-- Status bar -->
|
<!-- Status bar -->
|
||||||
<div class="card-static p-4 mb-8">
|
<div class="card-static p-4 mb-8">
|
||||||
<h3 class="text-sm text-gray-400 mb-3">Status History</h3>
|
<h3 class="text-sm text-gray-400 mb-3">Status History</h3>
|
||||||
|
<div class="relative">
|
||||||
<div id="status-bar" class="flex gap-0.5 h-8 rounded-lg overflow-hidden">
|
<div id="status-bar" class="flex gap-0.5 h-8 rounded-lg overflow-hidden">
|
||||||
<% if (barPings.length > 0) { %>
|
<% if (barPings.length > 0) { %>
|
||||||
<% barPings.forEach(function(c) {
|
<% barPings.forEach(function(c) {
|
||||||
const color = c.down === 0 ? 'bg-green-500/70' : (c.up === 0 ? 'bg-red-500/70' : 'bg-orange-400/70');
|
const color = c.down === 0 ? 'bg-green-500/70' : (c.up === 0 ? 'bg-red-500/70' : 'bg-orange-400/70');
|
||||||
const label = c.down === 0 ? 'Up' : (c.up === 0 ? 'Down' : 'Partial');
|
|
||||||
%>
|
%>
|
||||||
<div class="flex-1 rounded-sm <%= color %>" data-run="<%= c.run_id %>" data-up="<%= c.up %>" data-down="<%= c.down %>" title="<%= new Date(c.checked_at).toLocaleString() %> — <%= label %>"></div>
|
<div class="flex-1 rounded-sm <%= color %> cursor-pointer" data-run="<%= c.run_id %>" data-up="<%= c.up %>" data-down="<%= c.down %>" data-total="<%= c.total %>" data-time="<%= c.checked_at %>" data-regions="<%- JSON.stringify(c.regions).replace(/"/g, '"') %>"></div>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<div class="flex-1 bg-gray-800/50 text-center text-xs text-gray-600 leading-8 rounded-lg">No data</div>
|
<div class="flex-1 bg-gray-800/50 text-center text-xs text-gray-600 leading-8 rounded-lg">No data</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="status-tooltip" class="absolute hidden card-static px-3 py-2 text-xs pointer-events-none z-10 -top-2 -translate-y-full" style="min-width:160px"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Recent pings table -->
|
<!-- Recent pings table -->
|
||||||
|
|
@ -462,6 +466,66 @@
|
||||||
renderChart();
|
renderChart();
|
||||||
window.addEventListener('resize', renderChart);
|
window.addEventListener('resize', renderChart);
|
||||||
|
|
||||||
|
// ── Status bar hover tooltip ──────────────────────────────────────
|
||||||
|
const statusBar = document.getElementById('status-bar');
|
||||||
|
const statusTooltip = document.getElementById('status-tooltip');
|
||||||
|
|
||||||
|
statusBar.addEventListener('mousemove', (e) => {
|
||||||
|
const seg = e.target.closest('[data-run]');
|
||||||
|
if (!seg) { statusTooltip.classList.add('hidden'); return; }
|
||||||
|
|
||||||
|
const up = parseInt(seg.dataset.up || '0');
|
||||||
|
const down = parseInt(seg.dataset.down || '0');
|
||||||
|
const total = up + down;
|
||||||
|
const uptimePct = total > 0 ? Math.round((up / total) * 100) : 0;
|
||||||
|
const time = seg.dataset.time ? new Date(seg.dataset.time) : null;
|
||||||
|
const timeStr = time ? time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '';
|
||||||
|
const dateStr = time ? time.toLocaleDateString([], { month: 'short', day: 'numeric' }) : '';
|
||||||
|
|
||||||
|
const statusLabel = down === 0 ? '<span class="text-green-400">All Up</span>'
|
||||||
|
: up === 0 ? '<span class="text-red-400">All Down</span>'
|
||||||
|
: '<span class="text-orange-400">Partial</span>';
|
||||||
|
|
||||||
|
let html = `<div class="text-gray-400 mb-1">${dateStr} ${timeStr}</div>`;
|
||||||
|
html += `<div class="flex items-center justify-between gap-4 mb-1"><span>Status</span>${statusLabel}</div>`;
|
||||||
|
html += `<div class="flex items-center justify-between gap-4"><span class="text-gray-500">Uptime</span><span class="text-gray-200 font-mono">${uptimePct}%</span></div>`;
|
||||||
|
|
||||||
|
// Region breakdown
|
||||||
|
let regions = [];
|
||||||
|
try { regions = JSON.parse(seg.dataset.regions || '[]'); } catch {}
|
||||||
|
if (regions.length > 0) {
|
||||||
|
html += '<div class="mt-1.5 pt-1.5 border-t border-gray-700/50">';
|
||||||
|
for (const r of regions) {
|
||||||
|
const flag = {'eu-central':'🇩🇪','us-west':'🇺🇸'}[r.region] || '🌐';
|
||||||
|
const rLabel = r.region || 'unknown';
|
||||||
|
const status = r.up ? '<span class="text-green-400">Up</span>' : '<span class="text-red-400">Down</span>';
|
||||||
|
const lat = r.latency_ms != null ? `<span class="text-gray-400 font-mono">${r.latency_ms}ms</span>` : '';
|
||||||
|
html += `<div class="flex items-center justify-between gap-3"><span>${flag} ${rLabel}</span><span>${lat} ${status}</span></div>`;
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const rid = seg.dataset.run;
|
||||||
|
if (rid) html += `<div class="text-gray-600 mt-1 text-[10px] font-mono">${rid}</div>`;
|
||||||
|
|
||||||
|
statusTooltip.innerHTML = html;
|
||||||
|
statusTooltip.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Position horizontally centered on the segment
|
||||||
|
const barRect = statusBar.getBoundingClientRect();
|
||||||
|
const segRect = seg.getBoundingClientRect();
|
||||||
|
const segCenter = segRect.left + segRect.width / 2 - barRect.left;
|
||||||
|
const tw = statusTooltip.offsetWidth;
|
||||||
|
let left = segCenter - tw / 2;
|
||||||
|
if (left < 0) left = 0;
|
||||||
|
if (left + tw > barRect.width) left = barRect.width - tw;
|
||||||
|
statusTooltip.style.left = left + 'px';
|
||||||
|
});
|
||||||
|
|
||||||
|
statusBar.addEventListener('mouseleave', () => {
|
||||||
|
statusTooltip.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
// Running totals for incremental stat updates
|
// Running totals for incremental stat updates
|
||||||
let _total = <%= pings.length %>, _up = <%= upPings.length %>;
|
let _total = <%= pings.length %>, _up = <%= upPings.length %>;
|
||||||
let _latSum = <%= latencies.reduce((a,b)=>a+b,0) %>, _latCount = <%= latencies.length %>;
|
let _latSum = <%= latencies.reduce((a,b)=>a+b,0) %>, _latCount = <%= latencies.length %>;
|
||||||
|
|
@ -500,22 +564,27 @@
|
||||||
const rid = ping.run_id || ping.checked_at;
|
const rid = ping.run_id || ping.checked_at;
|
||||||
let existing = bar.querySelector(`[data-run="${rid}"]`);
|
let existing = bar.querySelector(`[data-run="${rid}"]`);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
// Update existing run segment
|
|
||||||
const up = parseInt(existing.dataset.up || '0') + (ping.up ? 1 : 0);
|
const up = parseInt(existing.dataset.up || '0') + (ping.up ? 1 : 0);
|
||||||
const down = parseInt(existing.dataset.down || '0') + (ping.up ? 0 : 1);
|
const down = parseInt(existing.dataset.down || '0') + (ping.up ? 0 : 1);
|
||||||
existing.dataset.up = up;
|
existing.dataset.up = up;
|
||||||
existing.dataset.down = down;
|
existing.dataset.down = down;
|
||||||
|
existing.dataset.total = up + down;
|
||||||
|
existing.dataset.time = ping.checked_at;
|
||||||
|
const regions = JSON.parse(existing.dataset.regions || '[]');
|
||||||
|
regions.push({ region: ping.region || '', up: ping.up, latency_ms: ping.latency_ms });
|
||||||
|
existing.dataset.regions = JSON.stringify(regions);
|
||||||
const color = down === 0 ? 'bg-green-500/70' : (up === 0 ? 'bg-red-500/70' : 'bg-orange-400/70');
|
const color = down === 0 ? 'bg-green-500/70' : (up === 0 ? 'bg-red-500/70' : 'bg-orange-400/70');
|
||||||
const label = down === 0 ? 'Up' : (up === 0 ? 'Down' : 'Partial');
|
existing.className = `flex-1 rounded-sm ${color} cursor-pointer`;
|
||||||
existing.className = `flex-1 rounded-sm ${color}`;
|
|
||||||
existing.title = `${new Date(ping.checked_at).toLocaleString()} — ${label}`;
|
|
||||||
} else {
|
} else {
|
||||||
const seg = document.createElement('div');
|
const seg = document.createElement('div');
|
||||||
seg.className = `flex-1 rounded-sm ${ping.up ? 'bg-green-500/70' : 'bg-red-500/70'}`;
|
const color = ping.up ? 'bg-green-500/70' : 'bg-red-500/70';
|
||||||
|
seg.className = `flex-1 rounded-sm ${color} cursor-pointer`;
|
||||||
seg.dataset.run = rid;
|
seg.dataset.run = rid;
|
||||||
seg.dataset.up = ping.up ? '1' : '0';
|
seg.dataset.up = ping.up ? '1' : '0';
|
||||||
seg.dataset.down = ping.up ? '0' : '1';
|
seg.dataset.down = ping.up ? '0' : '1';
|
||||||
seg.title = `${new Date(ping.checked_at).toLocaleString()} — ${ping.up ? 'Up' : 'Down'}`;
|
seg.dataset.total = '1';
|
||||||
|
seg.dataset.time = ping.checked_at;
|
||||||
|
seg.dataset.regions = JSON.stringify([{ region: ping.region || '', up: ping.up, latency_ms: ping.latency_ms }]);
|
||||||
bar.appendChild(seg);
|
bar.appendChild(seg);
|
||||||
while (bar.children.length > 60) bar.removeChild(bar.firstChild);
|
while (bar.children.length > 60) bar.removeChild(bar.firstChild);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue