Compare commits

...

10 Commits

Author SHA1 Message Date
Henry Dollman
a213b70a1c release 0.5.1 2024-10-02 19:58:44 -04:00
Henry Dollman
66cc0a4b24 log stats on startup if log level is debug 2024-10-02 19:58:02 -04:00
Henry Dollman
f051f6a5f8 add dockerManager / fix for Docker 24 and older
* dockerManager now handles all docker api interaction and container metrics tracking
* sets unlimited concurrency for docker 24 and older
2024-10-02 19:45:26 -04:00
Henry Dollman
b9f142c28c update go deps (gopsutil v4.24.9) 2024-10-02 19:11:13 -04:00
Henry Dollman
45e1283b83 move system.Info to Agent struct
* cleaner to store entire info struct rather than separate properties for unchanging values
2024-10-02 12:34:42 -04:00
Henry Dollman
94cb5f2798 fix uptime hours pluralization 2024-10-02 12:33:31 -04:00
Henry Dollman
2883467b2b update docker image workflow
- change tag to "*v"
- remove cache
2024-10-02 12:32:19 -04:00
Lindemberg Barbosa
0c77190161 fix: readme link for Monitoring additional disks, partitions, or remote mounts (#195) 2024-10-02 10:40:12 -04:00
Henry Dollman
8d4d072343 fix readme backticks for telnet command 2024-09-30 15:45:46 -04:00
Henry Dollman
d6e0daf52a update js deps and add package-lock.json (#192)
- replaces use-is-in-viewport package with lib/use-intersection-observer.ts due to npm dependency conflict
2024-09-30 14:37:59 -04:00
17 changed files with 4575 additions and 198 deletions

View File

@@ -3,7 +3,7 @@ name: Make docker images
on: on:
push: push:
tags: tags:
- '*' - 'v*'
jobs: jobs:
build: build:
@@ -71,5 +71,3 @@ jobs:
push: ${{ github.ref_type == 'tag' }} push: ${{ github.ref_type == 'tag' }}
tags: ${{ steps.metadata.outputs.tags }} tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }} labels: ${{ steps.metadata.outputs.labels }}
cache-from: type=gha
cache-to: type=gha

1
.gitignore vendored
View File

@@ -11,4 +11,3 @@ dist
beszel/cmd/hub/hub beszel/cmd/hub/hub
beszel/cmd/agent/agent beszel/cmd/agent/agent
node_modules node_modules
package-lock.json

View File

