Compare commits

..

44 Commits

Author SHA1 Message Date
Henry Dollman
42e2e3463e release 0.2.0 2024-08-22 15:43:44 -04:00
Henry Dollman
2ef54bae36 add linux/arm/v7 docker images 2024-08-22 15:29:14 -04:00
Henry Dollman
6766a8120a update go dependencies + hub dockerfile 2024-08-22 15:07:22 -04:00
Henry Dollman
9fcf1e7164 use grid layout for charts 2024-08-22 14:10:03 -04:00
Henry Dollman
130c9bd696 add temperature chart 2024-08-21 22:14:56 -04:00
Henry Dollman
88db920ebe add icon to time select 2024-08-21 15:36:46 -04:00
Henry Dollman
5278805d79 web ui design updates 2024-08-21 15:06:33 -04:00
Henry Dollman
55863f849c get rid of updatedSystem store 2024-08-21 12:50:12 -04:00
Henry Dollman
0590facc89 rerender table if hub version loaded after first render 2024-08-20 18:02:29 -04:00
Henry Dollman
3ff339ba1d change agent check for invalid data to use memory usage rather than networks 2024-08-20 17:40:27 -04:00
Henry Dollman
e92a1d8a4d log sent alert emails in pocketbase 2024-08-20 17:11:16 -04:00
Henry Dollman
2590f5612c add null values to create gaps in chart if expected interval is exceeded. closes #100 2024-08-20 17:10:32 -04:00
Henry Dollman
112685bdf4 Merge branch 'theRealBassist-fix-container-lifetime-check' 2024-08-20 14:23:50 -04:00
Henry Dollman
7e1455161d update container status check 2024-08-20 14:23:24 -04:00
Henry Dollman
be98b77634 Merge branch 'fix-container-lifetime-check' of https://github.com/theRealBassist/beszel into theRealBassist-fix-container-lifetime-check 2024-08-20 14:12:17 -04:00
Henry Dollman
cb75923f9f Merge branch 'theRealBassist-add-agent-version-info' 2024-08-20 14:04:37 -04:00
Henry Dollman
87ab4961fd move gh button + update agent display in table 2024-08-20 14:04:09 -04:00
Henry Dollman
d053f16058 add agent version to getkey route 2024-08-20 14:00:15 -04:00
theRealBassist
19272c05bf Adds a version listing in the hub for each agent. 2024-08-19 17:45:15 -04:00
theRealBassist
e56c5f30e0 Fixes edge cases where container health message does not have expected suffix 2024-08-19 15:29:50 -04:00
Henry Dollman
7cf7b706c1 add check to catch containers in restart loop. closes #103 2024-08-19 13:01:01 -04:00
Henry Dollman
58fe9f723a fix: uri encode system url. closes #102 2024-08-19 12:13:18 -04:00
Henry Dollman
a337ece52e Merge branch 'TOomaAh-feature/change-file-structure' 2024-08-18 18:33:17 -04:00
Henry Dollman
61723a24e0 tweak opacity of non-stacked area charts 2024-08-18 18:32:15 -04:00
Henry Dollman
b7934931cf refactor hub requestJsonFromAgent 2024-08-18 18:30:44 -04:00
Henry Dollman
0566433aa1 move longer records creation to a scheduled job 2024-08-18 18:23:17 -04:00
Henry Dollman
b5607025f7 slight improvements to agent memory usage 2024-08-18 17:45:39 -04:00
Henry Dollman
683dc74cbf update workflows 2024-08-14 14:56:01 -04:00
Henry Dollman
c7e67a9b63 refactor: agent and entities 2024-08-14 14:14:41 -04:00
Henry Dollman
083da9598e refactor: hub 2024-08-14 11:28:43 -04:00
Henry Dollman
f8d2161489 refactor: alerts / emails 2024-08-12 19:13:42 -04:00
Henry Dollman
d0d4e546d9 update dockerfiles and docker workflow 2024-08-11 16:21:02 -04:00
Henry Dollman
2ac5d797d2 update readme / .gitignore 2024-08-11 16:20:29 -04:00
Henry Dollman
4105567051 remove cobra dependency for agent 2024-08-11 15:54:27 -04:00
Henry Dollman
c06eabefe0 add docker compose examples for hub / agent to supplemental 2024-08-11 15:22:30 -04:00
Henry Dollman
9d2192f323 update migrations / gitignore 2024-08-11 15:14:58 -04:00
Henry Dollman
dd55e74ec9 update goreleaser config 2024-08-11 13:57:09 -04:00
Henry Dollman
9da1e5751a move application code into beszel folder 2024-08-11 13:41:57 -04:00
TOomaAh
ea71492d13 fix migrations 2024-08-11 02:18:24 +02:00
THOMAS B
b51770a703 Merge branch 'henrygd:main' into feature/change-file-structure 2024-08-10 11:21:02 +02:00
Henry Dollman
39596b8da8 add -p flag to hub installer and run on 0.0.0.0 2024-08-09 12:17:22 -04:00
TOomaAh
932d126117 remove global variable 2024-08-09 17:58:09 +02:00
TOomaAh
034a5c21eb change agent file structure 2024-08-09 17:52:02 +02:00
TOomaAh
d840178cc0 change hub file structure 2024-08-07 23:59:02 +02:00
119 changed files with 6777 additions and 1866 deletions

View File

@@ -13,11 +13,11 @@ jobs:
matrix:
include:
- image: henrygd/beszel
context: ./hub
dockerfile: ./hub/Dockerfile
context: ./beszel
dockerfile: ./beszel/dockerfile_Hub
- image: henrygd/beszel-agent
context: ./agent
dockerfile: ./agent/Dockerfile
context: ./beszel
dockerfile: ./beszel/dockerfile_Agent
permissions:
contents: read
packages: write
@@ -30,10 +30,10 @@ jobs:
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --no-save --cwd ./hub/site
run: bun install --no-save --cwd ./beszel/site
- name: Build site
run: bun run --cwd ./hub/site build
run: bun run --cwd ./beszel/site build
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -49,6 +49,7 @@ jobs:
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}
# https://github.com/docker/login-action
@@ -66,7 +67,7 @@ jobs:
with:
context: '${{ matrix.context }}'
file: ${{ matrix.dockerfile }}
platforms: linux/amd64,linux/arm64
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: ${{ github.ref_type == 'tag' }}
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}

View File

@@ -21,10 +21,10 @@ jobs:
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --no-save --cwd ./hub/site
run: bun install --no-save --cwd ./beszel/site
- name: Build site
run: bun run --cwd ./hub/site build
run: bun run --cwd ./beszel/site build
- name: Set up Go
uses: actions/setup-go@v5
@@ -34,17 +34,7 @@ jobs:
- name: GoReleaser beszel
uses: goreleaser/goreleaser-action@v6
with:
workdir: ./hub
distribution: goreleaser
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.TOKEN }}
- name: GoReleaser beszel-agent
uses: goreleaser/goreleaser-action@v6
with:
workdir: ./agent
workdir: ./beszel
distribution: goreleaser
version: latest
args: release --clean

7
.gitignore vendored
View File

@@ -3,8 +3,11 @@ pb_data
data
temp
.vscode
beszel
beszel-agent
beszel_data
beszel_data*
dist
dist
*.exe
beszel/cmd/hub/hub
beszel/cmd/agent/agent
node_modules

View File

@@ -1,42 +0,0 @@
# version: 1
project_name: beszel-agent
before:
hooks:
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- freebsd
goarch:
- amd64
- arm64
- arm
- mips64
ignore:
- goos: freebsd
goarch: arm
archives:
- format: tar.gz
name_template: >-
{{ .ProjectName }}_
{{- .Os }}_
{{- .Arch }}
# use zip for windows archives
format_overrides:
- goos: windows
format: zip
changelog:
disable: true
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'

View File

@@ -1,30 +0,0 @@
module beszel-agent
go 1.22.4
require (
github.com/blang/semver v3.5.1+incompatible
github.com/gliderlabs/ssh v0.3.7
github.com/rhysd/go-github-selfupdate v1.2.3
github.com/shirou/gopsutil/v4 v4.24.7
)
require (
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/go-ole/go-ole v1.3.0 // 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/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/tcnksm/go-gitconfig v0.1.2 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.8.0 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/oauth2 v0.22.0 // indirect
golang.org/x/sys v0.23.0 // indirect
)

View File

@@ -1,100 +0,0 @@
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/golang/protobuf v1.2.0/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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/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/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
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/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI=
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I=
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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/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/shirou/gopsutil/v4 v4.24.7 h1:V9UGTK4gQ8HvcnPKf6Zt3XHyQq/peaekfxpJ2HSocJk=
github.com/shirou/gopsutil/v4 v4.24.7/go.mod h1:0uW/073rP7FYLOkvxolUQM5rMOLTNmRXnFKafpb71rw=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.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.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
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/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
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.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
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.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/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=

View File

@@ -1,54 +0,0 @@
package main
import (
"fmt"
"os"
"strings"
"github.com/blang/semver"
"github.com/rhysd/go-github-selfupdate/selfupdate"
)
func updateBeszel() {
var latest *selfupdate.Release
var found bool
var err error
currentVersion := semver.MustParse(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))
}

69
beszel/.goreleaser.yml Normal file
View File

@@ -0,0 +1,69 @@
version: 2
project_name: beszel
before:
hooks:
- go mod tidy
builds:
- id: beszel
binary: beszel
main: cmd/hub/hub.go
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- amd64
- arm64
- arm
- id: beszel-agent
binary: beszel-agent
main: cmd/agent/agent.go
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- freebsd
goarch:
- amd64
- arm64
- arm
- mips64
ignore:
- goos: freebsd
goarch: arm
archives:
- id: beszel
format: tar.gz
builds:
- beszel-agent
name_template: >-
{{ .Binary }}_
{{- .Os }}_
{{- .Arch }}
- id: beszel-agent
format: tar.gz
builds:
- beszel
name_template: >-
{{ .Binary }}_
{{- .Os }}_
{{- .Arch }}
# use zip for windows archives
# format_overrides:
# - goos: windows
# format: zip
changelog:
disable: true
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'

42
beszel/cmd/agent/agent.go Normal file
View File

@@ -0,0 +1,42 @@
package main
import (
"beszel"
"beszel/internal/agent"
"beszel/internal/update"
"fmt"
"log"
"os"
"strings"
)
func main() {
// handle flags / subcommands
if len(os.Args) > 1 {
switch os.Args[1] {
case "-v":
fmt.Println(beszel.AppName+"-agent", beszel.Version)
case "update":
update.UpdateBeszelAgent()
}
os.Exit(0)
}
var pubKey []byte
if pubKeyEnv, exists := os.LookupEnv("KEY"); exists {
pubKey = []byte(pubKeyEnv)
} else {
log.Fatal("KEY environment variable is not set")
}
addr := ":45876"
if portEnvVar, exists := os.LookupEnv("PORT"); exists {
// allow passing an address in the form of "127.0.0.1:45876"
if !strings.Contains(portEnvVar, ":") {
portEnvVar = ":" + portEnvVar
}
addr = portEnvVar
}
agent.NewAgent(pubKey, addr).Run()
}

29
beszel/cmd/hub/hub.go Normal file
View File

@@ -0,0 +1,29 @@
package main
import (
"beszel"
"beszel/internal/hub"
"beszel/internal/update"
_ "beszel/migrations"
"github.com/pocketbase/pocketbase"
"github.com/spf13/cobra"
)
func main() {
app := pocketbase.NewWithConfig(pocketbase.Config{
DefaultDataDir: beszel.AppName + "_data",
})
app.RootCmd.Version = beszel.Version
app.RootCmd.Use = beszel.AppName
app.RootCmd.Short = ""
// add update command
app.RootCmd.AddCommand(&cobra.Command{
Use: "update",
Short: "Update " + beszel.AppName + " to the latest version",
Run: func(_ *cobra.Command, _ []string) { update.UpdateBeszel() },
})
hub.NewHub(app).Run()
}

View File

@@ -2,15 +2,15 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS builder
WORKDIR /app
# Download Go modules
COPY go.mod go.sum ./
RUN go mod download
# RUN go mod download
COPY *.go ./
COPY cmd ./cmd
COPY internal ./internal
# Build
ARG TARGETOS TARGETARCH
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent .
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./cmd/agent
# ? -------------------------
FROM scratch

View File

