diff options
| author | Fuwn <[email protected]> | 2026-01-20 16:34:01 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-01-20 16:34:01 -0800 |
| commit | d21e767ec97826beb878a501936bc03f1cb5d33b (patch) | |
| tree | 7ee6f279222991ee42f0705a544ac6d68a40edf4 /internal/server | |
| parent | feat: Add API access control (public/private/authenticated) (diff) | |
| download | kaze-d21e767ec97826beb878a501936bc03f1cb5d33b.tar.xz kaze-d21e767ec97826beb878a501936bc03f1cb5d33b.zip | |
feat: Add API-based refresh mode for smoother updates
Add display.refresh_mode option:
- 'page' (default): Full page refresh via meta refresh
- 'api': Fetch /api/status and update DOM without reload
Also add display.refresh_interval (default: 30s, min: 5s)
API mode updates: status indicators, response times, uptimes,
errors, overall status banner, and page title counts.
History bars remain static until full page refresh.
Diffstat (limited to 'internal/server')
| -rw-r--r-- | internal/server/server.go | 4 | ||||
| -rw-r--r-- | internal/server/templates/index.html | 173 |
2 files changed, 175 insertions, 2 deletions
diff --git a/internal/server/server.go b/internal/server/server.go index b0192a4..a5ded3d 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -178,6 +178,8 @@ type PageData struct { ThemeCSS template.CSS // OpenCode theme CSS (safe CSS) CustomHead template.HTML // Custom HTML for <head> (trusted) Scale float64 // UI scale factor (0.5-2.0) + RefreshMode string // page or api + RefreshInterval int // seconds } // StatusCounts holds monitor status counts for display @@ -277,6 +279,8 @@ func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { ThemeCSS: themeCSS, CustomHead: template.HTML(s.config.Site.CustomHead), Scale: s.config.Display.Scale, + RefreshMode: s.config.Display.RefreshMode, + RefreshInterval: s.config.Display.RefreshInterval, } overallUp := true diff --git a/internal/server/templates/index.html b/internal/server/templates/index.html index be80544..e84945e 100644 --- a/internal/server/templates/index.html +++ b/internal/server/templates/index.html @@ -389,8 +389,177 @@ }, { passive: true }); })(); - // Auto-refresh every 30 seconds - setTimeout(() => location.reload(), 30000); + // Auto-refresh + {{if eq .RefreshMode "api"}} + // API-based refresh (no page reload) + (function() { + const refreshInterval = {{.RefreshInterval}} * 1000; + + function getStatusColor(status) { + switch(status) { + case 'up': return 'bg-emerald-500'; + case 'degraded': return 'bg-yellow-500'; + case 'down': return 'bg-red-500'; + default: return 'bg-neutral-400'; + } + } + + 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'; + return 'text-red-600 dark:text-red-400'; + } + + function formatDuration(ms) { + if (ms < 1000) return ms + 'ms'; + return (ms / 1000).toFixed(2) + 's'; + } + + function formatUptime(pct) { + if (pct >= 99.99) return pct.toFixed(2) + '%'; + if (pct >= 99.9) return pct.toFixed(2) + '%'; + return pct.toFixed(1) + '%'; + } + + function simplifyError(err) { + if (!err) return ''; + const lower = err.toLowerCase(); + if (lower.includes('timeout') || lower.includes('deadline')) return 'Timeout'; + if (lower.includes('connection refused')) return 'Connection refused'; + if (lower.includes('no such host') || lower.includes('dns')) return 'DNS error'; + if (lower.includes('certificate') || lower.includes('x509') || lower.includes('tls')) return 'SSL/TLS error'; + if (lower.includes('eof') || lower.includes('reset by peer')) return 'Connection reset'; + return 'Error'; + } + + function getOverallStatus(stats) { + let hasDown = false; + let hasDegraded = false; + for (const name in stats) { + if (stats[name].CurrentStatus === 'down') hasDown = true; + if (stats[name].CurrentStatus === 'degraded') hasDegraded = true; + } + if (hasDown) return 'Major Outage'; + if (hasDegraded) return 'Partial Outage'; + return 'All Systems Operational'; + } + + function updateStatusCounts(stats) { + let up = 0, down = 0, degraded = 0; + for (const name in stats) { + switch(stats[name].CurrentStatus) { + case 'up': up++; break; + case 'down': down++; break; + case 'degraded': degraded++; break; + } + } + // Update page title + let title = '{{.Site.Name}} [↑' + up; + if (down > 0) title += '/' + down + '↓'; + title += ']'; + document.title = title; + } + + async function refresh() { + try { + const response = await fetch('/api/status'); + if (!response.ok) return; + + const stats = await response.json(); + + // Update each monitor + document.querySelectorAll('[data-monitor]').forEach(el => { + const name = el.getAttribute('data-monitor'); + const stat = stats[name]; + if (!stat) return; + + // Update status indicator + const statusDot = el.querySelector('.rounded-full'); + if (statusDot) { + statusDot.className = 'w-2 h-2 rounded-full flex-shrink-0 ' + getStatusColor(stat.CurrentStatus); + } + + // 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'); + if (infoDiv) { + let errorSpan = infoDiv.querySelector('.text-red-600, .dark\\:text-red-400'); + if (stat.LastError) { + if (!errorSpan) { + errorSpan = document.createElement('span'); + errorSpan.className = 'text-red-600 dark:text-red-400'; + infoDiv.appendChild(errorSpan); + } + errorSpan.textContent = simplifyError(stat.LastError); + } else if (errorSpan) { + errorSpan.remove(); + } + } + }); + + // Update overall status banner + const overallStatus = getOverallStatus(stats); + const banner = document.querySelector('.mb-8.p-4.rounded-lg.border'); + if (banner) { + const statusText = banner.querySelector('.font-medium'); + if (statusText) statusText.textContent = overallStatus; + + const dot = banner.querySelector('.rounded-full'); + if (dot) { + if (overallStatus === 'All Systems Operational') { + dot.className = 'w-3 h-3 rounded-full bg-emerald-500 animate-pulse'; + banner.className = 'mb-8 p-4 rounded-lg border bg-emerald-50 dark:bg-emerald-950/30 border-emerald-200 dark:border-emerald-900'; + } else if (overallStatus === 'Partial Outage') { + dot.className = 'w-3 h-3 rounded-full bg-yellow-500 animate-pulse'; + banner.className = 'mb-8 p-4 rounded-lg border bg-yellow-50 dark:bg-yellow-950/30 border-yellow-200 dark:border-yellow-900'; + } else { + dot.className = 'w-3 h-3 rounded-full bg-red-500 animate-pulse'; + banner.className = 'mb-8 p-4 rounded-lg border bg-red-50 dark:bg-red-950/30 border-red-200 dark:border-red-900'; + } + } + } + + // Update status counts in title + updateStatusCounts(stats); + + // Update last updated time + const timeEl = document.querySelector('[data-timestamp][data-format="datetime"]'); + if (timeEl) { + const now = new Date(); + {{if .UseBrowserTimezone}} + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + 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}} + } + + } catch (e) { + console.error('Failed to refresh status:', e); + } + } + + setInterval(refresh, refreshInterval); + })(); + {{else}} + // Page-based refresh (full reload) + setTimeout(() => location.reload(), {{.RefreshInterval}} * 1000); + {{end}} // Client-side timezone conversion {{if .UseBrowserTimezone}} |