Compare commits
24 Commits
package-re
...
cf9a842b7c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf9a842b7c | ||
|
|
3f314d132f | ||
|
|
ab4056d457 | ||
|
|
a6289fa53e | ||
|
|
0165f45e86 | ||
|
|
0bf517096b | ||
|
|
727b70c909 | ||
|
|
66ef3f2602 | ||
|
|
8aa3c1cf16 | ||
|
|
04087f3450 | ||
|
|
4f98e17217 | ||
|
|
73e997ecd9 | ||
|
|
1626844d5f | ||
|
|
21994f06f4 | ||
|
|
7fc156a743 | ||
|
|
ed5bc77e71 | ||
|
|
0fe9148d2d | ||
|
|
8f16109615 | ||
|
|
1c9a7143fd | ||
|
|
1c9a05b317 | ||
|
|
212cfe4e30 | ||
|
|
fe53dc53a7 | ||
|
|
558c53b550 | ||
|
|
682574f2bf |
@@ -1,48 +0,0 @@
|
||||
# Node.js dependencies
|
||||
node_modules
|
||||
src/site/node_modules
|
||||
|
||||
# Go build artifacts and binaries
|
||||
build
|
||||
dist
|
||||
*.exe
|
||||
beszel-agent
|
||||
beszel_data*
|
||||
pb_data
|
||||
data
|
||||
temp
|
||||
|
||||
# Development and IDE files
|
||||
.vscode
|
||||
.idea*
|
||||
*.swc
|
||||
__debug_*
|
||||
|
||||
# Git and version control
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Documentation and supplemental files
|
||||
*.md
|
||||
supplemental
|
||||
freebsd-port
|
||||
|
||||
# Test files (exclude from production builds)
|
||||
*_test.go
|
||||
coverage
|
||||
|
||||
# Docker files
|
||||
dockerfile_*
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.bak
|
||||
*.log
|
||||
|
||||
# OS specific files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# .NET build artifacts
|
||||
agent/lhm/obj
|
||||
agent/lhm/bin
|
||||
34
.github/workflows/docker-images.yml
vendored
@@ -13,44 +13,44 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- image: henrygd/beszel
|
||||
context: ./
|
||||
dockerfile: ./src/dockerfile_hub
|
||||
context: ./beszel
|
||||
dockerfile: ./beszel/dockerfile_hub
|
||||
registry: docker.io
|
||||
username_secret: DOCKERHUB_USERNAME
|
||||
password_secret: DOCKERHUB_TOKEN
|
||||
|
||||
- image: henrygd/beszel-agent
|
||||
context: ./
|
||||
dockerfile: ./src/dockerfile_agent
|
||||
context: ./beszel
|
||||
dockerfile: ./beszel/dockerfile_agent
|
||||
registry: docker.io
|
||||
username_secret: DOCKERHUB_USERNAME
|
||||
password_secret: DOCKERHUB_TOKEN
|
||||
|
||||
- image: henrygd/beszel-agent-nvidia
|
||||
context: ./
|
||||
dockerfile: ./src/dockerfile_agent_nvidia
|
||||
context: ./beszel
|
||||
dockerfile: ./beszel/dockerfile_agent_nvidia
|
||||
platforms: linux/amd64
|
||||
registry: docker.io
|
||||
username_secret: DOCKERHUB_USERNAME
|
||||
password_secret: DOCKERHUB_TOKEN
|
||||
|
||||
- image: ghcr.io/${{ github.repository }}/beszel
|
||||
context: ./
|
||||
dockerfile: ./src/dockerfile_hub
|
||||
context: ./beszel
|
||||
dockerfile: ./beszel/dockerfile_hub
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password_secret: GITHUB_TOKEN
|
||||
|
||||
- image: ghcr.io/${{ github.repository }}/beszel-agent
|
||||
context: ./
|
||||
dockerfile: ./src/dockerfile_agent
|
||||
context: ./beszel
|
||||
dockerfile: ./beszel/dockerfile_agent
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password_secret: GITHUB_TOKEN
|
||||
|
||||
- image: ghcr.io/${{ github.repository }}/beszel-agent-nvidia
|
||||
context: ./
|
||||
dockerfile: ./src/dockerfile_agent_nvidia
|
||||
context: ./beszel
|
||||
dockerfile: ./beszel/dockerfile_agent_nvidia
|
||||
platforms: linux/amd64
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -68,10 +68,10 @@ jobs:
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --no-save --cwd ./src/site
|
||||
run: bun install --no-save --cwd ./beszel/site
|
||||
|
||||
- name: Build site
|
||||
run: bun run --cwd ./src/site build
|
||||
run: bun run --cwd ./beszel/site build
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
@@ -93,9 +93,7 @@ jobs:
|
||||
|
||||
# https://github.com/docker/login-action
|
||||
- name: Login to Docker Hub
|
||||
env:
|
||||
password_secret_exists: ${{ secrets[matrix.password_secret] != '' && 'true' || 'false' }}
|
||||
if: github.event_name != 'pull_request' && env.password_secret_exists == 'true'
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ matrix.username || secrets[matrix.username_secret] }}
|
||||
@@ -110,6 +108,6 @@ jobs:
|
||||
context: "${{ matrix.context }}"
|
||||
file: ${{ matrix.dockerfile }}
|
||||
platforms: ${{ matrix.platforms || 'linux/amd64,linux/arm64,linux/arm/v7' }}
|
||||
push: ${{ github.ref_type == 'tag' && secrets[matrix.password_secret] != '' }}
|
||||
push: ${{ github.ref_type == 'tag' }}
|
||||
tags: ${{ steps.metadata.outputs.tags }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
|
||||
9
.github/workflows/release.yml
vendored
@@ -21,10 +21,10 @@ jobs:
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --no-save --cwd ./src/site
|
||||
run: bun install --no-save --cwd ./beszel/site
|
||||
|
||||
- name: Build site
|
||||
run: bun run --cwd ./src/site build
|
||||
run: bun run --cwd ./beszel/site build
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
@@ -38,17 +38,16 @@ jobs:
|
||||
|
||||
- name: Build .NET LHM executable for Windows sensors
|
||||
run: |
|
||||
dotnet build -c Release ./agent/lhm/beszel_lhm.csproj
|
||||
dotnet build -c Release ./beszel/internal/agent/lhm/beszel_lhm.csproj
|
||||
shell: bash
|
||||
|
||||
- name: GoReleaser beszel
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
workdir: ./
|
||||
workdir: ./beszel
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.TOKEN || secrets.GITHUB_TOKEN }}
|
||||
WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }}
|
||||
IS_FORK: ${{ github.repository_owner != 'henrygd' }}
|
||||
|
||||
6
.github/workflows/vulncheck.yml
vendored
@@ -15,7 +15,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
vulncheck:
|
||||
name: VulnCheck
|
||||
name: Analysis
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
@@ -23,8 +23,8 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
# cached: false
|
||||
go-version: 1.24.x
|
||||
cached: false
|
||||
- name: Get official govulncheck
|
||||
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
shell: bash
|
||||
|
||||
12
.gitignore
vendored
@@ -8,15 +8,15 @@ beszel_data
|
||||
beszel_data*
|
||||
dist
|
||||
*.exe
|
||||
src/cmd/hub/hub
|
||||
src/cmd/agent/agent
|
||||
beszel/cmd/hub/hub
|
||||
beszel/cmd/agent/agent
|
||||
node_modules
|
||||
build
|
||||
beszel/build
|
||||
*timestamp*
|
||||
.swc
|
||||
src/site/src/locales/**/*.ts
|
||||
beszel/site/src/locales/**/*.ts
|
||||
*.bak
|
||||
__debug_*
|
||||
agent/lhm/obj
|
||||
agent/lhm/bin
|
||||
beszel/internal/agent/lhm/obj
|
||||
beszel/internal/agent/lhm/bin
|
||||
dockerfile_agent_dev
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
//go:build !freebsd
|
||||
|
||||
// Package battery provides functions to check if the system has a battery and to get the battery stats.
|
||||
package battery
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
|
||||
"github.com/distatus/battery"
|
||||
)
|
||||
|
||||
var systemHasBattery = false
|
||||
var haveCheckedBattery = false
|
||||
|
||||
// HasReadableBattery checks if the system has a battery and returns true if it does.
|
||||
func HasReadableBattery() bool {
|
||||
if haveCheckedBattery {
|
||||
return systemHasBattery
|
||||
}
|
||||
haveCheckedBattery = true
|
||||
bat, err := battery.Get(0)
|
||||
if err == nil && bat != nil {
|
||||
systemHasBattery = true
|
||||
} else {
|
||||
slog.Debug("No battery found", "err", err)
|
||||
}
|
||||
return systemHasBattery
|
||||
}
|
||||
|
||||
// GetBatteryStats returns the current battery percent and charge state
|
||||
func GetBatteryStats() (batteryPercent uint8, batteryState uint8, err error) {
|
||||
if !systemHasBattery {
|
||||
return batteryPercent, batteryState, errors.ErrUnsupported
|
||||
}
|
||||
batteries, err := battery.GetAll()
|
||||
if err != nil || len(batteries) == 0 {
|
||||
return batteryPercent, batteryState, err
|
||||
}
|
||||
totalCapacity := float64(0)
|
||||
totalCharge := float64(0)
|
||||
for _, bat := range batteries {
|
||||
if bat.Design != 0 {
|
||||
totalCapacity += bat.Design
|
||||
} else {
|
||||
totalCapacity += bat.Full
|
||||
}
|
||||
totalCharge += bat.Current
|
||||
}
|
||||
batteryPercent = uint8(totalCharge / totalCapacity * 100)
|
||||
batteryState = uint8(batteries[0].State.Raw)
|
||||
return batteryPercent, batteryState, nil
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
//go:build freebsd
|
||||
|
||||
package battery
|
||||
|
||||
import "errors"
|
||||
|
||||
func HasReadableBattery() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func GetBatteryStats() (uint8, uint8, error) {
|
||||
return 0, 0, errors.ErrUnsupported
|
||||
}
|
||||
165
agent/update.go
@@ -1,165 +0,0 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/henrygd/beszel/src/ghupdate"
|
||||
)
|
||||
|
||||
// restarter knows how to restart the beszel-agent service.
|
||||
type restarter interface {
|
||||
Restart() error
|
||||
}
|
||||
|
||||
type systemdRestarter struct{ cmd string }
|
||||
|
||||
func (s *systemdRestarter) Restart() error {
|
||||
// Only restart if the service is active
|
||||
if err := exec.Command(s.cmd, "is-active", "beszel-agent.service").Run(); err != nil {
|
||||
return nil
|
||||
}
|
||||
ghupdate.ColorPrint(ghupdate.ColorYellow, "Restarting beszel-agent.service via systemd…")
|
||||
return exec.Command(s.cmd, "restart", "beszel-agent.service").Run()
|
||||
}
|
||||
|
||||
type openRCRestarter struct{ cmd string }
|
||||
|
||||
func (o *openRCRestarter) Restart() error {
|
||||
if err := exec.Command(o.cmd, "status", "beszel-agent").Run(); err != nil {
|
||||
return nil
|
||||
}
|
||||
ghupdate.ColorPrint(ghupdate.ColorYellow, "Restarting beszel-agent via OpenRC…")
|
||||
return exec.Command(o.cmd, "restart", "beszel-agent").Run()
|
||||
}
|
||||
|
||||
type openWRTRestarter struct{ cmd string }
|
||||
|
||||
func (w *openWRTRestarter) Restart() error {
|
||||
if err := exec.Command(w.cmd, "running", "beszel-agent").Run(); err != nil {
|
||||
return nil
|
||||
}
|
||||
ghupdate.ColorPrint(ghupdate.ColorYellow, "Restarting beszel-agent via procd…")
|
||||
return exec.Command(w.cmd, "restart", "beszel-agent").Run()
|
||||
}
|
||||
|
||||
type freeBSDRestarter struct{ cmd string }
|
||||
|
||||
func (f *freeBSDRestarter) Restart() error {
|
||||
if err := exec.Command(f.cmd, "beszel-agent", "status").Run(); err != nil {
|
||||
return nil
|
||||
}
|
||||
ghupdate.ColorPrint(ghupdate.ColorYellow, "Restarting beszel-agent via FreeBSD rc…")
|
||||
return exec.Command(f.cmd, "beszel-agent", "restart").Run()
|
||||
}
|
||||
|
||||
func detectRestarter() restarter {
|
||||
if path, err := exec.LookPath("systemctl"); err == nil {
|
||||
return &systemdRestarter{cmd: path}
|
||||
}
|
||||
if path, err := exec.LookPath("rc-service"); err == nil {
|
||||
return &openRCRestarter{cmd: path}
|
||||
}
|
||||
if path, err := exec.LookPath("service"); err == nil {
|
||||
if runtime.GOOS == "freebsd" {
|
||||
return &freeBSDRestarter{cmd: path}
|
||||
}
|
||||
return &openWRTRestarter{cmd: path}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update checks GitHub for a newer release of beszel-agent, applies it,
|
||||
// fixes SELinux context if needed, and restarts the service.
|
||||
func Update(useMirror bool) error {
|
||||
exePath, _ := os.Executable()
|
||||
|
||||
dataDir, err := getDataDir()
|
||||
if err != nil {
|
||||
dataDir = os.TempDir()
|
||||
}
|
||||
updated, err := ghupdate.Update(ghupdate.Config{
|
||||
ArchiveExecutable: "beszel-agent",
|
||||
DataDir: dataDir,
|
||||
UseMirror: useMirror,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if !updated {
|
||||
return nil
|
||||
}
|
||||
|
||||
// make sure the file is executable
|
||||
if err := os.Chmod(exePath, 0755); err != nil {
|
||||
ghupdate.ColorPrintf(ghupdate.ColorYellow, "Warning: failed to set executable permissions: %v", err)
|
||||
}
|
||||
// set ownership to beszel:beszel if possible
|
||||
if chownPath, err := exec.LookPath("chown"); err == nil {
|
||||
if err := exec.Command(chownPath, "beszel:beszel", exePath).Run(); err != nil {
|
||||
ghupdate.ColorPrintf(ghupdate.ColorYellow, "Warning: failed to set file ownership: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 6) Fix SELinux context if necessary
|
||||
if err := handleSELinuxContext(exePath); err != nil {
|
||||
ghupdate.ColorPrintf(ghupdate.ColorYellow, "Warning: SELinux context handling: %v", err)
|
||||
}
|
||||
|
||||
// 7) Restart service if running under a recognised init system
|
||||
if r := detectRestarter(); r != nil {
|
||||
if err := r.Restart(); err != nil {
|
||||
ghupdate.ColorPrintf(ghupdate.ColorYellow, "Warning: failed to restart service: %v", err)
|
||||
ghupdate.ColorPrint(ghupdate.ColorYellow, "Please restart the service manually.")
|
||||
} else {
|
||||
ghupdate.ColorPrint(ghupdate.ColorGreen, "Service restarted successfully")
|
||||
}
|
||||
} else {
|
||||
ghupdate.ColorPrint(ghupdate.ColorYellow, "No supported init system detected; please restart manually if needed.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleSELinuxContext restores or applies the correct SELinux label to the binary.
|
||||
func handleSELinuxContext(path string) error {
|
||||
out, err := exec.Command("getenforce").Output()
|
||||
if err != nil {
|
||||
// SELinux not enabled or getenforce not available
|
||||
return nil
|
||||
}
|
||||
state := strings.TrimSpace(string(out))
|
||||
if state == "Disabled" {
|
||||
return nil
|
||||
}
|
||||
|
||||
ghupdate.ColorPrint(ghupdate.ColorYellow, "SELinux is enabled; applying context…")
|
||||
var errs []string
|
||||
|
||||
// Try persistent context via semanage+restorecon
|
||||
if semanagePath, err := exec.LookPath("semanage"); err == nil {
|
||||
if err := exec.Command(semanagePath, "fcontext", "-a", "-t", "bin_t", path).Run(); err != nil {
|
||||
errs = append(errs, "semanage fcontext failed: "+err.Error())
|
||||
} else if restoreconPath, err := exec.LookPath("restorecon"); err == nil {
|
||||
if err := exec.Command(restoreconPath, "-v", path).Run(); err != nil {
|
||||
errs = append(errs, "restorecon failed: "+err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to temporary context via chcon
|
||||
if chconPath, err := exec.LookPath("chcon"); err == nil {
|
||||
if err := exec.Command(chconPath, "-t", "bin_t", path).Run(); err != nil {
|
||||
errs = append(errs, "chcon failed: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("SELinux context errors: %s", strings.Join(errs, "; "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -9,7 +9,7 @@ before:
|
||||
builds:
|
||||
- id: beszel
|
||||
binary: beszel
|
||||
main: src/cmd/hub/hub.go
|
||||
main: cmd/hub/hub.go
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
@@ -22,7 +22,7 @@ builds:
|
||||
|
||||
- id: beszel-agent
|
||||
binary: beszel-agent
|
||||
main: src/cmd/agent/agent.go
|
||||
main: cmd/agent/agent.go
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
@@ -38,25 +38,12 @@ builds:
|
||||
- mips64
|
||||
- riscv64
|
||||
- mipsle
|
||||
- mips
|
||||
- ppc64le
|
||||
gomips:
|
||||
- hardfloat
|
||||
- softfloat
|
||||
ignore:
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
- goos: openbsd
|
||||
goarch: arm
|
||||
- goos: linux
|
||||
goarch: mips64
|
||||
gomips: softfloat
|
||||
- goos: linux
|
||||
goarch: mipsle
|
||||
gomips: hardfloat
|
||||
- goos: linux
|
||||
goarch: mips
|
||||
gomips: hardfloat
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: darwin
|
||||
@@ -67,7 +54,7 @@ builds:
|
||||
archives:
|
||||
- id: beszel-agent
|
||||
formats: [tar.gz]
|
||||
ids:
|
||||
builds:
|
||||
- beszel-agent
|
||||
name_template: >-
|
||||
{{ .Binary }}_
|
||||
@@ -79,7 +66,7 @@ archives:
|
||||
|
||||
- id: beszel
|
||||
formats: [tar.gz]
|
||||
ids:
|
||||
builds:
|
||||
- beszel
|
||||
name_template: >-
|
||||
{{ .Binary }}_
|
||||
@@ -98,33 +85,33 @@ nfpms:
|
||||
API access.
|
||||
maintainer: henrygd <hank@henrygd.me>
|
||||
section: net
|
||||
ids:
|
||||
builds:
|
||||
- beszel-agent
|
||||
formats:
|
||||
- deb
|
||||
contents:
|
||||
- src: ./supplemental/debian/beszel-agent.service
|
||||
- src: ../supplemental/debian/beszel-agent.service
|
||||
dst: lib/systemd/system/beszel-agent.service
|
||||
packager: deb
|
||||
- src: ./supplemental/debian/copyright
|
||||
- src: ../supplemental/debian/copyright
|
||||
dst: usr/share/doc/beszel-agent/copyright
|
||||
packager: deb
|
||||
- src: ./supplemental/debian/lintian-overrides
|
||||
- src: ../supplemental/debian/lintian-overrides
|
||||
dst: usr/share/lintian/overrides/beszel-agent
|
||||
packager: deb
|
||||
scripts:
|
||||
postinstall: ./supplemental/debian/postinstall.sh
|
||||
preremove: ./supplemental/debian/prerm.sh
|
||||
postremove: ./supplemental/debian/postrm.sh
|
||||
postinstall: ../supplemental/debian/postinstall.sh
|
||||
preremove: ../supplemental/debian/prerm.sh
|
||||
postremove: ../supplemental/debian/postrm.sh
|
||||
deb:
|
||||
predepends:
|
||||
- adduser
|
||||
- debconf
|
||||
scripts:
|
||||
templates: ./supplemental/debian/templates
|
||||
templates: ../supplemental/debian/templates
|
||||
# Currently broken due to a bug in goreleaser
|
||||
# https://github.com/goreleaser/goreleaser/issues/5487
|
||||
#config: ./supplemental/debian/config.sh
|
||||
#config: ../supplemental/debian/config.sh
|
||||
|
||||
scoops:
|
||||
- ids: [beszel-agent]
|
||||
@@ -135,7 +122,6 @@ scoops:
|
||||
homepage: "https://beszel.dev"
|
||||
description: "Agent for Beszel, a lightweight server monitoring platform."
|
||||
license: MIT
|
||||
skip_upload: '{{ if eq (tolower .Env.IS_FORK) "true" }}true{{ else }}auto{{ end }}'
|
||||
|
||||
# # Needs choco installed, so doesn't build on linux / default gh workflow :(
|
||||
# chocolateys:
|
||||
@@ -169,7 +155,7 @@ brews:
|
||||
homepage: "https://beszel.dev"
|
||||
description: "Agent for Beszel, a lightweight server monitoring platform."
|
||||
license: MIT
|
||||
skip_upload: '{{ if eq (tolower .Env.IS_FORK) "true" }}true{{ else }}auto{{ end }}'
|
||||
skip_upload: auto
|
||||
extra_install: |
|
||||
(bin/"beszel-agent-launcher").write <<~EOS
|
||||
#!/bin/bash
|
||||
@@ -201,7 +187,7 @@ winget:
|
||||
release_notes_url: "https://github.com/henrygd/beszel/releases/tag/v{{ .Version }}"
|
||||
publisher_support_url: "https://github.com/henrygd/beszel/issues"
|
||||
short_description: "Agent for Beszel, a lightweight server monitoring platform."
|
||||
skip_upload: '{{ if eq (tolower .Env.IS_FORK) "true" }}true{{ else }}auto{{ end }}'
|
||||
skip_upload: auto
|
||||
description: |
|
||||
Beszel is a lightweight server monitoring platform that includes Docker
|
||||
statistics, historical data, and alert functions. It has a friendly web
|
||||
@@ -7,7 +7,7 @@ SKIP_WEB ?= false
|
||||
# Set executable extension based on target OS
|
||||
EXE_EXT := $(if $(filter windows,$(OS)),.exe,)
|
||||
|
||||
.PHONY: tidy build-agent build-hub build-hub-dev build clean lint dev-server dev-agent dev-hub dev generate-locales
|
||||
.PHONY: tidy build-agent build-hub build clean lint dev-server dev-agent dev-hub dev generate-locales
|
||||
.DEFAULT_GOAL := build
|
||||
|
||||
clean:
|
||||
@@ -26,11 +26,11 @@ tidy:
|
||||
|
||||
build-web-ui:
|
||||
@if command -v bun >/dev/null 2>&1; then \
|
||||
bun install --cwd ./src/site && \
|
||||
bun run --cwd ./src/site build; \
|
||||
bun install --cwd ./site && \
|
||||
bun run --cwd ./site build; \
|
||||
else \
|
||||
npm install --prefix ./src/site && \
|
||||
npm run --prefix ./src/site build; \
|
||||
npm install --prefix ./site && \
|
||||
npm run --prefix ./site build; \
|
||||
fi
|
||||
|
||||
# Conditional .NET build - only for Windows
|
||||
@@ -38,8 +38,8 @@ build-dotnet-conditional:
|
||||
@if [ "$(OS)" = "windows" ]; then \
|
||||
echo "Building .NET executable for Windows..."; \
|
||||
if command -v dotnet >/dev/null 2>&1; then \
|
||||
rm -rf ./agent/lhm/bin; \
|
||||
dotnet build -c Release ./agent/lhm/beszel_lhm.csproj; \
|
||||
rm -rf ./internal/agent/lhm/bin; \
|
||||
dotnet build -c Release ./internal/agent/lhm/beszel_lhm.csproj; \
|
||||
else \
|
||||
echo "Error: dotnet not found. Install .NET SDK to build Windows agent."; \
|
||||
exit 1; \
|
||||
@@ -48,51 +48,47 @@ build-dotnet-conditional:
|
||||
|
||||
# Update build-agent to include conditional .NET build
|
||||
build-agent: tidy build-dotnet-conditional
|
||||
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel-agent_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" ./src/cmd/agent
|
||||
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel-agent_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" beszel/cmd/agent
|
||||
|
||||
build-hub: tidy $(if $(filter false,$(SKIP_WEB)),build-web-ui)
|
||||
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" ./src/cmd/hub
|
||||
|
||||
build-hub-dev: tidy
|
||||
mkdir -p ./src/site/dist && touch ./src/site/dist/index.html
|
||||
GOOS=$(OS) GOARCH=$(ARCH) go build -tags development -o ./build/beszel-dev_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" ./src/cmd/hub
|
||||
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel_$(OS)_$(ARCH)$(EXE_EXT) -ldflags "-w -s" beszel/cmd/hub
|
||||
|
||||
build: build-agent build-hub
|
||||
|
||||
generate-locales:
|
||||
@if [ ! -f ./src/site/src/locales/en/en.ts ]; then \
|
||||
@if [ ! -f ./site/src/locales/en/en.ts ]; then \
|
||||
echo "Generating locales..."; \
|
||||
command -v bun >/dev/null 2>&1 && cd ./src/site && bun install && bun run sync || cd ./src/site && npm install && npm run sync; \
|
||||
command -v bun >/dev/null 2>&1 && cd ./site && bun install && bun run sync || cd ./site && npm install && npm run sync; \
|
||||
fi
|
||||
|
||||
dev-server: generate-locales
|
||||
cd ./src/site
|
||||
cd ./site
|
||||
@if command -v bun >/dev/null 2>&1; then \
|
||||
cd ./src/site && bun run dev --host 0.0.0.0; \
|
||||
cd ./site && bun run dev --host 0.0.0.0; \
|
||||
else \
|
||||
cd ./src/site && npm run dev --host 0.0.0.0; \
|
||||
cd ./site && npm run dev --host 0.0.0.0; \
|
||||
fi
|
||||
|
||||
dev-hub: export ENV=dev
|
||||
dev-hub:
|
||||
mkdir -p ./src/site/dist && touch ./src/site/dist/index.html
|
||||
mkdir -p ./site/dist && touch ./site/dist/index.html
|
||||
@if command -v entr >/dev/null 2>&1; then \
|
||||
find ./src/cmd/hub/*.go ./src/{alerts,hub,records,users}/*.go | entr -r -s "cd ./src/cmd/hub && go run -tags development . serve --http 0.0.0.0:8090"; \
|
||||
find ./cmd/hub/*.go ./internal/{alerts,hub,records,users}/*.go | entr -r -s "cd ./cmd/hub && go run . serve --http 0.0.0.0:8090"; \
|
||||
else \
|
||||
cd ./src/cmd/hub && go run -tags development . serve --http 0.0.0.0:8090; \
|
||||
cd ./cmd/hub && go run . serve --http 0.0.0.0:8090; \
|
||||
fi
|
||||
|
||||
dev-agent:
|
||||
@if command -v entr >/dev/null 2>&1; then \
|
||||
find ./src/cmd/agent/*.go ./agent/*.go | entr -r go run github.com/henrygd/beszel/src/cmd/agent; \
|
||||
find ./cmd/agent/*.go ./internal/agent/*.go | entr -r go run beszel/cmd/agent; \
|
||||
else \
|
||||
go run github.com/henrygd/beszel/src/cmd/agent; \
|
||||
go run beszel/cmd/agent; \
|
||||
fi
|
||||
|
||||
build-dotnet:
|
||||
@if command -v dotnet >/dev/null 2>&1; then \
|
||||
rm -rf ./agent/lhm/bin; \
|
||||
dotnet build -c Release ./agent/lhm/beszel_lhm.csproj; \
|
||||
rm -rf ./internal/agent/lhm/bin; \
|
||||
dotnet build -c Release ./internal/agent/lhm/beszel_lhm.csproj; \
|
||||
else \
|
||||
echo "dotnet not found"; \
|
||||
fi
|
||||
@@ -1,15 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/agent"
|
||||
"beszel/internal/agent/health"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
"github.com/henrygd/beszel/agent"
|
||||
"github.com/henrygd/beszel/agent/health"
|
||||
"github.com/spf13/pflag"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
@@ -17,24 +17,43 @@ import (
|
||||
type cmdOptions struct {
|
||||
key string // key is the public key(s) for SSH authentication.
|
||||
listen string // listen is the address or port to listen on.
|
||||
// TODO: add hubURL and token
|
||||
// hubURL string // hubURL is the URL of the hub to use.
|
||||
// token string // token is the token to use for authentication.
|
||||
}
|
||||
|
||||
// parse parses the command line flags and populates the config struct.
|
||||
// It returns true if a subcommand was handled and the program should exit.
|
||||
func (opts *cmdOptions) parse() bool {
|
||||
flag.StringVar(&opts.key, "key", "", "Public key(s) for SSH authentication")
|
||||
flag.StringVar(&opts.listen, "listen", "", "Address or port to listen on")
|
||||
|
||||
flag.Usage = func() {
|
||||
builder := strings.Builder{}
|
||||
builder.WriteString("Usage: ")
|
||||
builder.WriteString(os.Args[0])
|
||||
builder.WriteString(" [command] [flags]\n")
|
||||
builder.WriteString("\nCommands:\n")
|
||||
builder.WriteString(" health Check if the agent is running\n")
|
||||
builder.WriteString(" help Display this help message\n")
|
||||
builder.WriteString(" update Update to the latest version\n")
|
||||
builder.WriteString("\nFlags:\n")
|
||||
fmt.Print(builder.String())
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
subcommand := ""
|
||||
if len(os.Args) > 1 {
|
||||
subcommand = os.Args[1]
|
||||
}
|
||||
|
||||
// Subcommands that don't require any pflag parsing
|
||||
switch subcommand {
|
||||
case "-v", "version":
|
||||
fmt.Println(beszel.AppName+"-agent", beszel.Version)
|
||||
return true
|
||||
case "help":
|
||||
flag.Usage()
|
||||
return true
|
||||
case "update":
|
||||
agent.Update()
|
||||
return true
|
||||
case "health":
|
||||
err := health.Check()
|
||||
if err != nil {
|
||||
@@ -44,57 +63,7 @@ func (opts *cmdOptions) parse() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// pflag.CommandLine.ParseErrorsWhitelist.UnknownFlags = true
|
||||
pflag.StringVarP(&opts.key, "key", "k", "", "Public key(s) for SSH authentication")
|
||||
pflag.StringVarP(&opts.listen, "listen", "l", "", "Address or port to listen on")
|
||||
// pflag.StringVarP(&opts.hubURL, "hub-url", "u", "", "URL of the hub to use")
|
||||
// pflag.StringVarP(&opts.token, "token", "t", "", "Token to use for authentication")
|
||||
chinaMirrors := pflag.BoolP("china-mirrors", "c", false, "Use mirror for update (gh.beszel.dev) instead of GitHub")
|
||||
help := pflag.BoolP("help", "h", false, "Show this help message")
|
||||
|
||||
// Convert old single-dash long flags to double-dash for backward compatibility
|
||||
flagsToConvert := []string{"key", "listen"}
|
||||
for i, arg := range os.Args {
|
||||
for _, flag := range flagsToConvert {
|
||||
singleDash := "-" + flag
|
||||
doubleDash := "--" + flag
|
||||
if arg == singleDash {
|
||||
os.Args[i] = doubleDash
|
||||
break
|
||||
} else if strings.HasPrefix(arg, singleDash+"=") {
|
||||
os.Args[i] = doubleDash + arg[len(singleDash):]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pflag.Usage = func() {
|
||||
builder := strings.Builder{}
|
||||
builder.WriteString("Usage: ")
|
||||
builder.WriteString(os.Args[0])
|
||||
builder.WriteString(" [command] [flags]\n")
|
||||
builder.WriteString("\nCommands:\n")
|
||||
builder.WriteString(" health Check if the agent is running\n")
|
||||
// builder.WriteString(" help Display this help message\n")
|
||||
builder.WriteString(" update Update to the latest version\n")
|
||||
builder.WriteString("\nFlags:\n")
|
||||
fmt.Print(builder.String())
|
||||
pflag.PrintDefaults()
|
||||
}
|
||||
|
||||
// Parse all arguments with pflag
|
||||
pflag.Parse()
|
||||
|
||||
// Must run after pflag.Parse()
|
||||
switch {
|
||||
case *help || subcommand == "help":
|
||||
pflag.Usage()
|
||||
return true
|
||||
case subcommand == "update":
|
||||
agent.Update(*chinaMirrors)
|
||||
return true
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"beszel/internal/agent"
|
||||
"crypto/ed25519"
|
||||
"flag"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/henrygd/beszel/agent"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/ssh"
|
||||
@@ -246,7 +245,7 @@ func TestParseFlags(t *testing.T) {
|
||||
oldArgs := os.Args
|
||||
defer func() {
|
||||
os.Args = oldArgs
|
||||
pflag.CommandLine = pflag.NewFlagSet(os.Args[0], pflag.ExitOnError)
|
||||
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
@@ -270,22 +269,6 @@ func TestParseFlags(t *testing.T) {
|
||||
listen: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "key flag double dash",
|
||||
args: []string{"cmd", "--key", "testkey"},
|
||||
expected: cmdOptions{
|
||||
key: "testkey",
|
||||
listen: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "key flag short",
|
||||
args: []string{"cmd", "-k", "testkey"},
|
||||
expected: cmdOptions{
|
||||
key: "testkey",
|
||||
listen: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addr flag only",
|
||||
args: []string{"cmd", "-listen", ":8080"},
|
||||
@@ -294,22 +277,6 @@ func TestParseFlags(t *testing.T) {
|
||||
listen: ":8080",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addr flag double dash",
|
||||
args: []string{"cmd", "--listen", ":8080"},
|
||||
expected: cmdOptions{
|
||||
key: "",
|
||||
listen: ":8080",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addr flag short",
|
||||
args: []string{"cmd", "-l", ":8080"},
|
||||
expected: cmdOptions{
|
||||
key: "",
|
||||
listen: ":8080",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "both flags",
|
||||
args: []string{"cmd", "-key", "testkey", "-listen", ":8080"},
|
||||
@@ -323,12 +290,12 @@ func TestParseFlags(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Reset flags for each test
|
||||
pflag.CommandLine = pflag.NewFlagSet(tt.args[0], pflag.ExitOnError)
|
||||
flag.CommandLine = flag.NewFlagSet(tt.args[0], flag.ExitOnError)
|
||||
os.Args = tt.args
|
||||
|
||||
var opts cmdOptions
|
||||
opts.parse()
|
||||
pflag.Parse()
|
||||
flag.Parse()
|
||||
|
||||
assert.Equal(t, tt.expected, opts)
|
||||
})
|
||||
@@ -1,16 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/hub"
|
||||
_ "beszel/migrations"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
"github.com/henrygd/beszel/src/hub"
|
||||
_ "github.com/henrygd/beszel/src/migrations"
|
||||
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/plugins/migratecmd"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -46,13 +45,11 @@ func getBaseApp() *pocketbase.PocketBase {
|
||||
baseApp.RootCmd.Use = beszel.AppName
|
||||
baseApp.RootCmd.Short = ""
|
||||
// add update command
|
||||
updateCmd := &cobra.Command{
|
||||
baseApp.RootCmd.AddCommand(&cobra.Command{
|
||||
Use: "update",
|
||||
Short: "Update " + beszel.AppName + " to the latest version",
|
||||
Run: hub.Update,
|
||||
}
|
||||
updateCmd.Flags().Bool("china-mirrors", false, "Use mirror (gh.beszel.dev) instead of GitHub")
|
||||
baseApp.RootCmd.AddCommand(updateCmd)
|
||||
})
|
||||
// add health command
|
||||
baseApp.RootCmd.AddCommand(newHealthCmd())
|
||||
|
||||
@@ -2,15 +2,15 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ../go.mod ../go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source files
|
||||
COPY . ./
|
||||
COPY go.mod go.sum ./
|
||||
# RUN go mod download
|
||||
COPY *.go ./
|
||||
COPY cmd ./cmd
|
||||
COPY internal ./internal
|
||||
|
||||
# Build
|
||||
ARG TARGETOS TARGETARCH
|
||||
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./src/cmd/agent
|
||||
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./cmd/agent
|
||||
|
||||
RUN rm -rf /tmp/*
|
||||
|
||||
@@ -15,7 +15,7 @@ RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-
|
||||
# --------------------------
|
||||
# Final image: GPU-enabled agent with nvidia-smi
|
||||
# --------------------------
|
||||
FROM nvidia/cuda:12.2.2-base-ubuntu22.04
|
||||
FROM nvidia/cuda:12.9.1-base-ubuntu22.04
|
||||
COPY --from=builder /agent /agent
|
||||
|
||||
ENTRYPOINT ["/agent"]
|
||||
@@ -3,11 +3,16 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# Download Go modules
|
||||
COPY ../go.mod ../go.sum ./
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source files
|
||||
COPY . ./
|
||||
COPY *.go ./
|
||||
COPY cmd ./cmd
|
||||
COPY internal ./internal
|
||||
COPY migrations ./migrations
|
||||
COPY site/dist ./site/dist
|
||||
COPY site/*.go ./site
|
||||
|
||||
RUN apk add --no-cache \
|
||||
unzip \
|
||||
@@ -17,7 +22,7 @@ RUN update-ca-certificates
|
||||
|
||||
# Build
|
||||
ARG TARGETOS TARGETARCH
|
||||
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /beszel ./src/cmd/hub
|
||||
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /beszel ./cmd/hub
|
||||
|
||||
# ? -------------------------
|
||||
FROM scratch
|
||||
@@ -1,27 +1,26 @@
|
||||
module github.com/henrygd/beszel
|
||||
module beszel
|
||||
|
||||
go 1.25.1
|
||||
go 1.24.4
|
||||
|
||||
// lock shoutrrr to specific version to allow review before updating
|
||||
replace github.com/nicholas-fedor/shoutrrr => github.com/nicholas-fedor/shoutrrr v0.8.8
|
||||
|
||||
require (
|
||||
github.com/blang/semver v3.5.1+incompatible
|
||||
github.com/distatus/battery v0.11.0
|
||||
github.com/fxamacker/cbor/v2 v2.9.0
|
||||
github.com/gliderlabs/ssh v0.3.8
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/lxzan/gws v1.8.9
|
||||
github.com/nicholas-fedor/shoutrrr v0.8.17
|
||||
github.com/nicholas-fedor/shoutrrr v0.8.15
|
||||
github.com/pocketbase/dbx v1.11.0
|
||||
github.com/pocketbase/pocketbase v0.29.3
|
||||
github.com/pocketbase/pocketbase v0.29.0
|
||||
github.com/rhysd/go-github-selfupdate v1.2.3
|
||||
github.com/shirou/gopsutil/v4 v4.25.6
|
||||
github.com/spf13/cast v1.9.2
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/pflag v1.0.7
|
||||
github.com/stretchr/testify v1.11.0
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/crypto v0.40.0
|
||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -40,30 +39,34 @@ require (
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.1 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3 // indirect
|
||||
github.com/google/go-github/v30 v30.1.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250821153705-5981dea3221d // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/spf13/pflag v1.0.7 // indirect
|
||||
github.com/tcnksm/go-gitconfig v0.1.2 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/image v0.30.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/image v0.29.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
howett.net/plist v1.0.1 // indirect
|
||||
modernc.org/libc v1.66.3 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
modernc.org/libc v1.65.10 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.38.2 // indirect
|
||||
modernc.org/sqlite v1.38.0 // indirect
|
||||
)
|
||||
@@ -13,8 +13,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/distatus/battery v0.11.0 h1:KJk89gz90Iq/wJtbjjM9yUzBXV+ASV/EG2WOOL7N8lc=
|
||||
github.com/distatus/battery v0.11.0/go.mod h1:KmVkE8A8hpIX4T78QRdMktYpEp35QfOL8A8dwZBxq2k=
|
||||
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
|
||||
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
|
||||
github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=
|
||||
@@ -27,6 +25,7 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
@@ -47,28 +46,41 @@ github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtS
|
||||
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo=
|
||||
github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
|
||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jarcoal/httpmock v1.4.0 h1:BvhqnH0JAYbNudL2GMJKgOHe2CtKlzJ/5rWKyp+hc2k=
|
||||
github.com/jarcoal/httpmock v1.4.0/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lufia/plan9stats v0.0.0-20250821153705-5981dea3221d h1:vFzYZc8yji+9DmNRhpEbs8VBK4CgV/DPfGzeVJSSp/8=
|
||||
github.com/lufia/plan9stats v0.0.0-20250821153705-5981dea3221d/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/lxzan/gws v1.8.9 h1:VU3SGUeWlQrEwfUSfokcZep8mdg/BrUF+y73YYshdBM=
|
||||
github.com/lxzan/gws v1.8.9/go.mod h1:d9yHaR1eDTBHagQC6KY7ycUOaz5KWeqQtP3xu7aMK8Y=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
@@ -79,8 +91,11 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/nicholas-fedor/shoutrrr v0.8.8 h1:F/oyoatWK5cbHPPgkjRZrA0262TP7KWuUQz9KskRtR8=
|
||||
github.com/nicholas-fedor/shoutrrr v0.8.8/go.mod h1:T30Y+eoZFEjDk4HtOItcHQioZSOe3Z6a6aNfSz6jc5c=
|
||||
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
|
||||
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -88,12 +103,14 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU=
|
||||
github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
|
||||
github.com/pocketbase/pocketbase v0.29.3 h1:Mj8o5awsbVJIdIoTuQNhfC2oL/c4aImQ3RyfFZlzFVg=
|
||||
github.com/pocketbase/pocketbase v0.29.3/go.mod h1:oGpT67LObxCFK4V2fSL7J9YnPbBnnshOpJ5v3zcneww=
|
||||
github.com/pocketbase/pocketbase v0.29.0 h1:oL6qvkU2QSybClVtQdaq9Z1F3Wk59iKYCfIaf1R8KUs=
|
||||
github.com/pocketbase/pocketbase v0.29.0/go.mod h1:SqyH7o/3e+/uLySATlJqxH4S8gyU6R0adG56ZSV1vuU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rhysd/go-github-selfupdate v1.2.3 h1:iaa+J202f+Nc+A8zi75uccC8Wg3omaM7HDeimXA22Ag=
|
||||
github.com/rhysd/go-github-selfupdate v1.2.3/go.mod h1:mp/N8zj6jFfBQy/XMYoWsmfzxazpPAODuqarmPDe2Rg=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
@@ -108,12 +125,17 @@ github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
|
||||
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
|
||||
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
|
||||
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
|
||||
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
@@ -121,50 +143,65 @@ github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4=
|
||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
|
||||
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=
|
||||
golang.org/x/image v0.29.0/go.mod h1:RVJROnf3SLK8d26OW91j4FrIHGbsJ8QnbEocVTOWQDA=
|
||||
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
|
||||
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
|
||||
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
|
||||
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
@@ -175,6 +212,8 @@ modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.65.10 h1:ZwEk8+jhW7qBjHIT+wd0d9VjitRyQef9BnzlzGwMODc=
|
||||
modernc.org/libc v1.65.10/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po=
|
||||
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
||||
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
@@ -185,8 +224,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
||||
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
|
||||
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
@@ -2,6 +2,8 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/entities/system"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"log/slog"
|
||||
@@ -12,8 +14,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/henrygd/beszel"
|
||||
"github.com/henrygd/beszel/src/entities/system"
|
||||
"github.com/shirou/gopsutil/v4/host"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
@@ -1,9 +1,8 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/src/entities/system"
|
||||
)
|
||||
|
||||
// Not thread safe since we only access from gatherStats which is already locked
|
||||
@@ -4,18 +4,17 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/src/entities/system"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSessionCache_GetSet(t *testing.T) {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
synctest.Run(func() {
|
||||
cache := NewSessionCache(69 * time.Second)
|
||||
|
||||
testData := &system.CombinedData{
|
||||
@@ -1,6 +1,8 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/common"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -13,9 +15,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
"github.com/henrygd/beszel/src/common"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/lxzan/gws"
|
||||
"golang.org/x/crypto/ssh"
|
||||
@@ -4,6 +4,8 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/common"
|
||||
"crypto/ed25519"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -11,10 +13,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
|
||||
"github.com/henrygd/beszel/src/common"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -1,14 +1,13 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/agent/health"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/agent/health"
|
||||
)
|
||||
|
||||
// ConnectionManager manages the connection state and events for the agent.
|
||||
@@ -1,6 +1,7 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -8,8 +9,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/src/entities/system"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/container"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
@@ -14,8 +15,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/src/entities/container"
|
||||
|
||||
"github.com/blang/semver"
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
@@ -12,8 +13,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/src/entities/system"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
@@ -4,13 +4,12 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/src/entities/system"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -39,7 +39,7 @@ func TestHealth(t *testing.T) {
|
||||
// This test uses synctest to simulate time passing.
|
||||
// NOTE: This test requires GOEXPERIMENT=synctest to run.
|
||||
t.Run("check with simulated time", func(t *testing.T) {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
synctest.Run(func() {
|
||||
// Update the file to set the initial timestamp.
|
||||
require.NoError(t, Update(), "Update() failed inside synctest")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
@@ -10,8 +11,6 @@ import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/henrygd/beszel/src/entities/system"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/common"
|
||||
"github.com/shirou/gopsutil/v4/sensors"
|
||||
)
|
||||
@@ -4,13 +4,12 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/henrygd/beszel/src/entities/system"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/common"
|
||||
"github.com/shirou/gopsutil/v4/sensors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -46,10 +46,9 @@ var lhmFs embed.FS
|
||||
var (
|
||||
beszelLhm *lhmProcess
|
||||
beszelLhmOnce sync.Once
|
||||
useLHM = os.Getenv("LHM") == "true"
|
||||
)
|
||||
|
||||
var errNoSensors = errors.New("no sensors found (try running as admin with LHM=true)")
|
||||
var errNoSensors = errors.New("no sensors found (try running as admin)")
|
||||
|
||||
// newlhmProcess copies the embedded LHM executable to a temporary directory and starts it.
|
||||
func newlhmProcess() (*lhmProcess, error) {
|
||||
@@ -140,7 +139,7 @@ func (lhm *lhmProcess) cleanupProcess() {
|
||||
}
|
||||
|
||||
func (lhm *lhmProcess) getTemps(ctx context.Context) (temps []sensors.TemperatureStat, err error) {
|
||||
if !useLHM || lhm.stoppedNoSensors {
|
||||
if lhm.stoppedNoSensors {
|
||||
// Fall back to gopsutil if we can't get sensors from LHM
|
||||
return sensors.TemperaturesWithContext(ctx)
|
||||
}
|
||||
@@ -223,10 +222,6 @@ func getSensorTemps(ctx context.Context) (temps []sensors.TemperatureStat, err e
|
||||
}
|
||||
}()
|
||||
|
||||
if !useLHM {
|
||||
return sensors.TemperaturesWithContext(ctx)
|
||||
}
|
||||
|
||||
// Initialize process once
|
||||
beszelLhmOnce.Do(func() {
|
||||
beszelLhm, err = newlhmProcess()
|
||||
@@ -1,6 +1,9 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/common"
|
||||
"beszel/internal/entities/system"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -11,10 +14,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
"github.com/henrygd/beszel/src/common"
|
||||
"github.com/henrygd/beszel/src/entities/system"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/gliderlabs/ssh"
|
||||
@@ -1,6 +1,8 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/container"
|
||||
"beszel/internal/entities/system"
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
@@ -13,9 +15,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/src/entities/container"
|
||||
"github.com/henrygd/beszel/src/entities/system"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/gliderlabs/ssh"
|
||||
@@ -1,6 +1,8 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/entities/system"
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
@@ -9,10 +11,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
"github.com/henrygd/beszel/agent/battery"
|
||||
"github.com/henrygd/beszel/src/entities/system"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/cpu"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"github.com/shirou/gopsutil/v4/host"
|
||||
@@ -61,10 +59,10 @@ func (a *Agent) initializeSystemInfo() {
|
||||
}
|
||||
|
||||
// zfs
|
||||
if _, err := getARCSize(); err != nil {
|
||||
slog.Debug("Not monitoring ZFS ARC", "err", err)
|
||||
} else {
|
||||
if _, err := getARCSize(); err == nil {
|
||||
a.zfs = true
|
||||
} else {
|
||||
slog.Debug("Not monitoring ZFS ARC", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,11 +70,6 @@ func (a *Agent) initializeSystemInfo() {
|
||||
func (a *Agent) getSystemStats() system.Stats {
|
||||
systemStats := system.Stats{}
|
||||
|
||||
// battery
|
||||
if battery.HasReadableBattery() {
|
||||
systemStats.Battery[0], systemStats.Battery[1], _ = battery.GetBatteryStats()
|
||||
}
|
||||
|
||||
// cpu percent
|
||||
cpuPct, err := cpu.Percent(0, false)
|
||||
if err != nil {
|
||||
@@ -87,6 +80,7 @@ func (a *Agent) getSystemStats() system.Stats {
|
||||
|
||||
// load average
|
||||
if avgstat, err := load.Avg(); err == nil {
|
||||
// TODO: remove these in future release in favor of load avg array
|
||||
systemStats.LoadAvg[0] = avgstat.Load1
|
||||
systemStats.LoadAvg[1] = avgstat.Load5
|
||||
systemStats.LoadAvg[2] = avgstat.Load15
|
||||
56
beszel/internal/agent/update.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/rhysd/go-github-selfupdate/selfupdate"
|
||||
)
|
||||
|
||||
// Update updates beszel-agent to the latest version
|
||||
func Update() {
|
||||
var latest *selfupdate.Release
|
||||
var found bool
|
||||
var err error
|
||||
currentVersion := semver.MustParse(beszel.Version)
|
||||
fmt.Println("beszel-agent", currentVersion)
|
||||
fmt.Println("Checking for updates...")
|
||||
updater, _ := selfupdate.NewUpdater(selfupdate.Config{
|
||||
Filters: []string{"beszel-agent"},
|
||||
})
|
||||
latest, found, err = updater.DetectLatest("henrygd/beszel")
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Error checking for updates:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if !found {
|
||||
fmt.Println("No updates found")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
fmt.Println("Latest version:", latest.Version)
|
||||
|
||||
if latest.Version.LTE(currentVersion) {
|
||||
fmt.Println("You are up to date")
|
||||
return
|
||||
}
|
||||
|
||||
var binaryPath string
|
||||
fmt.Printf("Updating from %s to %s...\n", currentVersion, latest.Version)
|
||||
binaryPath, err = os.Executable()
|
||||
if err != nil {
|
||||
fmt.Println("Error getting binary path:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = selfupdate.UpdateTo(latest.AssetURL, binaryPath)
|
||||
if err != nil {
|
||||
fmt.Println("Please try rerunning with sudo. Error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Successfully updated to %s\n\n%s\n", latest.Version, strings.TrimSpace(latest.ReleaseNotes))
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/mailer"
|
||||
)
|
||||
@@ -87,7 +88,7 @@ var supportsTitle = map[string]struct{}{
|
||||
func NewAlertManager(app hubLike) *AlertManager {
|
||||
am := &AlertManager{
|
||||
hub: app,
|
||||
alertQueue: make(chan alertTask, 5),
|
||||
alertQueue: make(chan alertTask),
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
am.bindEvents()
|
||||
@@ -205,14 +206,16 @@ func (am *AlertManager) SendShoutrrrAlert(notificationUrl, title, message, link,
|
||||
}
|
||||
|
||||
func (am *AlertManager) SendTestNotification(e *core.RequestEvent) error {
|
||||
var data struct {
|
||||
URL string `json:"url"`
|
||||
info, _ := e.RequestInfo()
|
||||
if info.Auth == nil {
|
||||
return apis.NewForbiddenError("Forbidden", nil)
|
||||
}
|
||||
err := e.BindBody(&data)
|
||||
if err != nil || data.URL == "" {
|
||||
return e.BadRequestError("URL is required", err)
|
||||
url := e.Request.URL.Query().Get("url")
|
||||
// log.Println("url", url)
|
||||
if url == "" {
|
||||
return e.JSON(200, map[string]string{"err": "URL is required"})
|
||||
}
|
||||
err = am.SendShoutrrrAlert(data.URL, "Test Alert", "This is a notification from Beszel.", am.hub.Settings().Meta.AppURL, "View Beszel")
|
||||
err := am.SendShoutrrrAlert(url, "Test Alert", "This is a notification from Beszel.", am.hub.Settings().Meta.AppURL, "View Beszel")
|
||||
if err != nil {
|
||||
return e.JSON(200, map[string]string{"err": err.Error()})
|
||||
}
|
||||
@@ -12,7 +12,7 @@ func resolveHistoryOnAlertDelete(e *core.RecordEvent) error {
|
||||
if !e.Record.GetBool("triggered") {
|
||||
return e.Next()
|
||||
}
|
||||
_ = resolveAlertHistoryRecord(e.App, e.Record.Id)
|
||||
_ = resolveAlertHistoryRecord(e.App, e.Record)
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
@@ -36,16 +36,27 @@ func updateHistoryOnAlertUpdate(e *core.RecordEvent) error {
|
||||
}
|
||||
|
||||
// if new state is not triggered, check for matching alert history record and set it to resolved
|
||||
_ = resolveAlertHistoryRecord(e.App, new.Id)
|
||||
_ = resolveAlertHistoryRecord(e.App, new)
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
// resolveAlertHistoryRecord sets the resolved field to the current time
|
||||
func resolveAlertHistoryRecord(app core.App, alertRecordID string) error {
|
||||
alertHistoryRecord, err := app.FindFirstRecordByFilter("alerts_history", "alert_id={:alert_id} && resolved=null", dbx.Params{"alert_id": alertRecordID})
|
||||
if err != nil || alertHistoryRecord == nil {
|
||||
func resolveAlertHistoryRecord(app core.App, alertRecord *core.Record) error {
|
||||
alertHistoryRecords, err := app.FindRecordsByFilter(
|
||||
"alerts_history",
|
||||
"alert_id={:alert_id} && resolved=null",
|
||||
"-created",
|
||||
1,
|
||||
0,
|
||||
dbx.Params{"alert_id": alertRecord.Id},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(alertHistoryRecords) == 0 {
|
||||
return nil
|
||||
}
|
||||
alertHistoryRecord := alertHistoryRecords[0] // there should be only one record
|
||||
alertHistoryRecord.Set("resolved", time.Now().UTC())
|
||||
err = app.Save(alertHistoryRecord)
|
||||
if err != nil {
|
||||
@@ -1,13 +1,12 @@
|
||||
package alerts
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/src/entities/system"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
@@ -3,9 +3,8 @@ package system
|
||||
// TODO: this is confusing, make common package with common/types common/helpers etc
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/container"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/src/entities/container"
|
||||
)
|
||||
|
||||
type Stats struct {
|
||||
@@ -37,10 +36,8 @@ type Stats struct {
|
||||
LoadAvg15 float64 `json:"l15,omitempty" cbor:"25,keyasint,omitempty"`
|
||||
Bandwidth [2]uint64 `json:"b,omitzero" cbor:"26,keyasint,omitzero"` // [sent bytes, recv bytes]
|
||||
MaxBandwidth [2]uint64 `json:"bm,omitzero" cbor:"27,keyasint,omitzero"` // [sent bytes, recv bytes]
|
||||
LoadAvg [3]float64 `json:"la,omitempty" cbor:"28,keyasint"`
|
||||
// TODO: remove other load fields in future release in favor of load avg array
|
||||
LoadAvg [3]float64 `json:"la,omitempty" cbor:"28,keyasint"`
|
||||
Battery [2]uint8 `json:"bat,omitzero" cbor:"29,keyasint,omitzero"` // [percent, charge state, current]
|
||||
MaxMem float64 `json:"mm,omitempty" cbor:"30,keyasint,omitempty"`
|
||||
}
|
||||
|
||||
type GPUData struct {
|
||||
@@ -84,27 +81,27 @@ const (
|
||||
)
|
||||
|
||||
type Info struct {
|
||||
Hostname string `json:"h" cbor:"0,keyasint"`
|
||||
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"`
|
||||
Cores int `json:"c" cbor:"2,keyasint"`
|
||||
Threads int `json:"t,omitempty" cbor:"3,keyasint,omitempty"`
|
||||
CpuModel string `json:"m" cbor:"4,keyasint"`
|
||||
Uptime uint64 `json:"u" cbor:"5,keyasint"`
|
||||
Cpu float64 `json:"cpu" cbor:"6,keyasint"`
|
||||
MemPct float64 `json:"mp" cbor:"7,keyasint"`
|
||||
DiskPct float64 `json:"dp" cbor:"8,keyasint"`
|
||||
Bandwidth float64 `json:"b" cbor:"9,keyasint"`
|
||||
AgentVersion string `json:"v" cbor:"10,keyasint"`
|
||||
Podman bool `json:"p,omitempty" cbor:"11,keyasint,omitempty"`
|
||||
GpuPct float64 `json:"g,omitempty" cbor:"12,keyasint,omitempty"`
|
||||
DashboardTemp float64 `json:"dt,omitempty" cbor:"13,keyasint,omitempty"`
|
||||
Os Os `json:"os" cbor:"14,keyasint"`
|
||||
LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"`
|
||||
LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"`
|
||||
LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"`
|
||||
BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"`
|
||||
Hostname string `json:"h" cbor:"0,keyasint"`
|
||||
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"`
|
||||
Cores int `json:"c" cbor:"2,keyasint"`
|
||||
Threads int `json:"t,omitempty" cbor:"3,keyasint,omitempty"`
|
||||
CpuModel string `json:"m" cbor:"4,keyasint"`
|
||||
Uptime uint64 `json:"u" cbor:"5,keyasint"`
|
||||
Cpu float64 `json:"cpu" cbor:"6,keyasint"`
|
||||
MemPct float64 `json:"mp" cbor:"7,keyasint"`
|
||||
DiskPct float64 `json:"dp" cbor:"8,keyasint"`
|
||||
Bandwidth float64 `json:"b" cbor:"9,keyasint"`
|
||||
AgentVersion string `json:"v" cbor:"10,keyasint"`
|
||||
Podman bool `json:"p,omitempty" cbor:"11,keyasint,omitempty"`
|
||||
GpuPct float64 `json:"g,omitempty" cbor:"12,keyasint,omitempty"`
|
||||
DashboardTemp float64 `json:"dt,omitempty" cbor:"13,keyasint,omitempty"`
|
||||
Os Os `json:"os" cbor:"14,keyasint"`
|
||||
LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"`
|
||||
LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"`
|
||||
LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"`
|
||||
BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"`
|
||||
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
|
||||
// TODO: remove load fields in future release in favor of load avg array
|
||||
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
|
||||
}
|
||||
|
||||
// Final data structure to return to the hub
|
||||
@@ -1,6 +1,9 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"beszel/internal/common"
|
||||
"beszel/internal/hub/expirymap"
|
||||
"beszel/internal/hub/ws"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -8,10 +11,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/src/common"
|
||||
"github.com/henrygd/beszel/src/hub/expirymap"
|
||||
"github.com/henrygd/beszel/src/hub/ws"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/lxzan/gws"
|
||||
"github.com/pocketbase/dbx"
|
||||
@@ -4,6 +4,9 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"beszel/internal/agent"
|
||||
"beszel/internal/common"
|
||||
"beszel/internal/hub/ws"
|
||||
"crypto/ed25519"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -14,10 +17,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/agent"
|
||||
"github.com/henrygd/beszel/src/common"
|
||||
"github.com/henrygd/beszel/src/hub/ws"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
pbtests "github.com/pocketbase/pocketbase/tests"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -2,15 +2,15 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/henrygd/beszel/src/entities/system"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/spf13/cast"
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -279,8 +279,9 @@ func createFingerprintRecord(app core.App, systemID, token string) error {
|
||||
|
||||
// Returns the current config.yml file as a JSON object
|
||||
func GetYamlConfig(e *core.RequestEvent) error {
|
||||
if e.Auth.GetString("role") != "admin" {
|
||||
return e.ForbiddenError("Requires admin role", nil)
|
||||
info, _ := e.RequestInfo()
|
||||
if info.Auth == nil || info.Auth.GetString("role") != "admin" {
|
||||
return apis.NewForbiddenError("Forbidden", nil)
|
||||
}
|
||||
configContent, err := generateYAML(e.App)
|
||||
if err != nil {
|
||||
@@ -4,14 +4,12 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"beszel/internal/hub/config"
|
||||
"beszel/internal/tests"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/henrygd/beszel/src/tests"
|
||||
|
||||
"github.com/henrygd/beszel/src/hub/config"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -2,23 +2,25 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/alerts"
|
||||
"beszel/internal/hub/config"
|
||||
"beszel/internal/hub/systems"
|
||||
"beszel/internal/records"
|
||||
"beszel/internal/users"
|
||||
"beszel/site"
|
||||
"crypto/ed25519"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
"github.com/henrygd/beszel/src/alerts"
|
||||
"github.com/henrygd/beszel/src/hub/config"
|
||||
"github.com/henrygd/beszel/src/hub/systems"
|
||||
"github.com/henrygd/beszel/src/records"
|
||||
"github.com/henrygd/beszel/src/users"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
@@ -162,6 +164,55 @@ func (h *Hub) initialize(e *core.ServeEvent) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// startServer sets up the server for Beszel
|
||||
func (h *Hub) startServer(se *core.ServeEvent) error {
|
||||
// TODO: exclude dev server from production binary
|
||||
switch h.IsDev() {
|
||||
case true:
|
||||
proxy := httputil.NewSingleHostReverseProxy(&url.URL{
|
||||
Scheme: "http",
|
||||
Host: "localhost:5173",
|
||||
})
|
||||
se.Router.GET("/{path...}", func(e *core.RequestEvent) error {
|
||||
proxy.ServeHTTP(e.Response, e.Request)
|
||||
return nil
|
||||
})
|
||||
default:
|
||||
// parse app url
|
||||
parsedURL, err := url.Parse(h.appURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// fix base paths in html if using subpath
|
||||
basePath := strings.TrimSuffix(parsedURL.Path, "/") + "/"
|
||||
indexFile, _ := fs.ReadFile(site.DistDirFS, "index.html")
|
||||
indexContent := strings.ReplaceAll(string(indexFile), "./", basePath)
|
||||
indexContent = strings.Replace(indexContent, "{{V}}", beszel.Version, 1)
|
||||
indexContent = strings.Replace(indexContent, "{{HUB_URL}}", h.appURL, 1)
|
||||
// set up static asset serving
|
||||
staticPaths := [2]string{"/static/", "/assets/"}
|
||||
serveStatic := apis.Static(site.DistDirFS, false)
|
||||
// get CSP configuration
|
||||
csp, cspExists := GetEnv("CSP")
|
||||
// add route
|
||||
se.Router.GET("/{path...}", func(e *core.RequestEvent) error {
|
||||
// serve static assets if path is in staticPaths
|
||||
for i := range staticPaths {
|
||||
if strings.Contains(e.Request.URL.Path, staticPaths[i]) {
|
||||
e.Response.Header().Set("Cache-Control", "public, max-age=2592000")
|
||||
return serveStatic(e)
|
||||
}
|
||||
}
|
||||
if cspExists {
|
||||
e.Response.Header().Del("X-Frame-Options")
|
||||
e.Response.Header().Set("Content-Security-Policy", csp)
|
||||
}
|
||||
return e.HTML(http.StatusOK, indexContent)
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// registerCronJobs sets up scheduled tasks
|
||||
func (h *Hub) registerCronJobs(_ *core.ServeEvent) error {
|
||||
// delete old system_stats and alerts_history records once every hour
|
||||
@@ -173,48 +224,48 @@ func (h *Hub) registerCronJobs(_ *core.ServeEvent) error {
|
||||
|
||||
// custom api routes
|
||||
func (h *Hub) registerApiRoutes(se *core.ServeEvent) error {
|
||||
// auth protected routes
|
||||
apiAuth := se.Router.Group("/api/beszel")
|
||||
apiAuth.Bind(apis.RequireAuth())
|
||||
// auth optional routes
|
||||
apiNoAuth := se.Router.Group("/api/beszel")
|
||||
|
||||
// create first user endpoint only needed if no users exist
|
||||
if totalUsers, _ := se.App.CountRecords("users"); totalUsers == 0 {
|
||||
apiNoAuth.POST("/create-user", h.um.CreateFirstUser)
|
||||
}
|
||||
// check if first time setup on login page
|
||||
apiNoAuth.GET("/first-run", func(e *core.RequestEvent) error {
|
||||
total, err := e.App.CountRecords("users")
|
||||
return e.JSON(http.StatusOK, map[string]bool{"firstRun": err == nil && total == 0})
|
||||
})
|
||||
// get public key and version
|
||||
apiAuth.GET("/getkey", func(e *core.RequestEvent) error {
|
||||
// returns public key and version
|
||||
se.Router.GET("/api/beszel/getkey", func(e *core.RequestEvent) error {
|
||||
info, _ := e.RequestInfo()
|
||||
if info.Auth == nil {
|
||||
return apis.NewForbiddenError("Forbidden", nil)
|
||||
}
|
||||
return e.JSON(http.StatusOK, map[string]string{"key": h.pubKey, "v": beszel.Version})
|
||||
})
|
||||
// check if first time setup on login page
|
||||
se.Router.GET("/api/beszel/first-run", func(e *core.RequestEvent) error {
|
||||
total, err := h.CountRecords("users")
|
||||
return e.JSON(http.StatusOK, map[string]bool{"firstRun": err == nil && total == 0})
|
||||
})
|
||||
// send test notification
|
||||
apiAuth.POST("/test-notification", h.SendTestNotification)
|
||||
// get config.yml content
|
||||
apiAuth.GET("/config-yaml", config.GetYamlConfig)
|
||||
se.Router.GET("/api/beszel/send-test-notification", h.SendTestNotification)
|
||||
// API endpoint to get config.yml content
|
||||
se.Router.GET("/api/beszel/config-yaml", config.GetYamlConfig)
|
||||
// handle agent websocket connection
|
||||
apiNoAuth.GET("/agent-connect", h.handleAgentConnect)
|
||||
se.Router.GET("/api/beszel/agent-connect", h.handleAgentConnect)
|
||||
// get or create universal tokens
|
||||
apiAuth.GET("/universal-token", h.getUniversalToken)
|
||||
// update / delete user alerts
|
||||
apiAuth.POST("/user-alerts", alerts.UpsertUserAlerts)
|
||||
apiAuth.DELETE("/user-alerts", alerts.DeleteUserAlerts)
|
||||
|
||||
se.Router.GET("/api/beszel/universal-token", h.getUniversalToken)
|
||||
// create first user endpoint only needed if no users exist
|
||||
if totalUsers, _ := h.CountRecords("users"); totalUsers == 0 {
|
||||
se.Router.POST("/api/beszel/create-user", h.um.CreateFirstUser)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handler for universal token API endpoint (create, read, delete)
|
||||
func (h *Hub) getUniversalToken(e *core.RequestEvent) error {
|
||||
info, err := e.RequestInfo()
|
||||
if err != nil || info.Auth == nil {
|
||||
return apis.NewForbiddenError("Forbidden", nil)
|
||||
}
|
||||
|
||||
tokenMap := universalTokenMap.GetMap()
|
||||
userID := e.Auth.Id
|
||||
userID := info.Auth.Id
|
||||
query := e.Request.URL.Query()
|
||||
token := query.Get("token")
|
||||
tokenSet := token != ""
|
||||
|
||||
if token == "" {
|
||||
if !tokenSet {
|
||||
// return existing token if it exists
|
||||
if token, _, ok := tokenMap.GetByValue(userID); ok {
|
||||
return e.JSON(http.StatusOK, map[string]any{"token": token, "active": true})
|
||||
256
beszel/internal/hub/hub_test.go
Normal file
@@ -0,0 +1,256 @@
|
||||
//go:build testing
|
||||
// +build testing
|
||||
|
||||
package hub_test
|
||||
|
||||
import (
|
||||
"beszel/internal/tests"
|
||||
"testing"
|
||||
|
||||
"crypto/ed25519"
|
||||
"encoding/pem"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func getTestHub(t testing.TB) *tests.TestHub {
|
||||
hub, _ := tests.NewTestHub(t.TempDir())
|
||||
return hub
|
||||
}
|
||||
|
||||
func TestMakeLink(t *testing.T) {
|
||||
hub := getTestHub(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
appURL string
|
||||
parts []string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "no parts, no trailing slash in AppURL",
|
||||
appURL: "http://localhost:8090",
|
||||
parts: []string{},
|
||||
expected: "http://localhost:8090",
|
||||
},
|
||||
{
|
||||
name: "no parts, with trailing slash in AppURL",
|
||||
appURL: "http://localhost:8090/",
|
||||
parts: []string{},
|
||||
expected: "http://localhost:8090", // TrimSuffix should handle the trailing slash
|
||||
},
|
||||
{
|
||||
name: "one part",
|
||||
appURL: "http://example.com",
|
||||
parts: []string{"one"},
|
||||
expected: "http://example.com/one",
|
||||
},
|
||||
{
|
||||
name: "multiple parts",
|
||||
appURL: "http://example.com",
|
||||
parts: []string{"alpha", "beta", "gamma"},
|
||||
expected: "http://example.com/alpha/beta/gamma",
|
||||
},
|
||||
{
|
||||
name: "parts with spaces needing escaping",
|
||||
appURL: "http://example.com",
|
||||
parts: []string{"path with spaces", "another part"},
|
||||
expected: "http://example.com/path%20with%20spaces/another%20part",
|
||||
},
|
||||
{
|
||||
name: "parts with slashes needing escaping",
|
||||
appURL: "http://example.com",
|
||||
parts: []string{"a/b", "c"},
|
||||
expected: "http://example.com/a%2Fb/c", // url.PathEscape escapes '/'
|
||||
},
|
||||
{
|
||||
name: "AppURL with subpath, no trailing slash",
|
||||
appURL: "http://localhost/sub",
|
||||
parts: []string{"resource"},
|
||||
expected: "http://localhost/sub/resource",
|
||||
},
|
||||
{
|
||||
name: "AppURL with subpath, with trailing slash",
|
||||
appURL: "http://localhost/sub/",
|
||||
parts: []string{"item"},
|
||||
expected: "http://localhost/sub/item",
|
||||
},
|
||||
{
|
||||
name: "empty parts in the middle",
|
||||
appURL: "http://localhost",
|
||||
parts: []string{"first", "", "third"},
|
||||
expected: "http://localhost/first/third",
|
||||
},
|
||||
{
|
||||
name: "leading and trailing empty parts",
|
||||
appURL: "http://localhost",
|
||||
parts: []string{"", "path", ""},
|
||||
expected: "http://localhost/path",
|
||||
},
|
||||
{
|
||||
name: "parts with various special characters",
|
||||
appURL: "https://test.dev/",
|
||||
parts: []string{"p@th?", "key=value&"},
|
||||
expected: "https://test.dev/p@th%3F/key=value&",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Store original app URL and restore it after the test
|
||||
originalAppURL := hub.Settings().Meta.AppURL
|
||||
hub.Settings().Meta.AppURL = tt.appURL
|
||||
defer func() { hub.Settings().Meta.AppURL = originalAppURL }()
|
||||
|
||||
got := hub.MakeLink(tt.parts...)
|
||||
assert.Equal(t, tt.expected, got, "MakeLink generated URL does not match expected")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSSHKey(t *testing.T) {
|
||||
hub := getTestHub(t)
|
||||
|
||||
// Test Case 1: Key generation (no existing key)
|
||||
t.Run("KeyGeneration", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Ensure pubKey is initially empty or different to ensure GetSSHKey sets it
|
||||
hub.SetPubkey("")
|
||||
|
||||
signer, err := hub.GetSSHKey(tempDir)
|
||||
assert.NoError(t, err, "GetSSHKey should not error when generating a new key")
|
||||
assert.NotNil(t, signer, "GetSSHKey should return a non-nil signer")
|
||||
|
||||
// Check if private key file was created
|
||||
privateKeyPath := filepath.Join(tempDir, "id_ed25519")
|
||||
info, err := os.Stat(privateKeyPath)
|
||||
assert.NoError(t, err, "Private key file should be created")
|
||||
assert.False(t, info.IsDir(), "Private key path should be a file, not a directory")
|
||||
|
||||
// Check if h.pubKey was set
|
||||
assert.NotEmpty(t, hub.GetPubkey(), "h.pubKey should be set after key generation")
|
||||
assert.True(t, strings.HasPrefix(hub.GetPubkey(), "ssh-ed25519 "), "h.pubKey should start with 'ssh-ed25519 '")
|
||||
|
||||
// Verify the generated private key is parsable
|
||||
keyData, err := os.ReadFile(privateKeyPath)
|
||||
require.NoError(t, err)
|
||||
_, err = ssh.ParsePrivateKey(keyData)
|
||||
assert.NoError(t, err, "Generated private key should be parsable by ssh.ParsePrivateKey")
|
||||
})
|
||||
|
||||
// Test Case 2: Existing key
|
||||
t.Run("ExistingKey", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Manually create a valid key pair for the test
|
||||
rawPubKey, rawPrivKey, err := ed25519.GenerateKey(nil)
|
||||
require.NoError(t, err, "Failed to generate raw ed25519 key pair for pre-existing key test")
|
||||
|
||||
// Marshal the private key into OpenSSH PEM format
|
||||
pemBlock, err := ssh.MarshalPrivateKey(rawPrivKey, "")
|
||||
require.NoError(t, err, "Failed to marshal private key to PEM block for pre-existing key test")
|
||||
|
||||
privateKeyBytes := pem.EncodeToMemory(pemBlock)
|
||||
require.NotNil(t, privateKeyBytes, "PEM encoded private key bytes should not be nil")
|
||||
|
||||
privateKeyPath := filepath.Join(tempDir, "id_ed25519")
|
||||
err = os.WriteFile(privateKeyPath, privateKeyBytes, 0600)
|
||||
require.NoError(t, err, "Failed to write pre-existing private key")
|
||||
|
||||
// Determine the expected public key string
|
||||
sshPubKey, err := ssh.NewPublicKey(rawPubKey)
|
||||
require.NoError(t, err)
|
||||
expectedPubKeyStr := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(sshPubKey)))
|
||||
|
||||
// Reset h.pubKey to ensure it's set by GetSSHKey from the file
|
||||
hub.SetPubkey("")
|
||||
|
||||
signer, err := hub.GetSSHKey(tempDir)
|
||||
assert.NoError(t, err, "GetSSHKey should not error when reading an existing key")
|
||||
assert.NotNil(t, signer, "GetSSHKey should return a non-nil signer for an existing key")
|
||||
|
||||
// Check if h.pubKey was set correctly to the public key from the file
|
||||
assert.Equal(t, expectedPubKeyStr, hub.GetPubkey(), "h.pubKey should match the existing public key")
|
||||
|
||||
// Verify the signer's public key matches the original public key
|
||||
signerPubKey := signer.PublicKey()
|
||||
marshaledSignerPubKey := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(signerPubKey)))
|
||||
assert.Equal(t, expectedPubKeyStr, marshaledSignerPubKey, "Signer's public key should match the existing public key")
|
||||
})
|
||||
|
||||
// Test Case 3: Error cases
|
||||
t.Run("ErrorCases", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFunc func(dir string) error
|
||||
errorCheck func(t *testing.T, err error)
|
||||
}{
|
||||
{
|
||||
name: "CorruptedKey",
|
||||
setupFunc: func(dir string) error {
|
||||
return os.WriteFile(filepath.Join(dir, "id_ed25519"), []byte("this is not a valid SSH key"), 0600)
|
||||
},
|
||||
errorCheck: func(t *testing.T, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "ssh: no key found")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "PermissionDenied",
|
||||
setupFunc: func(dir string) error {
|
||||
// Create the key file
|
||||
keyPath := filepath.Join(dir, "id_ed25519")
|
||||
if err := os.WriteFile(keyPath, []byte("dummy content"), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
// Make it read-only (can't be opened for writing in case a new key needs to be written)
|
||||
return os.Chmod(keyPath, 0400)
|
||||
},
|
||||
errorCheck: func(t *testing.T, err error) {
|
||||
// On read-only key, the parser will attempt to parse it and fail with "ssh: no key found"
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "EmptyFile",
|
||||
setupFunc: func(dir string) error {
|
||||
// Create an empty file
|
||||
return os.WriteFile(filepath.Join(dir, "id_ed25519"), []byte{}, 0600)
|
||||
},
|
||||
errorCheck: func(t *testing.T, err error) {
|
||||
assert.Error(t, err)
|
||||
// The error from attempting to parse an empty file
|
||||
assert.Contains(t, err.Error(), "ssh: no key found")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Setup the test case
|
||||
err := tc.setupFunc(tempDir)
|
||||
require.NoError(t, err, "Setup failed")
|
||||
|
||||
// Reset h.pubKey before each test case
|
||||
hub.SetPubkey("")
|
||||
|
||||
// Attempt to get SSH key
|
||||
_, err = hub.GetSSHKey(tempDir)
|
||||
|
||||
// Verify the error
|
||||
tc.errorCheck(t, err)
|
||||
|
||||
// Check that pubKey was not set in error cases
|
||||
assert.Empty(t, hub.GetPubkey(), "h.pubKey should not be set if there was an error")
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
package hub
|
||||
|
||||
import "github.com/henrygd/beszel/src/hub/systems"
|
||||
import "beszel/internal/hub/systems"
|
||||
|
||||
// TESTING ONLY: GetSystemManager returns the system manager
|
||||
func (h *Hub) GetSystemManager() *systems.SystemManager {
|
||||
@@ -1,6 +1,9 @@
|
||||
package systems
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/entities/system"
|
||||
"beszel/internal/hub/ws"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -10,12 +13,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/src/hub/ws"
|
||||
|
||||
"github.com/henrygd/beszel/src/entities/system"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
@@ -1,18 +1,14 @@
|
||||
package systems
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/common"
|
||||
"beszel/internal/entities/system"
|
||||
"beszel/internal/hub/ws"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/src/hub/ws"
|
||||
|
||||
"github.com/henrygd/beszel/src/entities/system"
|
||||
|
||||
"github.com/henrygd/beszel/src/common"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/store"
|
||||
@@ -34,8 +30,10 @@ const (
|
||||
sessionTimeout = 4 * time.Second
|
||||
)
|
||||
|
||||
// errSystemExists is returned when attempting to add a system that already exists
|
||||
var errSystemExists = errors.New("system exists")
|
||||
var (
|
||||
// errSystemExists is returned when attempting to add a system that already exists
|
||||
errSystemExists = errors.New("system exists")
|
||||
)
|
||||
|
||||
// SystemManager manages a collection of monitored systems and their connections.
|
||||
// It handles system lifecycle, status updates, and maintains both SSH and WebSocket connections.
|
||||
@@ -4,17 +4,16 @@
|
||||
package systems_test
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/container"
|
||||
"beszel/internal/entities/system"
|
||||
"beszel/internal/hub/systems"
|
||||
"beszel/internal/tests"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/src/entities/container"
|
||||
"github.com/henrygd/beszel/src/entities/system"
|
||||
"github.com/henrygd/beszel/src/hub/systems"
|
||||
"github.com/henrygd/beszel/src/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -30,7 +29,7 @@ func TestSystemManagerNew(t *testing.T) {
|
||||
user, err := tests.CreateUser(hub, "test@test.com", "testtesttest")
|
||||
require.NoError(t, err)
|
||||
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
synctest.Run(func() {
|
||||
sm.Initialize()
|
||||
|
||||
record, err := tests.CreateRecord(hub, "systems", map[string]any{
|
||||
@@ -111,11 +110,9 @@ func TestSystemManagerNew(t *testing.T) {
|
||||
err = hub.Delete(record)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, sm.HasSystem(record.Id), "System should not exist in the store after deletion")
|
||||
})
|
||||
|
||||
testOld(t, hub)
|
||||
testOld(t, hub)
|
||||
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
time.Sleep(time.Second)
|
||||
synctest.Wait()
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
package systems
|
||||
|
||||
import (
|
||||
entities "beszel/internal/entities/system"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
entities "github.com/henrygd/beszel/src/entities/system"
|
||||
)
|
||||
|
||||
// TESTING ONLY: GetSystemCount returns the number of systems in the store
|
||||
@@ -101,10 +100,3 @@ func (sm *SystemManager) SetSystemStatusInDB(systemID string, status string) boo
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// TESTING ONLY: RemoveAllSystems removes all systems from the store
|
||||
func (sm *SystemManager) RemoveAllSystems() {
|
||||
for _, system := range sm.systems.GetAll() {
|
||||
sm.RemoveSystem(system.Id)
|
||||
}
|
||||
}
|
||||
57
beszel/internal/hub/update.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/rhysd/go-github-selfupdate/selfupdate"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Update updates beszel to the latest version
|
||||
func Update(_ *cobra.Command, _ []string) {
|
||||
var latest *selfupdate.Release
|
||||
var found bool
|
||||
var err error
|
||||
currentVersion := semver.MustParse(beszel.Version)
|
||||
fmt.Println("beszel", currentVersion)
|
||||
fmt.Println("Checking for updates...")
|
||||
updater, _ := selfupdate.NewUpdater(selfupdate.Config{
|
||||
Filters: []string{"beszel_"},
|
||||
})
|
||||
latest, found, err = updater.DetectLatest("henrygd/beszel")
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Error checking for updates:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if !found {
|
||||
fmt.Println("No updates found")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
fmt.Println("Latest version:", latest.Version)
|
||||
|
||||
if latest.Version.LTE(currentVersion) {
|
||||
fmt.Println("You are up to date")
|
||||
return
|
||||
}
|
||||
|
||||
var binaryPath string
|
||||
fmt.Printf("Updating from %s to %s...\n", currentVersion, latest.Version)
|
||||
binaryPath, err = os.Executable()
|
||||
if err != nil {
|
||||
fmt.Println("Error getting binary path:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = selfupdate.UpdateTo(latest.AssetURL, binaryPath)
|
||||
if err != nil {
|
||||
fmt.Println("Please try rerunning with sudo. Error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Successfully updated to %s\n\n%s\n", latest.Version, strings.TrimSpace(latest.ReleaseNotes))
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"beszel/internal/common"
|
||||
"beszel/internal/entities/system"
|
||||
"errors"
|
||||
"time"
|
||||
"weak"
|
||||
|
||||
"github.com/henrygd/beszel/src/entities/system"
|
||||
|
||||
"github.com/henrygd/beszel/src/common"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/lxzan/gws"
|
||||
"golang.org/x/crypto/ssh"
|
||||
@@ -4,12 +4,11 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"beszel/internal/common"
|
||||
"crypto/ed25519"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/src/common"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -2,6 +2,8 @@
|
||||
package records
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/container"
|
||||
"beszel/internal/entities/system"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -9,9 +11,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/src/entities/container"
|
||||
"github.com/henrygd/beszel/src/entities/system"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
@@ -40,14 +39,12 @@ type StatsRecord struct {
|
||||
}
|
||||
|
||||
// global variables for reusing allocations
|
||||
var (
|
||||
statsRecord StatsRecord
|
||||
containerStats []container.Stats
|
||||
sumStats system.Stats
|
||||
tempStats system.Stats
|
||||
queryParams = make(dbx.Params, 1)
|
||||
containerSums = make(map[string]*container.Stats)
|
||||
)
|
||||
var statsRecord StatsRecord
|
||||
var containerStats []container.Stats
|
||||
var sumStats system.Stats
|
||||
var tempStats system.Stats
|
||||
var queryParams = make(dbx.Params, 1)
|
||||
var containerSums = make(map[string]*container.Stats)
|
||||
|
||||
// Create longer records by averaging shorter records
|
||||
func (rm *RecordManager) CreateLongerRecords() {
|
||||
@@ -175,8 +172,6 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
|
||||
tempStats = system.Stats{}
|
||||
sum := &sumStats
|
||||
stats := &tempStats
|
||||
// necessary because uint8 is not big enough for the sum
|
||||
batterySum := 0
|
||||
|
||||
count := float64(len(records))
|
||||
tempCount := float64(0)
|
||||
@@ -213,11 +208,8 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
|
||||
sum.LoadAvg[2] += stats.LoadAvg[2]
|
||||
sum.Bandwidth[0] += stats.Bandwidth[0]
|
||||
sum.Bandwidth[1] += stats.Bandwidth[1]
|
||||
batterySum += int(stats.Battery[0])
|
||||
sum.Battery[1] = stats.Battery[1]
|
||||
// Set peak values
|
||||
sum.MaxCpu = max(sum.MaxCpu, stats.MaxCpu, stats.Cpu)
|
||||
sum.MaxMem = max(sum.MaxMem, stats.MaxMem, stats.MemUsed)
|
||||
sum.MaxNetworkSent = max(sum.MaxNetworkSent, stats.MaxNetworkSent, stats.NetworkSent)
|
||||
sum.MaxNetworkRecv = max(sum.MaxNetworkRecv, stats.MaxNetworkRecv, stats.NetworkRecv)
|
||||
sum.MaxDiskReadPs = max(sum.MaxDiskReadPs, stats.MaxDiskReadPs, stats.DiskReadPs)
|
||||
@@ -298,7 +290,6 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
|
||||
sum.LoadAvg[2] = twoDecimals(sum.LoadAvg[2] / count)
|
||||
sum.Bandwidth[0] = sum.Bandwidth[0] / uint64(count)
|
||||
sum.Bandwidth[1] = sum.Bandwidth[1] / uint64(count)
|
||||
sum.Battery[0] = uint8(batterySum / int(count))
|
||||
// Average temperatures
|
||||
if sum.Temperatures != nil && tempCount > 0 {
|
||||
for key := range sum.Temperatures {
|
||||
@@ -4,13 +4,12 @@
|
||||
package records_test
|
||||
|
||||
import (
|
||||
"beszel/internal/records"
|
||||
"beszel/internal/tests"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/src/records"
|
||||
"github.com/henrygd/beszel/src/tests"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
@@ -5,14 +5,10 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/henrygd/beszel/src/hub"
|
||||
"beszel/internal/hub"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
_ "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
@@ -90,38 +86,3 @@ func CreateRecord(app core.App, collectionName string, fields map[string]any) (*
|
||||
|
||||
return record, app.Save(record)
|
||||
}
|
||||
|
||||
func ClearCollection(t testing.TB, app core.App, collectionName string) error {
|
||||
_, err := app.DB().NewQuery(fmt.Sprintf("DELETE from %s", collectionName)).Execute()
|
||||
recordCount, err := app.CountRecords(collectionName)
|
||||
assert.EqualValues(t, recordCount, 0, "should have 0 records after clearing")
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *TestHub) Cleanup() {
|
||||
h.GetAlertManager().StopWorker()
|
||||
h.GetSystemManager().RemoveAllSystems()
|
||||
h.TestApp.Cleanup()
|
||||
}
|
||||
|
||||
func CreateSystems(app core.App, count int, userId string, status string) ([]*core.Record, error) {
|
||||
systems := make([]*core.Record, 0, count)
|
||||
for i := range count {
|
||||
system, err := CreateRecord(app, "systems", map[string]any{
|
||||
"name": fmt.Sprintf("test-system-%d", i),
|
||||
"host": fmt.Sprintf("127.0.0.%d", i),
|
||||
"port": "33914",
|
||||
"users": []string{userId},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
system.Set("status", status)
|
||||
err = app.SaveNoValidate(system)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
systems = append(systems, system)
|
||||
}
|
||||
return systems, nil
|
||||
}
|
||||
@@ -2,11 +2,10 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"beszel/migrations"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/henrygd/beszel/src/migrations"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
29
beszel/migrations/initial-settings.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
const (
|
||||
TempAdminEmail = "_@b.b"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
// initial settings
|
||||
settings := app.Settings()
|
||||
settings.Meta.AppName = "Beszel"
|
||||
settings.Meta.HideControls = true
|
||||
settings.Logs.MinLevel = 4
|
||||
if err := app.Save(settings); err != nil {
|
||||
return err
|
||||
}
|
||||
// create superuser
|
||||
collection, _ := app.FindCollectionByNameOrId(core.CollectionNameSuperusers)
|
||||
user := core.NewRecord(collection)
|
||||
user.SetEmail(TempAdminEmail)
|
||||
user.SetRandomPassword()
|
||||
return app.Save(user)
|
||||
}, nil)
|
||||
}
|
||||
BIN
beszel/site/bun.lockb
Executable file
2437
src/site/package-lock.json → beszel/site/package-lock.json
generated
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "beszel",
|
||||
"private": true,
|
||||
"version": "0.12.7",
|
||||
"version": "0.12.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"dev": "vite",
|
||||
"build": "lingui extract --overwrite && lingui compile && vite build",
|
||||
"preview": "vite preview",
|
||||
"sync": "lingui extract --overwrite && lingui compile",
|
||||
@@ -13,27 +13,26 @@
|
||||
"dependencies": {
|
||||
"@henrygd/queue": "^1.0.7",
|
||||
"@henrygd/semaphore": "^0.0.2",
|
||||
"@lingui/detect-locale": "^5.4.1",
|
||||
"@lingui/macro": "^5.4.1",
|
||||
"@lingui/react": "^5.4.1",
|
||||
"@lingui/detect-locale": "^5.3.3",
|
||||
"@lingui/macro": "^5.3.3",
|
||||
"@lingui/react": "^5.3.3",
|
||||
"@nanostores/react": "^0.7.3",
|
||||
"@nanostores/router": "^0.11.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-direction": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slider": "^1.3.5",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toast": "^1.2.15",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"@radix-ui/react-toast": "^1.2.14",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
@@ -41,26 +40,28 @@
|
||||
"lucide-react": "^0.452.0",
|
||||
"nanostores": "^0.11.4",
|
||||
"pocketbase": "^0.26.2",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"recharts": "^2.15.4",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"valibot": "^0.42.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lingui/cli": "^5.4.1",
|
||||
"@lingui/swc-plugin": "^5.6.1",
|
||||
"@lingui/vite-plugin": "^5.4.1",
|
||||
"@lingui/cli": "^5.3.3",
|
||||
"@lingui/swc-plugin": "^5.5.2",
|
||||
"@lingui/vite-plugin": "^5.3.3",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/vite": "^4.1.12",
|
||||
"@types/bun": "^1.2.20",
|
||||
"@types/react": "^19.1.11",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@vitejs/plugin-react-swc": "^4.0.1",
|
||||
"tailwindcss": "^4.1.12",
|
||||
"tw-animate-css": "^1.3.7",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.1.3"
|
||||
"@types/bun": "^1.2.19",
|
||||
"@types/react": "^18.3.23",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@vitejs/plugin-react-swc": "^3.11.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-rtl": "^0.9.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5"
|
||||
},
|
||||
"overrides": {
|
||||
"@nanostores/router": {
|
||||
6
beszel/site/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
Before Width: | Height: | Size: 906 B After Width: | Height: | Size: 906 B |
|
Before Width: | Height: | Size: 906 B After Width: | Height: | Size: 906 B |
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 903 B |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
@@ -13,15 +13,13 @@ import {
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { $publicKey } from "@/lib/stores"
|
||||
import { cn, generateToken, tokenMap, useBrowserStorage } from "@/lib/utils"
|
||||
import { pb, isReadOnlyUser } from "@/lib/api"
|
||||
import { $publicKey, pb } from "@/lib/stores"
|
||||
import { cn, generateToken, isReadOnlyUser, tokenMap, useLocalStorage } from "@/lib/utils"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { ChevronDownIcon, ExternalLinkIcon, PlusIcon } from "lucide-react"
|
||||
import { memo, useEffect, useRef, useState } from "react"
|
||||
import { $router, basePath, Link, navigate } from "./router"
|
||||
import { SystemRecord } from "@/types"
|
||||
import { SystemStatus } from "@/lib/enums"
|
||||
import { AppleIcon, DockerIcon, TuxIcon, WindowsIcon } from "./ui/icons"
|
||||
import { InputCopy } from "./ui/input-copy"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
@@ -77,7 +75,7 @@ export const SystemDialog = ({ setOpen, system }: { setOpen: (open: boolean) =>
|
||||
const port = useRef<HTMLInputElement>(null)
|
||||
const [hostValue, setHostValue] = useState(system?.host ?? "")
|
||||
const isUnixSocket = hostValue.startsWith("/")
|
||||
const [tab, setTab] = useBrowserStorage("as-tab", "docker")
|
||||
const [tab, setTab] = useLocalStorage("as-tab", "docker")
|
||||
const [token, setToken] = useState(system?.token ?? "")
|
||||
|
||||
useEffect(() => {
|
||||
@@ -97,7 +95,7 @@ export const SystemDialog = ({ setOpen, system }: { setOpen: (open: boolean) =>
|
||||
tokenMap.set(system.id, token)
|
||||
setToken(token)
|
||||
})()
|
||||
}, [system?.id, nextSystemToken])
|
||||
}, [system?.id])
|
||||
|
||||
async function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
@@ -107,7 +105,7 @@ export const SystemDialog = ({ setOpen, system }: { setOpen: (open: boolean) =>
|
||||
try {
|
||||
setOpen(false)
|
||||
if (system) {
|
||||
await pb.collection("systems").update(system.id, { ...data, status: SystemStatus.Pending })
|
||||
await pb.collection("systems").update(system.id, { ...data, status: "pending" })
|
||||
} else {
|
||||
const createdSystem = await pb.collection("systems").create(data)
|
||||
await pb.collection("fingerprints").create({
|
||||
@@ -133,7 +131,7 @@ export const SystemDialog = ({ setOpen, system }: { setOpen: (open: boolean) =>
|
||||
>
|
||||
<Tabs defaultValue={tab} onValueChange={setTab}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="mb-1 pb-1 max-w-100 truncate pr-8">
|
||||
<DialogTitle className="mb-2">
|
||||
{system ? `${t`Edit`} ${system?.name}` : <Trans>Add New System</Trans>}
|
||||
</DialogTitle>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
@@ -167,7 +165,9 @@ export const SystemDialog = ({ setOpen, system }: { setOpen: (open: boolean) =>
|
||||
<Trans>
|
||||
Copy the installation command for the agent below, or register agents automatically with a{" "}
|
||||
<Link
|
||||
onClick={() => setOpen(false)}
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
}}
|
||||
href={getPagePath($router, "settings", { name: "tokens" })}
|
||||
className="link"
|
||||
>
|
||||
@@ -274,7 +274,7 @@ interface CopyButtonProps {
|
||||
text: string
|
||||
onClick: () => void
|
||||
dropdownItems: DropdownItem[]
|
||||
icon?: React.ReactElement<any>
|
||||
icon?: React.ReactElement
|
||||
}
|
||||
|
||||
const CopyButton = memo((props: CopyButtonProps) => {
|
||||
@@ -2,8 +2,7 @@ import { ColumnDef } from "@tanstack/react-table"
|
||||
import { AlertsHistoryRecord } from "@/types"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { formatShortDate, toFixedFloat, formatDuration, cn } from "@/lib/utils"
|
||||
import { alertInfo } from "@/lib/alerts"
|
||||
import { alertInfo, formatShortDate, toFixedFloat, formatDuration, cn } from "@/lib/utils"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { t } from "@lingui/core/macro"
|
||||
|
||||
@@ -16,9 +15,7 @@ export const alertsHistoryColumns: ColumnDef<AlertsHistoryRecord>[] = [
|
||||
<Trans>System</Trans>
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="ps-2 max-w-60 truncate">{row.original.expand?.system?.name || row.original.system}</div>
|
||||
),
|
||||
cell: ({ row }) => <span className="ps-2">{row.original.expand?.system?.name || row.original.system}</span>,
|
||||
filterFn: (row, _, filterValue) => {
|
||||
const display = row.original.expand?.system?.name || row.original.system || ""
|
||||
return display.toLowerCase().includes(filterValue.toLowerCase())
|
||||
153
beszel/site/src/components/alerts/alert-button.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { Trans } from "@lingui/react/macro"
|
||||
import { memo, useMemo, useState } from "react"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { $alerts } from "@/lib/stores"
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { BellIcon, GlobeIcon, ServerIcon } from "lucide-react"
|
||||
import { alertInfo, cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { AlertRecord, SystemRecord } from "@/types"
|
||||
import { $router, Link } from "../router"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Checkbox } from "../ui/checkbox"
|
||||
import { SystemAlert, SystemAlertGlobal } from "./alerts-system"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
|
||||
export default memo(function AlertsButton({ system }: { system: SystemRecord }) {
|
||||
const alerts = useStore($alerts)
|
||||
const [opened, setOpened] = useState(false)
|
||||
|
||||
const hasAlert = alerts.some((alert) => alert.system === system.id)
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="icon" aria-label={t`Alerts`} data-nolink onClick={() => setOpened(true)}>
|
||||
<BellIcon
|
||||
className={cn("h-[1.2em] w-[1.2em] pointer-events-none", {
|
||||
"fill-primary": hasAlert,
|
||||
})}
|
||||
/>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-full sm:max-h-[95svh] overflow-auto max-w-[37rem]">
|
||||
{opened && <AlertDialogContent system={system} />}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
),
|
||||
[opened, hasAlert]
|
||||
)
|
||||
|
||||
// return useMemo(
|
||||
// () => (
|
||||
// <Sheet>
|
||||
// <SheetTrigger asChild>
|
||||
// <Button variant="ghost" size="icon" aria-label={t`Alerts`} data-nolink onClick={() => setOpened(true)}>
|
||||
// <BellIcon
|
||||
// className={cn("h-[1.2em] w-[1.2em] pointer-events-none", {
|
||||
// "fill-primary": hasAlert,
|
||||
// })}
|
||||
// />
|
||||
// </Button>
|
||||
// </SheetTrigger>
|
||||
// <SheetContent className="max-h-full overflow-auto w-[35em] p-4 sm:p-5">
|
||||
// {opened && <AlertDialogContent system={system} />}
|
||||
// </SheetContent>
|
||||
// </Sheet>
|
||||
// ),
|
||||
// [opened, hasAlert]
|
||||
// )
|
||||
})
|
||||
|
||||
function AlertDialogContent({ system }: { system: SystemRecord }) {
|
||||
const alerts = useStore($alerts)
|
||||
const [overwriteExisting, setOverwriteExisting] = useState<boolean | "indeterminate">(false)
|
||||
|
||||
/* key to prevent re-rendering */
|
||||
const alertsSignature: string[] = []
|
||||
|
||||
const systemAlerts = alerts.filter((alert) => {
|
||||
if (alert.system === system.id) {
|
||||
alertsSignature.push(alert.name, alert.min, alert.value)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}) as AlertRecord[]
|
||||
|
||||
return useMemo(() => {
|
||||
const data = Object.keys(alertInfo).map((name) => {
|
||||
const alert = alertInfo[name as keyof typeof alertInfo]
|
||||
return {
|
||||
name: name as keyof typeof alertInfo,
|
||||
alert,
|
||||
system,
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">
|
||||
<Trans>Alerts</Trans>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
See{" "}
|
||||
<Link href={getPagePath($router, "settings", { name: "notifications" })} className="link">
|
||||
notification settings
|
||||
</Link>{" "}
|
||||
to configure how you receive alerts.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Tabs defaultValue="system">
|
||||
<TabsList className="mb-1 -mt-0.5">
|
||||
<TabsTrigger value="system">
|
||||
<ServerIcon className="me-2 h-3.5 w-3.5" />
|
||||
{system.name}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="global">
|
||||
<GlobeIcon className="me-1.5 h-3.5 w-3.5" />
|
||||
<Trans>All Systems</Trans>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="system">
|
||||
<div className="grid gap-3">
|
||||
{data.map((d) => (
|
||||
<SystemAlert key={d.name} system={system} data={d} systemAlerts={systemAlerts} />
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="global">
|
||||
<label
|
||||
htmlFor="ovw"
|
||||
className="mb-3 flex gap-2 items-center justify-center cursor-pointer border rounded-sm py-3 px-4 border-destructive text-destructive font-semibold text-sm"
|
||||
>
|
||||
<Checkbox
|
||||
id="ovw"
|
||||
className="text-destructive border-destructive data-[state=checked]:bg-destructive"
|
||||
checked={overwriteExisting}
|
||||
onCheckedChange={setOverwriteExisting}
|
||||
/>
|
||||
<Trans>Overwrite existing alerts</Trans>
|
||||
</label>
|
||||
<div className="grid gap-3">
|
||||
{data.map((d) => (
|
||||
<SystemAlertGlobal key={d.name} data={d} overwrite={overwriteExisting} />
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</>
|
||||
)
|
||||
}, [alertsSignature.join(""), overwriteExisting])
|
||||
}
|
||||
311
beszel/site/src/components/alerts/alerts-system.tsx
Normal file
@@ -0,0 +1,311 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { Trans, Plural } from "@lingui/react/macro"
|
||||
import { $alerts, $systems, pb } from "@/lib/stores"
|
||||
import { alertInfo, cn } from "@/lib/utils"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { AlertInfo, AlertRecord, SystemRecord } from "@/types"
|
||||
import { lazy, Suspense, useMemo, useState } from "react"
|
||||
import { toast } from "../ui/use-toast"
|
||||
import { BatchService } from "pocketbase"
|
||||
import { getSemaphore } from "@henrygd/semaphore"
|
||||
|
||||
interface AlertData {
|
||||
checked?: boolean
|
||||
val?: number
|
||||
min?: number
|
||||
updateAlert?: (checked: boolean, value: number, min: number) => void
|
||||
name: keyof typeof alertInfo
|
||||
alert: AlertInfo
|
||||
system: SystemRecord
|
||||
}
|
||||
|
||||
const Slider = lazy(() => import("@/components/ui/slider"))
|
||||
|
||||
const failedUpdateToast = () =>
|
||||
toast({
|
||||
title: t`Failed to update alert`,
|
||||
description: t`Please check logs for more details.`,
|
||||
variant: "destructive",
|
||||
})
|
||||
|
||||
export function SystemAlert({
|
||||
system,
|
||||
systemAlerts,
|
||||
data,
|
||||
}: {
|
||||
system: SystemRecord
|
||||
systemAlerts: AlertRecord[]
|
||||
data: AlertData
|
||||
}) {
|
||||
const alert = systemAlerts.find((alert) => alert.name === data.name)
|
||||
|
||||
data.updateAlert = async (checked: boolean, value: number, min: number) => {
|
||||
try {
|
||||
if (alert && !checked) {
|
||||
await pb.collection("alerts").delete(alert.id)
|
||||
} else if (alert && checked) {
|
||||
await pb.collection("alerts").update(alert.id, { value, min, triggered: false })
|
||||
} else if (checked) {
|
||||
pb.collection("alerts").create({
|
||||
system: system.id,
|
||||
user: pb.authStore.record!.id,
|
||||
name: data.name,
|
||||
value: value,
|
||||
min: min,
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
failedUpdateToast()
|
||||
}
|
||||
}
|
||||
|
||||
if (alert) {
|
||||
data.checked = true
|
||||
data.val = alert.value
|
||||
data.min = alert.min || 1
|
||||
}
|
||||
|
||||
return <AlertContent data={data} />
|
||||
}
|
||||
|
||||
export const SystemAlertGlobal = ({ data, overwrite }: { data: AlertData; overwrite: boolean | "indeterminate" }) => {
|
||||
data.checked = false
|
||||
data.val = data.min = 0
|
||||
|
||||
// set of system ids that have an alert for this name when the component is mounted
|
||||
const existingAlertsSystems = useMemo(() => {
|
||||
const map = new Set<string>()
|
||||
const alerts = $alerts.get()
|
||||
for (const alert of alerts) {
|
||||
if (alert.name === data.name) {
|
||||
map.add(alert.system)
|
||||
}
|
||||
}
|
||||
return map
|
||||
}, [])
|
||||
|
||||
data.updateAlert = async (checked: boolean, value: number, min: number) => {
|
||||
const sem = getSemaphore("alerts")
|
||||
await sem.acquire()
|
||||
try {
|
||||
// if another update is waiting behind, don't start this one
|
||||
if (sem.size() > 1) {
|
||||
return
|
||||
}
|
||||
|
||||
const recordData: Partial<AlertRecord> = {
|
||||
value,
|
||||
min,
|
||||
triggered: false,
|
||||
}
|
||||
|
||||
const batch = batchWrapper("alerts", 25)
|
||||
const systems = $systems.get()
|
||||
const currentAlerts = $alerts.get()
|
||||
|
||||
// map of current alerts with this name right now by system id
|
||||
const currentAlertsSystems = new Map<string, AlertRecord>()
|
||||
for (const alert of currentAlerts) {
|
||||
if (alert.name === data.name) {
|
||||
currentAlertsSystems.set(alert.system, alert)
|
||||
}
|
||||
}
|
||||
|
||||
if (overwrite) {
|
||||
existingAlertsSystems.clear()
|
||||
}
|
||||
|
||||
const processSystem = async (system: SystemRecord): Promise<void> => {
|
||||
const existingAlert = existingAlertsSystems.has(system.id)
|
||||
|
||||
if (!overwrite && existingAlert) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentAlert = currentAlertsSystems.get(system.id)
|
||||
|
||||
// delete existing alert if unchecked
|
||||
if (!checked && currentAlert) {
|
||||
return batch.remove(currentAlert.id)
|
||||
}
|
||||
if (checked && currentAlert) {
|
||||
// update existing alert if checked
|
||||
return batch.update(currentAlert.id, recordData)
|
||||
}
|
||||
if (checked) {
|
||||
// create new alert if checked and not existing
|
||||
return batch.create({
|
||||
system: system.id,
|
||||
user: pb.authStore.record!.id,
|
||||
name: data.name,
|
||||
...recordData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// make sure current system is updated in the first batch
|
||||
await processSystem(data.system)
|
||||
for (const system of systems) {
|
||||
if (system.id === data.system.id) {
|
||||
continue
|
||||
}
|
||||
if (sem.size() > 1) {
|
||||
return
|
||||
}
|
||||
await processSystem(system)
|
||||
}
|
||||
await batch.send()
|
||||
} finally {
|
||||
sem.release()
|
||||
}
|
||||
}
|
||||
|
||||
return <AlertContent data={data} />
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a wrapper for performing batch operations on a specified collection.
|
||||
*/
|
||||
function batchWrapper(collection: string, batchSize: number) {
|
||||
let batch: BatchService | undefined
|
||||
let count = 0
|
||||
|
||||
const create = async <T extends Record<string, any>>(options: T) => {
|
||||
batch ||= pb.createBatch()
|
||||
batch.collection(collection).create(options)
|
||||
if (++count >= batchSize) {
|
||||
await send()
|
||||
}
|
||||
}
|
||||
|
||||
const update = async <T extends Record<string, any>>(id: string, data: T) => {
|
||||
batch ||= pb.createBatch()
|
||||
batch.collection(collection).update(id, data)
|
||||
if (++count >= batchSize) {
|
||||
await send()
|
||||
}
|
||||
}
|
||||
|
||||
const remove = async (id: string) => {
|
||||
batch ||= pb.createBatch()
|
||||
batch.collection(collection).delete(id)
|
||||
if (++count >= batchSize) {
|
||||
await send()
|
||||
}
|
||||
}
|
||||
|
||||
const send = async () => {
|
||||
if (count) {
|
||||
await batch?.send({ requestKey: null })
|
||||
batch = undefined
|
||||
count = 0
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
update,
|
||||
remove,
|
||||
send,
|
||||
create,
|
||||
}
|
||||
}
|
||||
|
||||
function AlertContent({ data }: { data: AlertData }) {
|
||||
const { name } = data
|
||||
|
||||
const singleDescription = data.alert.singleDesc?.()
|
||||
|
||||
const [checked, setChecked] = useState(data.checked || false)
|
||||
const [min, setMin] = useState(data.min || 10)
|
||||
const [value, setValue] = useState(data.val || (singleDescription ? 0 : data.alert.start ?? 80))
|
||||
|
||||
const Icon = alertInfo[name].icon
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-muted-foreground/15 hover:border-muted-foreground/20 transition-colors duration-100 group">
|
||||
<label
|
||||
htmlFor={`s${name}`}
|
||||
className={cn("flex flex-row items-center justify-between gap-4 cursor-pointer p-4", {
|
||||
"pb-0": checked,
|
||||
})}
|
||||
>
|
||||
<div className="grid gap-1 select-none">
|
||||
<p className="font-semibold flex gap-3 items-center">
|
||||
<Icon className="h-4 w-4 opacity-85" /> {data.alert.name()}
|
||||
</p>
|
||||
{!checked && <span className="block text-sm text-muted-foreground">{data.alert.desc()}</span>}
|
||||
</div>
|
||||
<Switch
|
||||
id={`s${name}`}
|
||||
checked={checked}
|
||||
onCheckedChange={(newChecked) => {
|
||||
setChecked(newChecked)
|
||||
data.updateAlert?.(newChecked, value, min)
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
{checked && (
|
||||
<div className="grid sm:grid-cols-2 mt-1.5 gap-5 px-4 pb-5 tabular-nums text-muted-foreground">
|
||||
<Suspense fallback={<div className="h-10" />}>
|
||||
{!singleDescription && (
|
||||
<div>
|
||||
<p id={`v${name}`} className="text-sm block h-8">
|
||||
<Trans>
|
||||
Average exceeds{" "}
|
||||
<strong className="text-foreground">
|
||||
{value}
|
||||
{data.alert.unit}
|
||||
</strong>
|
||||
</Trans>
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<Slider
|
||||
aria-labelledby={`v${name}`}
|
||||
defaultValue={[value]}
|
||||
onValueCommit={(val) => {
|
||||
data.updateAlert?.(true, val[0], min)
|
||||
}}
|
||||
onValueChange={(val) => {
|
||||
setValue(val[0])
|
||||
}}
|
||||
step={data.alert.step ?? 1}
|
||||
min={data.alert.min ?? 1}
|
||||
max={alertInfo[name].max ?? 99}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={cn(singleDescription && "col-span-full lowercase")}>
|
||||
<p id={`t${name}`} className="text-sm block h-8 first-letter:uppercase">
|
||||
{singleDescription && (
|
||||
<>
|
||||
{singleDescription}
|
||||
{` `}
|
||||
</>
|
||||
)}
|
||||
<Trans>
|
||||
For <strong className="text-foreground">{min}</strong>{" "}
|
||||
<Plural value={min} one="minute" other="minutes" />
|
||||
</Trans>
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<Slider
|
||||
aria-labelledby={`v${name}`}
|
||||
defaultValue={[min]}
|
||||
onValueCommit={(min) => {
|
||||
data.updateAlert?.(true, value, min[0])
|
||||
}}
|
||||
onValueChange={(val) => {
|
||||
setMin(val[0])
|
||||
}}
|
||||
min={1}
|
||||
max={60}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Suspense>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||
import { cn, formatShortDate, chartMargin } from "@/lib/utils"
|
||||
import { useYAxisWidth } from "./hooks"
|
||||
import { useYAxisWidth, cn, formatShortDate, chartMargin } from "@/lib/utils"
|
||||
import { ChartData, SystemStatsRecord } from "@/types"
|
||||
import { useMemo } from "react"
|
||||
|
||||
export type DataPoint = {
|
||||
label: string
|
||||
dataKey: (data: SystemStatsRecord) => number | undefined
|
||||
color: number | string
|
||||
color: string
|
||||
opacity: number
|
||||
}
|
||||
|
||||
@@ -19,7 +18,6 @@ export default function AreaChartDefault({
|
||||
tickFormatter,
|
||||
contentFormatter,
|
||||
dataPoints,
|
||||
domain,
|
||||
}: // logRender = false,
|
||||
{
|
||||
chartData: ChartData
|
||||
@@ -28,7 +26,6 @@ export default function AreaChartDefault({
|
||||
tickFormatter: (value: number, index: number) => string
|
||||
contentFormatter: ({ value, payload }: { value: number; payload: SystemStatsRecord }) => string
|
||||
dataPoints?: DataPoint[]
|
||||
domain?: [number, number]
|
||||
// logRender?: boolean
|
||||
}) {
|
||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||
@@ -54,7 +51,7 @@ export default function AreaChartDefault({
|
||||
orientation={chartData.orientation}
|
||||
className="tracking-tighter"
|
||||
width={yAxisWidth}
|
||||
domain={domain ?? [0, max ?? "auto"]}
|
||||
domain={[0, max ?? "auto"]}
|
||||
tickFormatter={(value, index) => updateYAxisWidth(tickFormatter(value, index))}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
@@ -71,7 +68,7 @@ export default function AreaChartDefault({
|
||||
}
|
||||
/>
|
||||
{dataPoints?.map((dataPoint, i) => {
|
||||
const color = `var(--chart-${dataPoint.color})`
|
||||
const color = `hsl(var(--chart-${dataPoint.color}))`
|
||||
return (
|
||||
<Area
|
||||
key={i}
|
||||
@@ -1,26 +1,23 @@
|
||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts"
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from "@/components/ui/chart"
|
||||
import { memo, useMemo } from "react"
|
||||
import { cn, formatShortDate, chartMargin, toFixedFloat, formatBytes, decimalString } from "@/lib/utils"
|
||||
import { useYAxisWidth, cn, formatShortDate, chartMargin, toFixedFloat, formatBytes, decimalString } from "@/lib/utils"
|
||||
// import Spinner from '../spinner'
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { $containerFilter, $userSettings } from "@/lib/stores"
|
||||
import { ChartData } from "@/types"
|
||||
import { Separator } from "../ui/separator"
|
||||
import { ChartType, Unit } from "@/lib/enums"
|
||||
import { useYAxisWidth } from "./hooks"
|
||||
|
||||
export default memo(function ContainerChart({
|
||||
dataKey,
|
||||
chartData,
|
||||
chartType,
|
||||
chartConfig,
|
||||
unit = "%",
|
||||
}: {
|
||||
dataKey: string
|
||||
chartData: ChartData
|
||||
chartType: ChartType
|
||||
chartConfig: ChartConfig
|
||||
unit?: string
|
||||
}) {
|
||||
const filter = useStore($containerFilter)
|
||||
@@ -31,6 +28,40 @@ export default memo(function ContainerChart({
|
||||
|
||||
const isNetChart = chartType === ChartType.Network
|
||||
|
||||
const chartConfig = useMemo(() => {
|
||||
const config = {} as Record<string, { label: string; color: string }>
|
||||
const totalUsage = new Map<string, number>()
|
||||
|
||||
// calculate total usage of each container
|
||||
for (const stats of containerData) {
|
||||
for (const key in stats) {
|
||||
if (!key || key === "created") continue
|
||||
|
||||
const currentTotal = totalUsage.get(key) ?? 0
|
||||
const increment = isNetChart
|
||||
? (stats[key]?.nr ?? 0) + (stats[key]?.ns ?? 0)
|
||||
: // @ts-ignore
|
||||
stats[key]?.[dataKey] ?? 0
|
||||
|
||||
totalUsage.set(key, currentTotal + increment)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort keys and generate colors based on usage
|
||||
const sortedEntries = Array.from(totalUsage.entries()).sort(([, a], [, b]) => b - a)
|
||||
|
||||
const length = sortedEntries.length
|
||||
sortedEntries.forEach(([key], i) => {
|
||||
const hue = ((i * 360) / length) % 360
|
||||
config[key] = {
|
||||
label: key,
|
||||
color: `hsl(${hue}, 60%, 55%)`,
|
||||
}
|
||||
})
|
||||
|
||||
return config satisfies ChartConfig
|
||||
}, [chartData])
|
||||
|
||||
const { toolTipFormatter, dataFunction, tickFormatter } = useMemo(() => {
|
||||
const obj = {} as {
|
||||
toolTipFormatter: (item: any, key: string) => React.ReactNode | string
|
||||
@@ -88,14 +119,7 @@ export default memo(function ContainerChart({
|
||||
return obj
|
||||
}, [])
|
||||
|
||||
// Filter with set lookup
|
||||
const filteredKeys = useMemo(() => {
|
||||
if (!filter) {
|
||||
return new Set<string>()
|
||||
}
|
||||
const filterLower = filter.toLowerCase()
|
||||
return new Set(Object.keys(chartConfig).filter((key) => !key.toLowerCase().includes(filterLower)))
|
||||
}, [chartConfig, filter])
|
||||
const filterLower = filter?.toLowerCase()
|
||||
|
||||
// console.log('rendered at', new Date())
|
||||
|
||||
@@ -138,9 +162,9 @@ export default memo(function ContainerChart({
|
||||
content={<ChartTooltipContent filter={filter} contentFormatter={toolTipFormatter} />}
|
||||
/>
|
||||
{Object.keys(chartConfig).map((key) => {
|
||||
const filtered = filteredKeys.has(key)
|
||||
const fillOpacity = filtered ? 0.05 : 0.4
|
||||
const strokeOpacity = filtered ? 0.1 : 1
|
||||
const filtered = filterLower && !key.toLowerCase().includes(filterLower)
|
||||
let fillOpacity = filtered ? 0.05 : 0.4
|
||||
let strokeOpacity = filtered ? 0.1 : 1
|
||||
return (
|
||||
<Area
|
||||
key={key}
|
||||