chore: rename project from go-upkeep to uptop #25

Merged
lerko merged 1 commits from chore/rename-uptop into main 2026-05-25 01:02:30 +00:00
36 changed files with 208 additions and 210 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/go-upkeep images: ${{ secrets.DOCKERHUB_USERNAME }}/uptop
tags: | tags: |
# This turns git tag "v1.0.0" into docker tag "1.0.0" # This turns git tag "v1.0.0" into docker tag "1.0.0"
type=semver,pattern={{version}} type=semver,pattern={{version}}
+2 -5
View File
@@ -26,8 +26,8 @@ go.work
# End of https://www.toptal.com/developers/gitignore/api/go # End of https://www.toptal.com/developers/gitignore/api/go
/goupkeep /uptop
upkeep.db uptop.db
.ssh .ssh
@@ -35,8 +35,5 @@ authorized_keys
tmp tmp
# Old repo
/go-upkeep/
*.local.json *.local.json
*.local.md *.local.md
+1 -1
View File
@@ -3,7 +3,7 @@
## Development ## Development
```sh ```sh
go run cmd/goupkeep/main.go -demo # starts with sample data go run cmd/uptop/main.go -demo # starts with sample data
ssh -p 23234 localhost # connect to TUI ssh -p 23234 localhost # connect to TUI
``` ```
+7 -7
View File
@@ -9,7 +9,7 @@ ENV CGO_ENABLED=1
ARG VERSION=dev ARG VERSION=dev
ARG COMMIT=none ARG COMMIT=none
ARG BUILD_DATE=unknown ARG BUILD_DATE=unknown
RUN go build -ldflags="-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${BUILD_DATE}" -o go-upkeep ./cmd/goupkeep/main.go RUN go build -ldflags="-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${BUILD_DATE}" -o uptop ./cmd/uptop/main.go
# --- Stage 2: Runner --- # --- Stage 2: Runner ---
FROM alpine:latest FROM alpine:latest
@@ -17,15 +17,15 @@ WORKDIR /app
RUN apk add --no-cache ca-certificates openssh-client RUN apk add --no-cache ca-certificates openssh-client
RUN mkdir /data RUN mkdir /data
COPY --from=builder /app/go-upkeep . COPY --from=builder /app/uptop .
# Set Default Configuration via ENV # Set Default Configuration via ENV
# Docker users can override these in docker-compose.yml # Docker users can override these in docker-compose.yml
ENV LIPGLOSS_RENDERER_HAS_DARK_BACKGROUND=true ENV LIPGLOSS_RENDERER_HAS_DARK_BACKGROUND=true
ENV UPKEEP_DB_TYPE=sqlite ENV UPTOP_DB_TYPE=sqlite
ENV UPKEEP_DB_DSN=/data/upkeep.db ENV UPTOP_DB_DSN=/data/uptop.db
ENV UPKEEP_KEYS=/data/authorized_keys ENV UPTOP_KEYS=/data/authorized_keys
ENV UPKEEP_PORT=23234 ENV UPTOP_PORT=23234
EXPOSE 23234 EXPOSE 23234
CMD ["./go-upkeep"] CMD ["./uptop"]
+26 -26
View File
@@ -1,4 +1,4 @@
# Go-Upkeep # uptop
Self-hosted uptime monitor with a TUI you can access over SSH. No browser, no install on the client — just `ssh -p 23234 your-server`. Self-hosted uptime monitor with a TUI you can access over SSH. No browser, no install on the client — just `ssh -p 23234 your-server`.
@@ -18,14 +18,14 @@ Built on the foundation of [RDGames/go-upkeep](https://github.com/RDGames/go-upk
## Quick start ## Quick start
```bash ```bash
go run cmd/goupkeep/main.go go run cmd/uptop/main.go
ssh -p 23234 localhost ssh -p 23234 localhost
``` ```
Seed some demo data to see it in action: Seed some demo data to see it in action:
```bash ```bash
go run cmd/goupkeep/main.go -demo go run cmd/uptop/main.go -demo
``` ```
## Install ## Install
@@ -33,34 +33,34 @@ go run cmd/goupkeep/main.go -demo
### From source ### From source
```bash ```bash
go install gitea.lerkolabs.com/lerko/uptime/cmd/goupkeep@latest go install gitea.lerkolabs.com/lerko/uptop/cmd/uptop@latest
``` ```
### Docker ### Docker
```bash ```bash
docker pull lerko/go-upkeep:latest docker pull lerko/uptop:latest
docker run -p 23234:23234 -p 8080:8080 -v ./data:/data lerko/go-upkeep docker run -p 23234:23234 -p 8080:8080 -v ./data:/data lerko/uptop
``` ```
### Binary ### Binary
Download from [Releases](https://gitea.lerkolabs.com/lerko/uptime/releases). Download from [Releases](https://gitea.lerkolabs.com/lerko/uptop/releases).
## Config as code ## Config as code
Export your current monitors: Export your current monitors:
```bash ```bash
goupkeep export -o monitors.yaml uptop export -o monitors.yaml
``` ```
Apply a config file: Apply a config file:
```bash ```bash
goupkeep apply -f monitors.yaml uptop apply -f monitors.yaml
goupkeep apply -f monitors.yaml --dry-run # see what would change uptop apply -f monitors.yaml --dry-run # see what would change
goupkeep apply -f monitors.yaml --prune # delete anything not in the YAML uptop apply -f monitors.yaml --prune # delete anything not in the YAML
``` ```
See [docs/config-as-code.md](docs/config-as-code.md) for the full reference. See [docs/config-as-code.md](docs/config-as-code.md) for the full reference.
@@ -81,28 +81,28 @@ services:
- ./data:/data - ./data:/data
- ./ssh_keys:/app/.ssh - ./ssh_keys:/app/.ssh
environment: environment:
- UPKEEP_DB_TYPE=sqlite - UPTOP_DB_TYPE=sqlite
- UPKEEP_DB_DSN=/data/upkeep.db - UPTOP_DB_DSN=/data/uptop.db
- UPKEEP_STATUS_ENABLED=true - UPTOP_STATUS_ENABLED=true
- UPKEEP_CLUSTER_SECRET=change-me - UPTOP_CLUSTER_SECRET=change-me
``` ```
First run: attach to the container (`docker attach go-upkeep`), go to the Users tab, add your SSH public key. Then detach with `Ctrl+P, Ctrl+Q` and connect normally over SSH. First run: attach to the container (`docker attach uptop`), go to the Users tab, add your SSH public key. Then detach with `Ctrl+P, Ctrl+Q` and connect normally over SSH.
## Environment variables ## Environment variables
| Variable | Default | What it does | | Variable | Default | What it does |
|---|---|---| |---|---|---|
| `UPKEEP_PORT` | `23234` | SSH server port | | `UPTOP_PORT` | `23234` | SSH server port |
| `UPKEEP_HTTP_PORT` | `8080` | HTTP server port (status page, push, metrics) | | `UPTOP_HTTP_PORT` | `8080` | HTTP server port (status page, push, metrics) |
| `UPKEEP_DB_TYPE` | `sqlite` | `sqlite` or `postgres` | | `UPTOP_DB_TYPE` | `sqlite` | `sqlite` or `postgres` |
| `UPKEEP_DB_DSN` | `upkeep.db` | Database path or connection string | | `UPTOP_DB_DSN` | `uptop.db` | Database path or connection string |
| `UPKEEP_STATUS_ENABLED` | `false` | Enable public status page | | `UPTOP_STATUS_ENABLED` | `false` | Enable public status page |
| `UPKEEP_STATUS_TITLE` | `System Status` | Status page title | | `UPTOP_STATUS_TITLE` | `System Status` | Status page title |
| `UPKEEP_CLUSTER_MODE` | `leader` | `leader` or `follower` | | `UPTOP_CLUSTER_MODE` | `leader` | `leader` or `follower` |
| `UPKEEP_PEER_URL` | | Leader URL for follower nodes | | `UPTOP_PEER_URL` | | Leader URL for follower nodes |
| `UPKEEP_CLUSTER_SECRET` | | Shared key for cluster + API auth | | `UPTOP_CLUSTER_SECRET` | | Shared key for cluster + API auth |
| `UPKEEP_INSECURE_SKIP_VERIFY` | `false` | Skip TLS verification for checks | | `UPTOP_INSECURE_SKIP_VERIFY` | `false` | Skip TLS verification for checks |
## Migrating from Uptime Kuma ## Migrating from Uptime Kuma
+32 -32
View File
@@ -5,14 +5,14 @@ import (
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"go-upkeep/internal/cluster" "gitea.lerkolabs.com/lerko/uptop/internal/cluster"
"go-upkeep/internal/config" "gitea.lerkolabs.com/lerko/uptop/internal/config"
"go-upkeep/internal/importer" "gitea.lerkolabs.com/lerko/uptop/internal/importer"
"go-upkeep/internal/models" "gitea.lerkolabs.com/lerko/uptop/internal/models"
"go-upkeep/internal/monitor" "gitea.lerkolabs.com/lerko/uptop/internal/monitor"
"go-upkeep/internal/server" "gitea.lerkolabs.com/lerko/uptop/internal/server"
"go-upkeep/internal/store" "gitea.lerkolabs.com/lerko/uptop/internal/store"
"go-upkeep/internal/tui" "gitea.lerkolabs.com/lerko/uptop/internal/tui"
"log" "log"
"os" "os"
"os/signal" "os/signal"
@@ -54,9 +54,9 @@ func main() {
func printVersion() { func printVersion() {
if version == "dev" { if version == "dev" {
fmt.Println("go-upkeep dev") fmt.Println("uptop dev")
} else { } else {
fmt.Printf("go-upkeep %s (%s, %s)\n", version, commit, date) fmt.Printf("uptop %s (%s, %s)\n", version, commit, date)
} }
} }
@@ -91,8 +91,8 @@ func runApply(args []string) {
filePath := fs.String("f", "", "Path to YAML config file (required)") filePath := fs.String("f", "", "Path to YAML config file (required)")
dryRun := fs.Bool("dry-run", false, "Show planned changes without applying") dryRun := fs.Bool("dry-run", false, "Show planned changes without applying")
prune := fs.Bool("prune", false, "Delete monitors/alerts not in YAML") prune := fs.Bool("prune", false, "Delete monitors/alerts not in YAML")
dbType := fs.String("db-type", envOrDefault("UPKEEP_DB_TYPE", "sqlite"), "Database type") dbType := fs.String("db-type", envOrDefault("UPTOP_DB_TYPE", "sqlite"), "Database type")
dsn := fs.String("dsn", envOrDefault("UPKEEP_DB_DSN", "upkeep.db"), "Database DSN") dsn := fs.String("dsn", envOrDefault("UPTOP_DB_DSN", "uptop.db"), "Database DSN")
_ = fs.Parse(args) // ExitOnError: parse errors exit before returning _ = fs.Parse(args) // ExitOnError: parse errors exit before returning
if *filePath == "" { if *filePath == "" {
@@ -124,8 +124,8 @@ func runApply(args []string) {
func runExport(args []string) { func runExport(args []string) {
fs := flag.NewFlagSet("export", flag.ExitOnError) fs := flag.NewFlagSet("export", flag.ExitOnError)
outPath := fs.String("o", "-", "Output file path (- for stdout)") outPath := fs.String("o", "-", "Output file path (- for stdout)")
dbType := fs.String("db-type", envOrDefault("UPKEEP_DB_TYPE", "sqlite"), "Database type") dbType := fs.String("db-type", envOrDefault("UPTOP_DB_TYPE", "sqlite"), "Database type")
dsn := fs.String("dsn", envOrDefault("UPKEEP_DB_DSN", "upkeep.db"), "Database DSN") dsn := fs.String("dsn", envOrDefault("UPTOP_DB_DSN", "uptop.db"), "Database DSN")
_ = fs.Parse(args) // ExitOnError: parse errors exit before returning _ = fs.Parse(args) // ExitOnError: parse errors exit before returning
s := openStore(*dbType, *dsn) s := openStore(*dbType, *dsn)
@@ -145,7 +145,7 @@ func runExport(args []string) {
func runServe(args []string) { func runServe(args []string) {
portVal := 23234 portVal := 23234
dbType := "sqlite" dbType := "sqlite"
dbDSN := "upkeep.db" dbDSN := "uptop.db"
httpPort := 8080 httpPort := 8080
enableStatus := false enableStatus := false
statusTitle := "System Status" statusTitle := "System Status"
@@ -153,50 +153,50 @@ func runServe(args []string) {
clusterPeer := "" clusterPeer := ""
clusterKey := "" clusterKey := ""
if v := os.Getenv("UPKEEP_PORT"); v != "" { if v := os.Getenv("UPTOP_PORT"); v != "" {
if p, err := strconv.Atoi(v); err == nil { if p, err := strconv.Atoi(v); err == nil {
portVal = p portVal = p
} }
} }
if v := os.Getenv("UPKEEP_DB_TYPE"); v != "" { if v := os.Getenv("UPTOP_DB_TYPE"); v != "" {
dbType = v dbType = v
} }
if v := os.Getenv("UPKEEP_DB_DSN"); v != "" { if v := os.Getenv("UPTOP_DB_DSN"); v != "" {
dbDSN = v dbDSN = v
} }
if v := os.Getenv("UPKEEP_HTTP_PORT"); v != "" { if v := os.Getenv("UPTOP_HTTP_PORT"); v != "" {
if p, err := strconv.Atoi(v); err == nil { if p, err := strconv.Atoi(v); err == nil {
httpPort = p httpPort = p
} }
} }
if v := os.Getenv("UPKEEP_STATUS_ENABLED"); v == "true" { if v := os.Getenv("UPTOP_STATUS_ENABLED"); v == "true" {
enableStatus = true enableStatus = true
} }
if v := os.Getenv("UPKEEP_STATUS_TITLE"); v != "" { if v := os.Getenv("UPTOP_STATUS_TITLE"); v != "" {
statusTitle = v statusTitle = v
} }
if v := os.Getenv("UPKEEP_CLUSTER_MODE"); v != "" { if v := os.Getenv("UPTOP_CLUSTER_MODE"); v != "" {
clusterMode = v clusterMode = v
} }
if v := os.Getenv("UPKEEP_PEER_URL"); v != "" { if v := os.Getenv("UPTOP_PEER_URL"); v != "" {
clusterPeer = v clusterPeer = v
} }
if v := os.Getenv("UPKEEP_CLUSTER_SECRET"); v != "" { if v := os.Getenv("UPTOP_CLUSTER_SECRET"); v != "" {
clusterKey = v clusterKey = v
} }
nodeID := os.Getenv("UPKEEP_NODE_ID") nodeID := os.Getenv("UPTOP_NODE_ID")
nodeName := os.Getenv("UPKEEP_NODE_NAME") nodeName := os.Getenv("UPTOP_NODE_NAME")
nodeRegion := os.Getenv("UPKEEP_NODE_REGION") nodeRegion := os.Getenv("UPTOP_NODE_REGION")
aggStrategy := os.Getenv("UPKEEP_AGG_STRATEGY") aggStrategy := os.Getenv("UPTOP_AGG_STRATEGY")
if clusterMode == "probe" { if clusterMode == "probe" {
if nodeID == "" { if nodeID == "" {
fmt.Fprintln(os.Stderr, "UPKEEP_NODE_ID is required for probe mode") fmt.Fprintln(os.Stderr, "UPTOP_NODE_ID is required for probe mode")
os.Exit(1) os.Exit(1)
} }
if clusterPeer == "" { if clusterPeer == "" {
fmt.Fprintln(os.Stderr, "UPKEEP_PEER_URL is required for probe mode") fmt.Fprintln(os.Stderr, "UPTOP_PEER_URL is required for probe mode")
os.Exit(1) os.Exit(1)
} }
@@ -270,7 +270,7 @@ func runServe(args []string) {
} }
eng := monitor.NewEngine(s) eng := monitor.NewEngine(s)
if os.Getenv("UPKEEP_INSECURE_SKIP_VERIFY") == "true" { if os.Getenv("UPTOP_INSECURE_SKIP_VERIFY") == "true" {
eng.SetInsecureSkipVerify(true) eng.SetInsecureSkipVerify(true)
} }
if aggStrategy != "" { if aggStrategy != "" {
@@ -305,7 +305,7 @@ func runServe(args []string) {
fmt.Printf("Error: %v\n", err) fmt.Printf("Error: %v\n", err)
} }
} else { } else {
fmt.Println("Go-Upkeep running in HEADLESS mode") fmt.Println("uptop running in HEADLESS mode")
done := make(chan os.Signal, 1) done := make(chan os.Signal, 1)
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
<-done <-done
+21 -21
View File
@@ -4,21 +4,21 @@ services:
# ------------------------- # -------------------------
leader: leader:
build: . build: .
container_name: upkeep-leader container_name: uptop-leader
ports: ports:
- "23234:23234" # SSH - "23234:23234" # SSH
- "8080:8080" # HTTP - "8080:8080" # HTTP
environment: environment:
- UPKEEP_DB_TYPE=postgres - UPTOP_DB_TYPE=postgres
# Note: Port 5432 is correct here because we are talking INSIDE the network # Note: Port 5432 is correct here because we are talking INSIDE the network
- UPKEEP_DB_DSN=postgres://devuser:devpass@leader-db:5432/upkeep_dev?sslmode=disable - UPTOP_DB_DSN=postgres://devuser:devpass@leader-db:5432/uptop_dev?sslmode=disable
- UPKEEP_HTTP_PORT=8080 - UPTOP_HTTP_PORT=8080
- UPKEEP_STATUS_ENABLED=true - UPTOP_STATUS_ENABLED=true
- UPKEEP_STATUS_TITLE=Leader Node - UPTOP_STATUS_TITLE=Leader Node
# Cluster Config # Cluster Config
- UPKEEP_CLUSTER_MODE=leader - UPTOP_CLUSTER_MODE=leader
- UPKEEP_CLUSTER_SECRET=mysecret - UPTOP_CLUSTER_SECRET=mysecret
depends_on: depends_on:
- leader-db - leader-db
stdin_open: true stdin_open: true
@@ -26,11 +26,11 @@ services:
leader-db: leader-db:
image: postgres:15-alpine image: postgres:15-alpine
container_name: upkeep-leader-db container_name: uptop-leader-db
environment: environment:
POSTGRES_USER: devuser POSTGRES_USER: devuser
POSTGRES_PASSWORD: devpass POSTGRES_PASSWORD: devpass
POSTGRES_DB: upkeep_dev POSTGRES_DB: uptop_dev
volumes: volumes:
- ./tmp/leader-data:/var/lib/postgresql/data - ./tmp/leader-data:/var/lib/postgresql/data
@@ -39,23 +39,23 @@ services:
# ------------------------- # -------------------------
follower: follower:
build: . build: .
container_name: upkeep-follower container_name: uptop-follower
ports: ports:
- "23233:23234" # SSH (Mapped to different host port) - "23233:23234" # SSH (Mapped to different host port)
- "8081:8080" # HTTP (Mapped to different host port) - "8081:8080" # HTTP (Mapped to different host port)
environment: environment:
- UPKEEP_DB_TYPE=postgres - UPTOP_DB_TYPE=postgres
# Connects to its OWN database # Connects to its OWN database
- UPKEEP_DB_DSN=postgres://devuser:devpass@follower-db:5432/upkeep_dev?sslmode=disable - UPTOP_DB_DSN=postgres://devuser:devpass@follower-db:5432/uptop_dev?sslmode=disable
- UPKEEP_HTTP_PORT=8080 - UPTOP_HTTP_PORT=8080
- UPKEEP_STATUS_ENABLED=true - UPTOP_STATUS_ENABLED=true
- UPKEEP_STATUS_TITLE=Follower Node - UPTOP_STATUS_TITLE=Follower Node
# Cluster Config # Cluster Config
- UPKEEP_CLUSTER_MODE=follower - UPTOP_CLUSTER_MODE=follower
- UPKEEP_CLUSTER_SECRET=mysecret - UPTOP_CLUSTER_SECRET=mysecret
# IMPORTANT: Uses the Service Name "leader" to connect internally # IMPORTANT: Uses the Service Name "leader" to connect internally
- UPKEEP_PEER_URL=http://leader:8080 - UPTOP_PEER_URL=http://leader:8080
depends_on: depends_on:
- follower-db - follower-db
stdin_open: true stdin_open: true
@@ -63,10 +63,10 @@ services:
follower-db: follower-db:
image: postgres:15-alpine image: postgres:15-alpine
container_name: upkeep-follower-db container_name: uptop-follower-db
environment: environment:
POSTGRES_USER: devuser POSTGRES_USER: devuser
POSTGRES_PASSWORD: devpass POSTGRES_PASSWORD: devpass
POSTGRES_DB: upkeep_dev POSTGRES_DB: uptop_dev
volumes: volumes:
- ./tmp/follower-data:/var/lib/postgresql/data - ./tmp/follower-data:/var/lib/postgresql/data
+8 -8
View File
@@ -4,19 +4,19 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: upkeep-dev container_name: uptop-dev
ports: ports:
- "23234:23234" # SSH Access - "23234:23234" # SSH Access
- "8080:8080" # HTTP (Push Monitors + Status Page) - "8080:8080" # HTTP (Push Monitors + Status Page)
environment: environment:
# --- Database Configuration (Postgres) --- # --- Database Configuration (Postgres) ---
- UPKEEP_DB_TYPE=postgres - UPTOP_DB_TYPE=postgres
- UPKEEP_DB_DSN=postgres://devuser:devpass@postgres:5432/upkeep_dev?sslmode=disable - UPTOP_DB_DSN=postgres://devuser:devpass@postgres:5432/uptop_dev?sslmode=disable
# --- Web Server Configuration (Phase 4) --- # --- Web Server Configuration (Phase 4) ---
- UPKEEP_HTTP_PORT=8080 - UPTOP_HTTP_PORT=8080
- UPKEEP_STATUS_ENABLED=true - UPTOP_STATUS_ENABLED=true
- UPKEEP_STATUS_TITLE=Dev Infrastructure Status - UPTOP_STATUS_TITLE=Dev Infrastructure Status
depends_on: depends_on:
- postgres - postgres
stdin_open: true # Required for 'docker attach' (Local Admin Console) stdin_open: true # Required for 'docker attach' (Local Admin Console)
@@ -25,11 +25,11 @@ services:
# The Database # The Database
postgres: postgres:
image: postgres:15-alpine image: postgres:15-alpine
container_name: upkeep-postgres container_name: uptop-postgres
environment: environment:
POSTGRES_USER: devuser POSTGRES_USER: devuser
POSTGRES_PASSWORD: devpass POSTGRES_PASSWORD: devpass
POSTGRES_DB: upkeep_dev POSTGRES_DB: uptop_dev
ports: ports:
- "5432:5432" # Expose for external DB tools (DBeaver, etc.) - "5432:5432" # Expose for external DB tools (DBeaver, etc.)
volumes: volumes:
+16 -16
View File
@@ -2,10 +2,10 @@ services:
leader: leader:
build: . build: .
environment: environment:
- UPKEEP_CLUSTER_MODE=leader - UPTOP_CLUSTER_MODE=leader
- UPKEEP_CLUSTER_SECRET=changeme - UPTOP_CLUSTER_SECRET=changeme
- UPKEEP_AGG_STRATEGY=any-down - UPTOP_AGG_STRATEGY=any-down
- UPKEEP_STATUS_ENABLED=true - UPTOP_STATUS_ENABLED=true
ports: ports:
- "8080:8080" - "8080:8080"
- "23234:23234" - "23234:23234"
@@ -13,23 +13,23 @@ services:
probe-us-east: probe-us-east:
build: . build: .
environment: environment:
- UPKEEP_CLUSTER_MODE=probe - UPTOP_CLUSTER_MODE=probe
- UPKEEP_NODE_ID=us-east-1 - UPTOP_NODE_ID=us-east-1
- UPKEEP_NODE_NAME=US East Probe - UPTOP_NODE_NAME=US East Probe
- UPKEEP_NODE_REGION=us-east - UPTOP_NODE_REGION=us-east
- UPKEEP_PEER_URL=http://leader:8080 - UPTOP_PEER_URL=http://leader:8080
- UPKEEP_CLUSTER_SECRET=changeme - UPTOP_CLUSTER_SECRET=changeme
depends_on: depends_on:
- leader - leader
probe-eu-west: probe-eu-west:
build: . build: .
environment: environment:
- UPKEEP_CLUSTER_MODE=probe - UPTOP_CLUSTER_MODE=probe
- UPKEEP_NODE_ID=eu-west-1 - UPTOP_NODE_ID=eu-west-1
- UPKEEP_NODE_NAME=EU West Probe - UPTOP_NODE_NAME=EU West Probe
- UPKEEP_NODE_REGION=eu-west - UPTOP_NODE_REGION=eu-west
- UPKEEP_PEER_URL=http://leader:8080 - UPTOP_PEER_URL=http://leader:8080
- UPKEEP_CLUSTER_SECRET=changeme - UPTOP_CLUSTER_SECRET=changeme
depends_on: depends_on:
- leader - leader
+6 -6
View File
@@ -3,16 +3,16 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: upkeep container_name: uptop
restart: unless-stopped restart: unless-stopped
ports: ports:
- "23234:23234" - "23234:23234"
- "8080:8080" - "8080:8080"
environment: environment:
- UPKEEP_DB_TYPE=sqlite - UPTOP_DB_TYPE=sqlite
- UPKEEP_DB_DSN=/data/upkeep.db - UPTOP_DB_DSN=/data/uptop.db
- UPKEEP_HTTP_PORT=8080 - UPTOP_HTTP_PORT=8080
- UPKEEP_STATUS_ENABLED=true - UPTOP_STATUS_ENABLED=true
- UPKEEP_STATUS_TITLE=System Status - UPTOP_STATUS_TITLE=System Status
volumes: volumes:
- ./data:/data - ./data:/data
+13 -13
View File
@@ -7,13 +7,13 @@ Define your monitors and alerts in a YAML file. Version control them, copy them
Export what you already have: Export what you already have:
```bash ```bash
goupkeep export -o monitors.yaml uptop export -o monitors.yaml
``` ```
That gives you a working file you can edit and re-apply: That gives you a working file you can edit and re-apply:
```bash ```bash
goupkeep apply -f monitors.yaml uptop apply -f monitors.yaml
``` ```
That's it. Apply only creates or updates — it won't delete anything unless you tell it to. That's it. Apply only creates or updates — it won't delete anything unless you tell it to.
@@ -184,34 +184,34 @@ All 9 providers work in the YAML. The `settings` map is different per type.
**Export current state:** **Export current state:**
```bash ```bash
goupkeep export -o monitors.yaml # to a file uptop export -o monitors.yaml # to a file
goupkeep export # to stdout uptop export # to stdout
``` ```
**Apply a config:** **Apply a config:**
```bash ```bash
goupkeep apply -f monitors.yaml uptop apply -f monitors.yaml
``` ```
**See what would change first:** **See what would change first:**
```bash ```bash
goupkeep apply -f monitors.yaml --dry-run uptop apply -f monitors.yaml --dry-run
``` ```
**Delete monitors not in the YAML:** **Delete monitors not in the YAML:**
```bash ```bash
goupkeep apply -f monitors.yaml --prune uptop apply -f monitors.yaml --prune
``` ```
Without `--prune`, apply never deletes anything. It only creates and updates. Without `--prune`, apply never deletes anything. It only creates and updates.
**Pointing at a different database:** **Pointing at a different database:**
```bash ```bash
goupkeep export -db-type postgres -dsn "host=localhost dbname=upkeep sslmode=disable" uptop export -db-type postgres -dsn "host=localhost dbname=uptop sslmode=disable"
goupkeep apply -f monitors.yaml -db-type postgres -dsn "..." uptop apply -f monitors.yaml -db-type postgres -dsn "..."
``` ```
Both commands respect the `UPKEEP_DB_TYPE` and `UPKEEP_DB_DSN` environment variables too. Both commands respect the `UPTOP_DB_TYPE` and `UPTOP_DB_DSN` environment variables too.
## How apply works ## How apply works
@@ -230,15 +230,15 @@ If something fails mid-apply, just fix the issue and run it again. It picks up w
```bash ```bash
# set up your monitors in the TUI first, then export # set up your monitors in the TUI first, then export
goupkeep export -o monitors.yaml uptop export -o monitors.yaml
# commit it # commit it
git add monitors.yaml && git commit -m "add monitor config" git add monitors.yaml && git commit -m "add monitor config"
# deploy to another instance # deploy to another instance
scp monitors.yaml prod-server: scp monitors.yaml prod-server:
ssh prod-server goupkeep apply -f monitors.yaml ssh prod-server uptop apply -f monitors.yaml
# or just keep it as a backup you can restore from # or just keep it as a backup you can restore from
goupkeep apply -f monitors.yaml uptop apply -f monitors.yaml
``` ```
+1 -1
View File
@@ -1,4 +1,4 @@
module go-upkeep module gitea.lerkolabs.com/lerko/uptop
go 1.24.4 go 1.24.4
+3 -3
View File
@@ -5,7 +5,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"go-upkeep/internal/models" "gitea.lerkolabs.com/lerko/uptop/internal/models"
"net/http" "net/http"
"net/smtp" "net/smtp"
"strconv" "strconv"
@@ -76,7 +76,7 @@ func pagerdutyPayload(routingKey, severity string) PayloadFunc {
"event_action": "trigger", "event_action": "trigger",
"payload": map[string]string{ "payload": map[string]string{
"summary": fmt.Sprintf("%s: %s", title, message), "summary": fmt.Sprintf("%s: %s", title, message),
"source": "go-upkeep", "source": "uptop",
"severity": severity, "severity": severity,
}, },
}) })
@@ -184,7 +184,7 @@ func (e *EmailProvider) Send(ctx context.Context, title, message string) error {
} }
auth := smtp.PlainAuth("", e.User, e.Pass, e.Host) auth := smtp.PlainAuth("", e.User, e.Pass, e.Host)
msg := []byte("To: " + e.To + "\r\n" + msg := []byte("To: " + e.To + "\r\n" +
"Subject: Go-Upkeep: " + title + "\r\n" + "Subject: uptop: " + title + "\r\n" +
"\r\n" + "\r\n" +
message + "\r\n") message + "\r\n")
return smtp.SendMail(e.Host+":"+e.Port, auth, e.From, []string{e.To}, msg) return smtp.SendMail(e.Host+":"+e.Port, auth, e.From, []string{e.To}, msg)
+1 -1
View File
@@ -3,7 +3,7 @@ package alert
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"go-upkeep/internal/models" "gitea.lerkolabs.com/lerko/uptop/internal/models"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
+1 -1
View File
@@ -3,7 +3,7 @@ package cluster
import ( import (
"context" "context"
"fmt" "fmt"
"go-upkeep/internal/monitor" "gitea.lerkolabs.com/lerko/uptop/internal/monitor"
"net/http" "net/http"
"strings" "strings"
"time" "time"
+2 -2
View File
@@ -3,8 +3,8 @@ package cluster
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"go-upkeep/internal/models" "gitea.lerkolabs.com/lerko/uptop/internal/models"
"go-upkeep/internal/monitor" "gitea.lerkolabs.com/lerko/uptop/internal/monitor"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"sync" "sync"
+2 -2
View File
@@ -6,8 +6,8 @@ import (
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"go-upkeep/internal/models" "gitea.lerkolabs.com/lerko/uptop/internal/models"
"go-upkeep/internal/monitor" "gitea.lerkolabs.com/lerko/uptop/internal/monitor"
"log" "log"
"net/http" "net/http"
"sync" "sync"
+2 -2
View File
@@ -2,8 +2,8 @@ package config
import ( import (
"fmt" "fmt"
"go-upkeep/internal/models" "gitea.lerkolabs.com/lerko/uptop/internal/models"
"go-upkeep/internal/store" "gitea.lerkolabs.com/lerko/uptop/internal/store"
"reflect" "reflect"
"strings" "strings"
) )
+2 -2
View File
@@ -1,8 +1,8 @@
package config package config
import ( import (
"go-upkeep/internal/models" "gitea.lerkolabs.com/lerko/uptop/internal/models"
"go-upkeep/internal/store" "gitea.lerkolabs.com/lerko/uptop/internal/store"
"strings" "strings"
"testing" "testing"
) )
+2 -2
View File
@@ -2,8 +2,8 @@ package config
import ( import (
"fmt" "fmt"
"go-upkeep/internal/models" "gitea.lerkolabs.com/lerko/uptop/internal/models"
"go-upkeep/internal/store" "gitea.lerkolabs.com/lerko/uptop/internal/store"
"os" "os"
"sort" "sort"
+1 -1
View File
@@ -1,7 +1,7 @@
package config package config
import ( import (
"go-upkeep/internal/models" "gitea.lerkolabs.com/lerko/uptop/internal/models"
"testing" "testing"
) )
+1 -1
View File
@@ -3,7 +3,7 @@ package importer
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"go-upkeep/internal/models" "gitea.lerkolabs.com/lerko/uptop/internal/models"
"os" "os"
"strings" "strings"
) )
+22 -22
View File
@@ -2,8 +2,8 @@ package metrics
import ( import (
"fmt" "fmt"
"go-upkeep/internal/models" "gitea.lerkolabs.com/lerko/uptop/internal/models"
"go-upkeep/internal/monitor" "gitea.lerkolabs.com/lerko/uptop/internal/monitor"
"net/http" "net/http"
"sort" "sort"
"strings" "strings"
@@ -16,74 +16,74 @@ func Handler(eng *monitor.Engine) http.HandlerFunc {
var b strings.Builder var b strings.Builder
writeHelp(&b, "upkeep_monitor_up", "gauge", "Whether the monitor is up (1) or down (0).") writeHelp(&b, "uptop_monitor_up", "gauge", "Whether the monitor is up (1) or down (0).")
for _, s := range sites { for _, s := range sites {
val := 0 val := 0
if s.Status == "UP" { if s.Status == "UP" {
val = 1 val = 1
} }
writeGauge(&b, "upkeep_monitor_up", labels(s), float64(val)) writeGauge(&b, "uptop_monitor_up", labels(s), float64(val))
} }
writeHelp(&b, "upkeep_monitor_latency_seconds", "gauge", "Last check latency in seconds.") writeHelp(&b, "uptop_monitor_latency_seconds", "gauge", "Last check latency in seconds.")
for _, s := range sites { for _, s := range sites {
writeGauge(&b, "upkeep_monitor_latency_seconds", labels(s), s.Latency.Seconds()) writeGauge(&b, "uptop_monitor_latency_seconds", labels(s), s.Latency.Seconds())
} }
writeHelp(&b, "upkeep_monitor_status_code", "gauge", "HTTP response status code of the last check.") writeHelp(&b, "uptop_monitor_status_code", "gauge", "HTTP response status code of the last check.")
for _, s := range sites { for _, s := range sites {
if s.Type != "http" { if s.Type != "http" {
continue continue
} }
writeGauge(&b, "upkeep_monitor_status_code", labels(s), float64(s.StatusCode)) writeGauge(&b, "uptop_monitor_status_code", labels(s), float64(s.StatusCode))
} }
writeHelp(&b, "upkeep_monitor_check_timestamp_seconds", "gauge", "Unix timestamp of the last check.") writeHelp(&b, "uptop_monitor_check_timestamp_seconds", "gauge", "Unix timestamp of the last check.")
for _, s := range sites { for _, s := range sites {
if s.LastCheck.IsZero() { if s.LastCheck.IsZero() {
continue continue
} }
writeGauge(&b, "upkeep_monitor_check_timestamp_seconds", labels(s), float64(s.LastCheck.Unix())) writeGauge(&b, "uptop_monitor_check_timestamp_seconds", labels(s), float64(s.LastCheck.Unix()))
} }
writeHelp(&b, "upkeep_monitor_paused", "gauge", "Whether the monitor is paused (1) or active (0).") writeHelp(&b, "uptop_monitor_paused", "gauge", "Whether the monitor is paused (1) or active (0).")
for _, s := range sites { for _, s := range sites {
val := 0 val := 0
if s.Paused { if s.Paused {
val = 1 val = 1
} }
writeGauge(&b, "upkeep_monitor_paused", labels(s), float64(val)) writeGauge(&b, "uptop_monitor_paused", labels(s), float64(val))
} }
writeHelp(&b, "upkeep_monitor_maintenance", "gauge", "Whether the monitor is in a maintenance window (1) or not (0).") writeHelp(&b, "uptop_monitor_maintenance", "gauge", "Whether the monitor is in a maintenance window (1) or not (0).")
for _, s := range sites { for _, s := range sites {
val := 0 val := 0
if eng.GetDisplayStatus(s) == "MAINT" { if eng.GetDisplayStatus(s) == "MAINT" {
val = 1 val = 1
} }
writeGauge(&b, "upkeep_monitor_maintenance", labels(s), float64(val)) writeGauge(&b, "uptop_monitor_maintenance", labels(s), float64(val))
} }
writeHelp(&b, "upkeep_monitor_cert_expiry_timestamp_seconds", "gauge", "Unix timestamp when the SSL certificate expires.") writeHelp(&b, "uptop_monitor_cert_expiry_timestamp_seconds", "gauge", "Unix timestamp when the SSL certificate expires.")
for _, s := range sites { for _, s := range sites {
if !s.HasSSL || s.CertExpiry.IsZero() { if !s.HasSSL || s.CertExpiry.IsZero() {
continue continue
} }
writeGauge(&b, "upkeep_monitor_cert_expiry_timestamp_seconds", labels(s), float64(s.CertExpiry.Unix())) writeGauge(&b, "uptop_monitor_cert_expiry_timestamp_seconds", labels(s), float64(s.CertExpiry.Unix()))
} }
writeHelp(&b, "upkeep_monitor_checks_total", "counter", "Total number of checks performed.") writeHelp(&b, "uptop_monitor_checks_total", "counter", "Total number of checks performed.")
writeHelp(&b, "upkeep_monitor_checks_up_total", "counter", "Total number of successful checks.") writeHelp(&b, "uptop_monitor_checks_up_total", "counter", "Total number of successful checks.")
for _, s := range sites { for _, s := range sites {
h, ok := eng.GetHistory(s.ID) h, ok := eng.GetHistory(s.ID)
if !ok { if !ok {
continue continue
} }
writeGauge(&b, "upkeep_monitor_checks_total", labels(s), float64(h.TotalChecks)) writeGauge(&b, "uptop_monitor_checks_total", labels(s), float64(h.TotalChecks))
writeGauge(&b, "upkeep_monitor_checks_up_total", labels(s), float64(h.UpChecks)) writeGauge(&b, "uptop_monitor_checks_up_total", labels(s), float64(h.UpChecks))
} }
writeHelp(&b, "upkeep_probe_up", "gauge", "Whether a probe node is online (1) or offline (0) based on last-seen time.") writeHelp(&b, "uptop_probe_up", "gauge", "Whether a probe node is online (1) or offline (0) based on last-seen time.")
for _, site := range sites { for _, site := range sites {
probeResults := eng.GetProbeResults(site.ID) probeResults := eng.GetProbeResults(site.ID)
for nodeID, result := range probeResults { for nodeID, result := range probeResults {
@@ -92,7 +92,7 @@ func Handler(eng *monitor.Engine) http.HandlerFunc {
val = 1 val = 1
} }
nodeLabels := fmt.Sprintf(`id="%d",name="%s",node="%s"`, site.ID, escapeLabelValue(site.Name), escapeLabelValue(nodeID)) nodeLabels := fmt.Sprintf(`id="%d",name="%s",node="%s"`, site.ID, escapeLabelValue(site.Name), escapeLabelValue(nodeID))
writeGauge(&b, "upkeep_probe_up", nodeLabels, float64(val)) writeGauge(&b, "uptop_probe_up", nodeLabels, float64(val))
} }
} }
+9 -9
View File
@@ -2,8 +2,8 @@ package metrics
import ( import (
"context" "context"
"go-upkeep/internal/models" "gitea.lerkolabs.com/lerko/uptop/internal/models"
"go-upkeep/internal/monitor" "gitea.lerkolabs.com/lerko/uptop/internal/monitor"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
@@ -94,13 +94,13 @@ func TestMetricsHandler(t *testing.T) {
} }
expected := []string{ expected := []string{
"# HELP upkeep_monitor_up", "# HELP uptop_monitor_up",
"# TYPE upkeep_monitor_up gauge", "# TYPE uptop_monitor_up gauge",
`upkeep_monitor_up{id="1",name="Example",type="http"}`, `uptop_monitor_up{id="1",name="Example",type="http"}`,
`upkeep_monitor_up{id="2",name="DNS Check",type="dns"}`, `uptop_monitor_up{id="2",name="DNS Check",type="dns"}`,
"# HELP upkeep_monitor_latency_seconds", "# HELP uptop_monitor_latency_seconds",
"# HELP upkeep_monitor_paused", "# HELP uptop_monitor_paused",
"# HELP upkeep_monitor_checks_total", "# HELP uptop_monitor_checks_total",
} }
for _, s := range expected { for _, s := range expected {
if !strings.Contains(body, s) { if !strings.Contains(body, s) {
+1 -1
View File
@@ -2,7 +2,7 @@ package monitor
import ( import (
"context" "context"
"go-upkeep/internal/models" "gitea.lerkolabs.com/lerko/uptop/internal/models"
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
+1 -1
View File
@@ -2,7 +2,7 @@ package monitor
import ( import (
"crypto/tls" "crypto/tls"
"go-upkeep/internal/models" "gitea.lerkolabs.com/lerko/uptop/internal/models"
"net" "net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
+3 -3
View File
@@ -4,9 +4,9 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"go-upkeep/internal/alert" "gitea.lerkolabs.com/lerko/uptop/internal/alert"
"go-upkeep/internal/models" "gitea.lerkolabs.com/lerko/uptop/internal/models"
"go-upkeep/internal/store" "gitea.lerkolabs.com/lerko/uptop/internal/store"
"math/rand/v2" "math/rand/v2"
"net/http" "net/http"
"sync" "sync"
+1 -1
View File
@@ -2,7 +2,7 @@ package monitor
import ( import (
"fmt" "fmt"
"go-upkeep/internal/models" "gitea.lerkolabs.com/lerko/uptop/internal/models"
"sync" "sync"
"testing" "testing"
"time" "time"
+8 -8
View File
@@ -4,11 +4,11 @@ import (
"crypto/subtle" "crypto/subtle"
"encoding/json" "encoding/json"
"fmt" "fmt"
"go-upkeep/internal/importer" "gitea.lerkolabs.com/lerko/uptop/internal/importer"
"go-upkeep/internal/metrics" "gitea.lerkolabs.com/lerko/uptop/internal/metrics"
"go-upkeep/internal/models" "gitea.lerkolabs.com/lerko/uptop/internal/models"
"go-upkeep/internal/monitor" "gitea.lerkolabs.com/lerko/uptop/internal/monitor"
"go-upkeep/internal/store" "gitea.lerkolabs.com/lerko/uptop/internal/store"
"html/template" "html/template"
"log" "log"
"net/http" "net/http"
@@ -59,7 +59,7 @@ var statusTpl = template.Must(template.New("status").Parse(`
<div id="summary" class="summary"></div> <div id="summary" class="summary"></div>
<div id="stale" class="stale-bar"></div> <div id="stale" class="stale-bar"></div>
<div id="cards"></div> <div id="cards"></div>
<div style="text-align: center; margin-top: 40px; color: #565f89; font-size: 0.8em;">Powered by Go-Upkeep</div> <div style="text-align: center; margin-top: 40px; color: #565f89; font-size: 0.8em;">Powered by uptop</div>
</div> </div>
<script> <script>
var lastUpdate = null; var lastUpdate = null;
@@ -161,7 +161,7 @@ type ServerConfig struct {
func Start(cfg ServerConfig, s store.Store, eng *monitor.Engine) *http.Server { func Start(cfg ServerConfig, s store.Store, eng *monitor.Engine) *http.Server {
if cfg.ClusterKey == "" { if cfg.ClusterKey == "" {
fmt.Println("WARNING: No UPKEEP_CLUSTER_SECRET set. Cluster API endpoints are unauthenticated.") fmt.Println("WARNING: No UPTOP_CLUSTER_SECRET set. Cluster API endpoints are unauthenticated.")
} }
mux := http.NewServeMux() mux := http.NewServeMux()
@@ -193,7 +193,7 @@ func Start(cfg ServerConfig, s store.Store, eng *monitor.Engine) *http.Server {
// 3. Config Export // 3. Config Export
mux.HandleFunc("/api/backup/export", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/api/backup/export", func(w http.ResponseWriter, r *http.Request) {
if cfg.ClusterKey == "" || !checkSecret(r.Header.Get("X-Upkeep-Secret"), cfg.ClusterKey) { if cfg.ClusterKey == "" || !checkSecret(r.Header.Get("X-Upkeep-Secret"), cfg.ClusterKey) {
http.Error(w, "Unauthorized: UPKEEP_CLUSTER_SECRET required", http.StatusUnauthorized) http.Error(w, "Unauthorized: UPTOP_CLUSTER_SECRET required", http.StatusUnauthorized)
return return
} }
data, err := s.ExportData() data, err := s.ExportData()
+2 -2
View File
@@ -4,8 +4,8 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"go-upkeep/internal/models" "gitea.lerkolabs.com/lerko/uptop/internal/models"
"go-upkeep/internal/monitor" "gitea.lerkolabs.com/lerko/uptop/internal/monitor"
"net" "net"
"net/http" "net/http"
"sync" "sync"
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"go-upkeep/internal/models" "gitea.lerkolabs.com/lerko/uptop/internal/models"
"log" "log"
"time" "time"
) )
+1 -1
View File
@@ -1,7 +1,7 @@
package store package store
import ( import (
"go-upkeep/internal/models" "gitea.lerkolabs.com/lerko/uptop/internal/models"
"testing" "testing"
) )
+1 -1
View File
@@ -1,7 +1,7 @@
package store package store
import ( import (
"go-upkeep/internal/models" "gitea.lerkolabs.com/lerko/uptop/internal/models"
) )
type Store interface { type Store interface {
+1 -1
View File
@@ -2,7 +2,7 @@ package tui
import ( import (
"fmt" "fmt"
"go-upkeep/internal/models" "gitea.lerkolabs.com/lerko/uptop/internal/models"
"strconv" "strconv"
"time" "time"
+3 -2
View File
@@ -2,12 +2,13 @@ package tui
import ( import (
"fmt" "fmt"
"go-upkeep/internal/models"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"gitea.lerkolabs.com/lerko/uptop/internal/models"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh" "github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
@@ -340,7 +341,7 @@ func (m Model) viewSitesTab() string {
BorderForeground(m.theme.Accent). BorderForeground(m.theme.Accent).
Padding(1, 3). Padding(1, 3).
Render( Render(
titleStyle.Render("Go-Upkeep") + "\n\n" + titleStyle.Render("uptop") + "\n\n" +
"No monitors configured yet.\n\n" + "No monitors configured yet.\n\n" +
subtleStyle.Render("[n] Add your first monitor"), subtleStyle.Render("[n] Add your first monitor"),
) )
+3 -3
View File
@@ -3,9 +3,9 @@ package tui
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"go-upkeep/internal/models" "gitea.lerkolabs.com/lerko/uptop/internal/models"
"go-upkeep/internal/monitor" "gitea.lerkolabs.com/lerko/uptop/internal/monitor"
"go-upkeep/internal/store" "gitea.lerkolabs.com/lerko/uptop/internal/store"
"math" "math"
"sort" "sort"
"strings" "strings"