@@ -11,7 +11,7 @@ require (
github.com/pocketbase/dbx v1.10.1 github.com/pocketbase/dbx v1.10.1
github.com/pocketbase/pocketbase v0.22.21 github.com/pocketbase/pocketbase v0.22.21
github.com/rhysd/go-github-selfupdate v1.2.3 github.com/rhysd/go-github-selfupdate v1.2.3
github.com/shirou/gopsutil/v4 v4.24.8 github.com/shirou/gopsutil/v4 v4.24.9
github.com/spf13/cobra v1.8.1 github.com/spf13/cobra v1.8.1
golang.org/x/crypto v0.27.0 golang.org/x/crypto v0.27.0
) )
@@ -25,7 +25,7 @@ require (
github.com/aws/aws-sdk-go-v2/config v1.27.39 // indirect github.com/aws/aws-sdk-go-v2/config v1.27.39 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.37 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.37 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.25 // indirect github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.26 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
@@ -34,7 +34,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.20 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.20 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.63.3 // indirect github.com/aws/aws-sdk-go-v2/service/s3 v1.64.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.23.3 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.23.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.3 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.31.3 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.31.3 // indirect
@@ -43,6 +43,7 @@ require (
github.com/disintegration/imaging v1.6.2 // indirect github.com/disintegration/imaging v1.6.2 // indirect
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.8.0 // indirect
github.com/fatih/color v1.17.0 // indirect github.com/fatih/color v1.17.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.5 // indirect github.com/gabriel-vasile/mimetype v1.4.5 // indirect
github.com/ganigeorgiev/fexpr v0.4.1 // indirect github.com/ganigeorgiev/fexpr v0.4.1 // indirect
@@ -67,7 +68,6 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/spf13/cast v1.7.0 // indirect github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/tcnksm/go-gitconfig v0.1.2 // indirect github.com/tcnksm/go-gitconfig v0.1.2 // indirect
@@ -90,8 +90,8 @@ require (
golang.org/x/time v0.6.0 // indirect golang.org/x/time v0.6.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/api v0.199.0 // indirect google.golang.org/api v0.199.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f // indirect
google.golang.org/grpc v1.67.0 // indirect google.golang.org/grpc v1.67.1 // indirect
google.golang.org/protobuf v1.34.2 // indirect google.golang.org/protobuf v1.34.2 // indirect
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect
modernc.org/libc v1.61.0 // indirect modernc.org/libc v1.61.0 // indirect

View File

@@ -36,8 +36,8 @@ github.com/aws/aws-sdk-go-v2/credentials v1.17.37 h1:G2aOH01yW8X373JK419THj5QVqu
github.com/aws/aws-sdk-go-v2/credentials v1.17.37/go.mod h1:0ecCjlb7htYCptRD45lXJ6aJDQac6D2NlKGpZqyTG6A= github.com/aws/aws-sdk-go-v2/credentials v1.17.37/go.mod h1:0ecCjlb7htYCptRD45lXJ6aJDQac6D2NlKGpZqyTG6A=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 h1:C/d03NAmh8C4BZXhuRNboF/DqhBkBCeDiJDcaqIT5pA= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 h1:C/d03NAmh8C4BZXhuRNboF/DqhBkBCeDiJDcaqIT5pA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14/go.mod h1:7I0Ju7p9mCIdlrfS+JCgqcYD0VXz/N4yozsox+0o078= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14/go.mod h1:7I0Ju7p9mCIdlrfS+JCgqcYD0VXz/N4yozsox+0o078=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.25 h1:HkpHeZMM39sGtMHVYG1buAg93vhj5d7F81y6G0OAbGc= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.26 h1:BTfwWNFVGLxW2bih/V2xhgCsYDQwG1cAWhWoW9Jx7wE=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.25/go.mod h1:j3Vz04ZjaWA6kygOsZRpmWe4CyGqfqq2u3unDTU0QGA= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.26/go.mod h1:LA1/FxoEFFmv7XpkB8KKqLAUz8AePdK9H0Ec7PUKazs=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 h1:kYQ3H1u0ANr9KEKlGs/jTLrBFPo8P8NaH/w7A01NeeM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 h1:kYQ3H1u0ANr9KEKlGs/jTLrBFPo8P8NaH/w7A01NeeM=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18/go.mod h1:r506HmK5JDUh9+Mw4CfGJGSSoqIiLCndAuqXuhbv67Y= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18/go.mod h1:r506HmK5JDUh9+Mw4CfGJGSSoqIiLCndAuqXuhbv67Y=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 h1:Z7IdFUONvTcvS7YuhtVxN99v2cCoHRXOS4mTr0B/pUc= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 h1:Z7IdFUONvTcvS7YuhtVxN99v2cCoHRXOS4mTr0B/pUc=
@@ -54,8 +54,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 h1:Xbwbmk44
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20/go.mod h1:oAfOFzUB14ltPZj1rWwRc3d/6OgD76R8KlvU3EqM9Fg= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20/go.mod h1:oAfOFzUB14ltPZj1rWwRc3d/6OgD76R8KlvU3EqM9Fg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18 h1:eb+tFOIl9ZsUe2259/BKPeniKuz4/02zZFH/i4Nf8Rg= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18 h1:eb+tFOIl9ZsUe2259/BKPeniKuz4/02zZFH/i4Nf8Rg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18/go.mod h1:GVCC2IJNJTmdlyEsSmofEy7EfJncP7DNnXDzRjJ5Keg= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18/go.mod h1:GVCC2IJNJTmdlyEsSmofEy7EfJncP7DNnXDzRjJ5Keg=
github.com/aws/aws-sdk-go-v2/service/s3 v1.63.3 h1:3zt8qqznMuAZWDTDpcwv9Xr11M/lVj2FsRR7oYBt0OA= github.com/aws/aws-sdk-go-v2/service/s3 v1.64.0 h1:I0p8knB/IDYSQ3dbanaCr4UhiYQ96bvKRhGYxvLyiD8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.63.3/go.mod h1:NLTqRLe3pUNu3nTEHI6XlHLKYmc8fbHUdMxAB6+s41Q= github.com/aws/aws-sdk-go-v2/service/s3 v1.64.0/go.mod h1:NLTqRLe3pUNu3nTEHI6XlHLKYmc8fbHUdMxAB6+s41Q=
github.com/aws/aws-sdk-go-v2/service/sso v1.23.3 h1:rs4JCczF805+FDv2tRhZ1NU0RB2H6ryAvsWPanAr72Y= github.com/aws/aws-sdk-go-v2/service/sso v1.23.3 h1:rs4JCczF805+FDv2tRhZ1NU0RB2H6ryAvsWPanAr72Y=
github.com/aws/aws-sdk-go-v2/service/sso v1.23.3/go.mod h1:XRlMvmad0ZNL+75C5FYdMvbbLkd6qiqz6foR1nA1PXY= github.com/aws/aws-sdk-go-v2/service/sso v1.23.3/go.mod h1:XRlMvmad0ZNL+75C5FYdMvbbLkd6qiqz6foR1nA1PXY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.3 h1:S7EPdMVZod8BGKQQPTBK+FcX9g7bKR7c4+HxWqHP7Vg= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.3 h1:S7EPdMVZod8BGKQQPTBK+FcX9g7bKR7c4+HxWqHP7Vg=
@@ -84,6 +84,8 @@ github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCO
github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c= github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE=
github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -227,12 +229,8 @@ 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 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shirou/gopsutil/v4 v4.24.8 h1:pVQjIenQkIhqO81mwTaXjTzOMT7d3TZkf43PlVFHENI= github.com/shirou/gopsutil/v4 v4.24.9 h1:KIV+/HaHD5ka5f570RZq+2SaeFsb/pq+fp2DGNWYoOI=
github.com/shirou/gopsutil/v4 v4.24.8/go.mod h1:wE0OrJtj4dG+hYkxqDH3QiBICdKSf04/npcvLLc/oRg= github.com/shirou/gopsutil/v4 v4.24.9/go.mod h1:3fkaHNeYsUFCGZ8+9vZVWtbyM1k2eRnlL+bWO8Bxa/Q=
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 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 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 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
@@ -379,15 +377,15 @@ google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eY
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4= google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 h1:N9BgCIAUvn/M+p4NJccWPWb3BWh88+zyL0ll9HgbEeM= google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f h1:cUMEy+8oS78BWIH9OWazBkzbr090Od9tWBNtZHkOhf0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 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.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=

View File

@@ -2,44 +2,30 @@
package agent package agent
import ( import (
"beszel/internal/entities/container"
"beszel/internal/entities/system" "beszel/internal/entities/system"
"context" "context"
"log/slog" "log/slog"
"net/http"
"os" "os"
"strings" "strings"
"sync"
"github.com/shirou/gopsutil/v4/common" "github.com/shirou/gopsutil/v4/common"
) )
type Agent struct { type Agent struct {
hostname string // Hostname of the system debug bool // true if LOG_LEVEL is set to debug
kernelVersion string // Kernel version of the system fsNames []string // List of filesystem device names being monitored
cpuModel string // CPU model of the system fsStats map[string]*system.FsStats // Keeps track of disk stats for each filesystem
cores int // Number of cores of the system netInterfaces map[string]struct{} // Stores all valid network interfaces
threads int // Number of threads of the system netIoStats system.NetIoStats // Keeps track of bandwidth usage
debug bool // true if LOG_LEVEL is set to debug dockerManager *dockerManager // Manages Docker API requests
fsNames []string // List of filesystem device names being monitored sensorsContext context.Context // Sensors context to override sys location
fsStats map[string]*system.FsStats // Keeps track of disk stats for each filesystem sensorsWhitelist map[string]struct{} // List of sensors to monitor
netInterfaces map[string]struct{} // Stores all valid network interfaces systemInfo system.Info // Host system info
netIoStats system.NetIoStats // Keeps track of bandwidth usage
containerStatsMap map[string]*container.Stats // Keeps track of container stats
containerStatsMutex sync.RWMutex // Mutex to prevent concurrent access to prevContainerStatsMap
dockerClient *http.Client // HTTP client to query docker api
apiContainerList *[]container.ApiInfo // List of containers from docker host
sensorsContext context.Context // Sensors context to override sys location
sensorsWhitelist map[string]struct{} // List of sensors to monitor
} }
func NewAgent() *Agent { func NewAgent() *Agent {
return &Agent{ return &Agent{
containerStatsMap: make(map[string]*container.Stats), sensorsContext: context.Background(),
containerStatsMutex: sync.RWMutex{},
netIoStats: system.NetIoStats{},
dockerClient: newDockerClient(),
sensorsContext: context.Background(),
} }
} }
@@ -73,22 +59,30 @@ func (a *Agent) Run(pubKey []byte, addr string) {
} }
} }
// initialize system info / docker manager
a.initializeSystemInfo() a.initializeSystemInfo()
a.initializeDiskInfo() a.initializeDiskInfo()
a.initializeNetIoStats() a.initializeNetIoStats()
a.dockerManager = newDockerManager()
// if debugging, print stats
if a.debug {
slog.Debug("Stats", "data", a.gatherStats())
}
a.startServer(pubKey, addr) a.startServer(pubKey, addr)
} }
func (a *Agent) gatherStats() system.CombinedData { func (a *Agent) gatherStats() system.CombinedData {
systemInfo, SystemStats := a.getSystemStats()
systemData := system.CombinedData{ systemData := system.CombinedData{
Stats: SystemStats, Stats: a.getSystemStats(),
Info: systemInfo, Info: a.systemInfo,
} }
// add docker stats // add docker stats
if containerStats, err := a.getDockerStats(); err == nil { if containerStats, err := a.dockerManager.getDockerStats(); err == nil {
systemData.Containers = containerStats systemData.Containers = containerStats
} else {
slog.Debug("Error getting docker stats", "err", err)
} }
// add extra filesystems // add extra filesystems
systemData.Stats.ExtraFs = make(map[string]*system.FsStats) systemData.Stats.ExtraFs = make(map[string]*system.FsStats)

View File

@@ -13,90 +13,110 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/blang/semver"
) )
type dockerManager struct {
client *http.Client // Client to query Docker API
wg sync.WaitGroup // WaitGroup to wait for all goroutines to finish
sem chan struct{} // Semaphore to limit concurrent container requests
containerStatsMutex sync.RWMutex // Mutex to prevent concurrent access to containerStatsMap
apiContainerList *[]container.ApiInfo // List of containers from Docker API
containerStatsMap map[string]*container.Stats // Keeps track of container stats
validIds map[string]struct{} // Map of valid container ids, used to prune invalid containers from containerStatsMap
}
// Add goroutine to the queue
func (d *dockerManager) queue() {
d.sem <- struct{}{}
d.wg.Add(1)
}
// Remove goroutine from the queue
func (d *dockerManager) dequeue() {
<-d.sem
d.wg.Done()
}
// Returns stats for all running containers // Returns stats for all running containers
func (a *Agent) getDockerStats() ([]*container.Stats, error) { func (dm *dockerManager) getDockerStats() ([]*container.Stats, error) {
resp, err := a.dockerClient.Get("http://localhost/containers/json") resp, err := dm.client.Get("http://localhost/containers/json")
if err != nil { if err != nil {
a.closeIdleConnections(err)
return nil, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&a.apiContainerList); err != nil { if err := json.NewDecoder(resp.Body).Decode(&dm.apiContainerList); err != nil {
slog.Error("Error decoding containers", "err", err)
return nil, err return nil, err
} }
containersLength := len(*a.apiContainerList) containersLength := len(*dm.apiContainerList)
containerStats := make([]*container.Stats, containersLength)
// store valid ids to clean up old container ids from map // store valid ids to clean up old container ids from map
validIds := make(map[string]struct{}, containersLength) if dm.validIds == nil {
dm.validIds = make(map[string]struct{}, containersLength)
} else {
clear(dm.validIds)
}
var wg sync.WaitGroup for _, ctr := range *dm.apiContainerList {
for i, ctr := range *a.apiContainerList {
ctr.IdShort = ctr.Id[:12] ctr.IdShort = ctr.Id[:12]
validIds[ctr.IdShort] = struct{}{} dm.validIds[ctr.IdShort] = struct{}{}
// check if container is less than 1 minute old (possible restart) // check if container is less than 1 minute old (possible restart)
// note: can't use Created field because it's not updated on restart // note: can't use Created field because it's not updated on restart
if strings.Contains(ctr.Status, "second") { if strings.Contains(ctr.Status, "second") {
// if so, remove old container data // if so, remove old container data
a.deleteContainerStatsSync(ctr.IdShort) dm.deleteContainerStatsSync(ctr.IdShort)
} }
wg.Add(1) dm.queue()
go func() { go func() {
defer wg.Done() defer dm.dequeue()
stats, err := a.getContainerStats(ctr) err := dm.updateContainerStats(ctr)
if err != nil { if err != nil {
// close idle connections if error is a network timeout dm.deleteContainerStatsSync(ctr.IdShort)
isTimeout := a.closeIdleConnections(err)
// delete container from map if not a timeout
if !isTimeout {
a.deleteContainerStatsSync(ctr.IdShort)
}
// retry once // retry once
stats, err = a.getContainerStats(ctr) err = dm.updateContainerStats(ctr)
if err != nil { if err != nil {
slog.Error("Error getting container stats", "err", err) slog.Error("Error getting container stats", "err", err)
} }
} }
containerStats[i] = stats
}() }()
} }
wg.Wait() dm.wg.Wait()
// remove old / invalid container stats // populate final stats and remove old / invalid container stats
for id := range a.containerStatsMap { stats := make([]*container.Stats, 0, containersLength)
if _, exists := validIds[id]; !exists { for id, v := range dm.containerStatsMap {
delete(a.containerStatsMap, id) if _, exists := dm.validIds[id]; !exists {
delete(dm.containerStatsMap, id)
} else {
stats = append(stats, v)
} }
} }
return containerStats, nil return stats, nil
} }
// Returns stats for individual container // Updates stats for individual container
func (a *Agent) getContainerStats(ctr container.ApiInfo) (*container.Stats, error) { func (dm *dockerManager) updateContainerStats(ctr container.ApiInfo) error {
name := ctr.Names[0][1:] name := ctr.Names[0][1:]
resp, err := a.dockerClient.Get("http://localhost/containers/" + ctr.IdShort + "/stats?stream=0&one-shot=1") resp, err := dm.client.Get("http://localhost/containers/" + ctr.IdShort + "/stats?stream=0&one-shot=1")
if err != nil { if err != nil {
return &container.Stats{Name: name}, err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
a.containerStatsMutex.Lock() dm.containerStatsMutex.Lock()
defer a.containerStatsMutex.Unlock() defer dm.containerStatsMutex.Unlock()
// add empty values if they doesn't exist in map // add empty values if they doesn't exist in map
stats, initialized := a.containerStatsMap[ctr.IdShort] stats, initialized := dm.containerStatsMap[ctr.IdShort]
if !initialized { if !initialized {
stats = &container.Stats{Name: name} stats = &container.Stats{Name: name}
a.containerStatsMap[ctr.IdShort] = stats dm.containerStatsMap[ctr.IdShort] = stats
} }
// reset current stats // reset current stats
@@ -108,12 +128,12 @@ func (a *Agent) getContainerStats(ctr container.ApiInfo) (*container.Stats, erro
// docker host container stats response // docker host container stats response
var res container.ApiStats var res container.ApiStats
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
return stats, err return err
} }
// check if container has valid data, otherwise may be in restart loop (#103) // check if container has valid data, otherwise may be in restart loop (#103)
if res.MemoryStats.Usage == 0 { if res.MemoryStats.Usage == 0 {
return stats, fmt.Errorf("%s - no memory stats - see https://github.com/henrygd/beszel/issues/144", name) return fmt.Errorf("%s - no memory stats - see https://github.com/henrygd/beszel/issues/144", name)
} }
// memory (https://docs.docker.com/reference/cli/docker/container/stats/) // memory (https://docs.docker.com/reference/cli/docker/container/stats/)
@@ -128,7 +148,7 @@ func (a *Agent) getContainerStats(ctr container.ApiInfo) (*container.Stats, erro
systemDelta := res.CPUStats.SystemUsage - stats.PrevCpu[1] systemDelta := res.CPUStats.SystemUsage - stats.PrevCpu[1]
cpuPct := float64(cpuDelta) / float64(systemDelta) * 100 cpuPct := float64(cpuDelta) / float64(systemDelta) * 100
if cpuPct > 100 { if cpuPct > 100 {
return stats, fmt.Errorf("%s cpu pct greater than 100: %+v", name, cpuPct) return fmt.Errorf("%s cpu pct greater than 100: %+v", name, cpuPct)
} }
stats.PrevCpu = [2]uint64{res.CPUStats.CPUUsage.TotalUsage, res.CPUStats.SystemUsage} stats.PrevCpu = [2]uint64{res.CPUStats.CPUUsage.TotalUsage, res.CPUStats.SystemUsage}
@@ -154,11 +174,18 @@ func (a *Agent) getContainerStats(ctr container.ApiInfo) (*container.Stats, erro
stats.NetworkSent = bytesToMegabytes(sent_delta) stats.NetworkSent = bytesToMegabytes(sent_delta)
stats.NetworkRecv = bytesToMegabytes(recv_delta) stats.NetworkRecv = bytesToMegabytes(recv_delta)
return stats, nil return nil
} }
// Creates a new http client for docker api // Delete container stats from map using mutex
func newDockerClient() *http.Client { func (dm *dockerManager) deleteContainerStatsSync(id string) {
dm.containerStatsMutex.Lock()
defer dm.containerStatsMutex.Unlock()
delete(dm.containerStatsMap, id)
}
// Creates a new http client for Docker API
func newDockerManager() *dockerManager {
dockerHost := "unix:///var/run/docker.sock" dockerHost := "unix:///var/run/docker.sock"
if dockerHostEnv, exists := os.LookupEnv("DOCKER_HOST"); exists { if dockerHostEnv, exists := os.LookupEnv("DOCKER_HOST"); exists {
slog.Info("DOCKER_HOST", "host", dockerHostEnv) slog.Info("DOCKER_HOST", "host", dockerHostEnv)
@@ -172,12 +199,8 @@ func newDockerClient() *http.Client {
} }
transport := &http.Transport{ transport := &http.Transport{
ForceAttemptHTTP2: false, DisableCompression: true,
IdleConnTimeout: 90 * time.Second, MaxConnsPerHost: 0,
DisableCompression: true,
MaxConnsPerHost: 10,
MaxIdleConnsPerHost: 10,
DisableKeepAlives: false,
} }
switch parsedURL.Scheme { switch parsedURL.Scheme {
@@ -194,18 +217,37 @@ func newDockerClient() *http.Client {
os.Exit(1) os.Exit(1)
} }
return &http.Client{ dockerClient := &dockerManager{
Timeout: time.Second, client: &http.Client{
Transport: transport, Timeout: time.Millisecond * 1100,
Transport: transport,
},
containerStatsMap: make(map[string]*container.Stats),
} }
}
// Closes idle connections on timeouts to prevent reuse of stale connections // Make sure sem is initialized
func (a *Agent) closeIdleConnections(err error) (isTimeout bool) { concurrency := 200
if netErr, ok := err.(net.Error); ok && netErr.Timeout() { defer func() { dockerClient.sem = make(chan struct{}, concurrency) }()
slog.Warn("Closing idle connections", "err", err)
a.dockerClient.Transport.(*http.Transport).CloseIdleConnections() // Check docker version
return true // (versions before 25.0.0 have a bug with one-shot which requires all requests to be made in one batch)
var versionInfo struct {
Version string `json:"Version"`
} }
return false resp, err := dockerClient.client.Get("http://localhost/version")
if err != nil {
return dockerClient
}
if err := json.NewDecoder(resp.Body).Decode(&versionInfo); err != nil {
return dockerClient
}
// if version > 25, one-shot works correctly and we can limit concurrent connections / goroutines to 5
if dockerVersion, err := semver.Parse(versionInfo.Version); err == nil && dockerVersion.Major > 24 {
concurrency = 5
}
slog.Debug("Docker", "version", versionInfo.Version, "concurrency", concurrency)
return dockerClient
} }

View File

@@ -18,26 +18,28 @@ import (
// Sets initial / non-changing values about the host system // Sets initial / non-changing values about the host system
func (a *Agent) initializeSystemInfo() { func (a *Agent) initializeSystemInfo() {
a.kernelVersion, _ = host.KernelVersion() a.systemInfo.AgentVersion = beszel.Version
a.hostname, _ = os.Hostname() a.systemInfo.Hostname, _ = os.Hostname()
a.systemInfo.KernelVersion, _ = host.KernelVersion()
// add cpu stats // cpu model
if info, err := cpu.Info(); err == nil && len(info) > 0 { if info, err := cpu.Info(); err == nil && len(info) > 0 {
a.cpuModel = info[0].ModelName a.systemInfo.CpuModel = info[0].ModelName
} }
a.cores, _ = cpu.Counts(false) // cores / threads
a.systemInfo.Cores, _ = cpu.Counts(false)
if threads, err := cpu.Counts(true); err == nil { if threads, err := cpu.Counts(true); err == nil {
if threads > 0 && threads < a.cores { if threads > 0 && threads < a.systemInfo.Cores {
// in lxc logical cores reflects container limits, so use that as cores if lower // in lxc logical cores reflects container limits, so use that as cores if lower
a.cores = threads a.systemInfo.Cores = threads
} else { } else {
a.threads = threads a.systemInfo.Threads = threads
} }
} }
} }
// Returns current info, stats about the host system // Returns current info, stats about the host system
func (a *Agent) getSystemStats() (system.Info, system.Stats) { func (a *Agent) getSystemStats() system.Stats {
systemStats := system.Stats{} systemStats := system.Stats{}
// cpu percent // cpu percent
@@ -173,19 +175,11 @@ func (a *Agent) getSystemStats() (system.Info, system.Stats) {
} }
} }
systemInfo := system.Info{ // update base system info
Cpu: systemStats.Cpu, a.systemInfo.Cpu = systemStats.Cpu
MemPct: systemStats.MemPct, a.systemInfo.MemPct = systemStats.MemPct
DiskPct: systemStats.DiskPct, a.systemInfo.DiskPct = systemStats.DiskPct
AgentVersion: beszel.Version, a.systemInfo.Uptime, _ = host.Uptime()
Hostname: a.hostname,
KernelVersion: a.kernelVersion,
CpuModel: a.cpuModel,
Cores: a.cores,
Threads: a.threads,
}
systemInfo.Uptime, _ = host.Uptime() return systemStats
return systemInfo, systemStats
} }

View File

@@ -2,13 +2,6 @@ package agent
import "math" import "math"
// delete container stats from map using mutex
func (a *Agent) deleteContainerStatsSync(id string) {
a.containerStatsMutex.Lock()
defer a.containerStatsMutex.Unlock()
delete(a.containerStatsMap, id)
}
func bytesToMegabytes(b float64) float64 { func bytesToMegabytes(b float64) float64 {
return twoDecimals(b / 1048576) return twoDecimals(b / 1048576)
} }

View File

@@ -44,17 +44,16 @@ type NetIoStats struct {
} }
type Info struct { type Info struct {
Hostname string `json:"h"` Hostname string `json:"h"`
KernelVersion string `json:"k,omitempty"` KernelVersion string `json:"k,omitempty"`
Cores int `json:"c"` Cores int `json:"c"`
Threads int `json:"t,omitempty"` Threads int `json:"t,omitempty"`
CpuModel string `json:"m"` CpuModel string `json:"m"`
// Os string `json:"o"` Uptime uint64 `json:"u"`
Uptime uint64 `json:"u"` Cpu float64 `json:"cpu"`
Cpu float64 `json:"cpu"` MemPct float64 `json:"mp"`
MemPct float64 `json:"mp"` DiskPct float64 `json:"dp"`
DiskPct float64 `json:"dp"` AgentVersion string `json:"v"`
AgentVersion string `json:"v"`
} }
// Final data structure to return to the hub // Final data structure to return to the hub

Binary file not shown.

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{ {
"name": "site", "name": "beszel",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
@@ -23,7 +23,7 @@
"@radix-ui/react-toast": "^1.2.1", "@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.2",
"@tanstack/react-table": "^8.20.5", "@tanstack/react-table": "^8.20.5",
"@vitejs/plugin-react": "^4.3.1", "@vitejs/plugin-react": "^4.3.2",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.0", "cmdk": "^1.0.0",
@@ -34,20 +34,19 @@
"pocketbase": "^0.21.5", "pocketbase": "^0.21.5",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"recharts": "^2.13.0-alpha.4", "recharts": "^2.13.0-alpha.5",
"tailwind-merge": "^2.5.2", "tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"use-is-in-viewport": "^1.0.9",
"valibot": "^0.36.0" "valibot": "^0.36.0"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "^1.1.8", "@types/bun": "^1.1.10",
"@types/react": "^18.3.5", "@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"postcss": "^8.4.44", "postcss": "^8.4.47",
"tailwindcss": "^3.4.10", "tailwindcss": "^3.4.13",
"typescript": "^5.5.4", "typescript": "^5.6.2",
"vite": "^5.4.2" "vite": "^5.4.8"
} }
} }

View File

@@ -6,19 +6,14 @@ import { useStore } from '@nanostores/react'
import Spinner from '../spinner' import Spinner from '../spinner'
import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon, MonitorIcon, XIcon } from 'lucide-react' import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon, MonitorIcon, XIcon } from 'lucide-react'
import ChartTimeSelect from '../charts/chart-time-select' import ChartTimeSelect from '../charts/chart-time-select'
import { import { chartTimeData, cn, getPbTimestamp, useLocalStorage } from '@/lib/utils'
chartTimeData,
cn,
getPbTimestamp,
useClampedIsInViewport,
useLocalStorage,
} from '@/lib/utils'
import { Separator } from '../ui/separator' import { Separator } from '../ui/separator'
import { scaleTime } from 'd3-scale' import { scaleTime } from 'd3-scale'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
import { Button, buttonVariants } from '../ui/button' import { Button, buttonVariants } from '../ui/button'
import { Input } from '../ui/input' import { Input } from '../ui/input'
import { Rows, TuxIcon } from '../ui/icons' import { Rows, TuxIcon } from '../ui/icons'
import { useIntersectionObserver } from '@/lib/use-intersection-observer'
const CpuChart = lazy(() => import('../charts/cpu-chart')) const CpuChart = lazy(() => import('../charts/cpu-chart'))
const ContainerCpuChart = lazy(() => import('../charts/container-cpu-chart')) const ContainerCpuChart = lazy(() => import('../charts/container-cpu-chart'))
@@ -196,7 +191,8 @@ export default function SystemDetail({ name }: { name: string }) {
} }
let uptime: number | string = system.info.u let uptime: number | string = system.info.u
if (system.info.u < 172800) { if (system.info.u < 172800) {
uptime = `${Math.trunc(uptime / 3600)} hours` const hours = Math.trunc(uptime / 3600)
uptime = `${hours} hour${hours > 1 ? 's' : ''}`
} else { } else {
uptime = `${Math.trunc(system.info?.u / 86400)} days` uptime = `${Math.trunc(system.info?.u / 86400)} days`
} }
@@ -504,13 +500,12 @@ function ChartCard({
grid?: boolean grid?: boolean
isContainerChart?: boolean isContainerChart?: boolean
}) { }) {
const target = useRef<HTMLDivElement>(null) const { isIntersecting, ref } = useIntersectionObserver()
const [isInViewport, wrappedTargetRef] = useClampedIsInViewport({ target: target })
return ( return (
<Card <Card
className={cn('pb-2 sm:pb-4 odd:last-of-type:col-span-full', { 'col-span-full': !grid })} className={cn('pb-2 sm:pb-4 odd:last-of-type:col-span-full', { 'col-span-full': !grid })}
ref={wrappedTargetRef} ref={ref}
> >
<CardHeader className="pb-5 pt-4 relative space-y-1 max-sm:py-3 max-sm:px-4"> <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> <CardTitle className="text-xl sm:text-2xl">{title}</CardTitle>
@@ -519,7 +514,7 @@ function ChartCard({
</CardHeader> </CardHeader>
<CardContent className="pl-0 w-[calc(100%-1.6em)] h-52 relative"> <CardContent className="pl-0 w-[calc(100%-1.6em)] h-52 relative">
{<Spinner />} {<Spinner />}
{isInViewport && <Suspense>{children}</Suspense>} {isIntersecting && <Suspense>{children}</Suspense>}
</CardContent> </CardContent>
</Card> </Card>
) )

View File

@@ -0,0 +1,169 @@
import { useEffect, useRef, useState } from 'react'
// adapted from usehooks-ts/use-intersection-observer
/** The hook internal state. */
type State = {
/** A boolean indicating if the element is intersecting. */
isIntersecting: boolean
/** The intersection observer entry. */
entry?: IntersectionObserverEntry
}
/** Represents the options for configuring the Intersection Observer. */
type UseIntersectionObserverOptions = {
/**
* The element that is used as the viewport for checking visibility of the target.
* @default null
*/
root?: Element | Document | null
/**
* A margin around the root.
* @default '0%'
*/
rootMargin?: string
/**
* A threshold indicating the percentage of the target's visibility needed to trigger the callback.
* @default 0
*/
threshold?: number | number[]
/**
* If true, freezes the intersection state once the element becomes visible.
* @default true
*/
freeze?: boolean
/**
* A callback function to be invoked when the intersection state changes.
* @param {boolean} isIntersecting - A boolean indicating if the element is intersecting.
* @param {IntersectionObserverEntry} entry - The intersection observer Entry.
* @default undefined
*/
onChange?: (isIntersecting: boolean, entry: IntersectionObserverEntry) => void
/**
* The initial state of the intersection.
* @default false
*/
initialIsIntersecting?: boolean
}
/**
* The return type of the useIntersectionObserver hook.
*
* Supports both tuple and object destructing.
* @param {(node: Element | null) => void} ref - The ref callback function.
* @param {boolean} isIntersecting - A boolean indicating if the element is intersecting.
* @param {IntersectionObserverEntry | undefined} entry - The intersection observer Entry.
*/
type IntersectionReturn = {
ref: (node?: Element | null) => void
isIntersecting: boolean
entry?: IntersectionObserverEntry
}
/**
* Custom hook that tracks the intersection of a DOM element with its containing element or the viewport using the [`Intersection Observer API`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
* @param {UseIntersectionObserverOptions} options - The options for the Intersection Observer.
* @returns {IntersectionReturn} The ref callback, a boolean indicating if the element is intersecting, and the intersection observer entry.
* @example
* ```tsx
* const { ref, isIntersecting, entry } = useIntersectionObserver({ threshold: 0.5 });
* ```
*/
export function useIntersectionObserver({
threshold = 0,
root = null,
rootMargin = '0%',
freeze = true,
initialIsIntersecting = false,
onChange,
}: UseIntersectionObserverOptions = {}): IntersectionReturn {
const [ref, setRef] = useState<Element | null>(null)
const [state, setState] = useState<State>(() => ({
isIntersecting: initialIsIntersecting,
entry: undefined,
}))
const callbackRef = useRef<UseIntersectionObserverOptions['onChange']>()
callbackRef.current = onChange
const frozen = state.entry?.isIntersecting && freeze
useEffect(() => {
// Ensure we have a ref to observe
if (!ref) return
// Ensure the browser supports the Intersection Observer API
if (!('IntersectionObserver' in window)) return
// Skip if frozen
if (frozen) return
let unobserve: (() => void) | undefined
const observer = new IntersectionObserver(
(entries: IntersectionObserverEntry[]): void => {
const thresholds = Array.isArray(observer.thresholds)
? observer.thresholds
: [observer.thresholds]
entries.forEach((entry) => {
const isIntersecting =
entry.isIntersecting &&
thresholds.some((threshold) => entry.intersectionRatio >= threshold)
setState({ isIntersecting, entry })
if (callbackRef.current) {
callbackRef.current(isIntersecting, entry)
}
if (isIntersecting && freeze && unobserve) {
unobserve()
unobserve = undefined
}
})
},
{ threshold, root, rootMargin }
)
observer.observe(ref)
return () => {
observer.disconnect()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
ref,
// eslint-disable-next-line react-hooks/exhaustive-deps
JSON.stringify(threshold),
root,
rootMargin,
frozen,
freeze,
])
// ensures that if the observed element changes, the intersection observer is reinitialized
const prevRef = useRef<Element | null>(null)
useEffect(() => {
if (
!ref &&
state.entry?.target &&
!freeze &&
!frozen &&
prevRef.current !== state.entry.target
) {
prevRef.current = state.entry.target
setState({ isIntersecting: initialIsIntersecting, entry: undefined })
}
}, [ref, state.entry, freeze, frozen, initialIsIntersecting])
return {
ref: setRef,
isIntersecting: !!state.isIntersecting,
entry: state.entry,
} as IntersectionReturn
}

View File

@@ -7,7 +7,6 @@ import { RecordModel, RecordSubscription } from 'pocketbase'
import { WritableAtom } from 'nanostores' import { WritableAtom } from 'nanostores'
import { timeDay, timeHour } from 'd3-time' import { timeDay, timeHour } from 'd3-time'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import useIsInViewport, { CallbackRef, HookOptions } from 'use-is-in-viewport'
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
@@ -214,24 +213,6 @@ export function useYAxisWidth() {
return { yAxisWidth, updateYAxisWidth } return { yAxisWidth, updateYAxisWidth }
} }
export function useClampedIsInViewport(options: HookOptions): [boolean | null, CallbackRef] {
const [isInViewport, wrappedTargetRef] = useIsInViewport(options)
const [wasInViewportAtleastOnce, setWasInViewportAtleastOnce] = useState(isInViewport)
useEffect(() => {
setWasInViewportAtleastOnce((prev) => {
// this will clamp it to the first true
// received from useIsInViewport
if (!prev) {
return isInViewport
}
return prev
})
}, [isInViewport])
return [wasInViewportAtleastOnce, wrappedTargetRef]
}
export function toFixedWithoutTrailingZeros(num: number, digits: number) { export function toFixedWithoutTrailingZeros(num: number, digits: number) {
return parseFloat(num.toFixed(digits)).toString() return parseFloat(num.toFixed(digits)).toString()
} }

View File

@@ -1,6 +1,6 @@
package beszel package beszel
const ( const (
Version = "0.5.0" Version = "0.5.1"
AppName = "beszel" AppName = "beszel"
) )

View File

@@ -108,7 +108,7 @@ Use `./beszel update` and `./beszel-agent update` to update to the latest versio
| Name | Default | Description | | Name | Default | Description |
| ------------------- | ------- | ---------------------------------------------------------------------------------------- | | ------------------- | ------- | ---------------------------------------------------------------------------------------- |
| `DOCKER_HOST` | unset | Overrides the docker host (docker.sock) if using a proxy.[^socket] | | `DOCKER_HOST` | unset | Overrides the docker host (docker.sock) if using a proxy.[^socket] |
| `EXTRA_FILESYSTEMS` | unset | See [Monitoring additional disks / partitions](#monitoring-additional-disks--partitions) | | `EXTRA_FILESYSTEMS` | unset | See [Monitoring additional disks, partitions, or remote mounts](#monitoring-additional-disks-partitions-or-remote-mounts) |
| `FILESYSTEM` | unset | Device, partition, or mount point to use for root disk stats. | | `FILESYSTEM` | unset | Device, partition, or mount point to use for root disk stats. |
| `KEY` | unset | Public SSH key to use for authentication. Provided in hub. | | `KEY` | unset | Public SSH key to use for authentication. Provided in hub. |
| `LOG_LEVEL` | info | Logging level. Valid values: "debug", "info", "warn", "error". | | `LOG_LEVEL` | info | Logging level. Valid values: "debug", "info", "warn", "error". |
@@ -217,7 +217,7 @@ Assuming the agent is running, the connection is probably being blocked by a fir
1. Add an inbound rule to the agent system's firewall(s) to allow TCP connections to the port. Check any active firewalls, like iptables, and your cloud provider's firewall settings if applicable. 1. Add an inbound rule to the agent system's firewall(s) to allow TCP connections to the port. Check any active firewalls, like iptables, and your cloud provider's firewall settings if applicable.
2. Alternatively, use software like [Cloudflare Tunnel](https://www.cloudflare.com/products/tunnel/), [WireGuard](https://www.wireguard.com/), or [Tailscale](https://tailscale.com/) to securely bypass your firewall. 2. Alternatively, use software like [Cloudflare Tunnel](https://www.cloudflare.com/products/tunnel/), [WireGuard](https://www.wireguard.com/), or [Tailscale](https://tailscale.com/) to securely bypass your firewall.
You can test connectivity by running telnet `<agent-ip> <port>`. You can test connectivity by running `telnet <agent-ip> <port>`.
### Connecting the hub and agent on the same system using Docker ### Connecting the hub and agent on the same system using Docker