From b36ac9523c1fa9716c35ea92eb649b97cac2afcc Mon Sep 17 00:00:00 2001 From: trafficlunar Date: Sun, 14 Dec 2025 14:27:59 +0000 Subject: [PATCH] feat: uptime percentages --- TODO.md | 3 +-- src/database.go | 43 ++++++++++++++++++++++++++++++++++++++++--- src/http.go | 5 ++++- www/index.html | 26 +++++++++++++++++++++++++- 4 files changed, 70 insertions(+), 7 deletions(-) diff --git a/TODO.md b/TODO.md index c8930d0..13fffbb 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,5 @@ - [ ] Accessibility - [ ] Better mobile support -- [ ] Uptime percentage - [ ] View incidents - [ ] Latency graphs? -- [ ] README thumbnail +- [ ] README diff --git a/src/database.go b/src/database.go index f8dcaee..7dfd24d 100644 --- a/src/database.go +++ b/src/database.go @@ -3,6 +3,7 @@ package main import ( "database/sql" "log" + "math" "time" _ "modernc.org/sqlite" @@ -52,6 +53,7 @@ func InitializeDatabase() { service.MinuteTimeline = generateRecap(rawTimeline, "minutes") service.HourTimeline = generateRecap(rawTimeline, "hours") service.DayTimeline = generateRecap(rawTimeline, "days") + calculateUptimePercentages(index) // incidents incidentRows, err := db.Query(`SELECT status, startTime, endTime FROM incidents WHERE service = ?`, service.Name) @@ -189,11 +191,11 @@ func generateRecap(rawTimeline []TimelineEntry, view string) []TimelineEntry { switch view { case "minutes": - timestamp = now.Add(-time.Duration(30-i) * time.Minute).Truncate(time.Minute) + timestamp = now.Add(-time.Duration(limit-i) * time.Minute).Truncate(time.Minute) case "hours": - timestamp = now.Add(-time.Duration(24-i) * time.Hour).Truncate(time.Hour) + timestamp = now.Add(-time.Duration(limit-i) * time.Hour).Truncate(time.Hour) case "days": - day := now.AddDate(0, 0, -(30 - i)) + day := now.AddDate(0, 0, -(limit - i)) timestamp = time.Date(day.Year(), day.Month(), day.Day(), 0, 0, 0, 0, time.UTC) } @@ -212,6 +214,40 @@ func generateRecap(rawTimeline []TimelineEntry, view string) []TimelineEntry { return timeline } +func calculateUptimePercentages(serviceIndex int) { + service := templateData.Services[serviceIndex] + + calculateUptime := func(timeline []TimelineEntry) float64 { + if len(timeline) == 0 { + return 0.0 + } + + online := 0 + total := 0 + + for _, entry := range timeline { + if entry.Status != "Unknown" { + total++ + if entry.Status == "Online" { + online++ + } + } + } + + if total == 0 { + return 0.0 + } + + return math.Floor(float64(online)/float64(total)*100*100) / 100 + } + + service.MinuteUptime = calculateUptime(service.MinuteTimeline) + service.HourUptime = calculateUptime(service.HourTimeline) + service.DayUptime = calculateUptime(service.DayTimeline) + + templateData.Services[serviceIndex] = service +} + // add entry to timeline and remove entries older than 30 days func AddToTimeline(serviceIndex int, status string) { service := templateData.Services[serviceIndex] @@ -297,6 +333,7 @@ func AddToTimeline(serviceIndex int, status string) { } templateData.Services[serviceIndex] = service + calculateUptimePercentages(serviceIndex) } func AddIncident(serviceIndex int, status string, startTime time.Time) { diff --git a/src/http.go b/src/http.go index 31b4d8d..88b273c 100644 --- a/src/http.go +++ b/src/http.go @@ -32,6 +32,9 @@ type Service struct { MinuteTimeline []TimelineEntry HourTimeline []TimelineEntry DayTimeline []TimelineEntry + MinuteUptime float64 + HourUptime float64 + DayUptime float64 Incidents []Incident } @@ -74,7 +77,7 @@ func renderError(w http.ResponseWriter, statusCode int, message string) { } func index(w http.ResponseWriter, req *http.Request) { - // Handle 404 + // handle 404 if req.URL.Path != "/" { renderError(w, http.StatusNotFound, "Page not found") return diff --git a/www/index.html b/www/index.html index 3d4b830..ccb9a99 100644 --- a/www/index.html +++ b/www/index.html @@ -232,15 +232,33 @@ } .bars-footer { + position: relative; display: flex; justify-content: space-between; font-size: 0.75rem; font-family: "JetBrains Mono", monospace; margin-top: 0.5rem; - color: rgba(0, 0, 0, 0.5); + color: rgba(0, 0, 0, 0.4); width: 100%; } + .bars-footer span { + padding: 0 0.5rem; + } + + .bars-footer hr { + flex-grow: 1; + border-color: rgba(0, 0, 0, 0.1); + } + + .uptime-percentage { + position: absolute; + left: 50%; + transform: translateX(-50%); + background-color: white; + color: rgba(0, 0, 0, 0.5); + } + footer { margin-top: auto; display: flex; @@ -487,6 +505,8 @@ {{ else if eq $.View "hours" }} @@ -503,6 +523,8 @@ @@ -520,6 +542,8 @@ {{ end }}