aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-01-20 16:37:19 -0800
committerFuwn <[email protected]>2026-01-20 16:37:19 -0800
commit9401bd4f8c3e76a8c9ec2b3e01630468c828e474 (patch)
tree64eec00ec9cf386278425c346a5d7dddf639fd31
parentfeat: Add API-based refresh mode for smoother updates (diff)
downloadkaze-9401bd4f8c3e76a8c9ec2b3e01630468c828e474.tar.xz
kaze-9401bd4f8c3e76a8c9ec2b3e01630468c828e474.zip
fix: Update history bars (ping ticks) in API refresh mode
Fetch /api/history/{name} for each monitor in parallel and rebuild tick elements with proper colors and tooltips. Also add data attributes to monitor elements for hide_ping and disable_tooltips settings.
-rw-r--r--internal/server/templates/index.html167
1 files changed, 144 insertions, 23 deletions
diff --git a/internal/server/templates/index.html b/internal/server/templates/index.html
index e84945e..b86d043 100644
--- a/internal/server/templates/index.html
+++ b/internal/server/templates/index.html
@@ -79,7 +79,7 @@
<div class="divide-y divide-neutral-200 dark:divide-neutral-800 group-content" data-group-content="{{$group.Name}}" data-default-collapsed="{{$group.DefaultCollapsed}}">
{{range .Monitors}}
{{$monitor := .}}
- <div class="p-4 hover:bg-neutral-100/50 dark:hover:bg-neutral-900/50 transition-colors" data-monitor="{{.Name}}" data-group="{{$group.Name}}">
+ <div class="p-4 hover:bg-neutral-100/50 dark:hover:bg-neutral-900/50 transition-colors" data-monitor="{{.Name}}" data-group="{{$group.Name}}"{{if .HidePing}} data-hide-ping{{end}}{{if .DisablePingTooltips}} data-disable-tooltips{{end}}>
<div class="flex items-start justify-between gap-4">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-2">
@@ -394,6 +394,9 @@
// API-based refresh (no page reload)
(function() {
const refreshInterval = {{.RefreshInterval}} * 1000;
+ const tickMode = '{{.TickMode}}';
+ const tickCount = {{.TickCount}};
+ const useBrowserTimezone = {{.UseBrowserTimezone}};
function getStatusColor(status) {
switch(status) {
@@ -404,6 +407,23 @@
}
}
+ function getTickColor(tick) {
+ if (!tick || tick.TotalChecks === 0) return 'bg-neutral-200 dark:bg-neutral-800';
+ if (tick.Status) {
+ // Ping mode
+ switch(tick.Status) {
+ case 'up': return 'bg-emerald-500';
+ case 'degraded': return 'bg-yellow-500';
+ case 'down': return 'bg-red-500';
+ default: return 'bg-neutral-200 dark:bg-neutral-800';
+ }
+ }
+ // Aggregated mode
+ if (tick.FailureCount > 0 && tick.SuccessCount === 0) return 'bg-red-500';
+ if (tick.FailureCount > 0) return 'bg-yellow-500';
+ return 'bg-emerald-500';
+ }
+
function getUptimeColor(uptime) {
if (uptime >= 99.0) return 'text-emerald-600 dark:text-emerald-400';
if (uptime >= 95.0) return 'text-yellow-600 dark:text-yellow-400';
@@ -453,24 +473,118 @@
case 'degraded': degraded++; break;
}
}
- // Update page title
let title = '{{.Site.Name}} [↑' + up;
if (down > 0) title += '/' + down + '↓';
title += ']';
document.title = title;
}
+ function formatTickHeader(timestamp, mode) {
+ const date = new Date(timestamp);
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+ if (mode === 'ping') {
+ return months[date.getMonth()] + ' ' + date.getDate() + ', ' +
+ String(date.getHours()).padStart(2, '0') + ':' +
+ String(date.getMinutes()).padStart(2, '0') + ':' +
+ String(date.getSeconds()).padStart(2, '0');
+ } else if (mode === 'minute') {
+ return months[date.getMonth()] + ' ' + date.getDate() + ', ' +
+ String(date.getHours()).padStart(2, '0') + ':' +
+ String(date.getMinutes()).padStart(2, '0');
+ } else if (mode === 'hour') {
+ return months[date.getMonth()] + ' ' + date.getDate() + ', ' +
+ String(date.getHours()).padStart(2, '0') + ':00';
+ } else if (mode === 'day') {
+ return months[date.getMonth()] + ' ' + date.getDate() + ', ' + date.getFullYear();
+ }
+ return timestamp;
+ }
+
+ function buildTickTooltip(tick, mode, hidePing) {
+ if (!tick || tick.TotalChecks === 0) {
+ return JSON.stringify({header: 'No data'});
+ }
+
+ const data = {
+ header: formatTickHeader(tick.Timestamp, mode),
+ timestamp: tick.Timestamp,
+ mode: mode,
+ rows: []
+ };
+
+ if (mode === 'ping') {
+ if (!hidePing) {
+ data.rows.push({label: 'Response', value: formatDuration(tick.ResponseTime)});
+ }
+ data.rows.push({label: 'Status', value: tick.Status || 'unknown'});
+ } else {
+ data.rows.push({label: 'Checks', value: tick.TotalChecks.toString()});
+ data.rows.push({label: 'Success', value: tick.SuccessCount.toString()});
+ if (tick.FailureCount > 0) {
+ data.rows.push({label: 'Failed', value: tick.FailureCount.toString()});
+ }
+ if (!hidePing && tick.AvgResponse > 0) {
+ data.rows.push({label: 'Avg Response', value: formatDuration(tick.AvgResponse)});
+ }
+ data.rows.push({label: 'Uptime', value: formatUptime(tick.UptimePercent)});
+ }
+
+ return JSON.stringify(data);
+ }
+
+ function updateHistoryBar(monitorEl, ticks, hidePing, disableTooltips) {
+ const historyBar = monitorEl.querySelector('.mt-3.flex.gap-px');
+ if (!historyBar) return;
+
+ // Build new tick elements
+ let html = '';
+ if (ticks && ticks.length > 0) {
+ for (const tick of ticks) {
+ const color = getTickColor(tick);
+ const tooltip = disableTooltips ? '' : ` data-tooltip='${buildTickTooltip(tick, tickMode, hidePing)}'`;
+ html += `<div class="flex-1 h-6 rounded-sm ${color}"${tooltip}></div>`;
+ }
+ } else {
+ // Empty ticks
+ for (let i = 0; i < tickCount; i++) {
+ const tooltip = disableTooltips ? '' : ` data-tooltip='{"header":"No data"}'`;
+ html += `<div class="flex-1 h-6 rounded-sm bg-neutral-200 dark:bg-neutral-800"${tooltip}></div>`;
+ }
+ }
+ historyBar.innerHTML = html;
+ }
+
async function refresh() {
try {
- const response = await fetch('/api/status');
- if (!response.ok) return;
+ // Fetch status for all monitors
+ const statusResponse = await fetch('/api/status');
+ if (!statusResponse.ok) return;
+ const stats = await statusResponse.json();
- const stats = await response.json();
+ // Get all monitor elements
+ const monitorEls = document.querySelectorAll('[data-monitor]');
- // Update each monitor
- document.querySelectorAll('[data-monitor]').forEach(el => {
+ // Fetch history for each monitor in parallel
+ const historyPromises = [];
+ const monitorNames = [];
+ monitorEls.forEach(el => {
const name = el.getAttribute('data-monitor');
+ monitorNames.push(name);
+ historyPromises.push(
+ fetch('/api/history/' + encodeURIComponent(name))
+ .then(r => r.ok ? r.json() : null)
+ .catch(() => null)
+ );
+ });
+
+ const historyResults = await Promise.all(historyPromises);
+
+ // Update each monitor
+ monitorEls.forEach((el, index) => {
+ const name = monitorNames[index];
const stat = stats[name];
+ const history = historyResults[index];
+
if (!stat) return;
// Update status indicator
@@ -480,22 +594,16 @@
}
// Update response time
- const infoSpans = el.querySelectorAll('.text-xs.text-neutral-500 > span');
- if (infoSpans.length > 0 && !el.querySelector('[data-hide-ping]')) {
- infoSpans[0].textContent = formatDuration(stat.LastResponseTime);
- }
-
- // Update uptime
- const uptimeEl = el.querySelector('.text-sm.font-medium');
- if (uptimeEl) {
- uptimeEl.textContent = formatUptime(stat.UptimePercent);
- uptimeEl.className = 'text-sm font-medium ' + getUptimeColor(stat.UptimePercent);
- }
-
- // Update error (find or create)
- const infoDiv = el.querySelector('.text-xs.text-neutral-500');
+ const infoDiv = el.querySelector('.flex.items-center.gap-4.text-xs');
if (infoDiv) {
- let errorSpan = infoDiv.querySelector('.text-red-600, .dark\\:text-red-400');
+ const spans = infoDiv.querySelectorAll(':scope > span');
+ const hidePing = el.hasAttribute('data-hide-ping');
+ if (spans.length > 0 && !hidePing) {
+ spans[0].textContent = formatDuration(stat.LastResponseTime);
+ }
+
+ // Update error
+ let errorSpan = infoDiv.querySelector('.text-red-600');
if (stat.LastError) {
if (!errorSpan) {
errorSpan = document.createElement('span');
@@ -507,6 +615,20 @@
errorSpan.remove();
}
}
+
+ // Update uptime
+ const uptimeEl = el.querySelector('.text-sm.font-medium');
+ if (uptimeEl) {
+ uptimeEl.textContent = formatUptime(stat.UptimePercent);
+ uptimeEl.className = 'text-sm font-medium ' + getUptimeColor(stat.UptimePercent);
+ }
+
+ // Update history bar
+ if (history && history.ticks) {
+ const hidePing = el.hasAttribute('data-hide-ping');
+ const disableTooltips = el.hasAttribute('data-disable-tooltips');
+ updateHistoryBar(el, history.ticks, hidePing, disableTooltips);
+ }
});
// Update overall status banner
@@ -543,7 +665,6 @@
timeEl.textContent = months[now.getMonth()] + ' ' + now.getDate() + ', ' + now.getFullYear() + ' ' +
String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
{{else}}
- // Server timezone - just update the time portion
timeEl.textContent = timeEl.textContent.replace(/\d{2}:\d{2}$/,
String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0'));
{{end}}