diff options
| author | Fuwn <[email protected]> | 2026-01-20 16:37:19 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-01-20 16:37:19 -0800 |
| commit | 9401bd4f8c3e76a8c9ec2b3e01630468c828e474 (patch) | |
| tree | 64eec00ec9cf386278425c346a5d7dddf639fd31 /internal/server/templates/index.html | |
| parent | feat: Add API-based refresh mode for smoother updates (diff) | |
| download | kaze-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.
Diffstat (limited to 'internal/server/templates/index.html')
| -rw-r--r-- | internal/server/templates/index.html | 167 |
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}} |