@@ -8,6 +8,8 @@ RUN go mod download
# Copy source files
COPY *.go ./
COPY cmd ./cmd
COPY internal ./internal
COPY migrations ./migrations
COPY site/dist ./site/dist
COPY site/*.go ./site
@@ -20,7 +22,7 @@ RUN update-ca-certificates
# Build
ARG TARGETOS TARGETARCH
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /beszel .
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /beszel ./cmd/hub
# ? -------------------------
FROM scratch

View File

@@ -4,44 +4,48 @@ go 1.22.4
require (
github.com/blang/semver v3.5.1+incompatible
github.com/gliderlabs/ssh v0.3.7
github.com/goccy/go-json v0.10.3
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61
github.com/pocketbase/dbx v1.10.1
github.com/pocketbase/pocketbase v0.22.18
github.com/pocketbase/pocketbase v0.22.19
github.com/rhysd/go-github-selfupdate v1.2.3
github.com/shirou/gopsutil/v4 v4.24.7
github.com/spf13/cobra v1.8.1
golang.org/x/crypto v0.25.0
golang.org/x/crypto v0.26.0
)
require (
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.9 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
github.com/aws/smithy-go v1.20.3 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.4 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.28 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.28 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.12 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.18 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.16 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.60.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.4 // indirect
github.com/aws/smithy-go v1.20.4 // indirect
github.com/disintegration/imaging v1.6.2 // indirect
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
github.com/ganigeorgiev/fexpr v0.4.1 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/go-github/v30 v30.1.0 // indirect
@@ -52,39 +56,44 @@ require (
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/lufia/plan9stats v0.0.0-20240819163618-b1d8f4d146e7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/ncruces/go-strftime v0.1.9 // 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/cast v1.6.0 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tcnksm/go-gitconfig v0.1.2 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.8.0 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opencensus.io v0.24.0 // indirect
gocloud.dev v0.38.0 // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/term v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
gocloud.dev v0.39.0 // indirect
golang.org/x/image v0.19.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/oauth2 v0.22.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/term v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/time v0.6.0 // indirect
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
google.golang.org/api v0.189.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240725223205-93522f1f2a9f // indirect
google.golang.org/api v0.194.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect
google.golang.org/grpc v1.65.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
modernc.org/gc/v3 v3.0.0-20240722195230-4a140ff9c08e // indirect
modernc.org/libc v1.55.5 // indirect
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect
modernc.org/libc v1.59.9 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.31.1 // indirect
modernc.org/sqlite v1.32.0 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
)

View File

@@ -1,16 +1,16 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
cloud.google.com/go/auth v0.7.2 h1:uiha352VrCDMXg+yoBtaD0tUF4Kv9vrtrWPYXwutnDE=
cloud.google.com/go/auth v0.7.2/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs=
cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI=
cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=
cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ=
cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc=
cloud.google.com/go/auth v0.9.1 h1:+pMtLEV2k0AXKvs/tGZojuj6QaioxfUjOpMsG5Gtx+w=
cloud.google.com/go/auth v0.9.1/go.mod h1:Sw8ocT5mhhXxFklyhT12Eiy0ed6tTrPMCJjSI8KhYLk=
cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM=
cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA=
cloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJahPw=
cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g=
cloud.google.com/go/iam v1.1.13 h1:7zWBXG9ERbMLrzQBRhFliAV+kjcRToDTgQT3CTwYyv4=
cloud.google.com/go/iam v1.1.13/go.mod h1:K8mY0uSXwEXS30KrnVb+j54LB/ntfZu1dr+4zFMNbus=
cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs=
cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
@@ -18,49 +18,51 @@ github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1r
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-sdk-go v1.51.30 h1:RVFkjn9P0JMwnuZCVH0TlV5k9zepHzlbc4943eZMhGw=
github.com/aws/aws-sdk-go v1.51.30/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM=
github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.9 h1:TC2vjvaAv1VNl9A0rm+SeuBjrzXnrlwk6Yop+gKRi38=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.9/go.mod h1:WPv2FRnkIOoDv/8j2gSUsI4qDc7392w5anFB/I89GZ8=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15/go.mod h1:haVfg3761/WF7YPuJOER2MP0k4UAXyHaLclKXB6usDg=
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2 h1:sZXIzO38GZOU+O0C+INqbH7C2yALwfMWpd64tONS/NE=
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2/go.mod h1:Lcxzg5rojyVPU/0eFwLtcyTaek/6Mtic5B1gJo7e/zE=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go-v2 v1.30.4 h1:frhcagrVNrzmT95RJImMHgabt99vkXGslubDaDagTk8=
github.com/aws/aws-sdk-go-v2 v1.30.4/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 h1:70PVAiL15/aBMh5LThwgXdSQorVr91L127ttckI9QQU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4/go.mod h1:/MQxMqci8tlqDH+pjmoLu1i0tbWCUP1hhyMRuFxpQCw=
github.com/aws/aws-sdk-go-v2/config v1.27.28 h1:OTxWGW/91C61QlneCtnD62NLb4W616/NM1jA8LhJqbg=
github.com/aws/aws-sdk-go-v2/config v1.27.28/go.mod h1:uzVRVtJSU5EFv6Fu82AoVFKozJi2ZCY6WRCXj06rbvs=
github.com/aws/aws-sdk-go-v2/credentials v1.17.28 h1:m8+AHY/ND8CMHJnPoH7PJIRakWGa4gbfbxuY9TGTUXM=
github.com/aws/aws-sdk-go-v2/credentials v1.17.28/go.mod h1:6TF7dSc78ehD1SL6KpRIPKMA1GyyWflIkjqg+qmf4+c=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 h1:yjwoSyDZF8Jth+mUk5lSPJCkMC0lMy6FaCD51jm6ayE=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12/go.mod h1:fuR57fAgMk7ot3WcNQfb6rSEn+SUffl7ri+aa8uKysI=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.12 h1:i7cJ1izNlox4ka6cvbHPTztYGtbpW4Je/jyQIKOIU4A=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.12/go.mod h1:lHnam/4CTEVHaANZD54IrpE80VLK+lUU84WEeJ1FJ8M=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 h1:TNyt/+X43KJ9IJJMjKfa3bNTiZbUP7DeCxfbTROESwY=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16/go.mod h1:2DwJF39FlNAUiX5pAc0UNeiz16lK2t7IaFcm0LFHEgc=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 h1:jYfy8UPmd+6kJW5YhY0L1/KftReOGxI/4NtVSTh9O/I=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16/go.mod h1:7ZfEPZxkW42Afq4uQB8H2E2e6ebh6mXTueEpYzjCzcs=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.16 h1:mimdLQkIX1zr8GIPY1ZtALdBQGxcASiBd2MOp8m/dMc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.16/go.mod h1:YHk6owoSwrIsok+cAH9PENCOGoH5PU2EllX4vLtSrsY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.18 h1:GckUnpm4EJOAio1c8o25a+b3lVfwVzC9gnSBqiiNmZM=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.18/go.mod h1:Br6+bxfG33Dk3ynmkhsW2Z/t9D4+lRqdLDNCKi85w0U=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 h1:tJ5RnkHCiSH0jyd6gROjlJtNwov0eGYNz8s8nFcR0jQ=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18/go.mod h1:++NHzT+nAF7ZPrHPsA+ENvsXkOO8wEu+C6RXltAG4/c=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.16 h1:jg16PhLPUiHIj8zYIW6bqzeQSuHVEiWnGA0Brz5Xv2I=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.16/go.mod h1:Uyk1zE1VVdsHSU7096h/rwnXDzOzYQVl+FNPhPw7ShY=
github.com/aws/aws-sdk-go-v2/service/s3 v1.60.0 h1:2QXGJvG19QwqXUvgcdoCOZPyLuvZf8LiXPCN4P53TdI=
github.com/aws/aws-sdk-go-v2/service/s3 v1.60.0/go.mod h1:BSPI0EfnYUuNHPS0uqIo5VrRwzie+Fp+YhQOUs16sKI=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 h1:zCsFCKvbj25i7p1u94imVoO447I/sFv8qq+lGJhRN0c=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.5/go.mod h1:ZeDX1SnKsVlejeuz41GiajjZpRSWR7/42q/EyA/QEiM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 h1:SKvPgvdvmiTWoi0GAJ7AsJfOz3ngVkD/ERbs5pUnHNI=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5/go.mod h1:20sz31hv/WsPa3HhU3hfrIet2kxM4Pe0r20eBZ20Tac=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.4 h1:iAckBT2OeEK/kBDyN/jDtpEExhjeeA/Im2q4X0rJZT8=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.4/go.mod h1:vmSqFK+BVIwVpDAGZB3CoCXHzurt4qBE8lf+I/kRTh0=
github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4=
github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -93,10 +95,15 @@ github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
github.com/ganigeorgiev/fexpr v0.4.1 h1:hpUgbUEEWIZhSDBtf4M9aUNfQQ0BZkGRaMePy7Gcx5k=
github.com/ganigeorgiev/fexpr v0.4.1/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
@@ -139,8 +146,8 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/pprof v0.0.0-20240625030939-27f56978b8b0 h1:e+8XbKB6IMn8A4OAyZccO4pYfB3s7bt6azNIPE7AnPg=
github.com/google/pprof v0.0.0-20240625030939-27f56978b8b0/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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=
@@ -172,6 +179,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61 h1:FwuzbVh87iLiUQj1+uQUsuw9x5t9m5n5g7rG7o4svW4=
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61/go.mod h1:paQfF1YtHe+GrGg5fOgjsjoCX/UKDr9bc1DoWpZfns8=
github.com/lufia/plan9stats v0.0.0-20240819163618-b1d8f4d146e7 h1:5RK988zAqB3/AN3opGfRpoQgAVqr6/A5+qRTi67VUZY=
github.com/lufia/plan9stats v0.0.0-20240819163618-b1d8f4d146e7/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@@ -193,8 +202,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pocketbase/dbx v1.10.1 h1:cw+vsyfCJD8YObOVeqb93YErnlxwYMkNZ4rwN0G0AaA=
github.com/pocketbase/dbx v1.10.1/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
github.com/pocketbase/pocketbase v0.22.18 h1:yVckUhi5GDORqCb0BbtlvRB1CVxHY9HO9btEaeZHVJU=
github.com/pocketbase/pocketbase v0.22.18/go.mod h1:0QFvDOOW7ANId78ChZSagyHbmP6CgMxDQrQFXzeaDpA=
github.com/pocketbase/pocketbase v0.22.19 h1:Hu9J2nsRQIaw8MiDLzE65xUPyMPjf4DcS2f+QmH1G+c=
github.com/pocketbase/pocketbase v0.22.19/go.mod h1:0QFvDOOW7ANId78ChZSagyHbmP6CgMxDQrQFXzeaDpA=
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
@@ -203,8 +214,14 @@ github.com/rhysd/go-github-selfupdate v1.2.3/go.mod h1:mp/N8zj6jFfBQy/XMYoWsmfzx
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=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/shirou/gopsutil/v4 v4.24.7 h1:V9UGTK4gQ8HvcnPKf6Zt3XHyQq/peaekfxpJ2HSocJk=
github.com/shirou/gopsutil/v4 v4.24.7/go.mod h1:0uW/073rP7FYLOkvxolUQM5rMOLTNmRXnFKafpb71rw=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -217,10 +234,14 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.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.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
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=
@@ -229,30 +250,32 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc=
go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4=
go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30=
go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4=
go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0=
gocloud.dev v0.38.0 h1:SpxfaOc/Fp4PeO8ui7wRcCZV0EgXZ+IWcVSLn6ZMSw0=
gocloud.dev v0.38.0/go.mod h1:3XjKvd2E5iVNu/xFImRzjN0d/fkNHe4s0RiKidpEUMQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
gocloud.dev v0.39.0 h1:EYABYGhAalPUaMrbSKOr5lejxoxvXj99nE8XFtsDgds=
gocloud.dev v0.39.0/go.mod h1:drz+VyYNBvrMTW0KZiBAYEdl8lbNZx+OQ7oQvdrFmSQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ=
golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@@ -270,48 +293,51 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
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.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
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.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -325,8 +351,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk=
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/api v0.189.0 h1:equMo30LypAkdkLMBqfeIqtyAnlyig1JSZArl4XPwdI=
google.golang.org/api v0.189.0/go.mod h1:FLWGJKb0hb+pU2j+rJqwbnsF+ym+fQs73rbJ+KAUgy8=
google.golang.org/api v0.194.0 h1:dztZKG9HgtIpbI35FhfuSNR/zmaMVdxNlntHj1sIS4s=
google.golang.org/api v0.194.0/go.mod h1:AgvUFdojGANh3vI+P7EVnxj3AISHllxGCJSFmggmnd0=
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.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -334,12 +360,12 @@ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20240722135656-d784300faade h1:lKFsS7wpngDgSCeFn7MoLy+wBDQZ1UQIJD4UNM1Qvkg=
google.golang.org/genproto v0.0.0-20240722135656-d784300faade/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY=
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY=
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240725223205-93522f1f2a9f h1:RARaIm8pxYuxyNPbBQf5igT7XdOyCNtat1qAT2ZxjU4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240725223205-93522f1f2a9f/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/genproto v0.0.0-20240814211410-ddb44dafa142 h1:oLiyxGgE+rt22duwci1+TG7bg2/L1LQsXwfjPlmuJA0=
google.golang.org/genproto v0.0.0-20240814211410-ddb44dafa142/go.mod h1:G11eXq53iI5Q+kyNOmCvnzBaxEA2Q/Ik5Tj7nqBE8j4=
google.golang.org/genproto/googleapis/api v0.0.0-20240812133136-8ffd90a71988 h1:+/tmTy5zAieooKIXfzDm9KiA3Bv6JBwriRN9LY+yayk=
google.golang.org/genproto/googleapis/api v0.0.0-20240812133136-8ffd90a71988/go.mod h1:4+X6GvPs+25wZKbQq9qyAXrwIRExv7w0Ea6MgZLZiDM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
@@ -363,9 +389,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -373,16 +398,16 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.20.5 h1:s04akhT2dysD0DFOlv9fkQ6oUTLPYgMnnDk9oaqjszM=
modernc.org/ccgo/v4 v4.20.5/go.mod h1:fYXClPUMWxWaz1Xj5sHbzW/ZENEFeuHLToqBxUk41nE=
modernc.org/ccgo/v4 v4.20.7 h1:skrinQsjxWfvj6nbC3ztZPJy+NuwmB3hV9zX/pthNYQ=
modernc.org/ccgo/v4 v4.20.7/go.mod h1:UOkI3JSG2zT4E2ioHlncSOZsXbuDCZLvPi3uMlZT5GY=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.3 h1:Ik4ZcMbC7aY4ZDPUhzXVXi7GMub9QcXLTfXn3mWpNw8=
modernc.org/gc/v2 v2.4.3/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v3 v3.0.0-20240722195230-4a140ff9c08e h1:WPC4v0rNIFb2PY+nBBEEKyugPPRHPzUgyN3xZPpGK58=
modernc.org/gc/v3 v3.0.0-20240722195230-4a140ff9c08e/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.55.5 h1:7duW01NsK0fIJ1xctdujeNDO46yPC89t0TZwDC5fE5k=
modernc.org/libc v1.55.5/go.mod h1:JXguUpMkbw1gknxspNE9XaG+kk9hDAAnBxpA6KGLiyA=
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a h1:CfbpOLEo2IwNzJdMvE8aiRbPMxoTpgAJeyePh0SmO8M=
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.59.9 h1:k+nNDDakwipimgmJ1D9H466LhFeSkaPPycAs1OZiDmY=
modernc.org/libc v1.59.9/go.mod h1:EY/egGEU7Ju66eU6SBqCNYaFUDuc4npICkMWnU5EE3A=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
@@ -391,8 +416,8 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.31.1 h1:XVU0VyzxrYHlBhIs1DiEgSl0ZtdnPtbLVy8hSkzxGrs=
modernc.org/sqlite v1.31.1/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA=
modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s=
modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View File

@@ -1,6 +1,10 @@
package main
package agent
import (
"beszel"
"beszel/internal/entities/container"
"beszel/internal/entities/system"
"bytes"
"context"
"encoding/json"
"fmt"
@@ -11,53 +15,67 @@ import (
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync"
"time"
sshServer "github.com/gliderlabs/ssh"
"github.com/shirou/gopsutil/v4/cpu"
"github.com/shirou/gopsutil/v4/disk"
"github.com/shirou/gopsutil/v4/host"
"github.com/shirou/gopsutil/v4/mem"
"github.com/shirou/gopsutil/v4/sensors"
sshServer "github.com/gliderlabs/ssh"
psutilNet "github.com/shirou/gopsutil/v4/net"
)
var Version = "0.1.2"
var containerStatsMap = make(map[string]*PrevContainerStats)
var containerStatsMutex = &sync.Mutex{}
var sem = make(chan struct{}, 15)
func acquireSemaphore() {
sem <- struct{}{}
type Agent struct {
addr string
pubKey []byte
sem chan struct{}
containerStatsMap map[string]*container.PrevContainerStats
containerStatsMutex *sync.Mutex
diskIoStats *system.DiskIoStats
netIoStats *system.NetIoStats
dockerClient *http.Client
containerStatsPool *sync.Pool
bufferPool *sync.Pool
}
func releaseSemaphore() {
<-sem
func NewAgent(pubKey []byte, addr string) *Agent {
return &Agent{
addr: addr,
pubKey: pubKey,
sem: make(chan struct{}, 15),
containerStatsMap: make(map[string]*container.PrevContainerStats),
containerStatsMutex: &sync.Mutex{},
diskIoStats: &system.DiskIoStats{},
netIoStats: &system.NetIoStats{},
dockerClient: newDockerClient(),
containerStatsPool: &sync.Pool{
New: func() interface{} {
return new(container.Stats)
},
},
bufferPool: &sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
},
}
}
var diskIoStats = DiskIoStats{
Read: 0,
Write: 0,
Time: time.Now(),
Filesystem: "",
func (a *Agent) acquireSemaphore() {
a.sem <- struct{}{}
}
var netIoStats = NetIoStats{
BytesRecv: 0,
BytesSent: 0,
Time: time.Now(),
Name: "",
func (a *Agent) releaseSemaphore() {
<-a.sem
}
// client for docker engine api
var dockerClient = newDockerClient()
func getSystemStats() (*SystemInfo, *SystemStats) {
systemStats := &SystemStats{}
func (a *Agent) getSystemStats() (*system.Info, *system.Stats) {
systemStats := &system.Stats{}
// cpu percent
cpuPct, err := cpu.Percent(0, false)
@@ -85,18 +103,18 @@ func getSystemStats() (*SystemInfo, *SystemStats) {
}
// disk i/o
if io, err := disk.IOCounters(diskIoStats.Filesystem); err == nil {
if io, err := disk.IOCounters(a.diskIoStats.Filesystem); err == nil {
for _, d := range io {
// add to systemStats
secondsElapsed := time.Since(diskIoStats.Time).Seconds()
readPerSecond := float64(d.ReadBytes-diskIoStats.Read) / secondsElapsed
secondsElapsed := time.Since(a.diskIoStats.Time).Seconds()
readPerSecond := float64(d.ReadBytes-a.diskIoStats.Read) / secondsElapsed
systemStats.DiskRead = bytesToMegabytes(readPerSecond)
writePerSecond := float64(d.WriteBytes-diskIoStats.Write) / secondsElapsed
writePerSecond := float64(d.WriteBytes-a.diskIoStats.Write) / secondsElapsed
systemStats.DiskWrite = bytesToMegabytes(writePerSecond)
// update diskIoStats
diskIoStats.Time = time.Now()
diskIoStats.Read = d.ReadBytes
diskIoStats.Write = d.WriteBytes
a.diskIoStats.Time = time.Now()
a.diskIoStats.Read = d.ReadBytes
a.diskIoStats.Write = d.WriteBytes
}
}
@@ -113,21 +131,37 @@ func getSystemStats() (*SystemInfo, *SystemStats) {
bytesRecv += v.BytesRecv
}
// add to systemStats
secondsElapsed := time.Since(netIoStats.Time).Seconds()
sentPerSecond := float64(bytesSent-netIoStats.BytesSent) / secondsElapsed
recvPerSecond := float64(bytesRecv-netIoStats.BytesRecv) / secondsElapsed
secondsElapsed := time.Since(a.netIoStats.Time).Seconds()
sentPerSecond := float64(bytesSent-a.netIoStats.BytesSent) / secondsElapsed
recvPerSecond := float64(bytesRecv-a.netIoStats.BytesRecv) / secondsElapsed
systemStats.NetworkSent = bytesToMegabytes(sentPerSecond)
systemStats.NetworkRecv = bytesToMegabytes(recvPerSecond)
// update netIoStats
netIoStats.BytesSent = bytesSent
netIoStats.BytesRecv = bytesRecv
netIoStats.Time = time.Now()
a.netIoStats.BytesSent = bytesSent
a.netIoStats.BytesRecv = bytesRecv
a.netIoStats.Time = time.Now()
}
systemInfo := &SystemInfo{
Cpu: systemStats.Cpu,
MemPct: systemStats.MemPct,
DiskPct: systemStats.DiskPct,
// temperatures
if temps, err := sensors.SensorsTemperatures(); err == nil {
systemStats.Temperatures = make(map[string]float64)
// log.Printf("Temperatures: %+v\n", temps)
for i, temp := range temps {
if _, ok := systemStats.Temperatures[temp.SensorKey]; ok {
// if key already exists, append int to key
systemStats.Temperatures[temp.SensorKey+"_"+strconv.Itoa(i)] = twoDecimals(temp.Temperature)
} else {
systemStats.Temperatures[temp.SensorKey] = twoDecimals(temp.Temperature)
}
}
// log.Printf("Temperature map: %+v\n", systemStats.Temperatures)
}
systemInfo := &system.Info{
Cpu: systemStats.Cpu,
MemPct: systemStats.MemPct,
DiskPct: systemStats.DiskPct,
AgentVersion: beszel.Version,
}
// add host info
@@ -147,24 +181,23 @@ func getSystemStats() (*SystemInfo, *SystemStats) {
}
return systemInfo, systemStats
}
func getDockerStats() ([]*ContainerStats, error) {
resp, err := dockerClient.Get("http://localhost/containers/json")
func (a *Agent) getDockerStats() ([]*container.Stats, error) {
resp, err := a.dockerClient.Get("http://localhost/containers/json")
if err != nil {
closeIdleConnections(err)
return []*ContainerStats{}, err
a.closeIdleConnections(err)
return nil, err
}
defer resp.Body.Close()
var containers []*Container
var containers []*container.ApiInfo
if err := json.NewDecoder(resp.Body).Decode(&containers); err != nil {
log.Printf("Error decoding containers: %+v\n", err)
return []*ContainerStats{}, err
return nil, err
}
containerStats := make([]*ContainerStats, 0, len(containers))
containerStats := make([]*container.Stats, 0, len(containers))
// store valid ids to clean up old container ids from map
validIds := make(map[string]struct{}, len(containers))
@@ -176,25 +209,23 @@ func getDockerStats() ([]*ContainerStats, error) {
validIds[ctr.IdShort] = struct{}{}
// check if container is less than 1 minute old (possible restart)
// note: can't use Created field because it's not updated on restart
if strings.HasSuffix(ctr.Status, "seconds") {
if strings.Contains(ctr.Status, "second") {
// if so, remove old container data
deleteContainerStatsSync(ctr.IdShort)
a.deleteContainerStatsSync(ctr.IdShort)
}
wg.Add(1)
go func() {
defer wg.Done()
cstats, err := getContainerStats(ctr)
cstats, err := a.getContainerStats(ctr)
if err != nil {
// Check if the error is a network timeout
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
// Close idle connections to prevent reuse of stale connections
closeIdleConnections(err)
} else {
// otherwise delete container from map
deleteContainerStatsSync(ctr.IdShort)
// close idle connections if error is a network timeout
isTimeout := a.closeIdleConnections(err)
// delete container from map if not a timeout
if !isTimeout {
a.deleteContainerStatsSync(ctr.IdShort)
}
// retry once
cstats, err = getContainerStats(ctr)
cstats, err = a.getContainerStats(ctr)
if err != nil {
log.Printf("Error getting container stats: %+v\n", err)
return
@@ -206,33 +237,50 @@ func getDockerStats() ([]*ContainerStats, error) {
wg.Wait()
for id := range containerStatsMap {
for id := range a.containerStatsMap {
if _, exists := validIds[id]; !exists {
// log.Printf("Removing container cpu map entry: %+v\n", id)
delete(containerStatsMap, id)
delete(a.containerStatsMap, id)
}
}
return containerStats, nil
}
func getContainerStats(ctr *Container) (*ContainerStats, error) {
func (a *Agent) getContainerStats(ctr *container.ApiInfo) (*container.Stats, error) {
// use semaphore to limit concurrency
acquireSemaphore()
defer releaseSemaphore()
resp, err := dockerClient.Get("http://localhost/containers/" + ctr.IdShort + "/stats?stream=0&one-shot=1")
a.acquireSemaphore()
defer a.releaseSemaphore()
resp, err := a.dockerClient.Get("http://localhost/containers/" + ctr.IdShort + "/stats?stream=0&one-shot=1")
if err != nil {
return &ContainerStats{}, err
return nil, err
}
defer resp.Body.Close()
var statsJson CStats
if err := json.NewDecoder(resp.Body).Decode(&statsJson); err != nil {
panic(err)
// get a buffer from the pool
buf := a.bufferPool.Get().(*bytes.Buffer)
defer a.bufferPool.Put(buf)
buf.Reset()
// read the response body into the buffer
_, err = io.Copy(buf, resp.Body)
if err != nil {
return nil, err
}
// unmarshal the json data from the buffer
var statsJson container.ApiStats
if err := json.Unmarshal(buf.Bytes(), &statsJson); err != nil {
return nil, err
}
name := ctr.Names[0][1:]
// check if container has valid data, otherwise may be in restart loop (#103)
if statsJson.MemoryStats.Usage == 0 {
return nil, fmt.Errorf("%s - invalid data", name)
}
// memory (https://docs.docker.com/reference/cli/docker/container/stats/)
memCache := statsJson.MemoryStats.Stats["inactive_file"]
if memCache == 0 {
@@ -240,14 +288,14 @@ func getContainerStats(ctr *Container) (*ContainerStats, error) {
}
usedMemory := statsJson.MemoryStats.Usage - memCache
containerStatsMutex.Lock()
defer containerStatsMutex.Unlock()
a.containerStatsMutex.Lock()
defer a.containerStatsMutex.Unlock()
// add empty values if they doesn't exist in map
stats, initialized := containerStatsMap[ctr.IdShort]
stats, initialized := a.containerStatsMap[ctr.IdShort]
if !initialized {
stats = &PrevContainerStats{}
containerStatsMap[ctr.IdShort] = stats
stats = &container.PrevContainerStats{}
a.containerStatsMap[ctr.IdShort] = stats
}
// cpu
@@ -255,7 +303,7 @@ func getContainerStats(ctr *Container) (*ContainerStats, error) {
systemDelta := statsJson.CPUStats.SystemUsage - stats.Cpu[1]
cpuPct := float64(cpuDelta) / float64(systemDelta) * 100
if cpuPct > 100 {
return &ContainerStats{}, fmt.Errorf("%s cpu pct greater than 100: %+v", name, cpuPct)
return nil, fmt.Errorf("%s cpu pct greater than 100: %+v", name, cpuPct)
}
stats.Cpu = [2]uint64{statsJson.CPUStats.CPUUsage.TotalUsage, statsJson.CPUStats.SystemUsage}
@@ -277,52 +325,50 @@ func getContainerStats(ctr *Container) (*ContainerStats, error) {
stats.Net.Recv = total_recv
stats.Net.Time = time.Now()
cStats := &ContainerStats{
Name: name,
Cpu: twoDecimals(cpuPct),
Mem: bytesToMegabytes(float64(usedMemory)),
NetworkSent: bytesToMegabytes(sent_delta),
NetworkRecv: bytesToMegabytes(recv_delta),
}
cStats := a.containerStatsPool.Get().(*container.Stats)
cStats.Name = name
cStats.Cpu = twoDecimals(cpuPct)
cStats.Mem = bytesToMegabytes(float64(usedMemory))
cStats.NetworkSent = bytesToMegabytes(sent_delta)
cStats.NetworkRecv = bytesToMegabytes(recv_delta)
return cStats, nil
}
// delete container stats from map using mutex
func deleteContainerStatsSync(id string) {
containerStatsMutex.Lock()
defer containerStatsMutex.Unlock()
delete(containerStatsMap, id)
func (a *Agent) deleteContainerStatsSync(id string) {
a.containerStatsMutex.Lock()
defer a.containerStatsMutex.Unlock()
delete(a.containerStatsMap, id)
}
func gatherStats() *SystemData {
systemInfo, systemStats := getSystemStats()
stats := &SystemData{
Stats: systemStats,
Info: systemInfo,
Containers: []*ContainerStats{},
func (a *Agent) gatherStats() *system.CombinedData {
systemInfo, systemStats := a.getSystemStats()
systemData := &system.CombinedData{
Stats: systemStats,
Info: systemInfo,
}
containerStats, err := getDockerStats()
if err == nil {
stats.Containers = containerStats
if containerStats, err := a.getDockerStats(); err == nil {
systemData.Containers = containerStats
}
// fmt.Printf("%+v\n", stats)
return stats
// fmt.Printf("%+v\n", systemData)
return systemData
}
func startServer(addr string, pubKey []byte) {
sshServer.Handle(func(s sshServer.Session) {
stats := gatherStats()
var jsonStats []byte
jsonStats, _ = json.Marshal(stats)
io.WriteString(s, string(jsonStats))
s.Exit(0)
})
// return container stats to pool
func (a *Agent) returnStatsToPool(containerStats []*container.Stats) {
for _, stats := range containerStats {
a.containerStatsPool.Put(stats)
}
}
log.Printf("Starting SSH server on %s", addr)
if err := sshServer.ListenAndServe(addr, nil, sshServer.NoPty(),
func (a *Agent) startServer() {
sshServer.Handle(a.handleSession)
log.Printf("Starting SSH server on %s", a.addr)
if err := sshServer.ListenAndServe(a.addr, nil, sshServer.NoPty(),
sshServer.PublicKeyAuth(func(ctx sshServer.Context, key sshServer.PublicKey) bool {
data := []byte(pubKey)
allowed, _, _, _, _ := sshServer.ParseAuthorizedKey(data)
allowed, _, _, _, _ := sshServer.ParseAuthorizedKey(a.pubKey)
return sshServer.KeysEqual(key, allowed)
}),
); err != nil {
@@ -330,42 +376,56 @@ func startServer(addr string, pubKey []byte) {
}
}
func main() {
// handle flags / subcommands
if len(os.Args) > 1 {
switch os.Args[1] {
case "-v":
fmt.Println("beszel-agent", Version)
case "update":
updateBeszel()
}
os.Exit(0)
}
var pubKey []byte
if pubKeyEnv, exists := os.LookupEnv("KEY"); exists {
pubKey = []byte(pubKeyEnv)
} else {
log.Fatal("KEY environment variable is not set")
func (a *Agent) handleSession(s sshServer.Session) {
stats := a.gatherStats()
defer a.returnStatsToPool(stats.Containers)
encoder := json.NewEncoder(s)
if err := encoder.Encode(stats); err != nil {
log.Println("Error encoding stats:", err.Error())
s.Exit(1)
return
}
s.Exit(0)
}
func (a *Agent) Run() {
if filesystem, exists := os.LookupEnv("FILESYSTEM"); exists {
diskIoStats.Filesystem = filesystem
a.diskIoStats.Filesystem = filesystem
} else {
diskIoStats.Filesystem = findDefaultFilesystem()
a.diskIoStats.Filesystem = findDefaultFilesystem()
}
initializeDiskIoStats()
initializeNetIoStats()
a.initializeDiskIoStats()
a.initializeNetIoStats()
if port, exists := os.LookupEnv("PORT"); exists {
// allow passing an address in the form of "127.0.0.1:45876"
if !strings.Contains(port, ":") {
port = ":" + port
a.startServer()
}
func (a *Agent) initializeDiskIoStats() {
if io, err := disk.IOCounters(a.diskIoStats.Filesystem); err == nil {
for _, d := range io {
a.diskIoStats.Time = time.Now()
a.diskIoStats.Read = d.ReadBytes
a.diskIoStats.Write = d.WriteBytes
}
startServer(port, pubKey)
} else {
startServer(":45876", pubKey)
}
}
func (a *Agent) initializeNetIoStats() {
if netIO, err := psutilNet.IOCounters(true); err == nil {
bytesSent := uint64(0)
bytesRecv := uint64(0)
for _, v := range netIO {
if skipNetworkInterface(&v) {
continue
}
log.Printf("Found network interface: %+v (%+v recv, %+v sent)\n", v.Name, v.BytesRecv, v.BytesSent)
bytesSent += v.BytesSent
bytesRecv += v.BytesRecv
}
a.netIoStats.BytesSent = bytesSent
a.netIoStats.BytesRecv = bytesRecv
a.netIoStats.Time = time.Now()
}
}
@@ -407,34 +467,6 @@ func skipNetworkInterface(v *psutilNet.IOCountersStat) bool {
}
}
func initializeDiskIoStats() {
if io, err := disk.IOCounters(diskIoStats.Filesystem); err == nil {
for _, d := range io {
diskIoStats.Time = time.Now()
diskIoStats.Read = d.ReadBytes
diskIoStats.Write = d.WriteBytes
}
}
}
func initializeNetIoStats() {
if netIO, err := psutilNet.IOCounters(true); err == nil {
bytesSent := uint64(0)
bytesRecv := uint64(0)
for _, v := range netIO {
if skipNetworkInterface(&v) {
continue
}
log.Printf("Found network interface: %+v (%+v recv, %+v sent)\n", v.Name, v.BytesRecv, v.BytesSent)
bytesSent += v.BytesSent
bytesRecv += v.BytesRecv
}
netIoStats.BytesSent = bytesSent
netIoStats.BytesRecv = bytesRecv
netIoStats.Time = time.Now()
}
}
func newDockerClient() *http.Client {
dockerHost := "unix:///var/run/docker.sock"
if dockerHostEnv, exists := os.LookupEnv("DOCKER_HOST"); exists {
@@ -450,6 +482,7 @@ func newDockerClient() *http.Client {
ForceAttemptHTTP2: false,
IdleConnTimeout: 90 * time.Second,
DisableCompression: true,
MaxConnsPerHost: 20,
MaxIdleConnsPerHost: 20,
DisableKeepAlives: false,
}
@@ -474,7 +507,12 @@ func newDockerClient() *http.Client {
}
}
func closeIdleConnections(err error) {
log.Printf("Closing idle connections. Error: %+v\n", err)
dockerClient.Transport.(*http.Transport).CloseIdleConnections()
// closes idle connections on timeouts to prevent reuse of stale connections
func (a *Agent) closeIdleConnections(err error) (isTimeout bool) {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
log.Printf("Closing idle connections. Error: %+v\n", err)
a.dockerClient.Transport.(*http.Transport).CloseIdleConnections()
return true
}
return false
}

View File

@@ -0,0 +1,153 @@
// Package alerts handles alert management and delivery.
package alerts
import (
"beszel/internal/entities/system"
"fmt"
"net/mail"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/tools/mailer"
)
type AlertManager struct {
app *pocketbase.PocketBase
mailClient mailer.Mailer
}
func NewAlertManager(app *pocketbase.PocketBase) *AlertManager {
return &AlertManager{
app: app,
mailClient: app.NewMailClient(),
}
}
func (am *AlertManager) HandleSystemAlerts(newStatus string, newRecord *models.Record, oldRecord *models.Record) {
alertRecords, err := am.app.Dao().FindRecordsByExpr("alerts",
dbx.NewExp("system = {:system}", dbx.Params{"system": oldRecord.GetId()}),
)
if err != nil || len(alertRecords) == 0 {
// log.Println("no alerts found for system")
return
}
// log.Println("found alerts", len(alertRecords))
var systemInfo *system.Info
for _, alertRecord := range alertRecords {
name := alertRecord.GetString("name")
switch name {
case "Status":
am.handleStatusAlerts(newStatus, oldRecord, alertRecord)
case "CPU", "Memory", "Disk":
if newStatus != "up" {
continue
}
if systemInfo == nil {
systemInfo = getSystemInfo(newRecord)
}
if name == "CPU" {
am.handleSlidingValueAlert(newRecord, alertRecord, name, systemInfo.Cpu)
} else if name == "Memory" {
am.handleSlidingValueAlert(newRecord, alertRecord, name, systemInfo.MemPct)
} else if name == "Disk" {
am.handleSlidingValueAlert(newRecord, alertRecord, name, systemInfo.DiskPct)
}
}
}
}
func getSystemInfo(record *models.Record) *system.Info {
var SystemInfo system.Info
record.UnmarshalJSONField("info", &SystemInfo)
return &SystemInfo
}
func (am *AlertManager) handleSlidingValueAlert(newRecord *models.Record, alertRecord *models.Record, name string, curValue float64) {
triggered := alertRecord.GetBool("triggered")
threshold := alertRecord.GetFloat("value")
// fmt.Println(name, curValue, "threshold", threshold, "triggered", triggered)
var subject string
var body string
if !triggered && curValue > threshold {
alertRecord.Set("triggered", true)
systemName := newRecord.GetString("name")
subject = fmt.Sprintf("%s usage above threshold on %s", name, systemName)
body = fmt.Sprintf("%s usage on %s is %.1f%%.\n\n%s\n\n- Beszel", name, systemName, curValue, am.app.Settings().Meta.AppUrl+"/system/"+systemName)
} else if triggered && curValue <= threshold {
alertRecord.Set("triggered", false)
systemName := newRecord.GetString("name")
subject = fmt.Sprintf("%s usage below threshold on %s", name, systemName)
body = fmt.Sprintf("%s usage on %s is below threshold at %.1f%%.\n\n%s\n\n- Beszel", name, systemName, curValue, am.app.Settings().Meta.AppUrl+"/system/"+systemName)
} else {
// fmt.Println(name, "not triggered")
return
}
if err := am.app.Dao().SaveRecord(alertRecord); err != nil {
// app.Logger().Error("failed to save alert record", "err", err.Error())
return
}
// expand the user relation and send the alert
if errs := am.app.Dao().ExpandRecord(alertRecord, []string{"user"}, nil); len(errs) > 0 {
// app.Logger().Error("failed to expand user relation", "errs", errs)
return
}
if user := alertRecord.ExpandedOne("user"); user != nil {
am.sendAlert(&mailer.Message{
To: []mail.Address{{Address: user.GetString("email")}},
Subject: subject,
Text: body,
})
}
}
func (am *AlertManager) handleStatusAlerts(newStatus string, oldRecord *models.Record, alertRecord *models.Record) error {
var alertStatus string
switch newStatus {
case "up":
if oldRecord.GetString("status") == "down" {
alertStatus = "up"
}
case "down":
if oldRecord.GetString("status") == "up" {
alertStatus = "down"
}
}
if alertStatus == "" {
return nil
}
// expand the user relation
if errs := am.app.Dao().ExpandRecord(alertRecord, []string{"user"}, nil); len(errs) > 0 {
return fmt.Errorf("failed to expand: %v", errs)
}
user := alertRecord.ExpandedOne("user")
if user == nil {
return nil
}
emoji := "\U0001F534"
if alertStatus == "up" {
emoji = "\u2705"
}
// send alert
systemName := oldRecord.GetString("name")
am.sendAlert(&mailer.Message{
To: []mail.Address{{Address: user.GetString("email")}},
Subject: fmt.Sprintf("Connection to %s is %s %v", systemName, alertStatus, emoji),
Text: fmt.Sprintf("Connection to %s is %s\n\n- Beszel", systemName, alertStatus),
})
return nil
}
func (am *AlertManager) sendAlert(message *mailer.Message) {
// fmt.Println("sending alert", "to", message.To, "subj", message.Subject, "body", message.Text)
message.From = mail.Address{
Address: am.app.Settings().Meta.SenderAddress,
Name: am.app.Settings().Meta.SenderName,
}
if err := am.mailClient.Send(message); err != nil {
am.app.Logger().Error("Failed to send alert: ", "err", err.Error())
} else {
am.app.Logger().Info("Sent alert", "to", message.To, "subj", message.Subject)
}
}

View File

@@ -1,50 +1,9 @@
package main
package container
import "time"
type SystemData struct {
Stats *SystemStats `json:"stats"`
Info *SystemInfo `json:"info"`
Containers []*ContainerStats `json:"container"`
}
type SystemInfo struct {
Cores int `json:"c"`
Threads int `json:"t"`
CpuModel string `json:"m"`
// Os string `json:"o"`
Uptime uint64 `json:"u"`
Cpu float64 `json:"cpu"`
MemPct float64 `json:"mp"`
DiskPct float64 `json:"dp"`
}
type SystemStats struct {
Cpu float64 `json:"cpu"`
Mem float64 `json:"m"`
MemUsed float64 `json:"mu"`
MemPct float64 `json:"mp"`
MemBuffCache float64 `json:"mb"`
Swap float64 `json:"s"`
SwapUsed float64 `json:"su"`
Disk float64 `json:"d"`
DiskUsed float64 `json:"du"`
DiskPct float64 `json:"dp"`
DiskRead float64 `json:"dr"`
DiskWrite float64 `json:"dw"`
NetworkSent float64 `json:"ns"`
NetworkRecv float64 `json:"nr"`
}
type ContainerStats struct {
Name string `json:"n"`
Cpu float64 `json:"c"`
Mem float64 `json:"m"`
NetworkSent float64 `json:"ns"`
NetworkRecv float64 `json:"nr"`
}
type Container struct {
// Docker container info from /containers/json
type ApiInfo struct {
Id string
IdShort string
Names []string
@@ -66,7 +25,8 @@ type Container struct {
// Mounts []MountPoint
}
type CStats struct {
// Docker container resources from /containers/{id}/stats
type ApiStats struct {
// Common stats
// Read time.Time `json:"read"`
// PreRead time.Time `json:"preread"`
@@ -153,20 +113,16 @@ type NetworkStats struct {
TxBytes uint64 `json:"tx_bytes"`
}
type DiskIoStats struct {
Read uint64
Write uint64
Time time.Time
Filesystem string
}
type NetIoStats struct {
BytesRecv uint64
BytesSent uint64
Time time.Time
Name string
// Container stats to return to the hub
type Stats struct {
Name string `json:"n"`
Cpu float64 `json:"c"`
Mem float64 `json:"m"`
NetworkSent float64 `json:"ns"`
NetworkRecv float64 `json:"nr"`
}
// Keeps track of container stats from previous run
type PrevContainerStats struct {
Cpu [2]uint64
Net struct {

View File

@@ -0,0 +1,57 @@
package system
import (
"beszel/internal/entities/container"
"time"
)
type Stats struct {
Cpu float64 `json:"cpu"`
Mem float64 `json:"m"`
MemUsed float64 `json:"mu"`
MemPct float64 `json:"mp"`
MemBuffCache float64 `json:"mb"`
Swap float64 `json:"s"`
SwapUsed float64 `json:"su"`
Disk float64 `json:"d"`
DiskUsed float64 `json:"du"`
DiskPct float64 `json:"dp"`
DiskRead float64 `json:"dr"`
DiskWrite float64 `json:"dw"`
NetworkSent float64 `json:"ns"`
NetworkRecv float64 `json:"nr"`
Temperatures map[string]float64 `json:"t,omitempty"`
}
type DiskIoStats struct {
Read uint64
Write uint64
Time time.Time
Filesystem string
}
type NetIoStats struct {
BytesRecv uint64
BytesSent uint64
Time time.Time
Name string
}
type Info struct {
Cores int `json:"c"`
Threads int `json:"t"`
CpuModel string `json:"m"`
// Os string `json:"o"`
Uptime uint64 `json:"u"`
Cpu float64 `json:"cpu"`
MemPct float64 `json:"mp"`
DiskPct float64 `json:"dp"`
AgentVersion string `json:"v"`
}
// Final data structure to return to the hub
type CombinedData struct {
Stats *Stats `json:"stats"`
Info *Info `json:"info"`
Containers []*container.Stats `json:"container"`
}

448
beszel/internal/hub/hub.go Normal file
View File

@@ -0,0 +1,448 @@
package hub
import (
"beszel"
"beszel/internal/alerts"
"beszel/internal/entities/system"
"beszel/internal/records"
"beszel/site"
"crypto/ed25519"
"encoding/pem"
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"
"sync"
"time"
"github.com/goccy/go-json"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/plugins/migratecmd"
"github.com/pocketbase/pocketbase/tools/cron"
"golang.org/x/crypto/ssh"
)
type Hub struct {
app *pocketbase.PocketBase
connectionLock *sync.Mutex
systemConnections map[string]*ssh.Client
sshClientConfig *ssh.ClientConfig
pubKey string
}
func NewHub(app *pocketbase.PocketBase) *Hub {
return &Hub{
app: app,
connectionLock: &sync.Mutex{},
systemConnections: make(map[string]*ssh.Client),
}
}
func (h *Hub) Run() {
var rm *records.RecordManager
var am *alerts.AlertManager
// loosely check if it was executed using "go run"
isGoRun := strings.HasPrefix(os.Args[0], os.TempDir())
// // enable auto creation of migration files when making collection changes in the Admin UI
migratecmd.MustRegister(h.app, h.app.RootCmd, migratecmd.Config{
// (the isGoRun check is to enable it only during development)
Automigrate: isGoRun,
Dir: "../../migrations",
})
// initial setup
h.app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
// set up record manager and alert manager
rm = records.NewRecordManager(h.app)
am = alerts.NewAlertManager(h.app)
// create ssh client config
err := h.createSSHClientConfig()
if err != nil {
log.Fatal(err)
}
// set auth settings
usersCollection, err := h.app.Dao().FindCollectionByNameOrId("users")
if err != nil {
return err
}
usersAuthOptions := usersCollection.AuthOptions()
usersAuthOptions.AllowUsernameAuth = false
if os.Getenv("DISABLE_PASSWORD_AUTH") == "true" {
usersAuthOptions.AllowEmailAuth = false
} else {
usersAuthOptions.AllowEmailAuth = true
}
usersCollection.SetOptions(usersAuthOptions)
if err := h.app.Dao().SaveCollection(usersCollection); err != nil {
return err
}
return nil
})
// serve site
h.app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
switch isGoRun {
case true:
proxy := httputil.NewSingleHostReverseProxy(&url.URL{
Scheme: "http",
Host: "localhost:5173",
})
e.Router.GET("/static/*", apis.StaticDirectoryHandler(os.DirFS("../../site/public/static"), false))
e.Router.Any("/*", echo.WrapHandler(proxy))
// e.Router.Any("/", echo.WrapHandler(proxy))
default:
e.Router.GET("/static/*", apis.StaticDirectoryHandler(site.Static, false))
e.Router.Any("/*", apis.StaticDirectoryHandler(site.Dist, true))
}
return nil
})
// set up scheduled jobs / ticker for system updates
h.app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
// 15 second ticker for system updates
go h.startSystemUpdateTicker()
// set up cron jobs
scheduler := cron.New()
// delete old records once every hour
scheduler.MustAdd("delete old records", "8 * * * *", rm.DeleteOldRecords)
// create longer records every 10 minutes
scheduler.MustAdd("create longer records", "*/10 * * * *", rm.CreateLongerRecords)
scheduler.Start()
return nil
})
// custom api routes
h.app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
// returns public key
e.Router.GET("/api/beszel/getkey", func(c echo.Context) error {
requestData := apis.RequestInfo(c)
if requestData.AuthRecord == nil {
return apis.NewForbiddenError("Forbidden", nil)
}
return c.JSON(http.StatusOK, map[string]string{"key": h.pubKey, "v": beszel.Version})
})
// check if first time setup on login page
e.Router.GET("/api/beszel/first-run", func(c echo.Context) error {
adminNum, err := h.app.Dao().TotalAdmins()
if err != nil {
return err
}
return c.JSON(http.StatusOK, map[string]bool{"firstRun": adminNum == 0})
})
return nil
})
// user creation - set default role to user if unset
h.app.OnModelBeforeCreate("users").Add(func(e *core.ModelEvent) error {
user := e.Model.(*models.Record)
if user.GetString("role") == "" {
user.Set("role", "user")
}
return nil
})
// system creation defaults
h.app.OnModelBeforeCreate("systems").Add(func(e *core.ModelEvent) error {
record := e.Model.(*models.Record)
record.Set("info", system.Info{})
record.Set("status", "pending")
return nil
})
// immediately create connection for new systems
h.app.OnModelAfterCreate("systems").Add(func(e *core.ModelEvent) error {
go h.updateSystem(e.Model.(*models.Record))
return nil
})
// do things after a systems record is updated
h.app.OnModelAfterUpdate("systems").Add(func(e *core.ModelEvent) error {
newRecord := e.Model.(*models.Record)
oldRecord := newRecord.OriginalCopy()
newStatus := newRecord.GetString("status")
// if system is disconnected and connection exists, remove it
if newStatus == "down" || newStatus == "paused" {
h.deleteSystemConnection(newRecord)
}
// if system is set to pending (unpause), try to connect immediately
if newStatus == "pending" {
go h.updateSystem(newRecord)
}
// alerts
am.HandleSystemAlerts(newStatus, newRecord, oldRecord)
return nil
})
// do things after a systems record is deleted
h.app.OnModelAfterDelete("systems").Add(func(e *core.ModelEvent) error {
// if system connection exists, close it
h.deleteSystemConnection(e.Model.(*models.Record))
return nil
})
if err := h.app.Start(); err != nil {
log.Fatal(err)
}
}
func (h *Hub) startSystemUpdateTicker() {
ticker := time.NewTicker(15 * time.Second)
for range ticker.C {
h.updateSystems()
}
}
func (h *Hub) updateSystems() {
records, err := h.app.Dao().FindRecordsByFilter(
"2hz5ncl8tizk5nx", // systems collection
"status != 'paused'", // filter
"updated", // sort
-1, // limit
0, // offset
)
// log.Println("records", len(records))
if err != nil || len(records) == 0 {
// h.app.Logger().Error("Failed to query systems")
return
}
fiftySecondsAgo := time.Now().UTC().Add(-50 * time.Second)
batchSize := len(records)/4 + 1
done := 0
for _, record := range records {
// break if batch size reached or if the system was updated less than 50 seconds ago
if done >= batchSize || record.GetDateTime("updated").Time().After(fiftySecondsAgo) {
break
}
// don't increment for down systems to avoid them jamming the queue
// because they're always first when sorted by least recently updated
if record.GetString("status") != "down" {
done++
}
go h.updateSystem(record)
}
}
func (h *Hub) updateSystem(record *models.Record) {
var client *ssh.Client
var err error
// check if system connection data exists
if _, ok := h.systemConnections[record.Id]; ok {
client = h.systemConnections[record.Id]
} else {
// create system connection
client, err = h.createSystemConnection(record)
if err != nil {
h.app.Logger().Error("Failed to connect:", "err", err.Error(), "system", record.GetString("host"), "port", record.GetString("port"))
h.updateSystemStatus(record, "down")
return
}
h.connectionLock.Lock()
h.systemConnections[record.Id] = client
h.connectionLock.Unlock()
}
// get system stats from agent
var systemData system.CombinedData
if err := requestJsonFromAgent(client, &systemData); err != nil {
if err.Error() == "bad client" {
// if previous connection was closed, try again
h.app.Logger().Error("Existing SSH connection closed. Retrying...", "host", record.GetString("host"), "port", record.GetString("port"))
h.deleteSystemConnection(record)
h.updateSystem(record)
return
}
h.app.Logger().Error("Failed to get system stats: ", "err", err.Error())
h.updateSystemStatus(record, "down")
return
}
// update system record
record.Set("status", "up")
record.Set("info", systemData.Info)
if err := h.app.Dao().SaveRecord(record); err != nil {
h.app.Logger().Error("Failed to update record: ", "err", err.Error())
}
// add new system_stats record
system_stats, _ := h.app.Dao().FindCollectionByNameOrId("system_stats")
systemStatsRecord := models.NewRecord(system_stats)
systemStatsRecord.Set("system", record.Id)
systemStatsRecord.Set("stats", systemData.Stats)
systemStatsRecord.Set("type", "1m")
if err := h.app.Dao().SaveRecord(systemStatsRecord); err != nil {
h.app.Logger().Error("Failed to save record: ", "err", err.Error())
}
// add new container_stats record
if len(systemData.Containers) > 0 {
container_stats, _ := h.app.Dao().FindCollectionByNameOrId("container_stats")
containerStatsRecord := models.NewRecord(container_stats)
containerStatsRecord.Set("system", record.Id)
containerStatsRecord.Set("stats", systemData.Containers)
containerStatsRecord.Set("type", "1m")
if err := h.app.Dao().SaveRecord(containerStatsRecord); err != nil {
h.app.Logger().Error("Failed to save record: ", "err", err.Error())
}
}
}
// set system to specified status and save record
func (h *Hub) updateSystemStatus(record *models.Record, status string) {
if record.GetString("status") != status {
record.Set("status", status)
if err := h.app.Dao().SaveRecord(record); err != nil {
h.app.Logger().Error("Failed to update record: ", "err", err.Error())
}
}
}
func (h *Hub) deleteSystemConnection(record *models.Record) {
if _, ok := h.systemConnections[record.Id]; ok {
if h.systemConnections[record.Id] != nil {
h.systemConnections[record.Id].Close()
}
h.connectionLock.Lock()
defer h.connectionLock.Unlock()
delete(h.systemConnections, record.Id)
}
}
func (h *Hub) createSystemConnection(record *models.Record) (*ssh.Client, error) {
client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%s", record.GetString("host"), record.GetString("port")), h.sshClientConfig)
if err != nil {
return nil, err
}
return client, nil
}
func (h *Hub) createSSHClientConfig() error {
key, err := h.getSSHKey()
if err != nil {
h.app.Logger().Error("Failed to get SSH key: ", "err", err.Error())
return err
}
// Create the Signer for this private key.
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
return err
}
h.sshClientConfig = &ssh.ClientConfig{
User: "u",
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 5 * time.Second,
}
return nil
}
func requestJsonFromAgent(client *ssh.Client, systemData *system.CombinedData) error {
session, err := client.NewSession()
if err != nil {
return fmt.Errorf("bad client")
}
defer session.Close()
stdout, err := session.StdoutPipe()
if err != nil {
return err
}
if err := session.Shell(); err != nil {
return err
}
if err := json.NewDecoder(stdout).Decode(systemData); err != nil {
return err
}
// wait for the session to complete
if err := session.Wait(); err != nil {
return err
}
return nil
}
func (h *Hub) getSSHKey() ([]byte, error) {
dataDir := h.app.DataDir()
// check if the key pair already exists
existingKey, err := os.ReadFile(dataDir + "/id_ed25519")
if err == nil {
if pubKey, err := os.ReadFile(h.app.DataDir() + "/id_ed25519.pub"); err == nil {
h.pubKey = strings.TrimSuffix(string(pubKey), "\n")
}
// return existing private key
return existingKey, nil
}
// Generate the Ed25519 key pair
pubKey, privKey, err := ed25519.GenerateKey(nil)
if err != nil {
// h.app.Logger().Error("Error generating key pair:", "err", err.Error())
return nil, err
}
// Get the private key in OpenSSH format
privKeyBytes, err := ssh.MarshalPrivateKey(privKey, "")
if err != nil {
// h.app.Logger().Error("Error marshaling private key:", "err", err.Error())
return nil, err
}
// Save the private key to a file
privateFile, err := os.Create(dataDir + "/id_ed25519")
if err != nil {
// h.app.Logger().Error("Error creating private key file:", "err", err.Error())
return nil, err
}
defer privateFile.Close()
if err := pem.Encode(privateFile, privKeyBytes); err != nil {
// h.app.Logger().Error("Error writing private key to file:", "err", err.Error())
return nil, err
}
// Generate the public key in OpenSSH format
publicKey, err := ssh.NewPublicKey(pubKey)
if err != nil {
return nil, err
}
pubKeyBytes := ssh.MarshalAuthorizedKey(publicKey)
h.pubKey = strings.TrimSuffix(string(pubKeyBytes), "\n")
// Save the public key to a file
publicFile, err := os.Create(dataDir + "/id_ed25519.pub")
if err != nil {
return nil, err
}
defer publicFile.Close()
if _, err := publicFile.Write(pubKeyBytes); err != nil {
return nil, err
}
h.app.Logger().Info("ed25519 SSH key pair generated successfully.")
h.app.Logger().Info("Private key saved to: " + dataDir + "/id_ed25519")
h.app.Logger().Info("Public key saved to: " + dataDir + "/id_ed25519.pub")
existingKey, err = os.ReadFile(dataDir + "/id_ed25519")
if err == nil {
return existingKey, nil
}
return nil, err
}

