1 Commits

Author SHA1 Message Date
lerko d1bd66deb3 docs: add install instructions and Kuma migration guide to README
CI / test (pull_request) Successful in 4m37s
CI / lint (pull_request) Successful in 1m12s
2026-05-24 14:16:06 -04:00
42 changed files with 260 additions and 487 deletions
-6
View File
@@ -18,12 +18,6 @@ jobs:
with: with:
go-version: "1.24" go-version: "1.24"
- uses: actions/cache@v4
with:
path: ~/.cache/go-build
key: go-build-${{ hashFiles('**/*.go', 'go.sum') }}
restore-keys: go-build-
- name: Install build tools - name: Install build tools
run: apk add --no-cache gcc musl-dev run: apk add --no-cache gcc musl-dev
+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 }}/uptop images: ${{ secrets.DOCKERHUB_USERNAME }}/go-upkeep
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}}
+5 -2
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
/uptop /goupkeep
uptop.db upkeep.db
.ssh .ssh
@@ -35,5 +35,8 @@ 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/uptop/main.go -demo # starts with sample data go run cmd/goupkeep/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 uptop ./cmd/uptop/main.go 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
# --- 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/uptop . COPY --from=builder /app/go-upkeep .
# 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 UPTOP_DB_TYPE=sqlite ENV UPKEEP_DB_TYPE=sqlite
ENV UPTOP_DB_DSN=/data/uptop.db ENV UPKEEP_DB_DSN=/data/upkeep.db
ENV UPTOP_KEYS=/data/authorized_keys ENV UPKEEP_KEYS=/data/authorized_keys
ENV UPTOP_PORT=23234 ENV UPKEEP_PORT=23234
EXPOSE 23234 EXPOSE 23234
CMD ["./uptop"] CMD ["./go-upkeep"]
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2026 Roman Dvořák Copyright (c) 2024 Roman Dvořák
Copyright (c) 2026 Tyler Koenig Copyright (c) 2026 Tyler Koenig
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
+26 -26
View File
@@ -1,4 +1,4 @@
# uptop # Go-Upkeep
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/uptop/main.go go run cmd/goupkeep/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/uptop/main.go -demo go run cmd/goupkeep/main.go -demo
``` ```
## Install ## Install
@@ -33,34 +33,34 @@ go run cmd/uptop/main.go -demo
### From source ### From source
```bash ```bash
go install gitea.lerkolabs.com/lerko/uptop/cmd/uptop@latest go install gitea.lerkolabs.com/lerko/uptime/cmd/goupkeep@latest
``` ```
### Docker ### Docker
```bash ```bash
docker pull lerko/uptop:latest docker pull lerko/go-upkeep:latest
docker run -p 23234:23234 -p 8080:8080 -v ./data:/data lerko/uptop docker run -p 23234:23234 -p 8080:8080 -v ./data:/data lerko/go-upkeep
``` ```
### Binary ### Binary
Download from [Releases](https://gitea.lerkolabs.com/lerko/uptop/releases). Download from [Releases](https://gitea.lerkolabs.com/lerko/uptime/releases).
## Config as code ## Config as code
Export your current monitors: Export your current monitors:
```bash ```bash
uptop export -o monitors.yaml goupkeep export -o monitors.yaml
``` ```
Apply a config file: Apply a config file:
```bash ```bash
uptop apply -f monitors.yaml goupkeep apply -f monitors.yaml
uptop apply -f monitors.yaml --dry-run # see what would change goupkeep apply -f monitors.yaml --dry-run # see what would change
uptop apply -f monitors.yaml --prune # delete anything not in the YAML goupkeep 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:
- UPTOP_DB_TYPE=sqlite - UPKEEP_DB_TYPE=sqlite
- UPTOP_DB_DSN=/data/uptop.db - UPKEEP_DB_DSN=/data/upkeep.db
- UPTOP_STATUS_ENABLED=true - UPKEEP_STATUS_ENABLED=true
- UPTOP_CLUSTER_SECRET=change-me - UPKEEP_CLUSTER_SECRET=change-me
``` ```
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. 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.
## Environment variables ## Environment variables
| Variable | Default | What it does | | Variable | Default | What it does |
|---|---|---| |---|---|---|
| `UPTOP_PORT` | `23234` | SSH server port | | `UPKEEP_PORT` | `23234` | SSH server port |
| `UPTOP_HTTP_PORT` | `8080` | HTTP server port (status page, push, metrics) | | `UPKEEP_HTTP_PORT` | `8080` | HTTP server port (status page, push, metrics) |
| `UPTOP_DB_TYPE` | `sqlite` | `sqlite` or `postgres` | | `UPKEEP_DB_TYPE` | `sqlite` | `sqlite` or `postgres` |
| `UPTOP_DB_DSN` | `uptop.db` | Database path or connection string | | `UPKEEP_DB_DSN` | `upkeep.db` | Database path or connection string |
| `UPTOP_STATUS_ENABLED` | `false` | Enable public status page | | `UPKEEP_STATUS_ENABLED` | `false` | Enable public status page |
| `UPTOP_STATUS_TITLE` | `System Status` | Status page title | | `UPKEEP_STATUS_TITLE` | `System Status` | Status page title |
| `UPTOP_CLUSTER_MODE` | `leader` | `leader` or `follower` | | `UPKEEP_CLUSTER_MODE` | `leader` | `leader` or `follower` |
| `UPTOP_PEER_URL` | | Leader URL for follower nodes | | `UPKEEP_PEER_URL` | | Leader URL for follower nodes |
| `UPTOP_CLUSTER_SECRET` | | Shared key for cluster + API auth | | `UPKEEP_CLUSTER_SECRET` | | Shared key for cluster + API auth |
| `UPTOP_INSECURE_SKIP_VERIFY` | `false` | Skip TLS verification for checks | | `UPKEEP_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"
"gitea.lerkolabs.com/lerko/uptop/internal/cluster" "go-upkeep/internal/cluster"
"gitea.lerkolabs.com/lerko/uptop/internal/config" "go-upkeep/internal/config"
"gitea.lerkolabs.com/lerko/uptop/internal/importer" "go-upkeep/internal/importer"
"gitea.lerkolabs.com/lerko/uptop/internal/models" "go-upkeep/internal/models"
"gitea.lerkolabs.com/lerko/uptop/internal/monitor" "go-upkeep/internal/monitor"
"gitea.lerkolabs.com/lerko/uptop/internal/server" "go-upkeep/internal/server"
"gitea.lerkolabs.com/lerko/uptop/internal/store" "go-upkeep/internal/store"
"gitea.lerkolabs.com/lerko/uptop/internal/tui" "go-upkeep/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("uptop dev") fmt.Println("go-upkeep dev")
} else { } else {
fmt.Printf("uptop %s (%s, %s)\n", version, commit, date) fmt.Printf("go-upkeep %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("UPTOP_DB_TYPE", "sqlite"), "Database type") dbType := fs.String("db-type", envOrDefault("UPKEEP_DB_TYPE", "sqlite"), "Database type")
dsn := fs.String("dsn", envOrDefault("UPTOP_DB_DSN", "uptop.db"), "Database DSN") dsn := fs.String("dsn", envOrDefault("UPKEEP_DB_DSN", "upkeep.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("UPTOP_DB_TYPE", "sqlite"), "Database type") dbType := fs.String("db-type", envOrDefault("UPKEEP_DB_TYPE", "sqlite"), "Database type")
dsn := fs.String("dsn", envOrDefault("UPTOP_DB_DSN", "uptop.db"), "Database DSN") dsn := fs.String("dsn", envOrDefault("UPKEEP_DB_DSN", "upkeep.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 := "uptop.db" dbDSN := "upkeep.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("UPTOP_PORT"); v != "" { if v := os.Getenv("UPKEEP_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("UPTOP_DB_TYPE"); v != "" { if v := os.Getenv("UPKEEP_DB_TYPE"); v != "" {
dbType = v dbType = v
} }
if v := os.Getenv("UPTOP_DB_DSN"); v != "" { if v := os.Getenv("UPKEEP_DB_DSN"); v != "" {
dbDSN = v dbDSN = v
} }
if v := os.Getenv("UPTOP_HTTP_PORT"); v != "" { if v := os.Getenv("UPKEEP_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("UPTOP_STATUS_ENABLED"); v == "true" { if v := os.Getenv("UPKEEP_STATUS_ENABLED"); v == "true" {
enableStatus = true enableStatus = true
} }
if v := os.Getenv("UPTOP_STATUS_TITLE"); v != "" { if v := os.Getenv("UPKEEP_STATUS_TITLE"); v != "" {
statusTitle = v statusTitle = v
} }
if v := os.Getenv("UPTOP_CLUSTER_MODE"); v != "" { if v := os.Getenv("UPKEEP_CLUSTER_MODE"); v != "" {
clusterMode = v clusterMode = v
} }
if v := os.Getenv("UPTOP_PEER_URL"); v != "" { if v := os.Getenv("UPKEEP_PEER_URL"); v != "" {
clusterPeer = v clusterPeer = v
} }
if v := os.Getenv("UPTOP_CLUSTER_SECRET"); v != "" { if v := os.Getenv("UPKEEP_CLUSTER_SECRET"); v != "" {
clusterKey = v clusterKey = v
} }
nodeID := os.Getenv("UPTOP_NODE_ID") nodeID := os.Getenv("UPKEEP_NODE_ID")
nodeName := os.Getenv("UPTOP_NODE_NAME") nodeName := os.Getenv("UPKEEP_NODE_NAME")
nodeRegion := os.Getenv("UPTOP_NODE_REGION") nodeRegion := os.Getenv("UPKEEP_NODE_REGION")
aggStrategy := os.Getenv("UPTOP_AGG_STRATEGY") aggStrategy := os.Getenv("UPKEEP_AGG_STRATEGY")
if clusterMode == "probe" { if clusterMode == "probe" {
if nodeID == "" { if nodeID == "" {
fmt.Fprintln(os.Stderr, "UPTOP_NODE_ID is required for probe mode") fmt.Fprintln(os.Stderr, "UPKEEP_NODE_ID is required for probe mode")
os.Exit(1) os.Exit(1)
} }
if clusterPeer == "" { if clusterPeer == "" {
fmt.Fprintln(os.Stderr, "UPTOP_PEER_URL is required for probe mode") fmt.Fprintln(os.Stderr, "UPKEEP_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("UPTOP_INSECURE_SKIP_VERIFY") == "true" { if os.Getenv("UPKEEP_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("uptop running in HEADLESS mode") fmt.Println("Go-Upkeep 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: uptop-leader container_name: upkeep-leader
ports: ports:
- "23234:23234" # SSH - "23234:23234" # SSH
- "8080:8080" # HTTP - "8080:8080" # HTTP
environment: environment:
- UPTOP_DB_TYPE=postgres - UPKEEP_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
- UPTOP_DB_DSN=postgres://devuser:devpass@leader-db:5432/uptop_dev?sslmode=disable - UPKEEP_DB_DSN=postgres://devuser:devpass@leader-db:5432/upkeep_dev?sslmode=disable
- UPTOP_HTTP_PORT=8080 - UPKEEP_HTTP_PORT=8080
- UPTOP_STATUS_ENABLED=true - UPKEEP_STATUS_ENABLED=true
- UPTOP_STATUS_TITLE=Leader Node - UPKEEP_STATUS_TITLE=Leader Node
# Cluster Config # Cluster Config
- UPTOP_CLUSTER_MODE=leader - UPKEEP_CLUSTER_MODE=leader
- UPTOP_CLUSTER_SECRET=mysecret - UPKEEP_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: uptop-leader-db container_name: upkeep-leader-db
environment: environment:
POSTGRES_USER: devuser POSTGRES_USER: devuser
POSTGRES_PASSWORD: devpass POSTGRES_PASSWORD: devpass
POSTGRES_DB: uptop_dev POSTGRES_DB: upkeep_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: uptop-follower container_name: upkeep-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:
- UPTOP_DB_TYPE=postgres - UPKEEP_DB_TYPE=postgres
# Connects to its OWN database # Connects to its OWN database
- UPTOP_DB_DSN=postgres://devuser:devpass@follower-db:5432/uptop_dev?sslmode=disable - UPKEEP_DB_DSN=postgres://devuser:devpass@follower-db:5432/upkeep_dev?sslmode=disable
- UPTOP_HTTP_PORT=8080 - UPKEEP_HTTP_PORT=8080
- UPTOP_STATUS_ENABLED=true - UPKEEP_STATUS_ENABLED=true
- UPTOP_STATUS_TITLE=Follower Node - UPKEEP_STATUS_TITLE=Follower Node
# Cluster Config # Cluster Config
- UPTOP_CLUSTER_MODE=follower - UPKEEP_CLUSTER_MODE=follower
- UPTOP_CLUSTER_SECRET=mysecret - UPKEEP_CLUSTER_SECRET=mysecret
# IMPORTANT: Uses the Service Name "leader" to connect internally # IMPORTANT: Uses the Service Name "leader" to connect internally
- UPTOP_PEER_URL=http://leader:8080 - UPKEEP_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: uptop-follower-db container_name: upkeep-follower-db
environment: environment:
POSTGRES_USER: devuser POSTGRES_USER: devuser
POSTGRES_PASSWORD: devpass POSTGRES_PASSWORD: devpass
POSTGRES_DB: uptop_dev POSTGRES_DB: upkeep_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: uptop-dev container_name: upkeep-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) ---
- UPTOP_DB_TYPE=postgres - UPKEEP_DB_TYPE=postgres
- UPTOP_DB_DSN=postgres://devuser:devpass@postgres:5432/uptop_dev?sslmode=disable - UPKEEP_DB_DSN=postgres://devuser:devpass@postgres:5432/upkeep_dev?sslmode=disable
# --- Web Server Configuration (Phase 4) --- # --- Web Server Configuration (Phase 4) ---
- UPTOP_HTTP_PORT=8080 - UPKEEP_HTTP_PORT=8080
- UPTOP_STATUS_ENABLED=true - UPKEEP_STATUS_ENABLED=true
- UPTOP_STATUS_TITLE=Dev Infrastructure Status - UPKEEP_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: uptop-postgres container_name: upkeep-postgres
environment: environment:
POSTGRES_USER: devuser POSTGRES_USER: devuser
POSTGRES_PASSWORD: devpass POSTGRES_PASSWORD: devpass
POSTGRES_DB: uptop_dev POSTGRES_DB: upkeep_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:
- UPTOP_CLUSTER_MODE=leader - UPKEEP_CLUSTER_MODE=leader
- UPTOP_CLUSTER_SECRET=changeme - UPKEEP_CLUSTER_SECRET=changeme
- UPTOP_AGG_STRATEGY=any-down - UPKEEP_AGG_STRATEGY=any-down
- UPTOP_STATUS_ENABLED=true - UPKEEP_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:
- UPTOP_CLUSTER_MODE=probe - UPKEEP_CLUSTER_MODE=probe
- UPTOP_NODE_ID=us-east-1 - UPKEEP_NODE_ID=us-east-1
- UPTOP_NODE_NAME=US East Probe - UPKEEP_NODE_NAME=US East Probe
- UPTOP_NODE_REGION=us-east - UPKEEP_NODE_REGION=us-east
- UPTOP_PEER_URL=http://leader:8080 - UPKEEP_PEER_URL=http://leader:8080
- UPTOP_CLUSTER_SECRET=changeme - UPKEEP_CLUSTER_SECRET=changeme
depends_on: depends_on:
- leader - leader
probe-eu-west: probe-eu-west:
build: . build: .
environment: environment:
- UPTOP_CLUSTER_MODE=probe - UPKEEP_CLUSTER_MODE=probe
- UPTOP_NODE_ID=eu-west-1 - UPKEEP_NODE_ID=eu-west-1
- UPTOP_NODE_NAME=EU West Probe - UPKEEP_NODE_NAME=EU West Probe
- UPTOP_NODE_REGION=eu-west - UPKEEP_NODE_REGION=eu-west
- UPTOP_PEER_URL=http://leader:8080 - UPKEEP_PEER_URL=http://leader:8080
- UPTOP_CLUSTER_SECRET=changeme - UPKEEP_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: uptop container_name: upkeep
restart: unless-stopped restart: unless-stopped
ports: ports:
- "23234:23234" - "23234:23234"
- "8080:8080" - "8080:8080"
environment: environment:
- UPTOP_DB_TYPE=sqlite - UPKEEP_DB_TYPE=sqlite
- UPTOP_DB_DSN=/data/uptop.db - UPKEEP_DB_DSN=/data/upkeep.db
- UPTOP_HTTP_PORT=8080 - UPKEEP_HTTP_PORT=8080
- UPTOP_STATUS_ENABLED=true - UPKEEP_STATUS_ENABLED=true
- UPTOP_STATUS_TITLE=System Status - UPKEEP_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
uptop export -o monitors.yaml goupkeep 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
uptop apply -f monitors.yaml goupkeep 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
uptop export -o monitors.yaml # to a file goupkeep export -o monitors.yaml # to a file
uptop export # to stdout goupkeep export # to stdout
``` ```
**Apply a config:** **Apply a config:**
```bash ```bash
uptop apply -f monitors.yaml goupkeep apply -f monitors.yaml
``` ```
**See what would change first:** **See what would change first:**
```bash ```bash
uptop apply -f monitors.yaml --dry-run goupkeep apply -f monitors.yaml --dry-run
``` ```
**Delete monitors not in the YAML:** **Delete monitors not in the YAML:**
```bash ```bash
uptop apply -f monitors.yaml --prune goupkeep 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
uptop export -db-type postgres -dsn "host=localhost dbname=uptop sslmode=disable" goupkeep export -db-type postgres -dsn "host=localhost dbname=upkeep sslmode=disable"
uptop apply -f monitors.yaml -db-type postgres -dsn "..." goupkeep apply -f monitors.yaml -db-type postgres -dsn "..."
``` ```
Both commands respect the `UPTOP_DB_TYPE` and `UPTOP_DB_DSN` environment variables too. Both commands respect the `UPKEEP_DB_TYPE` and `UPKEEP_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
uptop export -o monitors.yaml goupkeep 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 uptop apply -f monitors.yaml ssh prod-server goupkeep 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
uptop apply -f monitors.yaml goupkeep apply -f monitors.yaml
``` ```
+1 -1
View File
@@ -1,4 +1,4 @@
module gitea.lerkolabs.com/lerko/uptop module go-upkeep
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"
"gitea.lerkolabs.com/lerko/uptop/internal/models" "go-upkeep/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": "uptop", "source": "go-upkeep",
"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: uptop: " + title + "\r\n" + "Subject: Go-Upkeep: " + 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"
"gitea.lerkolabs.com/lerko/uptop/internal/models" "go-upkeep/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"
"gitea.lerkolabs.com/lerko/uptop/internal/monitor" "go-upkeep/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"
"gitea.lerkolabs.com/lerko/uptop/internal/models" "go-upkeep/internal/models"
"gitea.lerkolabs.com/lerko/uptop/internal/monitor" "go-upkeep/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"
"gitea.lerkolabs.com/lerko/uptop/internal/models" "go-upkeep/internal/models"
"gitea.lerkolabs.com/lerko/uptop/internal/monitor" "go-upkeep/internal/monitor"
"log" "log"
"net/http" "net/http"
"sync" "sync"
+2 -2
View File
@@ -2,8 +2,8 @@ package config
import ( import (
"fmt" "fmt"
"gitea.lerkolabs.com/lerko/uptop/internal/models" "go-upkeep/internal/models"
"gitea.lerkolabs.com/lerko/uptop/internal/store" "go-upkeep/internal/store"
"reflect" "reflect"
"strings" "strings"
) )
+2 -2
View File
@@ -1,8 +1,8 @@
package config package config
import ( import (
"gitea.lerkolabs.com/lerko/uptop/internal/models" "go-upkeep/internal/models"
"gitea.lerkolabs.com/lerko/uptop/internal/store" "go-upkeep/internal/store"
"strings" "strings"
"testing" "testing"
) )
+2 -2
View File
@@ -2,8 +2,8 @@ package config
import ( import (
"fmt" "fmt"
"gitea.lerkolabs.com/lerko/uptop/internal/models" "go-upkeep/internal/models"
"gitea.lerkolabs.com/lerko/uptop/internal/store" "go-upkeep/internal/store"
"os" "os"
"sort" "sort"
+1 -1
View File
@@ -1,7 +1,7 @@
package config package config
import ( import (
"gitea.lerkolabs.com/lerko/uptop/internal/models" "go-upkeep/internal/models"
"testing" "testing"
) )
+1 -1
View File
@@ -3,7 +3,7 @@ package importer
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"gitea.lerkolabs.com/lerko/uptop/internal/models" "go-upkeep/internal/models"
"os" "os"
"strings" "strings"
) )
+22 -22
View File
@@ -2,8 +2,8 @@ package metrics
import ( import (
"fmt" "fmt"
"gitea.lerkolabs.com/lerko/uptop/internal/models" "go-upkeep/internal/models"
"gitea.lerkolabs.com/lerko/uptop/internal/monitor" "go-upkeep/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, "uptop_monitor_up", "gauge", "Whether the monitor is up (1) or down (0).") writeHelp(&b, "upkeep_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, "uptop_monitor_up", labels(s), float64(val)) writeGauge(&b, "upkeep_monitor_up", labels(s), float64(val))
} }
writeHelp(&b, "uptop_monitor_latency_seconds", "gauge", "Last check latency in seconds.") writeHelp(&b, "upkeep_monitor_latency_seconds", "gauge", "Last check latency in seconds.")
for _, s := range sites { for _, s := range sites {
writeGauge(&b, "uptop_monitor_latency_seconds", labels(s), s.Latency.Seconds()) writeGauge(&b, "upkeep_monitor_latency_seconds", labels(s), s.Latency.Seconds())
} }
writeHelp(&b, "uptop_monitor_status_code", "gauge", "HTTP response status code of the last check.") writeHelp(&b, "upkeep_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, "uptop_monitor_status_code", labels(s), float64(s.StatusCode)) writeGauge(&b, "upkeep_monitor_status_code", labels(s), float64(s.StatusCode))
} }
writeHelp(&b, "uptop_monitor_check_timestamp_seconds", "gauge", "Unix timestamp of the last check.") writeHelp(&b, "upkeep_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, "uptop_monitor_check_timestamp_seconds", labels(s), float64(s.LastCheck.Unix())) writeGauge(&b, "upkeep_monitor_check_timestamp_seconds", labels(s), float64(s.LastCheck.Unix()))
} }
writeHelp(&b, "uptop_monitor_paused", "gauge", "Whether the monitor is paused (1) or active (0).") writeHelp(&b, "upkeep_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, "uptop_monitor_paused", labels(s), float64(val)) writeGauge(&b, "upkeep_monitor_paused", labels(s), float64(val))
} }
writeHelp(&b, "uptop_monitor_maintenance", "gauge", "Whether the monitor is in a maintenance window (1) or not (0).") writeHelp(&b, "upkeep_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, "uptop_monitor_maintenance", labels(s), float64(val)) writeGauge(&b, "upkeep_monitor_maintenance", labels(s), float64(val))
} }
writeHelp(&b, "uptop_monitor_cert_expiry_timestamp_seconds", "gauge", "Unix timestamp when the SSL certificate expires.") writeHelp(&b, "upkeep_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, "uptop_monitor_cert_expiry_timestamp_seconds", labels(s), float64(s.CertExpiry.Unix())) writeGauge(&b, "upkeep_monitor_cert_expiry_timestamp_seconds", labels(s), float64(s.CertExpiry.Unix()))
} }
writeHelp(&b, "uptop_monitor_checks_total", "counter", "Total number of checks performed.") writeHelp(&b, "upkeep_monitor_checks_total", "counter", "Total number of checks performed.")
writeHelp(&b, "uptop_monitor_checks_up_total", "counter", "Total number of successful checks.") writeHelp(&b, "upkeep_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, "uptop_monitor_checks_total", labels(s), float64(h.TotalChecks)) writeGauge(&b, "upkeep_monitor_checks_total", labels(s), float64(h.TotalChecks))
writeGauge(&b, "uptop_monitor_checks_up_total", labels(s), float64(h.UpChecks)) writeGauge(&b, "upkeep_monitor_checks_up_total", labels(s), float64(h.UpChecks))
} }
writeHelp(&b, "uptop_probe_up", "gauge", "Whether a probe node is online (1) or offline (0) based on last-seen time.") writeHelp(&b, "upkeep_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, "uptop_probe_up", nodeLabels, float64(val)) writeGauge(&b, "upkeep_probe_up", nodeLabels, float64(val))
} }
} }
+9 -9
View File
@@ -2,8 +2,8 @@ package metrics
import ( import (
"context" "context"
"gitea.lerkolabs.com/lerko/uptop/internal/models" "go-upkeep/internal/models"
"gitea.lerkolabs.com/lerko/uptop/internal/monitor" "go-upkeep/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 uptop_monitor_up", "# HELP upkeep_monitor_up",
"# TYPE uptop_monitor_up gauge", "# TYPE upkeep_monitor_up gauge",
`uptop_monitor_up{id="1",name="Example",type="http"}`, `upkeep_monitor_up{id="1",name="Example",type="http"}`,
`uptop_monitor_up{id="2",name="DNS Check",type="dns"}`, `upkeep_monitor_up{id="2",name="DNS Check",type="dns"}`,
"# HELP uptop_monitor_latency_seconds", "# HELP upkeep_monitor_latency_seconds",
"# HELP uptop_monitor_paused", "# HELP upkeep_monitor_paused",
"# HELP uptop_monitor_checks_total", "# HELP upkeep_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"
"gitea.lerkolabs.com/lerko/uptop/internal/models" "go-upkeep/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"
"gitea.lerkolabs.com/lerko/uptop/internal/models" "go-upkeep/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"
"gitea.lerkolabs.com/lerko/uptop/internal/alert" "go-upkeep/internal/alert"
"gitea.lerkolabs.com/lerko/uptop/internal/models" "go-upkeep/internal/models"
"gitea.lerkolabs.com/lerko/uptop/internal/store" "go-upkeep/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"
"gitea.lerkolabs.com/lerko/uptop/internal/models" "go-upkeep/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"
"gitea.lerkolabs.com/lerko/uptop/internal/importer" "go-upkeep/internal/importer"
"gitea.lerkolabs.com/lerko/uptop/internal/metrics" "go-upkeep/internal/metrics"
"gitea.lerkolabs.com/lerko/uptop/internal/models" "go-upkeep/internal/models"
"gitea.lerkolabs.com/lerko/uptop/internal/monitor" "go-upkeep/internal/monitor"
"gitea.lerkolabs.com/lerko/uptop/internal/store" "go-upkeep/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 uptop</div> <div style="text-align: center; margin-top: 40px; color: #565f89; font-size: 0.8em;">Powered by Go-Upkeep</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 UPTOP_CLUSTER_SECRET set. Cluster API endpoints are unauthenticated.") fmt.Println("WARNING: No UPKEEP_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: UPTOP_CLUSTER_SECRET required", http.StatusUnauthorized) http.Error(w, "Unauthorized: UPKEEP_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"
"gitea.lerkolabs.com/lerko/uptop/internal/models" "go-upkeep/internal/models"
"gitea.lerkolabs.com/lerko/uptop/internal/monitor" "go-upkeep/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"
"gitea.lerkolabs.com/lerko/uptop/internal/models" "go-upkeep/internal/models"
"log" "log"
"time" "time"
) )
+1 -1
View File
@@ -1,7 +1,7 @@
package store package store
import ( import (
"gitea.lerkolabs.com/lerko/uptop/internal/models" "go-upkeep/internal/models"
"testing" "testing"
) )
+1 -1
View File
@@ -1,7 +1,7 @@
package store package store
import ( import (
"gitea.lerkolabs.com/lerko/uptop/internal/models" "go-upkeep/internal/models"
) )
type Store interface { type Store interface {
+1 -1
View File
@@ -319,7 +319,7 @@ func (m *Model) initAlertHuhForm() tea.Cmd {
).Title("Gotify Settings").WithHideFunc(func() bool { ).Title("Gotify Settings").WithHideFunc(func() bool {
return m.alertFormData.AlertType != "gotify" return m.alertFormData.AlertType != "gotify"
}), }),
).WithTheme(m.theme.HuhTheme()) ).WithTheme(huh.ThemeDracula())
return m.huhForm.Init() return m.huhForm.Init()
} }
+3 -3
View File
@@ -2,7 +2,7 @@ package tui
import ( import (
"fmt" "fmt"
"gitea.lerkolabs.com/lerko/uptop/internal/models" "go-upkeep/internal/models"
"strconv" "strconv"
"time" "time"
@@ -11,7 +11,7 @@ import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
) )
var maintStyle lipgloss.Style var maintStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#bb9af7"))
type maintFormData struct { type maintFormData struct {
Title string Title string
@@ -187,7 +187,7 @@ func (m *Model) initMaintHuhForm() tea.Cmd {
).Title("Duration").WithHideFunc(func() bool { ).Title("Duration").WithHideFunc(func() bool {
return m.maintFormData.Type == "incident" return m.maintFormData.Type == "incident"
}), }),
).WithTheme(m.theme.HuhTheme()) ).WithTheme(huh.ThemeDracula())
return m.huhForm.Init() return m.huhForm.Init()
} }
+11 -18
View File
@@ -2,13 +2,12 @@ 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"
@@ -38,7 +37,10 @@ func typeIcon(siteType string, collapsed bool) string {
} }
} }
var siteGroupStyle lipgloss.Style var siteGroupStyle = lipgloss.NewStyle().
Padding(0, 1).
Bold(true).
Foreground(lipgloss.Color("#7D56F4"))
type siteFormData struct { type siteFormData struct {
Name string Name string
@@ -338,10 +340,10 @@ func (m Model) viewSitesTab() string {
if len(m.sites) == 0 { if len(m.sites) == 0 {
welcome := lipgloss.NewStyle(). welcome := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()). Border(lipgloss.RoundedBorder()).
BorderForeground(m.theme.Accent). BorderForeground(lipgloss.Color("#7D56F4")).
Padding(1, 3). Padding(1, 3).
Render( Render(
titleStyle.Render("uptop") + "\n\n" + titleStyle.Render("Go-Upkeep") + "\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"),
) )
@@ -507,7 +509,7 @@ func (m *Model) initSiteHuhForm() tea.Cmd {
Description("Required for HTTP monitors"). Description("Required for HTTP monitors").
Value(&m.siteFormData.URL). Value(&m.siteFormData.URL).
Validate(func(s string) error { Validate(func(s string) error {
if m.siteFormData.SiteType != "http" { if m.siteFormData.SiteType == "push" || m.siteFormData.SiteType == "group" {
return nil return nil
} }
if s == "" { if s == "" {
@@ -553,15 +555,12 @@ func (m *Model) initSiteHuhForm() tea.Cmd {
Description("Target port for TCP port monitors"). Description("Target port for TCP port monitors").
Value(&m.siteFormData.Port). Value(&m.siteFormData.Port).
Validate(func(s string) error { Validate(func(s string) error {
if m.siteFormData.SiteType != "port" {
return nil
}
v, err := strconv.Atoi(s) v, err := strconv.Atoi(s)
if err != nil { if err != nil {
return fmt.Errorf("must be a number") return fmt.Errorf("must be a number")
} }
if v < 1 || v > 65535 { if v < 0 || v > 65535 {
return fmt.Errorf("port must be 1-65535") return fmt.Errorf("port must be 0-65535")
} }
return nil return nil
}), }),
@@ -616,9 +615,6 @@ func (m *Model) initSiteHuhForm() tea.Cmd {
Placeholder("7"). Placeholder("7").
Value(&m.siteFormData.Threshold). Value(&m.siteFormData.Threshold).
Validate(func(s string) error { Validate(func(s string) error {
if !m.siteFormData.CheckSSL {
return nil
}
v, err := strconv.Atoi(s) v, err := strconv.Atoi(s)
if err != nil { if err != nil {
return fmt.Errorf("must be a number") return fmt.Errorf("must be a number")
@@ -632,9 +628,6 @@ func (m *Model) initSiteHuhForm() tea.Cmd {
Placeholder("0"). Placeholder("0").
Value(&m.siteFormData.Retries). Value(&m.siteFormData.Retries).
Validate(func(s string) error { Validate(func(s string) error {
if m.siteFormData.SiteType == "group" {
return nil
}
v, err := strconv.Atoi(s) v, err := strconv.Atoi(s)
if err != nil { if err != nil {
return fmt.Errorf("must be a number") return fmt.Errorf("must be a number")
@@ -649,7 +642,7 @@ func (m *Model) initSiteHuhForm() tea.Cmd {
).Title("Advanced").WithHideFunc(func() bool { ).Title("Advanced").WithHideFunc(func() bool {
return m.siteFormData.SiteType == "group" return m.siteFormData.SiteType == "group"
}), }),
).WithTheme(m.theme.HuhTheme()) ).WithTheme(huh.ThemeDracula())
return m.huhForm.Init() return m.huhForm.Init()
} }
+1 -1
View File
@@ -94,7 +94,7 @@ func (m *Model) initUserHuhForm() tea.Cmd {
huh.NewOption("Admin", "admin"), huh.NewOption("Admin", "admin"),
).Value(&m.userFormData.Role), ).Value(&m.userFormData.Role),
).Title("SSH Access"), ).Title("SSH Access"),
).WithTheme(m.theme.HuhTheme()) ).WithTheme(huh.ThemeDracula())
return m.huhForm.Init() return m.huhForm.Init()
} }
+19 -5
View File
@@ -6,11 +6,25 @@ import (
) )
var ( var (
tableHeaderStyle lipgloss.Style tableHeaderStyle = lipgloss.NewStyle().
tableCellStyle lipgloss.Style Foreground(lipgloss.Color("#7D56F4")).
tableSelectedStyle lipgloss.Style Bold(true).
tableBorderStyle lipgloss.Style Padding(0, 1)
tableZebraStyle lipgloss.Style
tableCellStyle = lipgloss.NewStyle().Padding(0, 1)
tableSelectedStyle = lipgloss.NewStyle().
Padding(0, 1).
Bold(true).
Foreground(lipgloss.Color("#ffffff")).
Background(lipgloss.Color("#3b3b5c"))
tableBorderStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#444"))
tableZebraStyle = lipgloss.NewStyle().
Padding(0, 1).
Background(lipgloss.Color("#1a1a2e"))
) )
type StyleOverride func(row, col int) *lipgloss.Style type StyleOverride func(row, col int) *lipgloss.Style
-191
View File
@@ -1,191 +0,0 @@
package tui
import (
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
)
type Theme struct {
Name string
// Base layers
Bg lipgloss.Color
Surface lipgloss.Color
Panel lipgloss.Color
Border lipgloss.Color
// Text
Fg lipgloss.Color
Muted lipgloss.Color
Subtle lipgloss.Color
// Semantic
Success lipgloss.Color
Warning lipgloss.Color
Danger lipgloss.Color
Info lipgloss.Color
Accent lipgloss.Color
Purple lipgloss.Color
// Table
ZebraBg lipgloss.Color
// Selection
SelectedFg lipgloss.Color
SelectedBg lipgloss.Color
}
var themes = []Theme{
themeFlexokiDark,
themeTokyoNight,
themeCatppuccinMocha,
themeNord,
themeGruvbox,
}
var themeFlexokiDark = Theme{
Name: "Flexoki Dark",
Bg: "#1C1B1A",
Surface: "#282726",
Panel: "#343331",
Border: "#575653",
Fg: "#CECDC3",
Muted: "#878580",
Subtle: "#6F6E69",
Success: "#879A39",
Warning: "#D0A215",
Danger: "#D14D41",
Info: "#4385BE",
Accent: "#3AA99F",
Purple: "#8B7EC8",
ZebraBg: "#222120",
SelectedFg: "#FFFCF0",
SelectedBg: "#403E3C",
}
var themeTokyoNight = Theme{
Name: "Tokyo Night",
Bg: "#1a1b26",
Surface: "#24283b",
Panel: "#292e42",
Border: "#3b4261",
Fg: "#c0caf5",
Muted: "#a9b1d6",
Subtle: "#565f89",
Success: "#9ece6a",
Warning: "#e0af68",
Danger: "#f7768e",
Info: "#7aa2f7",
Accent: "#7dcfff",
Purple: "#bb9af7",
ZebraBg: "#1c1d28",
SelectedFg: "#c0caf5",
SelectedBg: "#292e42",
}
var themeGruvbox = Theme{
Name: "Gruvbox",
Bg: "#282828",
Surface: "#3c3836",
Panel: "#504945",
Border: "#665c54",
Fg: "#ebdbb2",
Muted: "#bdae93",
Subtle: "#7c6f64",
Success: "#b8bb26",
Warning: "#fabd2f",
Danger: "#fb4934",
Info: "#83a598",
Accent: "#8ec07c",
Purple: "#d3869b",
ZebraBg: "#2a2a2a",
SelectedFg: "#fbf1c7",
SelectedBg: "#504945",
}
var themeCatppuccinMocha = Theme{
Name: "Catppuccin Mocha",
Bg: "#1e1e2e",
Surface: "#313244",
Panel: "#45475a",
Border: "#585b70",
Fg: "#cdd6f4",
Muted: "#a6adc8",
Subtle: "#6c7086",
Success: "#a6e3a1",
Warning: "#f9e2af",
Danger: "#f38ba8",
Info: "#89b4fa",
Accent: "#94e2d5",
Purple: "#cba6f7",
ZebraBg: "#232334",
SelectedFg: "#cdd6f4",
SelectedBg: "#45475a",
}
var themeNord = Theme{
Name: "Nord",
Bg: "#2e3440",
Surface: "#3b4252",
Panel: "#434c5e",
Border: "#4c566a",
Fg: "#d8dee9",
Muted: "#d8dee9",
Subtle: "#4c566a",
Success: "#a3be8c",
Warning: "#ebcb8b",
Danger: "#bf616a",
Info: "#81a1c1",
Accent: "#88c0d0",
Purple: "#b48ead",
ZebraBg: "#323845",
SelectedFg: "#eceff4",
SelectedBg: "#434c5e",
}
func (t Theme) HuhTheme() *huh.Theme {
ht := huh.ThemeBase()
ht.Focused.Base = ht.Focused.Base.BorderForeground(t.Border)
ht.Focused.Card = ht.Focused.Base
ht.Focused.Title = ht.Focused.Title.Foreground(t.Accent).Bold(true)
ht.Focused.NoteTitle = ht.Focused.NoteTitle.Foreground(t.Accent).Bold(true).MarginBottom(1)
ht.Focused.Description = ht.Focused.Description.Foreground(t.Muted)
ht.Focused.ErrorIndicator = ht.Focused.ErrorIndicator.Foreground(t.Danger)
ht.Focused.ErrorMessage = ht.Focused.ErrorMessage.Foreground(t.Danger)
ht.Focused.SelectSelector = ht.Focused.SelectSelector.Foreground(t.Purple)
ht.Focused.NextIndicator = ht.Focused.NextIndicator.Foreground(t.Purple)
ht.Focused.PrevIndicator = ht.Focused.PrevIndicator.Foreground(t.Purple)
ht.Focused.Option = ht.Focused.Option.Foreground(t.Fg)
ht.Focused.MultiSelectSelector = ht.Focused.MultiSelectSelector.Foreground(t.Purple)
ht.Focused.SelectedOption = ht.Focused.SelectedOption.Foreground(t.Success)
ht.Focused.SelectedPrefix = lipgloss.NewStyle().Foreground(t.Success).SetString("✓ ")
ht.Focused.UnselectedPrefix = lipgloss.NewStyle().Foreground(t.Subtle).SetString("• ")
ht.Focused.UnselectedOption = ht.Focused.UnselectedOption.Foreground(t.Fg)
ht.Focused.FocusedButton = ht.Focused.FocusedButton.Foreground(t.Bg).Background(t.Accent)
ht.Focused.Next = ht.Focused.FocusedButton
ht.Focused.BlurredButton = ht.Focused.BlurredButton.Foreground(t.Fg).Background(t.Surface)
ht.Focused.TextInput.Cursor = ht.Focused.TextInput.Cursor.Foreground(t.Accent)
ht.Focused.TextInput.Placeholder = ht.Focused.TextInput.Placeholder.Foreground(t.Subtle)
ht.Focused.TextInput.Prompt = ht.Focused.TextInput.Prompt.Foreground(t.Purple)
ht.Blurred = ht.Focused
ht.Blurred.Base = ht.Focused.Base.BorderStyle(lipgloss.HiddenBorder())
ht.Blurred.Card = ht.Blurred.Base
ht.Blurred.NextIndicator = lipgloss.NewStyle()
ht.Blurred.PrevIndicator = lipgloss.NewStyle()
ht.Group.Title = ht.Focused.Title
ht.Group.Description = ht.Focused.Description
return ht
}
func themeByName(name string) Theme {
for _, t := range themes {
if t.Name == name {
return t
}
}
return themes[0]
}
+20 -60
View File
@@ -3,9 +3,9 @@ package tui
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"gitea.lerkolabs.com/lerko/uptop/internal/models" "go-upkeep/internal/models"
"gitea.lerkolabs.com/lerko/uptop/internal/monitor" "go-upkeep/internal/monitor"
"gitea.lerkolabs.com/lerko/uptop/internal/store" "go-upkeep/internal/store"
"math" "math"
"sort" "sort"
"strings" "strings"
@@ -20,34 +20,16 @@ import (
) )
var ( var (
subtleStyle lipgloss.Style subtleStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#9ca0b0", Dark: "#565f89"})
specialStyle lipgloss.Style specialStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"})
warnStyle lipgloss.Style warnStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#F0E442", Dark: "#F0E442"})
dangerStyle lipgloss.Style dangerStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#F25D94", Dark: "#F25D94"})
titleStyle lipgloss.Style titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#7D56F4")).Bold(true)
activeTab lipgloss.Style
inactiveTab lipgloss.Style activeTab = lipgloss.NewStyle().Border(lipgloss.NormalBorder(), false, false, true, false).BorderForeground(lipgloss.Color("#7D56F4")).Foreground(lipgloss.Color("#7D56F4")).Bold(true).Padding(0, 1)
inactiveTab = lipgloss.NewStyle().Padding(0, 1).Foreground(lipgloss.AdaptiveColor{Light: "#AAA", Dark: "#555"})
) )
func applyTheme(t Theme) {
subtleStyle = lipgloss.NewStyle().Foreground(t.Subtle)
specialStyle = lipgloss.NewStyle().Foreground(t.Success)
warnStyle = lipgloss.NewStyle().Foreground(t.Warning)
dangerStyle = lipgloss.NewStyle().Foreground(t.Danger)
titleStyle = lipgloss.NewStyle().Foreground(t.Accent).Bold(true)
activeTab = lipgloss.NewStyle().Border(lipgloss.NormalBorder(), false, false, true, false).BorderForeground(t.Accent).Foreground(t.Accent).Bold(true).Padding(0, 1)
inactiveTab = lipgloss.NewStyle().Padding(0, 1).Foreground(t.Muted)
tableHeaderStyle = lipgloss.NewStyle().Foreground(t.Accent).Bold(true).Padding(0, 1)
tableCellStyle = lipgloss.NewStyle().Padding(0, 1)
tableSelectedStyle = lipgloss.NewStyle().Padding(0, 1).Bold(true).Foreground(t.SelectedFg).Background(t.SelectedBg)
tableBorderStyle = lipgloss.NewStyle().Foreground(t.Border)
tableZebraStyle = lipgloss.NewStyle().Padding(0, 1).Background(t.ZebraBg)
siteGroupStyle = lipgloss.NewStyle().Padding(0, 1).Bold(true).Foreground(t.Accent)
maintStyle = lipgloss.NewStyle().Foreground(t.Purple)
}
var pulseFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} var pulseFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
const ( const (
@@ -99,11 +81,9 @@ type Model struct {
deleteName string deleteName string
deleteTab int deleteTab int
collapsed map[int]bool collapsed map[int]bool
store store.Store store store.Store
engine *monitor.Engine engine *monitor.Engine
theme Theme
themeIndex int
// harmonica animation state // harmonica animation state
pulseSpring harmonica.Spring pulseSpring harmonica.Spring
@@ -127,19 +107,6 @@ func InitialModel(isAdmin bool, s store.Store, eng *monitor.Engine) Model {
z := zone.New() z := zone.New()
spring := harmonica.NewSpring(harmonica.FPS(10), 6.0, 0.4) spring := harmonica.NewSpring(harmonica.FPS(10), 6.0, 0.4)
collapsed := loadCollapsed(s) collapsed := loadCollapsed(s)
themeName, _ := s.GetPreference("theme")
theme := themeByName(themeName)
themeIdx := 0
for i, t := range themes {
if t.Name == theme.Name {
themeIdx = i
break
}
}
applyTheme(theme)
return Model{ return Model{
state: stateDashboard, state: stateDashboard,
logViewport: vpLogs, logViewport: vpLogs,
@@ -150,8 +117,6 @@ func InitialModel(isAdmin bool, s store.Store, eng *monitor.Engine) Model {
zones: z, zones: z,
pulseSpring: spring, pulseSpring: spring,
collapsed: collapsed, collapsed: collapsed,
theme: theme,
themeIndex: themeIdx,
} }
} }
@@ -493,11 +458,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.refreshData() m.refreshData()
} }
} }
case "T":
m.themeIndex = (m.themeIndex + 1) % len(themes)
m.theme = themes[m.themeIndex]
applyTheme(m.theme)
_ = m.store.SetPreference("theme", m.theme.Name)
case "d", "backspace": case "d", "backspace":
if m.currentTab == 0 && len(m.sites) > 0 { if m.currentTab == 0 && len(m.sites) > 0 {
m.deleteID = m.sites[m.cursor].ID m.deleteID = m.sites[m.cursor].ID
@@ -763,7 +723,7 @@ func (m Model) View() string {
hint := subtleStyle.Render("[y] Confirm [n] Cancel") hint := subtleStyle.Render("[y] Confirm [n] Cancel")
box := lipgloss.NewStyle(). box := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()). Border(lipgloss.RoundedBorder()).
BorderForeground(m.theme.Danger). BorderForeground(lipgloss.Color("#F25D94")).
Padding(1, 3). Padding(1, 3).
Render(msg + "\n\n" + hint) Render(msg + "\n\n" + hint)
return lipgloss.NewStyle().Padding(2, 4).Render(box) return lipgloss.NewStyle().Padding(2, 4).Render(box)
@@ -915,19 +875,19 @@ func (m Model) viewDashboard() string {
var footer string var footer string
if m.filterMode { if m.filterMode {
cursor := lipgloss.NewStyle().Foreground(m.theme.Accent).Render("│") cursor := lipgloss.NewStyle().Foreground(lipgloss.Color("#7D56F4")).Render("│")
footer = "\n" + titleStyle.Render("/") + " " + m.filterText + cursor + " " + subtleStyle.Render("[Enter]Apply [Esc]Clear") footer = "\n" + titleStyle.Render("/") + " " + m.filterText + cursor + " " + subtleStyle.Render("[Enter]Apply [Esc]Clear")
} else { } else {
var keys string var keys string
switch m.currentTab { switch m.currentTab {
case 0: case 0:
keys = "[/]Filter [n]New [e]Edit [i]Info [d]Del [p]Pause [T]Theme [Tab]Switch [q]Quit" keys = "[/]Filter [n]New [e]Edit [i]Info [d]Del [p]Pause [Tab]Switch [q]Quit"
case 4: case 4:
keys = "[n]New [x]End [d]Del [T]Theme [Tab]Switch [q]Quit" keys = "[n]New [x]End [d]Del [Tab]Switch [q]Quit"
case 5: case 5:
keys = "[n]Add [d]Revoke [T]Theme [Tab]Switch [q]Quit" keys = "[n]Add [d]Revoke [Tab]Switch [q]Quit"
default: default:
keys = "[T]Theme [Tab]Switch [q]Quit" keys = "[Tab]Switch [q]Quit"
} }
footer = "\n" + statusLine + " " + subtleStyle.Render(keys) footer = "\n" + statusLine + " " + subtleStyle.Render(keys)
if m.filterText != "" && m.currentTab == 0 { if m.filterText != "" && m.currentTab == 0 {