Compare commits
12 Commits
1fa2b1d98c
...
2026.05.3
| Author | SHA1 | Date | |
|---|---|---|---|
| b70edaace5 | |||
| 9d12e3ecf1 | |||
| 36a4b69837 | |||
| fee84c9363 | |||
| 87edd4aa40 | |||
| 602f1b2c52 | |||
| 6e659cf6ee | |||
| 0a56f01929 | |||
| b5b9cc81a5 | |||
| f64b46f055 | |||
| d038361320 | |||
| d03dc0c1ea |
@@ -18,6 +18,12 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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"]
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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,4 +1,4 @@
|
|||||||
module go-upkeep
|
module gitea.lerkolabs.com/lerko/uptop
|
||||||
|
|
||||||
go 1.24.4
|
go 1.24.4
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,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,7 +1,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"go-upkeep/internal/models"
|
"gitea.lerkolabs.com/lerko/uptop/internal/models"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,7 +1,7 @@
|
|||||||
package store
|
package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"go-upkeep/internal/models"
|
"gitea.lerkolabs.com/lerko/uptop/internal/models"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(huh.ThemeDracula())
|
).WithTheme(m.theme.HuhTheme())
|
||||||
|
|
||||||
return m.huhForm.Init()
|
return m.huhForm.Init()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
var maintStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#bb9af7"))
|
var maintStyle lipgloss.Style
|
||||||
|
|
||||||
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(huh.ThemeDracula())
|
).WithTheme(m.theme.HuhTheme())
|
||||||
|
|
||||||
return m.huhForm.Init()
|
return m.huhForm.Init()
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-11
@@ -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"
|
||||||
@@ -37,10 +38,7 @@ func typeIcon(siteType string, collapsed bool) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var siteGroupStyle = lipgloss.NewStyle().
|
var siteGroupStyle lipgloss.Style
|
||||||
Padding(0, 1).
|
|
||||||
Bold(true).
|
|
||||||
Foreground(lipgloss.Color("#7D56F4"))
|
|
||||||
|
|
||||||
type siteFormData struct {
|
type siteFormData struct {
|
||||||
Name string
|
Name string
|
||||||
@@ -340,10 +338,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(lipgloss.Color("#7D56F4")).
|
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"),
|
||||||
)
|
)
|
||||||
@@ -509,7 +507,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 == "push" || m.siteFormData.SiteType == "group" {
|
if m.siteFormData.SiteType != "http" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if s == "" {
|
if s == "" {
|
||||||
@@ -555,12 +553,15 @@ 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 < 0 || v > 65535 {
|
if v < 1 || v > 65535 {
|
||||||
return fmt.Errorf("port must be 0-65535")
|
return fmt.Errorf("port must be 1-65535")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}),
|
}),
|
||||||
@@ -615,6 +616,9 @@ 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")
|
||||||
@@ -628,6 +632,9 @@ 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")
|
||||||
@@ -642,7 +649,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(huh.ThemeDracula())
|
).WithTheme(m.theme.HuhTheme())
|
||||||
|
|
||||||
return m.huhForm.Init()
|
return m.huhForm.Init()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(huh.ThemeDracula())
|
).WithTheme(m.theme.HuhTheme())
|
||||||
|
|
||||||
return m.huhForm.Init()
|
return m.huhForm.Init()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,25 +6,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
tableHeaderStyle = lipgloss.NewStyle().
|
tableHeaderStyle lipgloss.Style
|
||||||
Foreground(lipgloss.Color("#7D56F4")).
|
tableCellStyle lipgloss.Style
|
||||||
Bold(true).
|
tableSelectedStyle lipgloss.Style
|
||||||
Padding(0, 1)
|
tableBorderStyle lipgloss.Style
|
||||||
|
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
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
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]
|
||||||
|
}
|
||||||
+57
-17
@@ -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"
|
||||||
@@ -20,16 +20,34 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
subtleStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#9ca0b0", Dark: "#565f89"})
|
subtleStyle lipgloss.Style
|
||||||
specialStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"})
|
specialStyle lipgloss.Style
|
||||||
warnStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#F0E442", Dark: "#F0E442"})
|
warnStyle lipgloss.Style
|
||||||
dangerStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#F25D94", Dark: "#F25D94"})
|
dangerStyle lipgloss.Style
|
||||||
titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#7D56F4")).Bold(true)
|
titleStyle lipgloss.Style
|
||||||
|
activeTab 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.Style
|
||||||
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 (
|
||||||
@@ -84,6 +102,8 @@ type Model struct {
|
|||||||
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
|
||||||
@@ -107,6 +127,19 @@ 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,
|
||||||
@@ -117,6 +150,8 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,6 +493,11 @@ 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
|
||||||
@@ -723,7 +763,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(lipgloss.Color("#F25D94")).
|
BorderForeground(m.theme.Danger).
|
||||||
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)
|
||||||
@@ -875,19 +915,19 @@ func (m Model) viewDashboard() string {
|
|||||||
|
|
||||||
var footer string
|
var footer string
|
||||||
if m.filterMode {
|
if m.filterMode {
|
||||||
cursor := lipgloss.NewStyle().Foreground(lipgloss.Color("#7D56F4")).Render("│")
|
cursor := lipgloss.NewStyle().Foreground(m.theme.Accent).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 [Tab]Switch [q]Quit"
|
keys = "[/]Filter [n]New [e]Edit [i]Info [d]Del [p]Pause [T]Theme [Tab]Switch [q]Quit"
|
||||||
case 4:
|
case 4:
|
||||||
keys = "[n]New [x]End [d]Del [Tab]Switch [q]Quit"
|
keys = "[n]New [x]End [d]Del [T]Theme [Tab]Switch [q]Quit"
|
||||||
case 5:
|
case 5:
|
||||||
keys = "[n]Add [d]Revoke [Tab]Switch [q]Quit"
|
keys = "[n]Add [d]Revoke [T]Theme [Tab]Switch [q]Quit"
|
||||||
default:
|
default:
|
||||||
keys = "[Tab]Switch [q]Quit"
|
keys = "[T]Theme [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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user