View File

@@ -0,0 +1,314 @@
// Package records handles creating longer records and deleting old records.
package records
import (
"beszel/internal/entities/container"
"beszel/internal/entities/system"
"log"
"math"
"time"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
)
type RecordManager struct {
app *pocketbase.PocketBase
}
type LongerRecordData struct {
shorterType string
longerType string
longerTimeDuration time.Duration
expectedShorterRecords int
}
type RecordDeletionData struct {
recordType string
retention time.Duration
}
func NewRecordManager(app *pocketbase.PocketBase) *RecordManager {
return &RecordManager{app}
}
// Create longer records by averaging shorter records
func (rm *RecordManager) CreateLongerRecords() {
// start := time.Now()
recordData := []LongerRecordData{
{
shorterType: "1m",
expectedShorterRecords: 10,
longerType: "10m",
longerTimeDuration: -10 * time.Minute,
},
{
shorterType: "10m",
expectedShorterRecords: 2,
longerType: "20m",
longerTimeDuration: -20 * time.Minute,
},
{
shorterType: "20m",
expectedShorterRecords: 6,
longerType: "120m",
longerTimeDuration: -120 * time.Minute,
},
{
shorterType: "120m",
expectedShorterRecords: 4,
longerType: "480m",
longerTimeDuration: -480 * time.Minute,
},
}
// wrap the operations in a transaction
rm.app.Dao().RunInTransaction(func(txDao *daos.Dao) error {
activeSystems, err := txDao.FindRecordsByExpr("systems", dbx.NewExp("status = 'up'"))
if err != nil {
log.Println("failed to get active systems", "err", err.Error())
return err
}
collections := map[string]*models.Collection{}
for _, collectionName := range []string{"system_stats", "container_stats"} {
collection, _ := txDao.FindCollectionByNameOrId(collectionName)
collections[collectionName] = collection
}
// loop through all active systems, time periods, and collections
for _, system := range activeSystems {
// log.Println("processing system", system.GetString("name"))
for _, recordData := range recordData {
// log.Println("processing longer record type", recordData.longerType)
// add one minute padding for longer records because they are created slightly later than the job start time
longerRecordPeriod := time.Now().UTC().Add(recordData.longerTimeDuration + time.Minute)
// shorter records are created independently of longer records, so we shouldn't need to add padding
shorterRecordPeriod := time.Now().UTC().Add(recordData.longerTimeDuration)
// loop through both collections
for _, collection := range collections {
// check creation time of last longer record if not 10m, since 10m is created every run
if recordData.longerType != "10m" {
lastLongerRecord, err := txDao.FindFirstRecordByFilter(
collection.Id,
"type = {:type} && system = {:system} && created > {:created}",
dbx.Params{"type": recordData.longerType, "system": system.Id, "created": longerRecordPeriod},
)
// continue if longer record exists
if err == nil || lastLongerRecord != nil {
// log.Println("longer record found. continuing")
continue
}
}
// get shorter records from the past x minutes
allShorterRecords, err := txDao.FindRecordsByExpr(
collection.Id,
dbx.NewExp(
"type = {:type} AND system = {:system} AND created > {:created}",
dbx.Params{"type": recordData.shorterType, "system": system.Id, "created": shorterRecordPeriod},
),
)
// continue if not enough shorter records
if err != nil || len(allShorterRecords) < recordData.expectedShorterRecords {
// log.Println("not enough shorter records. continue.", len(allShorterRecords), recordData.expectedShorterRecords)
continue
}
// average the shorter records and create longer record
var stats interface{}
switch collection.Name {
case "system_stats":
stats = rm.AverageSystemStats(allShorterRecords)
case "container_stats":
stats = rm.AverageContainerStats(allShorterRecords)
}
longerRecord := models.NewRecord(collection)
longerRecord.Set("system", system.Id)
longerRecord.Set("stats", stats)
longerRecord.Set("type", recordData.longerType)
if err := txDao.SaveRecord(longerRecord); err != nil {
log.Println("failed to save longer record", "err", err.Error())
}
}
}
}
return nil
})
// log.Println("finished creating longer records", "time (ms)", time.Since(start).Milliseconds())
}
// Calculate the average stats of a list of system_stats records with reflect
// func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Stats {
// count := float64(len(records))
// sum := reflect.New(reflect.TypeOf(system.Stats{})).Elem()
// var stats system.Stats
// for _, record := range records {
// record.UnmarshalJSONField("stats", &stats)
// statValue := reflect.ValueOf(stats)
// for i := 0; i < statValue.NumField(); i++ {
// field := sum.Field(i)
// field.SetFloat(field.Float() + statValue.Field(i).Float())
// }
// }
// average := reflect.New(reflect.TypeOf(system.Stats{})).Elem()
// for i := 0; i < sum.NumField(); i++ {
// average.Field(i).SetFloat(twoDecimals(sum.Field(i).Float() / count))
// }
// return average.Interface().(system.Stats)
// }
// Calculate the average stats of a list of system_stats records without reflect
func (rm *RecordManager) AverageSystemStats(records []*models.Record) system.Stats {
var sum system.Stats
sum.Temperatures = make(map[string]float64)
count := float64(len(records))
// use different counter for temps in case some records don't have them
tempCount := float64(0)
var stats system.Stats
for _, record := range records {
record.UnmarshalJSONField("stats", &stats)
sum.Cpu += stats.Cpu
sum.Mem += stats.Mem
sum.MemUsed += stats.MemUsed
sum.MemPct += stats.MemPct
sum.MemBuffCache += stats.MemBuffCache
sum.Swap += stats.Swap
sum.SwapUsed += stats.SwapUsed
sum.Disk += stats.Disk
sum.DiskUsed += stats.DiskUsed
sum.DiskPct += stats.DiskPct
sum.DiskRead += stats.DiskRead
sum.DiskWrite += stats.DiskWrite
sum.NetworkSent += stats.NetworkSent
sum.NetworkRecv += stats.NetworkRecv
if stats.Temperatures != nil {
tempCount++
for key, value := range stats.Temperatures {
if _, ok := sum.Temperatures[key]; !ok {
sum.Temperatures[key] = 0
}
sum.Temperatures[key] += value
}
}
}
stats = system.Stats{
Cpu: twoDecimals(sum.Cpu / count),
Mem: twoDecimals(sum.Mem / count),
MemUsed: twoDecimals(sum.MemUsed / count),
MemPct: twoDecimals(sum.MemPct / count),
MemBuffCache: twoDecimals(sum.MemBuffCache / count),
Swap: twoDecimals(sum.Swap / count),
SwapUsed: twoDecimals(sum.SwapUsed / count),
Disk: twoDecimals(sum.Disk / count),
DiskUsed: twoDecimals(sum.DiskUsed / count),
DiskPct: twoDecimals(sum.DiskPct / count),
DiskRead: twoDecimals(sum.DiskRead / count),
DiskWrite: twoDecimals(sum.DiskWrite / count),
NetworkSent: twoDecimals(sum.NetworkSent / count),
NetworkRecv: twoDecimals(sum.NetworkRecv / count),
}
if len(sum.Temperatures) != 0 {
stats.Temperatures = make(map[string]float64)
for key, value := range sum.Temperatures {
stats.Temperatures[key] = twoDecimals(value / tempCount)
}
}
return stats
}
// Calculate the average stats of a list of container_stats records
func (rm *RecordManager) AverageContainerStats(records []*models.Record) (stats []container.Stats) {
sums := make(map[string]*container.Stats)
count := float64(len(records))
var containerStats []container.Stats
for _, record := range records {
record.UnmarshalJSONField("stats", &containerStats)
for _, stat := range containerStats {
if _, ok := sums[stat.Name]; !ok {
sums[stat.Name] = &container.Stats{Name: stat.Name, Cpu: 0, Mem: 0}
}
sums[stat.Name].Cpu += stat.Cpu
sums[stat.Name].Mem += stat.Mem
sums[stat.Name].NetworkSent += stat.NetworkSent
sums[stat.Name].NetworkRecv += stat.NetworkRecv
}
}
for _, value := range sums {
stats = append(stats, container.Stats{
Name: value.Name,
Cpu: twoDecimals(value.Cpu / count),
Mem: twoDecimals(value.Mem / count),
NetworkSent: twoDecimals(value.NetworkSent / count),
NetworkRecv: twoDecimals(value.NetworkRecv / count),
})
}
return stats
}
func (rm *RecordManager) DeleteOldRecords() {
// start := time.Now()
collections := []string{"system_stats", "container_stats"}
recordData := []RecordDeletionData{
{
recordType: "1m",
retention: time.Hour,
},
{
recordType: "10m",
retention: 12 * time.Hour,
},
{
recordType: "20m",
retention: 24 * time.Hour,
},
{
recordType: "120m",
retention: 7 * 24 * time.Hour,
},
{
recordType: "480m",
retention: 30 * 24 * time.Hour,
},
}
rm.app.Dao().RunInTransaction(func(txDao *daos.Dao) error {
for _, recordData := range recordData {
exp := dbx.NewExp(
"type = {:type} AND created < {:created}",
dbx.Params{"type": recordData.recordType, "created": time.Now().UTC().Add(-recordData.retention)},
)
for _, collectionSlug := range collections {
collectionRecords, err := txDao.FindRecordsByExpr(collectionSlug, exp)
if err != nil {
return err
}
for _, record := range collectionRecords {
err := txDao.DeleteRecord(record)
if err != nil {
rm.app.Logger().Error("Failed to delete records", "err", err.Error())
return err
}
}
}
}
return nil
})
// log.Println("finished deleting old records", "time (ms)", time.Since(start).Milliseconds())
}
/* Round float to two decimals */
func twoDecimals(value float64) float64 {
return math.Round(value*100) / 100
}

