From 74d42acb59f323ded9e50ad95241386280eb24ab Mon Sep 17 00:00:00 2001 From: Zoe <62722391+juls0730@users.noreply.github.com> Date: Sat, 17 Jan 2026 23:50:07 -0600 Subject: [PATCH] add betteruptime support --- .github/workflows/docker-publish.yml | 2 - README.md | 17 ++-- src/services/uptimeService.go | 126 ++++++++++++++++++++++----- zqdgr.config.json | 2 +- 4 files changed, 116 insertions(+), 31 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index cbbaa52..b93dac6 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -10,8 +10,6 @@ on: jobs: build-and-push: - env: - RUNNER_TOOL_CACHE: /toolcache runs-on: ubuntu-latest permissions: diff --git a/README.md b/README.md index 964afc0..553bd04 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,10 @@ Passport is a simple, fast, and lightweight web dashboard/new tab replacement. ### Prerequisites -- [ZQDGR](https://github.com/juls0730/zqdgr) -- [Go](https://go.dev/doc/install) -- [sqlite3](https://www.sqlite.org/download.html) -- [TailwdinCSS CLI](https://github.com/tailwindlabs/tailwindcss/releases/latest) +- [ZQDGR](https://github.com/juls0730/zqdgr) +- [Go](https://go.dev/doc/install) +- [sqlite3](https://www.sqlite.org/download.html) +- [TailwdinCSS CLI](https://github.com/tailwindlabs/tailwindcss/releases/latest) ## Usage @@ -85,10 +85,11 @@ The weather integration is optional, and will be enabled automatically if you pr The uptime integration is optional, and will be enabled automatically if you provide an API key. The following only applies if you are using the UptimeRobot integration. -| Environment Variable | Description | Required | Default | -| ------------------------- | ------------------------------------------------- | -------- | ------- | -| `PASSPORT_UPTIME_API_KEY` | The UptimeRobot API key | true | | -| `UPTIME_UPDATE_INTERVAL` | The interval in seconds to update the uptime data | false | 300 | +| Environment Variable | Description | Required | Default | +| ------------------------- | ------------------------------------------------------------------ | -------- | ----------- | +| `UPTIME_PROVIDER` | The uptime provider to use, either `uptimerobot` or `betteruptime` | false | uptimerobot | +| `PASSPORT_UPTIME_API_KEY` | The UptimeRobot API key | true | | +| `UPTIME_UPDATE_INTERVAL` | The interval in seconds to update the uptime data | false | 300 | ### Adding links and categories diff --git a/src/services/uptimeService.go b/src/services/uptimeService.go index 81d06a9..b75d15a 100644 --- a/src/services/uptimeService.go +++ b/src/services/uptimeService.go @@ -6,6 +6,7 @@ import ( "io" "log" "net/http" + "sort" "sync" "time" ) @@ -16,19 +17,20 @@ type DepricatedUptimeConfig struct { } type UptimeConfig struct { + Provider string `env:"UPTIME_PROVIDER" envDefault:"uptimerobot"` APIKey string UpdateInterval int `env:"UPTIME_UPDATE_INTERVAL" envDefault:"300"` } -type UptimeRobotSite struct { - FriendlyName string `json:"friendly_name"` - Url string `json:"url"` - Status int `json:"status"` - Up bool `json:"-"` +type UptimeSite struct { + FriendlyName string + Url string + Up bool } type UptimeManager struct { - sites []UptimeRobotSite + provider string + sites []UptimeSite lastUpdate time.Time mutex sync.RWMutex updateChan chan struct{} @@ -38,7 +40,7 @@ type UptimeManager struct { func NewUptimeManager(config *UptimeConfig) *UptimeManager { if config.APIKey == "" { - log.Fatalln("UptimeRobot API Key is required!") + log.Fatalln("An API Key is required to use Uptime Monitoring!") return nil } @@ -48,10 +50,11 @@ func NewUptimeManager(config *UptimeConfig) *UptimeManager { } uptimeManager := &UptimeManager{ + provider: config.Provider, updateChan: make(chan struct{}), updateInterval: updateInterval, apiKey: config.APIKey, - sites: []UptimeRobotSite{}, + sites: []UptimeSite{}, } go uptimeManager.updateWorker() @@ -61,7 +64,7 @@ func NewUptimeManager(config *UptimeConfig) *UptimeManager { return uptimeManager } -func (u *UptimeManager) GetUptime() []UptimeRobotSite { +func (u *UptimeManager) GetUptime() []UptimeSite { u.mutex.RLock() defer u.mutex.RUnlock() return u.sites @@ -81,36 +84,119 @@ func (u *UptimeManager) updateWorker() { } } +type UptimeRobotSite struct { + FriendlyName string `json:"friendly_name"` + Url string `json:"url"` + Status int `json:"status"` +} + type UptimeRobotResponse struct { Monitors []UptimeRobotSite `json:"monitors"` } +type BetterUptimeSite struct { + MonitorType string `json:"type"` + Attributes struct { + PronounceableName string `json:"pronounceable_name"` + Url string `json:"url"` + Status string `json:"status"` + } `json:"attributes"` +} + +type BetterUptimeResponse struct { + Monitors []BetterUptimeSite `json:"data"` +} + func (u *UptimeManager) update() { + var monitors []UptimeSite + switch u.provider { + case "uptimerobot": + monitors = u.updateUptimeRobot() + case "betteruptime": + monitors = u.updateBetterUptime() + default: + log.Fatalln("Invalid Uptime Provider!") + } + + u.mutex.Lock() + u.sites = monitors + u.lastUpdate = time.Now() + u.mutex.Unlock() +} + +func (u *UptimeManager) updateUptimeRobot() []UptimeSite { resp, err := http.Post("https://api.uptimerobot.com/v2/getMonitors?api_key="+u.apiKey, "application/json", nil) if err != nil { fmt.Printf("Error fetching uptime data: %v\n", err) - return + return []UptimeSite{} } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { fmt.Printf("Error reading response: %v\n", err) - return + return []UptimeSite{} } - var monitors UptimeRobotResponse - if err := json.Unmarshal(body, &monitors); err != nil { + var rawMonitors UptimeRobotResponse + if err := json.Unmarshal(body, &rawMonitors); err != nil { fmt.Printf("Error parsing uptime data: %v\n", err) - return + return []UptimeSite{} } - for i, monitor := range monitors.Monitors { - monitors.Monitors[i].Up = monitor.Status == 2 + var monitors []UptimeSite + for _, rawMonitor := range rawMonitors.Monitors { + monitors = append(monitors, UptimeSite{ + FriendlyName: rawMonitor.FriendlyName, + Url: rawMonitor.Url, + Up: rawMonitor.Status == 2, + }) } - u.mutex.Lock() - u.sites = monitors.Monitors - u.lastUpdate = time.Now() - u.mutex.Unlock() + return monitors +} + +func (u *UptimeManager) updateBetterUptime() []UptimeSite { + client := &http.Client{} + req, err := http.NewRequest("GET", "https://uptime.betterstack.com/api/v2/monitors", nil) + if err != nil { + fmt.Printf("Error fetching uptime data: %v\n", err) + return []UptimeSite{} + } + req.Header.Add("Authorization", "Bearer "+u.apiKey) + + resp, err := client.Do(req) + if err != nil { + fmt.Printf("Error fetching uptime data: %v\n", err) + return []UptimeSite{} + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Printf("Error reading response: %v\n", err) + return []UptimeSite{} + } + + var rawMonitors BetterUptimeResponse + if err := json.Unmarshal(body, &rawMonitors); err != nil { + fmt.Printf("Error parsing uptime data: %v\n", err) + return []UptimeSite{} + } + + // alphabetically sort the monitors because UptimeRobot does, but BetterUptime doesnt (or sorts by something else?), and I want them to be consistent + sort.Slice(rawMonitors.Monitors, func(i, j int) bool { + return rawMonitors.Monitors[i].Attributes.PronounceableName < rawMonitors.Monitors[j].Attributes.PronounceableName + }) + + var monitors []UptimeSite + for _, rawMonitor := range rawMonitors.Monitors { + monitors = append(monitors, UptimeSite{ + FriendlyName: rawMonitor.Attributes.PronounceableName, + Url: rawMonitor.Attributes.Url, + Up: rawMonitor.Attributes.Status == "up", + }) + } + + return monitors } diff --git a/zqdgr.config.json b/zqdgr.config.json index ba3229d..146e04d 100644 --- a/zqdgr.config.json +++ b/zqdgr.config.json @@ -18,7 +18,7 @@ "build:arm64": "zqdgr generate && GOOS=linux GOARCH=arm64 go build -tags netgo,prod -ldflags=\"-w -s\" -o passport-linux-arm64 src/main.go && upx passport-linux-arm64" }, "pattern": "src/**/*.{go,hbs,css,js,svg,png,jpg,jpeg,webp,woff2,ico,webp}", - "excluded_files": [ + "excluded_globs": [ "src/assets/styles" ], "shutdown_signal": "SIGINT"