aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/server/server.go101
-rw-r--r--internal/server/templates/index.html93
2 files changed, 125 insertions, 69 deletions
diff --git a/internal/server/server.go b/internal/server/server.go
index a5ded3d..d7dacaa 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -71,6 +71,13 @@ func New(cfg *config.Config, store *storage.Storage, sched *monitor.Scheduler, l
mux.HandleFunc("GET /api/monitor/{name}", s.withAPIAuth(s.handleAPIMonitor))
mux.HandleFunc("GET /api/history/{name}", s.withAPIAuth(s.handleAPIHistory))
+ // Full page data endpoint - public if refresh_mode=api, otherwise follows api.access
+ if cfg.Display.RefreshMode == "api" {
+ mux.HandleFunc("GET /api/page", s.handleAPIPage)
+ } else {
+ mux.HandleFunc("GET /api/page", s.withAPIAuth(s.handleAPIPage))
+ }
+
// Create HTTP server
s.server = &http.Server{
Addr: fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port),
@@ -536,6 +543,100 @@ func (s *Server) handleAPIHistory(w http.ResponseWriter, r *http.Request) {
})
}
+// APIPageResponse contains all data needed to render/update the status page
+type APIPageResponse struct {
+ Monitors map[string]APIMonitorData `json:"monitors"`
+ OverallStatus string `json:"overall_status"`
+ Counts StatusCounts `json:"counts"`
+ LastUpdated time.Time `json:"last_updated"`
+}
+
+// APIMonitorData contains monitor data for the API page response
+type APIMonitorData struct {
+ Status string `json:"status"`
+ ResponseTime int64 `json:"response_time"`
+ Uptime float64 `json:"uptime"`
+ LastError string `json:"last_error,omitempty"`
+ SSLDaysLeft int `json:"ssl_days_left,omitempty"`
+ Ticks []*storage.TickData `json:"ticks"`
+}
+
+// handleAPIPage returns all data needed to update the status page in a single request
+func (s *Server) handleAPIPage(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ // Get all monitor stats
+ stats, err := s.storage.GetAllMonitorStats(ctx)
+ if err != nil {
+ s.jsonError(w, "Failed to get stats", http.StatusInternalServerError)
+ return
+ }
+
+ response := APIPageResponse{
+ Monitors: make(map[string]APIMonitorData),
+ LastUpdated: time.Now(),
+ }
+
+ overallUp := true
+ hasDegraded := false
+
+ // Build monitor data with history
+ for _, group := range s.config.Groups {
+ for _, monCfg := range group.Monitors {
+ stat, ok := stats[monCfg.Name]
+ if !ok {
+ continue
+ }
+
+ // Get history ticks
+ ticks, err := s.storage.GetAggregatedHistory(
+ ctx,
+ monCfg.Name,
+ s.config.Display.TickCount,
+ s.config.Display.TickMode,
+ s.config.Display.PingFixedSlots,
+ )
+ if err != nil {
+ s.logger.Error("failed to get tick history", "monitor", monCfg.Name, "error", err)
+ ticks = nil
+ }
+
+ response.Monitors[monCfg.Name] = APIMonitorData{
+ Status: stat.CurrentStatus,
+ ResponseTime: stat.LastResponseTime,
+ Uptime: stat.UptimePercent,
+ LastError: stat.LastError,
+ SSLDaysLeft: stat.SSLDaysLeft,
+ Ticks: ticks,
+ }
+
+ // Track status counts
+ response.Counts.Total++
+ switch stat.CurrentStatus {
+ case "down":
+ overallUp = false
+ response.Counts.Down++
+ case "degraded":
+ hasDegraded = true
+ response.Counts.Degraded++
+ case "up":
+ response.Counts.Up++
+ }
+ }
+ }
+
+ // Determine overall status
+ if !overallUp {
+ response.OverallStatus = "Major Outage"
+ } else if hasDegraded {
+ response.OverallStatus = "Partial Outage"
+ } else {
+ response.OverallStatus = "All Systems Operational"
+ }
+
+ s.jsonResponse(w, response)
+}
+
// jsonResponse writes a JSON response
func (s *Server) jsonResponse(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json")
diff --git a/internal/server/templates/index.html b/internal/server/templates/index.html
index b86d043..6471e53 100644
--- a/internal/server/templates/index.html
+++ b/internal/server/templates/index.html
@@ -452,33 +452,6 @@
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;
- }
- }
- 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'];
@@ -556,41 +529,21 @@
async function refresh() {
try {
- // Fetch status for all monitors
- const statusResponse = await fetch('/api/status');
- if (!statusResponse.ok) return;
- const stats = await statusResponse.json();
-
- // Get all monitor elements
- const monitorEls = document.querySelectorAll('[data-monitor]');
-
- // 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);
+ // Fetch all page data in a single request
+ const response = await fetch('/api/page');
+ if (!response.ok) return;
+ const data = await response.json();
// Update each monitor
- monitorEls.forEach((el, index) => {
- const name = monitorNames[index];
- const stat = stats[name];
- const history = historyResults[index];
-
- if (!stat) return;
+ document.querySelectorAll('[data-monitor]').forEach(el => {
+ const name = el.getAttribute('data-monitor');
+ const monitor = data.monitors[name];
+ if (!monitor) 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);
+ statusDot.className = 'w-2 h-2 rounded-full flex-shrink-0 ' + getStatusColor(monitor.status);
}
// Update response time
@@ -599,18 +552,18 @@
const spans = infoDiv.querySelectorAll(':scope > span');
const hidePing = el.hasAttribute('data-hide-ping');
if (spans.length > 0 && !hidePing) {
- spans[0].textContent = formatDuration(stat.LastResponseTime);
+ spans[0].textContent = formatDuration(monitor.response_time);
}
// Update error
let errorSpan = infoDiv.querySelector('.text-red-600');
- if (stat.LastError) {
+ if (monitor.last_error) {
if (!errorSpan) {
errorSpan = document.createElement('span');
errorSpan.className = 'text-red-600 dark:text-red-400';
infoDiv.appendChild(errorSpan);
}
- errorSpan.textContent = simplifyError(stat.LastError);
+ errorSpan.textContent = simplifyError(monitor.last_error);
} else if (errorSpan) {
errorSpan.remove();
}
@@ -619,31 +572,30 @@
// 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);
+ uptimeEl.textContent = formatUptime(monitor.uptime);
+ uptimeEl.className = 'text-sm font-medium ' + getUptimeColor(monitor.uptime);
}
// Update history bar
- if (history && history.ticks) {
+ if (monitor.ticks) {
const hidePing = el.hasAttribute('data-hide-ping');
const disableTooltips = el.hasAttribute('data-disable-tooltips');
- updateHistoryBar(el, history.ticks, hidePing, disableTooltips);
+ updateHistoryBar(el, monitor.ticks, hidePing, disableTooltips);
}
});
// 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;
+ if (statusText) statusText.textContent = data.overall_status;
const dot = banner.querySelector('.rounded-full');
if (dot) {
- if (overallStatus === 'All Systems Operational') {
+ if (data.overall_status === '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') {
+ } else if (data.overall_status === '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 {
@@ -654,12 +606,15 @@
}
// Update status counts in title
- updateStatusCounts(stats);
+ let title = '{{.Site.Name}} [↑' + data.counts.Up;
+ if (data.counts.Down > 0) title += '/' + data.counts.Down + '↓';
+ title += ']';
+ document.title = title;
// Update last updated time
const timeEl = document.querySelector('[data-timestamp][data-format="datetime"]');
if (timeEl) {
- const now = new Date();
+ const now = new Date(data.last_updated);
{{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() + ' ' +