View File

@@ -0,0 +1,99 @@
package update
import (
"beszel"
"fmt"
"os"
"strings"
"github.com/blang/semver"
"github.com/rhysd/go-github-selfupdate/selfupdate"
)
func UpdateBeszel() {
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))
}
func UpdateBeszelAgent() {
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))
}

4597
beszel/site/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 378 B

After

Width:  |  Height:  |  Size: 378 B

View File

Before

Width:  |  Height:  |  Size: 196 B

After

Width:  |  Height:  |  Size: 196 B

View File

Before

Width:  |  Height:  |  Size: 506 B

After

Width:  |  Height:  |  Size: 506 B

View File

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 295 B

View File

Before

Width:  |  Height:  |  Size: 906 B

After

Width:  |  Height:  |  Size: 906 B

View File

Before

Width:  |  Height:  |  Size: 906 B

After

Width:  |  Height:  |  Size: 906 B

View File

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 903 B

View File

Before

Width:  |  Height:  |  Size: 907 B

After

Width:  |  Height:  |  Size: 907 B

View File

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 406 B

View File

Before

Width:  |  Height:  |  Size: 470 B

After

Width:  |  Height:  |  Size: 470 B

View File

Before

Width:  |  Height:  |  Size: 302 B

After

Width:  |  Height:  |  Size: 302 B

View File

Before

Width:  |  Height:  |  Size: 299 B

After

Width:  |  Height:  |  Size: 299 B

View File

Before

Width:  |  Height:  |  Size: 856 B

After

Width:  |  Height:  |  Size: 856 B

View File

Before

Width:  |  Height:  |  Size: 257 B

After

Width:  |  Height:  |  Size: 257 B

View File

Before

Width:  |  Height:  |  Size: 276 B

After

Width:  |  Height:  |  Size: 276 B

View File

Before

Width:  |  Height:  |  Size: 227 B

After

Width:  |  Height:  |  Size: 227 B

View File

Before

Width:  |  Height:  |  Size: 495 B

After

Width:  |  Height:  |  Size: 495 B

View File

Before

Width:  |  Height:  |  Size: 154 B

After

Width:  |  Height:  |  Size: 154 B

View File

Before

Width:  |  Height:  |  Size: 206 B

After

Width:  |  Height:  |  Size: 206 B

View File

Before

Width:  |  Height:  |  Size: 371 B

After

Width:  |  Height:  |  Size: 371 B

View File

@@ -13,12 +13,13 @@ import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@/comp
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { $publicKey, pb } from '@/lib/stores'
import { Copy, Plus } from 'lucide-react'
import { useState, useRef, MutableRefObject, useEffect } from 'react'
import { Copy, PlusIcon } from 'lucide-react'
import { useState, useRef, MutableRefObject } from 'react'
import { useStore } from '@nanostores/react'
import { copyToClipboard } from '@/lib/utils'
import { cn, copyToClipboard, isReadOnlyUser } from '@/lib/utils'
import { navigate } from './router'
export function AddSystemButton() {
export function AddSystemButton({ className }: { className?: string }) {
const [open, setOpen] = useState(false)
const port = useRef() as MutableRefObject<HTMLInputElement>
const publicKey = useStore($publicKey)
@@ -38,16 +39,6 @@ export function AddSystemButton() {
# FILESYSTEM: /dev/sda1 # set to the correct filesystem for disk I/O stats`)
}
useEffect(() => {
if (publicKey || !open) {
return
}
// get public key
pb.send('/api/beszel/getkey', {}).then(({ key }) => {
$publicKey.set(key)
})
}, [open])
async function handleSubmit(e: SubmitEvent) {
e.preventDefault()
const formData = new FormData(e.target as HTMLFormElement)
@@ -56,6 +47,7 @@ export function AddSystemButton() {
try {
setOpen(false)
await pb.collection('systems').create(data)
navigate('/')
// console.log(record)
} catch (e) {
console.log(e)
@@ -65,9 +57,12 @@ export function AddSystemButton() {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline" className="flex gap-1">
<Plus className="h-4 w-4 mr-auto" />
Add <span className="hidden sm:inline">System</span>
<Button
variant="outline"
className={cn('flex gap-1 max-xs:h-[2.4rem]', className, isReadOnlyUser() && 'hidden')}
>
<PlusIcon className="h-4 w-4 -ml-1" />
Add <span className="hidden xs:inline">System</span>
</Button>
</DialogTrigger>
<DialogContent className="w-[90%] sm:max-w-[425px] rounded-lg">

View File

@@ -83,7 +83,7 @@ export default function BandwidthChart({
name="Sent"
type="monotoneX"
fill="hsl(var(--chart-5))"
fillOpacity={0.4}
fillOpacity={0.2}
stroke="hsl(var(--chart-5))"
// animationDuration={1200}
isAnimationActive={false}
@@ -93,7 +93,7 @@ export default function BandwidthChart({
name="Received"
type="monotoneX"
fill="hsl(var(--chart-2))"
fillOpacity={0.4}
fillOpacity={0.2}
stroke="hsl(var(--chart-2))"
// animationDuration={1200}
isAnimationActive={false}

View File

@@ -9,6 +9,7 @@ import { $chartTime } from '@/lib/stores'
import { chartTimeData, cn } from '@/lib/utils'
import { ChartTimes } from '@/types'
import { useStore } from '@nanostores/react'
import { HistoryIcon } from 'lucide-react'
export default function ChartTimeSelect({ className }: { className?: string }) {
const chartTime = useStore($chartTime)
@@ -19,7 +20,8 @@ export default function ChartTimeSelect({ className }: { className?: string }) {
value={chartTime}
onValueChange={(value: ChartTimes) => $chartTime.set(value)}
>
<SelectTrigger className={cn(className, 'px-5')}>
<SelectTrigger className={cn(className, 'relative pl-10 pr-5')}>
<HistoryIcon className="h-4 w-4 absolute left-4 top-1/2 -translate-y-1/2 opacity-80" />
<SelectValue />
</SelectTrigger>
<SelectContent>

View File

@@ -74,6 +74,7 @@ export default function ContainerCpuChart({
>
<AreaChart
accessibilityLayer
// syncId={'cpu'}
data={chartData}
margin={{
top: 10,

View File

@@ -29,7 +29,12 @@ export default function CpuChart({
'opacity-100': yAxisSet,
})}
>
<AreaChart accessibilityLayer data={systemData} margin={{ top: 10 }}>
<AreaChart
accessibilityLayer
data={systemData}
margin={{ top: 10 }}
// syncId={'cpu'}
>
<CartesianGrid vertical={false} />
<YAxis
className="tracking-tighter"

View File

@@ -87,7 +87,7 @@ export default function DiskIoChart({
name="Write"
type="monotoneX"
fill="hsl(var(--chart-3))"
fillOpacity={0.4}
fillOpacity={0.3}
stroke="hsl(var(--chart-3))"
// animationDuration={1200}
isAnimationActive={false}
@@ -97,7 +97,7 @@ export default function DiskIoChart({
name="Read"
type="monotoneX"
fill="hsl(var(--chart-1))"
fillOpacity={0.4}
fillOpacity={0.3}
stroke="hsl(var(--chart-1))"
// animationDuration={1200}
isAnimationActive={false}

View File

@@ -46,6 +46,7 @@ export default function MemChart({
<YAxis
// use "ticks" instead of domain / tickcount if need more control
domain={[0, totalMem]}
tickCount={9}
className="tracking-tighter"
width={yAxisWidth}
tickLine={false}

View File

@@ -0,0 +1,128 @@
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from 'recharts'
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from '@/components/ui/chart'
import {
chartTimeData,
cn,
formatShortDate,
toFixedWithoutTrailingZeros,
useYaxisWidth,
} from '@/lib/utils'
import { useStore } from '@nanostores/react'
import { $chartTime } from '@/lib/stores'
import { SystemStatsRecord } from '@/types'
import { useMemo, useRef } from 'react'
export default function TemperatureChart({
ticks,
systemData,
}: {
ticks: number[]
systemData: SystemStatsRecord[]
}) {
const chartRef = useRef<HTMLDivElement>(null)
const yAxisWidth = useYaxisWidth(chartRef)
const chartTime = useStore($chartTime)
/** Format temperature data for chart and assign colors */
const newChartData = useMemo(() => {
const chartData = { data: [], colors: {} } as {
data: Record<string, number | string>[]
colors: Record<string, string>
}
const tempSums = {} as Record<string, number>
for (let data of systemData) {
let newData = { created: data.created } as Record<string, number | string>
let keys = Object.keys(data.stats?.t ?? {})
for (let i = 0; i < keys.length; i++) {
let key = keys[i]
newData[key] = data.stats.t![key]
tempSums[key] = (tempSums[key] ?? 0) + newData[key]
}
chartData.data.push(newData)
}
const keys = Object.keys(tempSums).sort((a, b) => tempSums[b] - tempSums[a])
for (let key of keys) {
chartData.colors[key] = `hsl(${((keys.indexOf(key) * 360) / keys.length) % 360}, 60%, 55%)`
}
return chartData
}, [systemData])
const yAxisSet = useMemo(() => yAxisWidth !== 180, [yAxisWidth])
return (
<div ref={chartRef}>
{/* {!yAxisSet && <Spinner />} */}
<ChartContainer
config={{}}
className={cn('h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity', {
'opacity-100': yAxisSet,
})}
>
<LineChart
accessibilityLayer
data={newChartData.data}
margin={{
left: 0,
right: 0,
top: 10,
bottom: 0,
}}
>
<CartesianGrid vertical={false} />
<YAxis
className="tracking-tighter"
width={yAxisWidth}
tickFormatter={(value) => toFixedWithoutTrailingZeros(value, 2)}
tickLine={false}
axisLine={false}
unit={' °C'}
/>
<XAxis
dataKey="created"
domain={[ticks[0], ticks.at(-1)!]}
ticks={ticks}
type="number"
scale={'time'}
minTickGap={35}
tickMargin={8}
axisLine={false}
tickFormatter={chartTimeData[chartTime].format}
/>
<ChartTooltip
animationEasing="ease-out"
animationDuration={150}
// @ts-ignore
itemSorter={(a, b) => b.value - a.value}
content={
<ChartTooltipContent
unit=" °C"
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
indicator="line"
/>
}
/>
{Object.keys(newChartData.colors).map((key) => (
<Line
key={key}
dataKey={key}
name={key}
type="monotoneX"
dot={false}
strokeWidth={1.5}
stroke={newChartData.colors[key]}
isAnimationActive={false}
/>
))}
<ChartLegend content={<ChartLegendContent />} />
</LineChart>
</ChartContainer>
</div>
)
}

View File

@@ -43,7 +43,7 @@ export default function CommandPalette() {
return (
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Type a command or search..." />
<CommandInput placeholder="Search for systems or settings..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions">
@@ -77,7 +77,7 @@ export default function CommandPalette() {
<CommandItem
key={system.id}
onSelect={() => {
navigate(`/system/${system.name}`)
navigate(`/system/${encodeURIComponent(system.name)}`)
setOpen(false)
}}
>

View File

@@ -0,0 +1,85 @@
import { Suspense, lazy, useEffect, useState } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
import { $alerts, $hubVersion, $systems, pb } from '@/lib/stores'
import { useStore } from '@nanostores/react'
import { GithubIcon } from 'lucide-react'
import { Separator } from '../ui/separator'
import { updateRecordList, updateSystemList } from '@/lib/utils'
import { AlertRecord, SystemRecord } from '@/types'
import { Input } from '../ui/input'
const SystemsTable = lazy(() => import('../systems-table/systems-table'))
export default function () {
const hubVersion = useStore($hubVersion)
const [filter, setFilter] = useState<string>()
useEffect(() => {
document.title = 'Dashboard / Beszel'
// make sure we have the latest list of systems
updateSystemList()
// subscribe to real time updates for systems / alerts
pb.collection<SystemRecord>('systems').subscribe('*', (e) => {
updateRecordList(e, $systems)
})
pb.collection<AlertRecord>('alerts').subscribe('*', (e) => {
updateRecordList(e, $alerts)
})
return () => {
pb.collection('systems').unsubscribe('*')
pb.collection('alerts').unsubscribe('*')
}
}, [])
return (
<>
<Card>
<CardHeader className="pb-5 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1">
<div className="grid md:flex gap-3 w-full items-end">
<div className="px-2 sm:px-1">
<CardTitle className="mb-2.5">All Systems</CardTitle>
<CardDescription>
Updated in real time. Press{' '}
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-0.5 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
<span className="text-xs"></span>K
</kbd>{' '}
to open the command palette.
</CardDescription>
</div>
<Input
placeholder="Filter..."
onChange={(e) => setFilter(e.target.value)}
className="w-full md:w-56 lg:w-80 ml-auto pl-4"
/>
</div>
</CardHeader>
<CardContent className="max-sm:p-2">
<Suspense>
<SystemsTable filter={filter} />
</Suspense>
</CardContent>
</Card>
{hubVersion && (
<div className="flex gap-1.5 justify-end items-center pr-3 sm:pr-6 mt-3.5 text-xs opacity-80">
<a
href="https://github.com/henrygd/beszel"
target="_blank"
className="flex items-center gap-0.5 text-muted-foreground hover:text-foreground duration-75"
>
<GithubIcon className="h-3 w-3" /> GitHub
</a>
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
<a
href="https://github.com/henrygd/beszel/releases"
target="_blank"
className="text-muted-foreground hover:text-foreground duration-75"
>
Beszel {hubVersion}
</a>
</div>
)}
</>
)
}

View File

@@ -1,4 +1,4 @@
import { $updatedSystem, $systems, pb, $chartTime } from '@/lib/stores'
import { $systems, pb, $chartTime } from '@/lib/stores'
import { ContainerStatsRecord, SystemRecord, SystemStatsRecord } from '@/types'
import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
@@ -20,13 +20,13 @@ const DiskIoChart = lazy(() => import('../charts/disk-io-chart'))
const BandwidthChart = lazy(() => import('../charts/bandwidth-chart'))
const ContainerNetChart = lazy(() => import('../charts/container-net-chart'))
const SwapChart = lazy(() => import('../charts/swap-chart'))
const TemperatureChart = lazy(() => import('../charts/temperature-chart'))
export default function ServerDetail({ name }: { name: string }) {
export default function SystemDetail({ name }: { name: string }) {
const systems = useStore($systems)
const updatedSystem = useStore($updatedSystem)
const chartTime = useStore($chartTime)
const [ticks, setTicks] = useState([] as number[])
const [server, setServer] = useState({} as SystemRecord)
const [system, setSystem] = useState({} as SystemRecord)
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
const [hasDockerStats, setHasDocker] = useState(false)
const [dockerCpuChartData, setDockerCpuChartData] = useState<Record<string, number | string>[]>(
@@ -58,26 +58,32 @@ export default function ServerDetail({ name }: { name: string }) {
useEffect(resetCharts, [chartTime])
useEffect(() => {
if (server.id && server.name === name) {
if (system.id && system.name === name) {
return
}
const matchingServer = systems.find((s) => s.name === name) as SystemRecord
if (matchingServer) {
setServer(matchingServer)
const matchingSystem = systems.find((s) => s.name === name) as SystemRecord
if (matchingSystem) {
setSystem(matchingSystem)
}
}, [name, server, systems])
}, [name, system, systems])
// update server when new data is available
// update system when new data is available
useEffect(() => {
if (updatedSystem.id === server.id) {
setServer(updatedSystem)
if (!system.id) {
return
}
}, [updatedSystem])
pb.collection<SystemRecord>('systems').subscribe(system.id, (e) => {
setSystem(e.record)
})
return () => {
pb.collection('systems').unsubscribe(system.id)
}
}, [system])
async function getStats<T>(collection: string): Promise<T[]> {
return await pb.collection<T>(collection).getFullList({
filter: pb.filter('system={:id} && created > {:created} && type={:type}', {
id: server.id,
id: system.id,
created: getPbTimestamp(chartTime),
type: chartTimeData[chartTime].type,
}),
@@ -86,27 +92,49 @@ export default function ServerDetail({ name }: { name: string }) {
})
}
// add empty values between records to make gaps if interval is too large
function addEmptyValues<T extends SystemStatsRecord | ContainerStatsRecord>(
records: T[],
expectedInterval: number
) {
const modifiedRecords: T[] = []
let prevTime = 0
for (let i = 0; i < records.length; i++) {
const record = records[i]
record.created = new Date(record.created).getTime()
if (prevTime) {
const interval = record.created - prevTime
// if interval is too large, add a null record
if (interval - interval * 0.5 > expectedInterval) {
// @ts-ignore
modifiedRecords.push({ created: null, stats: null })
}
}
prevTime = record.created
modifiedRecords.push(record)
}
return modifiedRecords
}
// get stats
useEffect(() => {
if (!server.id || !chartTime) {
if (!system.id || !chartTime) {
return
}
Promise.allSettled([
getStats<SystemStatsRecord>('system_stats'),
getStats<ContainerStatsRecord>('container_stats'),
]).then(([systemStats, containerStats]) => {
const expectedInterval = chartTimeData[chartTime].expectedInterval
if (containerStats.status === 'fulfilled' && containerStats.value.length) {
makeContainerData(addEmptyValues(containerStats.value, expectedInterval))
setHasDocker(true)
makeContainerData(containerStats.value)
}
if (systemStats.status === 'fulfilled') {
for (const record of systemStats.value) {
record.created = new Date(record.created).getTime()
}
setSystemStats(systemStats.value)
setSystemStats(addEmptyValues(systemStats.value, expectedInterval))
}
})
}, [server, chartTime])
}, [system, chartTime])
useEffect(() => {
if (!systemStats.length) {
@@ -125,6 +153,13 @@ export default function ServerDetail({ name }: { name: string }) {
const dockerMemData = []
const dockerNetData = []
for (let { created, stats } of containers) {
if (!created) {
let nullData = { time: null } as unknown
dockerCpuData.push(nullData as Record<string, number | string>)
dockerMemData.push(nullData as Record<string, number | string>)
dockerNetData.push(nullData as Record<string, number | number[]>)
continue
}
const time = new Date(created).getTime()
let cpuData = { time } as Record<string, number | string>
let memData = { time } as Record<string, number | string>
@@ -138,77 +173,78 @@ export default function ServerDetail({ name }: { name: string }) {
dockerMemData.push(memData)
dockerNetData.push(netData)
}
// console.log('dockerMemData', dockerMemData)
setDockerCpuChartData(dockerCpuData)
setDockerMemChartData(dockerMemData)
setDockerNetChartData(dockerNetData)
}, [])
const uptime = useMemo(() => {
let uptime = server.info?.u || 0
let uptime = system.info?.u || 0
if (uptime < 172800) {
return `${Math.trunc(uptime / 3600)} hours`
}
return `${Math.trunc(server.info?.u / 86400)} days`
}, [server.info?.u])
return `${Math.trunc(system.info?.u / 86400)} days`
}, [system.info?.u])
if (!server.id) {
if (!system.id) {
return null
}
return (
<div className="grid gap-4 mb-10">
<Card>
<div className="grid gap-2 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
<h1 className="text-[1.6rem] font-semibold">{server.name}</h1>
<div className="flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90">
<div className="capitalize flex gap-2 items-center">
<span className={cn('relative flex h-3 w-3')}>
{server.status === 'up' && (
<div className="grid lg:grid-cols-2 gap-4 mb-10">
<Card className="col-span-full">
<div className="grid lg:flex items-center gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
<div>
<h1 className="text-[1.6rem] font-semibold mb-1.5">{system.name}</h1>
<div className="flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90">
<div className="capitalize flex gap-2 items-center">
<span className={cn('relative flex h-3 w-3')}>
{system.status === 'up' && (
<span
className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
style={{ animationDuration: '1.5s' }}
></span>
)}
<span
className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
style={{ animationDuration: '1.5s' }}
className={cn('relative inline-flex rounded-full h-3 w-3', {
'bg-green-500': system.status === 'up',
'bg-red-500': system.status === 'down',
'bg-primary/40': system.status === 'paused',
'bg-yellow-500': system.status === 'pending',
})}
></span>
)}
<span
className={cn('relative inline-flex rounded-full h-3 w-3', {
'bg-green-500': server.status === 'up',
'bg-red-500': server.status === 'down',
'bg-primary/40': server.status === 'paused',
'bg-yellow-500': server.status === 'pending',
})}
></span>
</span>
{server.status}
</div>
<Separator orientation="vertical" className="h-4 bg-primary/30" />
<div className="flex gap-1.5">
<GlobeIcon className="h-4 w-4 mt-[1px]" /> {server.host}
</div>
{server.info?.u && (
<TooltipProvider>
<Tooltip>
</span>
{system.status}
</div>
<Separator orientation="vertical" className="h-4 bg-primary/30" />
<div className="flex gap-1.5">
<GlobeIcon className="h-4 w-4 mt-[1px]" /> {system.host}
</div>
{system.info?.u && (
<TooltipProvider>
<Tooltip>
<Separator orientation="vertical" className="h-4 bg-primary/30" />
<TooltipTrigger asChild>
<div className="flex gap-1.5">
<ClockArrowUp className="h-4 w-4 mt-[1px]" /> {uptime}
</div>
</TooltipTrigger>
<TooltipContent>Uptime</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{system.info?.m && (
<>
<Separator orientation="vertical" className="h-4 bg-primary/30" />
<TooltipTrigger asChild>
<div className="flex gap-1.5">
<ClockArrowUp className="h-4 w-4 mt-[1px]" /> {uptime}
</div>
</TooltipTrigger>
<TooltipContent>Uptime</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{server.info?.m && (
<>
<Separator orientation="vertical" className="h-4 bg-primary/30" />
<div className="flex gap-1.5">
<CpuIcon className="h-4 w-4 mt-[1px]" />
{server.info.m} ({server.info.c}c / {server.info.t}t)
</div>
</>
)}
<div className="flex gap-1.5">
<CpuIcon className="h-4 w-4 mt-[1px]" />
{system.info.m} ({system.info.c}c / {system.info.t}t)
</div>
</>
)}
</div>
</div>
<ChartTimeSelect className="mt-2 -ml-1 sm:hidden" />
<ChartTimeSelect className="w-full lg:w-40 xl:w-52 ml-auto max-sm:-mb-1" />
</div>
</Card>
@@ -238,10 +274,13 @@ export default function ServerDetail({ name }: { name: string }) {
</ChartCard>
)}
<ChartCard
title="Disk Usage"
description="Usage of partition where the root filesystem is mounted"
>
{systemStats.at(-1)?.stats.t && (
<ChartCard title="Temperature" description="Temperatures of system sensors">
<TemperatureChart ticks={ticks} systemData={systemStats} />
</ChartCard>
)}
<ChartCard title="Disk Usage" description="Space usage of root partition">
<DiskChart ticks={ticks} systemData={systemStats} />
</ChartCard>
@@ -263,7 +302,8 @@ export default function ServerDetail({ name }: { name: string }) {
</ChartCard>
{/* add space for tooltip if more than 12 containers */}
{Object.keys(dockerNetChartData[0]).length > 12 && (
<div
<span
className="block"
style={{
height: (Object.keys(dockerNetChartData[0]).length - 13) * 18,
}}
@@ -287,15 +327,15 @@ function ChartCard({
const target = useRef<HTMLDivElement>(null)
const [isInViewport, wrappedTargetRef] = useClampedIsInViewport({ target: target })
return (
<Card className="pb-2 sm:pb-4 col-span-full" ref={wrappedTargetRef}>
<Card className="pb-2 sm:pb-4 even:last-of-type:col-span-full" ref={wrappedTargetRef}>
<CardHeader className="pb-5 pt-4 relative space-y-1 max-sm:py-3 max-sm:px-4">
<CardTitle className="text-xl sm:text-2xl">{title}</CardTitle>
<CardDescription>{description}</CardDescription>
<div className="w-full pt-1 sm:w-40 hidden sm:block absolute top-1.5 right-3.5">
{/* <div className="w-full pt-1 sm:w-40 hidden sm:block absolute top-1.5 right-3.5">
<ChartTimeSelect />
</div>
</div> */}
</CardHeader>
<CardContent className="pl-1 w-[calc(100%-1.6em)] h-52 relative">
<CardContent className="pl-0 w-[calc(100%-1.6em)] h-52 relative">
{<Spinner />}
{isInViewport && <Suspense>{children}</Suspense>}
</CardContent>

View File

@@ -21,7 +21,6 @@ import {
} from '@/components/ui/table'
import { Button, buttonVariants } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
DropdownMenu,
@@ -55,11 +54,11 @@ import {
PauseCircleIcon,
PlayCircleIcon,
Trash2Icon,
WifiIcon,
} from 'lucide-react'
import { useMemo, useState } from 'react'
import { $systems, pb } from '@/lib/stores'
import { useEffect, useMemo, useState } from 'react'
import { $hubVersion, $systems, pb } from '@/lib/stores'
import { useStore } from '@nanostores/react'
import { AddSystemButton } from '../add-system'
import { cn, copyToClipboard, isReadOnlyUser } from '@/lib/utils'
import AlertsButton from '../table-alerts'
import { navigate } from '../router'
@@ -82,7 +81,12 @@ function CellFormatter(info: CellContext<SystemRecord, unknown>) {
)
}
function sortableHeader(column: Column<SystemRecord, unknown>, name: string, Icon: any) {
function sortableHeader(
column: Column<SystemRecord, unknown>,
name: string,
Icon: any,
hideSortIcon = false
) {
return (
<Button
variant="ghost"
@@ -91,16 +95,23 @@ function sortableHeader(column: Column<SystemRecord, unknown>, name: string, Ico
>
<Icon className="mr-2 h-4 w-4" />
{name}
<ArrowUpDown className="ml-2 h-4 w-4" />
{!hideSortIcon && <ArrowUpDown className="ml-2 h-4 w-4" />}
</Button>
)
}
export default function SystemsTable() {
export default function SystemsTable({ filter }: { filter?: string }) {
const data = useStore($systems)
const hubVersion = useStore($hubVersion)
const [sorting, setSorting] = useState<SortingState>([])
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
useEffect(() => {
if (filter !== undefined) {
table.getColumn('name')?.setFilterValue(filter)
}
}, [filter])
const columns: ColumnDef<SystemRecord>[] = useMemo(() => {
return [
{
@@ -150,6 +161,29 @@ export default function SystemsTable() {
cell: CellFormatter,
header: ({ column }) => sortableHeader(column, 'Disk', HardDrive),
},
{
accessorKey: 'info.v',
size: 50,
cell: (info) => {
const version = info.getValue() as string
if (!version || !hubVersion) {
return null
}
return (
<span className="flex gap-2 items-center md:pr-5 tabular-nums pl-1">
<span
className={cn(
'w-2 h-2 left-0 rounded-full',
version === hubVersion ? 'bg-green-500' : 'bg-yellow-500'
)}
style={{ marginBottom: '-1px' }}
></span>
<span>{info.getValue() as string}</span>
</span>
)
},
header: ({ column }) => sortableHeader(column, 'Agent', WifiIcon, true),
},
{
id: 'actions',
size: 120,
@@ -226,7 +260,7 @@ export default function SystemsTable() {
},
},
]
}, [])
}, [hubVersion])
const table = useReactTable({
data,
@@ -248,80 +282,64 @@ export default function SystemsTable() {
})
return (
<>
<div className="w-full">
<div className="flex items-center mb-4 gap-2">
<Input
// @ts-ignore
placeholder="Filter..."
value={(table.getColumn('name')?.getFilterValue() as string) ?? ''}
onChange={(event) => table.getColumn('name')?.setFilterValue(event.target.value)}
className="max-w-sm"
/>
<div className={cn('ml-auto flex gap-2', isReadOnlyUser() && 'hidden')}>
<AddSystemButton />
</div>
</div>
<div className="rounded-md border overflow-hidden">
<Table>
<TableHeader className="bg-muted/40">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead className="px-2" key={header.id}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.original.id}
data-state={row.getIsSelected() && 'selected'}
className={cn('cursor-pointer transition-opacity', {
'opacity-50': row.original.status === 'paused',
})}
onClick={(e) => {
const target = e.target as HTMLElement
if (!target.closest('[data-nolink]') && e.currentTarget.contains(target)) {
navigate(`/system/${row.original.name}`)
}
<div className="rounded-md border overflow-hidden">
<Table>
<TableHeader className="bg-muted/40">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead className="px-2" key={header.id}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.original.id}
data-state={row.getIsSelected() && 'selected'}
className={cn('cursor-pointer transition-opacity', {
'opacity-50': row.original.status === 'paused',
})}
onClick={(e) => {
const target = e.target as HTMLElement
if (!target.closest('[data-nolink]') && e.currentTarget.contains(target)) {
navigate(`/system/${encodeURIComponent(row.original.name)}`)
}
}}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
style={{
width:
cell.column.getSize() === Number.MAX_SAFE_INTEGER
? 'auto'
: cell.column.getSize(),
}}
className={'overflow-hidden relative py-2.5'}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
style={{
width:
cell.column.getSize() === Number.MAX_SAFE_INTEGER
? 'auto'
: cell.column.getSize(),
}}
className={'overflow-hidden relative py-2.5'}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No systems found
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
</>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No systems found
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
)
}

View File

@@ -258,7 +258,7 @@ const ChartLegendContent = React.forwardRef<
nameKey?: string
}
>(({ className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey }, ref) => {
const { config } = useChart()
// const { config } = useChart()
if (!payload?.length) {
return null
@@ -268,33 +268,35 @@ const ChartLegendContent = React.forwardRef<
<div
ref={ref}
className={cn(
'flex items-center justify-center gap-4',
'flex items-center justify-center gap-4 gap-y-1 flex-wrap',
verticalAlign === 'top' ? 'pb-3' : 'pt-3',
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || 'value'}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
// const key = `${nameKey || item.dataKey || 'value'}`
// const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
'flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground'
// 'flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground text-muted-foreground'
'flex items-center gap-1.5 text-muted-foreground'
)}
>
{itemConfig?.icon && !hideIcon ? (
{/* {itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
) : ( */}
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
{item.value}
{/* )} */}
{/* {itemConfig?.label} */}
</div>
)
})}

View File

@@ -11,14 +11,14 @@ export const $authenticated = atom(pb.authStore.isValid)
/** List of system records */
export const $systems = atom([] as SystemRecord[])
/** Last updated system record (realtime) */
export const $updatedSystem = atom({} as SystemRecord)
/** List of alert records */
export const $alerts = atom([] as AlertRecord[])
/** SSH public key */
export const $publicKey = atom('')
/** Beszel hub version */
export const $hubVersion = atom('')
/** Chart time period */
export const $chartTime = atom('1h') as WritableAtom<ChartTimes>

View File

@@ -159,6 +159,7 @@ export function getPbTimestamp(timeString: ChartTimes) {
export const chartTimeData: ChartTimeData = {
'1h': {
type: '1m',
expectedInterval: 60_000,
label: '1 hour',
// ticks: 12,
format: (timestamp: string) => hourWithMinutes(timestamp),
@@ -166,6 +167,7 @@ export const chartTimeData: ChartTimeData = {
},
'12h': {
type: '10m',
expectedInterval: 60_000 * 10,
label: '12 hours',
ticks: 12,
format: (timestamp: string) => hourWithMinutes(timestamp),
@@ -173,12 +175,14 @@ export const chartTimeData: ChartTimeData = {
},
'24h': {
type: '20m',
expectedInterval: 60_000 * 20,
label: '24 hours',
format: (timestamp: string) => hourWithMinutes(timestamp),
getOffset: (endTime: Date) => timeHour.offset(endTime, -24),
},
'1w': {
type: '120m',
expectedInterval: 60_000 * 120,
label: '1 week',
ticks: 7,
format: (timestamp: string) => formatShortDate(timestamp),
@@ -186,6 +190,7 @@ export const chartTimeData: ChartTimeData = {
},
'30d': {
type: '480m',
expectedInterval: 60_000 * 480,
label: '30 days',
ticks: 30,
format: (timestamp: string) => formatDay(timestamp),
@@ -203,7 +208,7 @@ export function useYaxisWidth(chartRef: React.RefObject<HTMLDivElement>) {
if (yAxisElement) {
// console.log('yAxisElement', yAxisElement)
clearInterval(interval)
setYAxisWidth(yAxisElement.getBoundingClientRect().width + 22)
setYAxisWidth(yAxisElement.getBoundingClientRect().width + 24)
}
}, 16)
return () => clearInterval(interval)

View File

@@ -3,20 +3,12 @@ import React, { Suspense, lazy, useEffect } from 'react'
import ReactDOM from 'react-dom/client'
import Home from './components/routes/home.tsx'
import { ThemeProvider } from './components/theme-provider.tsx'
import { $alerts, $authenticated, $updatedSystem, $systems, pb } from './lib/stores.ts'
import { $authenticated, $systems, pb, $publicKey, $hubVersion } from './lib/stores.ts'
import { ModeToggle } from './components/mode-toggle.tsx'
import {
cn,
isAdmin,
updateAlerts,
updateFavicon,
updateRecordList,
updateSystemList,
} from './lib/utils.ts'
import { cn, isAdmin, updateAlerts, updateFavicon, updateSystemList } from './lib/utils.ts'
import { buttonVariants } from './components/ui/button.tsx'
import {
DatabaseBackupIcon,
GithubIcon,
LockKeyholeIcon,
LogOutIcon,
LogsIcon,
@@ -27,12 +19,6 @@ import {
import { useStore } from '@nanostores/react'
import { Toaster } from './components/ui/toaster.tsx'
import { Logo } from './components/logo.tsx'
import {
TooltipProvider,
Tooltip,
TooltipTrigger,
TooltipContent,
} from '@/components/ui/tooltip.tsx'
import {
DropdownMenu,
DropdownMenuContent,
@@ -42,9 +28,9 @@ import {
DropdownMenuTrigger,
DropdownMenuLabel,
} from './components/ui/dropdown-menu.tsx'
import { AlertRecord, SystemRecord } from './types'
import { $router, Link, navigate } from './components/router.tsx'
import ServerDetail from './components/routes/system.tsx'
import SystemDetail from './components/routes/system.tsx'
import { AddSystemButton } from './components/add-system.tsx'
// const ServerDetail = lazy(() => import('./components/routes/system.tsx'))
const CommandPalette = lazy(() => import('./components/command-palette.tsx'))
@@ -60,21 +46,14 @@ const App = () => {
pb.authStore.onChange(() => {
$authenticated.set(pb.authStore.isValid)
})
// get version / public key
pb.send('/api/beszel/getkey', {}).then((data) => {
$publicKey.set(data.key)
$hubVersion.set(data.v)
})
// get servers / alerts
updateSystemList()
updateAlerts()
// subscribe to real time updates for systems / alerts
pb.collection<SystemRecord>('systems').subscribe('*', (e) => {
updateRecordList(e, $systems)
$updatedSystem.set(e.record)
})
pb.collection<AlertRecord>('alerts').subscribe('*', (e) => {
updateRecordList(e, $alerts)
})
return () => {
pb.collection('systems').unsubscribe('*')
pb.collection('alerts').unsubscribe('*')
}
}, [])
// update favicon
@@ -104,7 +83,7 @@ const App = () => {
} else if (page.path === '/') {
return <Home />
} else if (page.route === 'server') {
return <ServerDetail name={page.params.name} />
return <SystemDetail name={page.params.name} />
}
}
@@ -122,7 +101,7 @@ const Layout = () => {
return (
<>
<div className="container">
<div className="flex items-center h-14 md:h-16 bg-card px-6 border bt-0 rounded-md my-4">
<div className="flex items-center h-14 md:h-16 bg-card px-4 pr-3 sm:px-6 border bt-0 rounded-md my-4">
<Link
href="/"
aria-label="Home"
@@ -135,26 +114,8 @@ const Layout = () => {
<Logo className="h-[1.15em] fill-foreground" />
</Link>
<div className={'flex ml-auto'}>
<div className={'flex ml-auto items-center'}>
<ModeToggle />
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<a
title={'Github'}
aria-label="Github repo"
href={'https://github.com/henrygd/beszel'}
target="_blank"
className={cn('', buttonVariants({ variant: 'ghost', size: 'icon' }))}
>
<GithubIcon className="h-[1.2rem] w-[1.2rem]" />
</a>
</TooltipTrigger>
<TooltipContent>
<p>Github Repository</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
@@ -164,7 +125,7 @@ const Layout = () => {
<UserIcon className="h-[1.2rem] w-[1.2rem]" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-44">
<DropdownMenuContent className="min-w-44">
<DropdownMenuLabel>{pb.authStore.model?.email}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
@@ -210,6 +171,7 @@ const Layout = () => {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<AddSystemButton className="ml-2" />
</div>
</div>
</div>

View File

@@ -6,6 +6,7 @@ export interface SystemRecord extends RecordModel {
status: 'up' | 'down' | 'paused' | 'pending'
port: string
info: SystemInfo
v: string
}
export interface SystemInfo {
@@ -56,6 +57,8 @@ export interface SystemStats {
ns: number
/** network received (mb) */
nr: number
/** temperatures */
t?: Record<string, number>
}
export interface ContainerStatsRecord extends RecordModel {
@@ -95,6 +98,7 @@ export type ChartTimes = '1h' | '12h' | '24h' | '1w' | '30d'
export interface ChartTimeData {
[key: string]: {
type: '1m' | '10m' | '20m' | '120m' | '480m'
expectedInterval: number
label: string
ticks?: number
format: (timestamp: string) => string

Some files were not shown because too many files have changed in this diff Show More