mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-22 13:36:16 +01:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5f9e2615c | ||
|
|
4a78ce1b16 | ||
|
|
f8f1e01cb4 | ||
|
|
c7463f2b9f | ||
|
|
a975466fc7 | ||
|
|
539c0ccb1d | ||
|
|
5f4dcb09ea | ||
|
|
6de5dce176 | ||
|
|
b5c158d1b3 | ||
|
|
7f01d1ec7e | ||
|
|
8bf7a0e1d6 | ||
|
|
140fd93ec9 | ||
|
|
bdcb34c989 | ||
|
|
aaaa86b147 | ||
|
|
6e9b84c6c7 | ||
|
|
cce241caa4 | ||
|
|
1e9787c4d7 | ||
|
|
71aa9946f5 | ||
|
|
12239808fc | ||
|
|
94e9d4f270 | ||
|
|
34a8053967 |
@@ -9,43 +9,45 @@ require (
|
|||||||
github.com/goccy/go-json v0.10.3
|
github.com/goccy/go-json v0.10.3
|
||||||
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61
|
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61
|
||||||
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.22
|
||||||
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.9
|
github.com/shirou/gopsutil/v4 v4.24.9
|
||||||
|
github.com/spf13/cast v1.7.0
|
||||||
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.28.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
|
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2 v1.31.0 // indirect
|
github.com/aws/aws-sdk-go-v2 v1.32.2 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.5 // indirect
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.27.39 // indirect
|
github.com/aws/aws-sdk-go-v2/config v1.28.0 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.37 // indirect
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.41 // 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.17 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.26 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.33 // 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.21 // 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.21 // 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
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.18 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.21 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect
|
||||||
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.4.2 // 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.12.2 // 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.18.2 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.64.0 // indirect
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.66.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.24.2 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.3 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.31.3 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 // indirect
|
||||||
github.com/aws/smithy-go v1.21.0 // indirect
|
github.com/aws/smithy-go v1.22.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
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/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.6 // indirect
|
||||||
github.com/ganigeorgiev/fexpr v0.4.1 // indirect
|
github.com/ganigeorgiev/fexpr v0.4.1 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||||
@@ -62,13 +64,12 @@ require (
|
|||||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
|
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.23 // indirect
|
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
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/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
|
||||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||||
@@ -78,22 +79,22 @@ require (
|
|||||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
go.opencensus.io v0.24.0 // indirect
|
go.opencensus.io v0.24.0 // indirect
|
||||||
gocloud.dev v0.39.0 // indirect
|
gocloud.dev v0.40.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
|
||||||
golang.org/x/image v0.20.0 // indirect
|
golang.org/x/image v0.21.0 // indirect
|
||||||
golang.org/x/net v0.29.0 // indirect
|
golang.org/x/net v0.30.0 // indirect
|
||||||
golang.org/x/oauth2 v0.23.0 // indirect
|
golang.org/x/oauth2 v0.23.0 // indirect
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
golang.org/x/sync v0.8.0 // indirect
|
||||||
golang.org/x/sys v0.25.0 // indirect
|
golang.org/x/sys v0.26.0 // indirect
|
||||||
golang.org/x/term v0.24.0 // indirect
|
golang.org/x/term v0.25.0 // indirect
|
||||||
golang.org/x/text v0.18.0 // indirect
|
golang.org/x/text v0.19.0 // indirect
|
||||||
golang.org/x/time v0.6.0 // indirect
|
golang.org/x/time v0.7.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.201.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect
|
||||||
google.golang.org/grpc v1.67.1 // indirect
|
google.golang.org/grpc v1.67.1 // indirect
|
||||||
google.golang.org/protobuf v1.34.2 // indirect
|
google.golang.org/protobuf v1.35.1 // indirect
|
||||||
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect
|
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852 // indirect
|
||||||
modernc.org/libc v1.61.0 // indirect
|
modernc.org/libc v1.61.0 // indirect
|
||||||
modernc.org/mathutil v1.6.0 // indirect
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
modernc.org/memory v1.8.0 // indirect
|
modernc.org/memory v1.8.0 // indirect
|
||||||
|
|||||||
153
beszel/go.sum
153
beszel/go.sum
@@ -1,8 +1,8 @@
|
|||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ=
|
cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
|
||||||
cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc=
|
cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
|
||||||
cloud.google.com/go/auth v0.9.5 h1:4CTn43Eynw40aFVr3GpPqsQponx2jv0BQpjvajsbbzw=
|
cloud.google.com/go/auth v0.9.8 h1:+CSJ0Gw9iVeSENVCKJoLHhdUykDgXSc4Qn+gu2BRtR8=
|
||||||
cloud.google.com/go/auth v0.9.5/go.mod h1:Xo0n7n66eHyOWWCnitop6870Ilwo3PiZyodVkkH1xWM=
|
cloud.google.com/go/auth v0.9.8/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI=
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
|
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/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
|
||||||
cloud.google.com/go/compute v1.14.0 h1:hfm2+FfxVmnRlh6LpB7cg1ZNU+5edAHmW679JePztk0=
|
cloud.google.com/go/compute v1.14.0 h1:hfm2+FfxVmnRlh6LpB7cg1ZNU+5edAHmW679JePztk0=
|
||||||
@@ -26,44 +26,44 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
|
|||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||||
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
|
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 v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.31.0 h1:3V05LbxTSItI5kUqNwhJrrrY1BAXxXt0sN0l72QmG5U=
|
github.com/aws/aws-sdk-go-v2 v1.32.2 h1:AkNLZEyYMLnx/Q/mSKkcMqwNFXMAvFto9bNsHqcTduI=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.31.0/go.mod h1:ztolYtaEUtdpf9Wftr31CJfLVjOnD/CVRkKOOYgF8hA=
|
github.com/aws/aws-sdk-go-v2 v1.32.2/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.5 h1:xDAuZTn4IMm8o1LnBZvmrL8JA1io4o3YWNXgohbf20g=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 h1:pT3hpW0cOHRJx8Y0DfJUEQuqPild8jRGmSFmBgvydr0=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.5/go.mod h1:wYSv6iDS621sEFLfKvpPE2ugjTuGlAG7iROg0hLOkfc=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6/go.mod h1:j/I2++U0xX+cr44QjHay4Cvxj6FUbnxrgmqN3H1jTZA=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.27.39 h1:FCylu78eTGzW1ynHcongXK9YHtoXD5AiiUqq3YfJYjU=
|
github.com/aws/aws-sdk-go-v2/config v1.28.0 h1:FosVYWcqEtWNxHn8gB/Vs6jOlNwSoyOCA/g/sxyySOQ=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.27.39/go.mod h1:wczj2hbyskP4LjMKBEZwPRO1shXY+GsQleab+ZXT2ik=
|
github.com/aws/aws-sdk-go-v2/config v1.28.0/go.mod h1:pYhbtvg1siOOg8h5an77rXle9tVG8T+BWLWAo7cOukc=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.37 h1:G2aOH01yW8X373JK419THj5QVqu9vKEwxSEsGxihoW0=
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.41 h1:7gXo+Axmp+R4Z+AK8YFQO0ZV3L0gizGINCOWxSLY9W8=
|
||||||
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.41/go.mod h1:u4Eb8d3394YLubphT4jLEwN1rLNq2wFOlT6OuxFwPzU=
|
||||||
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.17 h1:TMH3f/SCAWdNtXXVPPu5D6wrr4G5hI1rAxbcocKfC7Q=
|
||||||
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.17/go.mod h1:1ZRXLdTpzdJb9fwTMXiLipENRxkGMTn1sfKexGllQCw=
|
||||||
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.33 h1:X+4YY5kZRI/cOoSMVMGTqFXHAMg1bvvay7IBcqHpybQ=
|
||||||
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/feature/s3/manager v1.17.33/go.mod h1:DPynzu+cn92k5UQ6tZhX+wfTB4ah6QDU/NgdHqatmvk=
|
||||||
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.21 h1:UAsR3xA31QGf79WzpG/ixT9FZvQlh5HY1NRqSHBNOCk=
|
||||||
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.21/go.mod h1:JNr43NFf5L9YaG3eKTm7HQzls9J+A9YYcGI5Quh1r2Y=
|
||||||
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.21 h1:6jZVETqmYCadGFvrYEQfC5fAQmlo80CeL5psbno6r0s=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18/go.mod h1:DkKMmksZVVyat+Y+r1dEOgJEfUeA7UngIHWeKsi0yNc=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21/go.mod h1:1SR0GbLlnN3QUmYaflZNiH1ql+1qrSiB2vwcJ+4UM60=
|
||||||
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 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/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.18 h1:OWYvKL53l1rbsUmW7bQyJVsYU/Ii3bbAAQIIFNbM0Tk=
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.21 h1:7edmS3VOBDhK00b/MwGtGglCm7hhwNYnjJs/PgFdMQE=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.18/go.mod h1:CUx0G1v3wG6l01tUB+j7Y8kclA8NSqK4ef0YG79a4cg=
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.21/go.mod h1:Q9o5h4HoIWG8XfzxqiuK/CGUbepCJ8uTlaE3bAbxytQ=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5 h1:QFASJGfT8wMXtuP3D5CRmMjARHv9ZmzFUMJznHDOY3w=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5/go.mod h1:QdZ3OmoIjSX+8D1OPAzPxDfjXASbBMDsz9qvtyIhtik=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.20 h1:rTWjG6AvWekO2B1LHeM3ktU7MqyX9rzWQ7hgzneZW7E=
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.2 h1:4FMHqLfk0efmTqhXVRL5xYRqlEBNBiRI7N6w4jsEdd4=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.20/go.mod h1:RGW2DDpVc8hu6Y6yG8G5CHVmVOAn1oV8rNKOHRJyswg=
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.2/go.mod h1:LWoqeWlK9OZeJxsROW2RqrSPvQHKTpp69r/iDjwsSaw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 h1:Xbwbmk44URTiHNx6PNo0ujDE6ERlsCKJD3u1zfnzAPg=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 h1:s7NA1SOw8q/5c0wr8477yOPp0z+uBaXBnLE0XYb0POA=
|
||||||
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.12.2/go.mod h1:fnjjWyAW/Pj5HYOxl9LJqWtEwS7W2qgcRLWP+uWbss0=
|
||||||
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.18.2 h1:t7iUP9+4wdc5lt3E41huP+GvQZJD38WLsgVp4iOtAjg=
|
||||||
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.18.2/go.mod h1:/niFCtmuQNxqx9v8WAPq5qh7EH25U4BF6tjoyq9bObM=
|
||||||
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.66.0 h1:xA6XhTF7PE89BCNHJbQi8VvPzcgMtmGC5dr8S8N7lHk=
|
||||||
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/s3 v1.66.0/go.mod h1:cB6oAuus7YXRZhWCc1wIwPywwZ1XwweNp2TVAEGYeB8=
|
||||||
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.24.2 h1:bSYXVyUzoTHoKalBmwaZxs97HU9DWWI3ehHSAMa7xOk=
|
||||||
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.24.2/go.mod h1:skMqY7JElusiOUjMJMOv1jJsP7YUg7DrhgqZZWuzu1U=
|
||||||
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.28.2 h1:AhmO1fHINP9vFYUE0LHzCWg/LfUWUF+zFPEcY9QXb7o=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.3/go.mod h1:FnvDM4sfa+isJ3kDXIzAB9GAwVSzFzSy97uZ3IsHo4E=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2/go.mod h1:o8aQygT2+MVP0NaV6kbdE1YnnIM8RRVQzoeUH45GOdI=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.31.3 h1:VzudTFrDCIDakXtemR7l6Qzt2+JYsVqo2MxBPt5k8T8=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 h1:CiS7i0+FUe+/YY1GvIBLLrR/XNGZ4CtM1Ll0XavNuVo=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.31.3/go.mod h1:yMWe0F+XG0DkRZK5ODZhG7BEFYhLXi2dqGsv6tX0cgI=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.32.2/go.mod h1:HtaiBI8CjYoNVde8arShXb94UbQQi9L4EMr6D+xGBwo=
|
||||||
github.com/aws/smithy-go v1.21.0 h1:H7L8dtDRk0P1Qm6y0ji7MCYMQObJ5R9CRpyPhRUkLYA=
|
github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM=
|
||||||
github.com/aws/smithy-go v1.21.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
github.com/aws/smithy-go v1.22.0/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 h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
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=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
@@ -97,8 +97,8 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw
|
|||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
|
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
|
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
|
||||||
github.com/ganigeorgiev/fexpr v0.4.1 h1:hpUgbUEEWIZhSDBtf4M9aUNfQQ0BZkGRaMePy7Gcx5k=
|
github.com/ganigeorgiev/fexpr v0.4.1 h1:hpUgbUEEWIZhSDBtf4M9aUNfQQ0BZkGRaMePy7Gcx5k=
|
||||||
github.com/ganigeorgiev/fexpr v0.4.1/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
|
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 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
|
||||||
@@ -198,8 +198,8 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
|
|||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
|
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||||
@@ -217,8 +217,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
|||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pocketbase/dbx v1.10.1 h1:cw+vsyfCJD8YObOVeqb93YErnlxwYMkNZ4rwN0G0AaA=
|
github.com/pocketbase/dbx v1.10.1 h1:cw+vsyfCJD8YObOVeqb93YErnlxwYMkNZ4rwN0G0AaA=
|
||||||
github.com/pocketbase/dbx v1.10.1/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
|
github.com/pocketbase/dbx v1.10.1/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
|
||||||
github.com/pocketbase/pocketbase v0.22.21 h1:DGPCxn6co8VuTV0mton4NFO/ON49XiFMszRr+Mysy48=
|
github.com/pocketbase/pocketbase v0.22.22 h1:iA128U+cmM9euxPpuCN7blmQ2FZNzOix2aUUcnbbQu8=
|
||||||
github.com/pocketbase/pocketbase v0.22.21/go.mod h1:Cw5E4uoGhKItBIE2lJL3NfmiUr9Syk2xaNJ2G7Dssow=
|
github.com/pocketbase/pocketbase v0.22.22/go.mod h1:u+l7T04g7eBXetoodXLch3WoV/QonRf1qYq+2vuTKuI=
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
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/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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
@@ -275,20 +275,20 @@ go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2
|
|||||||
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
|
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
|
||||||
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
|
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
|
||||||
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
|
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
|
||||||
gocloud.dev v0.39.0 h1:EYABYGhAalPUaMrbSKOr5lejxoxvXj99nE8XFtsDgds=
|
gocloud.dev v0.40.0 h1:f8LgP+4WDqOG/RXoUcyLpeIAGOcAbZrZbDQCUee10ng=
|
||||||
gocloud.dev v0.39.0/go.mod h1:drz+VyYNBvrMTW0KZiBAYEdl8lbNZx+OQ7oQvdrFmSQ=
|
gocloud.dev v0.40.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-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-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-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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw=
|
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
|
||||||
golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM=
|
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
@@ -306,8 +306,8 @@ 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-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-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.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
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.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||||
@@ -334,23 +334,23 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.26.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-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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
|
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||||
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.5/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.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.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.7.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-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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
@@ -358,14 +358,14 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
|
|||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
|
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||||
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
|
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
|
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
|
||||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||||
google.golang.org/api v0.199.0 h1:aWUXClp+VFJmqE0JPvpZOK3LDQMyFKYIow4etYd9qxs=
|
google.golang.org/api v0.201.0 h1:+7AD9JNM3tREtawRMu8sOjSbb8VYcYXJG/2eEOmfDu0=
|
||||||
google.golang.org/api v0.199.0/go.mod h1:ohG4qSztDJmZdjK/Ar6MhbAmb/Rpi4JHOqagsh90K28=
|
google.golang.org/api v0.201.0/go.mod h1:HVY0FCHVs89xIW9fzf/pBvOEm+OolHa86G/txFezyq4=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
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.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
@@ -373,12 +373,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-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-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-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU=
|
google.golang.org/genproto v0.0.0-20241007155032-5fefd90f89a9 h1:nFS3IivktIU5Mk6KQa+v6RKkHUpdQpphqGNLxqNnbEk=
|
||||||
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4=
|
google.golang.org/genproto v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:tEzYTYZxbmVNOu0OAFH9HzdJtLn6h4Aj89zzlBCdHms=
|
||||||
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-20240930140551-af27646dc61f h1:cUMEy+8oS78BWIH9OWazBkzbr090Od9tWBNtZHkOhf0=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
|
||||||
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=
|
||||||
@@ -395,9 +395,10 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
|||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/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/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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
@@ -416,8 +417,8 @@ modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
|||||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||||
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
|
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/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-20241004144649-1aea3fae8852 h1:IYXPPTTjjoSHvUClZIYexDiO7g+4x+XveKT4gCIAwiY=
|
||||||
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||||
modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE=
|
modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE=
|
||||||
modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0=
|
modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0=
|
||||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
package agent
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"beszel"
|
||||||
"beszel/internal/entities/system"
|
"beszel/internal/entities/system"
|
||||||
"context"
|
"context"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -47,6 +48,8 @@ func (a *Agent) Run(pubKey []byte, addr string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
slog.Debug(beszel.Version)
|
||||||
|
|
||||||
// Set sensors context (allows overriding sys location for sensors)
|
// Set sensors context (allows overriding sys location for sensors)
|
||||||
if sysSensors, exists := os.LookupEnv("SYS_SENSORS"); exists {
|
if sysSensors, exists := os.LookupEnv("SYS_SENSORS"); exists {
|
||||||
slog.Info("SYS_SENSORS", "path", sysSensors)
|
slog.Info("SYS_SENSORS", "path", sysSensors)
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ func (dm *dockerManager) getDockerStats() ([]*container.Stats, error) {
|
|||||||
clear(dm.validIds)
|
clear(dm.validIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var failedContainters []container.ApiInfo
|
||||||
|
|
||||||
for _, ctr := range *dm.apiContainerList {
|
for _, ctr := range *dm.apiContainerList {
|
||||||
ctr.IdShort = ctr.Id[:12]
|
ctr.IdShort = ctr.Id[:12]
|
||||||
dm.validIds[ctr.IdShort] = struct{}{}
|
dm.validIds[ctr.IdShort] = struct{}{}
|
||||||
@@ -74,18 +76,33 @@ func (dm *dockerManager) getDockerStats() ([]*container.Stats, error) {
|
|||||||
defer dm.dequeue()
|
defer dm.dequeue()
|
||||||
err := dm.updateContainerStats(ctr)
|
err := dm.updateContainerStats(ctr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dm.deleteContainerStatsSync(ctr.IdShort)
|
dm.containerStatsMutex.Lock()
|
||||||
// retry once
|
delete(dm.containerStatsMap, ctr.IdShort)
|
||||||
err = dm.updateContainerStats(ctr)
|
failedContainters = append(failedContainters, ctr)
|
||||||
if err != nil {
|
dm.containerStatsMutex.Unlock()
|
||||||
slog.Error("Error getting container stats", "err", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
dm.wg.Wait()
|
dm.wg.Wait()
|
||||||
|
|
||||||
|
// retry failed containers separately so we can run them in parallel (docker 24 bug)
|
||||||
|
if len(failedContainters) > 0 {
|
||||||
|
slog.Debug("Retrying failed containers", "count", len(failedContainters))
|
||||||
|
// time.Sleep(time.Millisecond * 1100)
|
||||||
|
for _, ctr := range failedContainters {
|
||||||
|
dm.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer dm.wg.Done()
|
||||||
|
err = dm.updateContainerStats(ctr)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error getting container stats", "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
dm.wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
// populate final stats and remove old / invalid container stats
|
// populate final stats and remove old / invalid container stats
|
||||||
stats := make([]*container.Stats, 0, containersLength)
|
stats := make([]*container.Stats, 0, containersLength)
|
||||||
for id, v := range dm.containerStatsMap {
|
for id, v := range dm.containerStatsMap {
|
||||||
@@ -217,9 +234,20 @@ func newDockerManager() *dockerManager {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// configurable timeout
|
||||||
|
timeout := time.Millisecond * 2100
|
||||||
|
if t, set := os.LookupEnv("DOCKER_TIMEOUT"); set {
|
||||||
|
timeout, err = time.ParseDuration(t)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
slog.Info("DOCKER_TIMEOUT", "timeout", timeout)
|
||||||
|
}
|
||||||
|
|
||||||
dockerClient := &dockerManager{
|
dockerClient := &dockerManager{
|
||||||
client: &http.Client{
|
client: &http.Client{
|
||||||
Timeout: time.Second * 8,
|
Timeout: timeout,
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
},
|
},
|
||||||
containerStatsMap: make(map[string]*container.Stats),
|
containerStatsMap: make(map[string]*container.Stats),
|
||||||
@@ -246,7 +274,6 @@ func newDockerManager() *dockerManager {
|
|||||||
// if version > 24, one-shot works correctly and we can limit concurrent operations
|
// if version > 24, one-shot works correctly and we can limit concurrent operations
|
||||||
if dockerVersion, err := semver.Parse(versionInfo.Version); err == nil && dockerVersion.Major > 24 {
|
if dockerVersion, err := semver.Parse(versionInfo.Version); err == nil && dockerVersion.Major > 24 {
|
||||||
concurrency = 5
|
concurrency = 5
|
||||||
dockerClient.client.Timeout = time.Millisecond * 1100
|
|
||||||
}
|
}
|
||||||
slog.Debug("Docker", "version", versionInfo.Version, "concurrency", concurrency)
|
slog.Debug("Docker", "version", versionInfo.Version, "concurrency", concurrency)
|
||||||
|
|
||||||
|
|||||||
@@ -171,33 +171,35 @@ func (a *Agent) getSystemStats() system.Stats {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// temperatures
|
// temperatures (skip if sensors whitelist is set to empty string)
|
||||||
temps, err := sensors.TemperaturesWithContext(a.sensorsContext)
|
if a.sensorsWhitelist == nil || len(a.sensorsWhitelist) > 0 {
|
||||||
if err != nil && a.debug {
|
temps, err := sensors.TemperaturesWithContext(a.sensorsContext)
|
||||||
err.(*sensors.Warnings).Verbose = true
|
if err != nil {
|
||||||
slog.Debug("Sensor error", "errs", err)
|
// err.(*sensors.Warnings).Verbose = true
|
||||||
}
|
slog.Debug("Sensor error", "err", err)
|
||||||
if len(temps) > 0 {
|
|
||||||
slog.Debug("Temperatures", "data", temps)
|
|
||||||
systemStats.Temperatures = make(map[string]float64, len(temps))
|
|
||||||
for i, sensor := range temps {
|
|
||||||
// skip if temperature is 0
|
|
||||||
if sensor.Temperature <= 0 || sensor.Temperature >= 200 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := systemStats.Temperatures[sensor.SensorKey]; ok {
|
|
||||||
// if key already exists, append int to key
|
|
||||||
systemStats.Temperatures[sensor.SensorKey+"_"+strconv.Itoa(i)] = twoDecimals(sensor.Temperature)
|
|
||||||
} else {
|
|
||||||
systemStats.Temperatures[sensor.SensorKey] = twoDecimals(sensor.Temperature)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// remove sensors from systemStats if whitelist exists and sensor is not in whitelist
|
slog.Debug("Temperature", "sensors", temps)
|
||||||
// (do this here instead of in initial loop so we have correct keys if int was appended)
|
if len(temps) > 0 {
|
||||||
if a.sensorsWhitelist != nil {
|
systemStats.Temperatures = make(map[string]float64, len(temps))
|
||||||
for key := range systemStats.Temperatures {
|
for i, sensor := range temps {
|
||||||
if _, nameInWhitelist := a.sensorsWhitelist[key]; !nameInWhitelist {
|
// skip if temperature is 0
|
||||||
delete(systemStats.Temperatures, key)
|
if sensor.Temperature <= 0 || sensor.Temperature >= 200 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := systemStats.Temperatures[sensor.SensorKey]; ok {
|
||||||
|
// if key already exists, append int to key
|
||||||
|
systemStats.Temperatures[sensor.SensorKey+"_"+strconv.Itoa(i)] = twoDecimals(sensor.Temperature)
|
||||||
|
} else {
|
||||||
|
systemStats.Temperatures[sensor.SensorKey] = twoDecimals(sensor.Temperature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// remove sensors from systemStats if whitelist exists and sensor is not in whitelist
|
||||||
|
// (do this here instead of in initial loop so we have correct keys if int was appended)
|
||||||
|
if a.sensorsWhitelist != nil {
|
||||||
|
for key := range systemStats.Temperatures {
|
||||||
|
if _, nameInWhitelist := a.sensorsWhitelist[key]; !nameInWhitelist {
|
||||||
|
delete(systemStats.Temperatures, key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,6 +211,7 @@ func (a *Agent) getSystemStats() system.Stats {
|
|||||||
a.systemInfo.DiskPct = systemStats.DiskPct
|
a.systemInfo.DiskPct = systemStats.DiskPct
|
||||||
a.systemInfo.Uptime, _ = host.Uptime()
|
a.systemInfo.Uptime, _ = host.Uptime()
|
||||||
a.systemInfo.Bandwidth = twoDecimals(systemStats.NetworkSent + systemStats.NetworkRecv)
|
a.systemInfo.Bandwidth = twoDecimals(systemStats.NetworkSent + systemStats.NetworkRecv)
|
||||||
|
slog.Debug("sysinfo", "data", a.systemInfo)
|
||||||
|
|
||||||
return systemStats
|
return systemStats
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -284,10 +284,10 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *models.Record, systemIn
|
|||||||
if float32(alert.count) >= minCount {
|
if float32(alert.count) >= minCount {
|
||||||
if !alert.triggered && alert.val > alert.threshold {
|
if !alert.triggered && alert.val > alert.threshold {
|
||||||
alert.triggered = true
|
alert.triggered = true
|
||||||
am.sendSystemAlert(alert)
|
go am.sendSystemAlert(alert)
|
||||||
} else if alert.triggered && alert.val <= alert.threshold {
|
} else if alert.triggered && alert.val <= alert.threshold {
|
||||||
alert.triggered = false
|
alert.triggered = false
|
||||||
am.sendSystemAlert(alert)
|
go am.sendSystemAlert(alert)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,7 +339,7 @@ func (am *AlertManager) sendSystemAlert(alert SystemAlertData) {
|
|||||||
UserID: user.GetId(),
|
UserID: user.GetId(),
|
||||||
Title: subject,
|
Title: subject,
|
||||||
Message: body,
|
Message: body,
|
||||||
Link: am.app.Settings().Meta.AppUrl + "/system/" + url.QueryEscape(systemName),
|
Link: am.app.Settings().Meta.AppUrl + "/system/" + url.PathEscape(systemName),
|
||||||
LinkText: "View " + systemName,
|
LinkText: "View " + systemName,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -390,7 +390,7 @@ func (am *AlertManager) HandleStatusAlerts(newStatus string, oldSystemRecord *mo
|
|||||||
UserID: user.GetId(),
|
UserID: user.GetId(),
|
||||||
Title: fmt.Sprintf("Connection to %s is %s %v", systemName, alertStatus, emoji),
|
Title: fmt.Sprintf("Connection to %s is %s %v", systemName, alertStatus, emoji),
|
||||||
Message: fmt.Sprintf("Connection to %s is %s", systemName, alertStatus),
|
Message: fmt.Sprintf("Connection to %s is %s", systemName, alertStatus),
|
||||||
Link: am.app.Settings().Meta.AppUrl + "/system/" + url.QueryEscape(systemName),
|
Link: am.app.Settings().Meta.AppUrl + "/system/" + url.PathEscape(systemName),
|
||||||
LinkText: "View " + systemName,
|
LinkText: "View " + systemName,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
222
beszel/internal/hub/config.go
Normal file
222
beszel/internal/hub/config.go
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
package hub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"beszel/internal/entities/system"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v5"
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
"github.com/pocketbase/pocketbase/apis"
|
||||||
|
"github.com/pocketbase/pocketbase/models"
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Systems []SystemConfig `yaml:"systems"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SystemConfig struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Port uint16 `yaml:"port"`
|
||||||
|
Users []string `yaml:"users"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Syncs systems with the config.yml file
|
||||||
|
func (h *Hub) syncSystemsWithConfig() error {
|
||||||
|
configPath := filepath.Join(h.app.DataDir(), "config.yml")
|
||||||
|
configData, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
err = yaml.Unmarshal(configData, &config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse config.yml: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.Systems) == 0 {
|
||||||
|
log.Println("No systems defined in config.yml.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstUser *models.Record
|
||||||
|
|
||||||
|
// Create a map of email to user ID
|
||||||
|
userEmailToID := make(map[string]string)
|
||||||
|
users, err := h.app.Dao().FindRecordsByExpr("users", dbx.NewExp("id != ''"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(users) > 0 {
|
||||||
|
firstUser = users[0]
|
||||||
|
for _, user := range users {
|
||||||
|
userEmailToID[user.GetString("email")] = user.Id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add default settings for systems if not defined in config
|
||||||
|
for i := range config.Systems {
|
||||||
|
system := &config.Systems[i]
|
||||||
|
if system.Port == 0 {
|
||||||
|
system.Port = 45876
|
||||||
|
}
|
||||||
|
if len(users) > 0 && len(system.Users) == 0 {
|
||||||
|
// default to first user if none are defined
|
||||||
|
system.Users = []string{firstUser.Id}
|
||||||
|
} else {
|
||||||
|
// Convert email addresses to user IDs
|
||||||
|
userIDs := make([]string, 0, len(system.Users))
|
||||||
|
for _, email := range system.Users {
|
||||||
|
if id, ok := userEmailToID[email]; ok {
|
||||||
|
userIDs = append(userIDs, id)
|
||||||
|
} else {
|
||||||
|
log.Printf("User %s not found", email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
system.Users = userIDs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get existing systems
|
||||||
|
existingSystems, err := h.app.Dao().FindRecordsByExpr("systems", dbx.NewExp("id != ''"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a map of existing systems for easy lookup
|
||||||
|
existingSystemsMap := make(map[string]*models.Record)
|
||||||
|
for _, system := range existingSystems {
|
||||||
|
key := system.GetString("host") + ":" + system.GetString("port")
|
||||||
|
existingSystemsMap[key] = system
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process systems from config
|
||||||
|
for _, sysConfig := range config.Systems {
|
||||||
|
key := sysConfig.Host + ":" + strconv.Itoa(int(sysConfig.Port))
|
||||||
|
if existingSystem, ok := existingSystemsMap[key]; ok {
|
||||||
|
// Update existing system
|
||||||
|
existingSystem.Set("name", sysConfig.Name)
|
||||||
|
existingSystem.Set("users", sysConfig.Users)
|
||||||
|
existingSystem.Set("port", sysConfig.Port)
|
||||||
|
if err := h.app.Dao().SaveRecord(existingSystem); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
delete(existingSystemsMap, key)
|
||||||
|
} else {
|
||||||
|
// Create new system
|
||||||
|
systemsCollection, err := h.app.Dao().FindCollectionByNameOrId("systems")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to find systems collection: %v", err)
|
||||||
|
}
|
||||||
|
newSystem := models.NewRecord(systemsCollection)
|
||||||
|
newSystem.Set("name", sysConfig.Name)
|
||||||
|
newSystem.Set("host", sysConfig.Host)
|
||||||
|
newSystem.Set("port", sysConfig.Port)
|
||||||
|
newSystem.Set("users", sysConfig.Users)
|
||||||
|
newSystem.Set("info", system.Info{})
|
||||||
|
newSystem.Set("status", "pending")
|
||||||
|
if err := h.app.Dao().SaveRecord(newSystem); err != nil {
|
||||||
|
return fmt.Errorf("failed to create new system: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete systems not in config
|
||||||
|
for _, system := range existingSystemsMap {
|
||||||
|
if err := h.app.Dao().DeleteRecord(system); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Systems synced with config.yml")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates content for the config.yml file as a YAML string
|
||||||
|
func (h *Hub) generateConfigYAML() (string, error) {
|
||||||
|
// Fetch all systems from the database
|
||||||
|
systems, err := h.app.Dao().FindRecordsByFilter("systems", "id != ''", "name", -1, 0)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a Config struct to hold the data
|
||||||
|
config := Config{
|
||||||
|
Systems: make([]SystemConfig, 0, len(systems)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all users at once
|
||||||
|
allUserIDs := make([]string, 0)
|
||||||
|
for _, system := range systems {
|
||||||
|
allUserIDs = append(allUserIDs, system.GetStringSlice("users")...)
|
||||||
|
}
|
||||||
|
userEmailMap, err := h.getUserEmailMap(allUserIDs)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate the Config struct with system data
|
||||||
|
for _, system := range systems {
|
||||||
|
userIDs := system.GetStringSlice("users")
|
||||||
|
userEmails := make([]string, 0, len(userIDs))
|
||||||
|
for _, userID := range userIDs {
|
||||||
|
if email, ok := userEmailMap[userID]; ok {
|
||||||
|
userEmails = append(userEmails, email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sysConfig := SystemConfig{
|
||||||
|
Name: system.GetString("name"),
|
||||||
|
Host: system.GetString("host"),
|
||||||
|
Port: cast.ToUint16(system.Get("port")),
|
||||||
|
Users: userEmails,
|
||||||
|
}
|
||||||
|
config.Systems = append(config.Systems, sysConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal the Config struct to YAML
|
||||||
|
yamlData, err := yaml.Marshal(&config)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a header to the YAML
|
||||||
|
yamlData = append([]byte("# Values for port and users are optional.\n# Defaults are port 45876 and the first created user.\n\n"), yamlData...)
|
||||||
|
|
||||||
|
return string(yamlData), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// New helper function to get a map of user IDs to emails
|
||||||
|
func (h *Hub) getUserEmailMap(userIDs []string) (map[string]string, error) {
|
||||||
|
users, err := h.app.Dao().FindRecordsByIds("users", userIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userEmailMap := make(map[string]string, len(users))
|
||||||
|
for _, user := range users {
|
||||||
|
userEmailMap[user.Id] = user.GetString("email")
|
||||||
|
}
|
||||||
|
|
||||||
|
return userEmailMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the current config.yml file as a JSON object
|
||||||
|
func (h *Hub) getYamlConfig(c echo.Context) error {
|
||||||
|
requestData := apis.RequestInfo(c)
|
||||||
|
if requestData.AuthRecord == nil || requestData.AuthRecord.GetString("role") != "admin" {
|
||||||
|
return apis.NewForbiddenError("Forbidden", nil)
|
||||||
|
}
|
||||||
|
configContent, err := h.generateConfigYAML()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.JSON(200, map[string]string{"config": configContent})
|
||||||
|
}
|
||||||
@@ -42,6 +42,8 @@ type Hub struct {
|
|||||||
am *alerts.AlertManager
|
am *alerts.AlertManager
|
||||||
um *users.UserManager
|
um *users.UserManager
|
||||||
rm *records.RecordManager
|
rm *records.RecordManager
|
||||||
|
systemStats *models.Collection
|
||||||
|
containerStats *models.Collection
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHub(app *pocketbase.PocketBase) *Hub {
|
func NewHub(app *pocketbase.PocketBase) *Hub {
|
||||||
@@ -56,14 +58,10 @@ func NewHub(app *pocketbase.PocketBase) *Hub {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Hub) Run() {
|
func (h *Hub) Run() {
|
||||||
// rm := records.NewRecordManager(h.app)
|
|
||||||
// am := alerts.NewAlertManager(h.app)
|
|
||||||
// um := users.NewUserManager(h.app)
|
|
||||||
|
|
||||||
// loosely check if it was executed using "go run"
|
// loosely check if it was executed using "go run"
|
||||||
isGoRun := strings.HasPrefix(os.Args[0], os.TempDir())
|
isGoRun := strings.HasPrefix(os.Args[0], os.TempDir())
|
||||||
|
|
||||||
// // enable auto creation of migration files when making collection changes in the Admin UI
|
// enable auto creation of migration files when making collection changes in the Admin UI
|
||||||
migratecmd.MustRegister(h.app, h.app.RootCmd, migratecmd.Config{
|
migratecmd.MustRegister(h.app, h.app.RootCmd, migratecmd.Config{
|
||||||
// (the isGoRun check is to enable it only during development)
|
// (the isGoRun check is to enable it only during development)
|
||||||
Automigrate: isGoRun,
|
Automigrate: isGoRun,
|
||||||
@@ -93,7 +91,8 @@ func (h *Hub) Run() {
|
|||||||
if err := h.app.Dao().SaveCollection(usersCollection); err != nil {
|
if err := h.app.Dao().SaveCollection(usersCollection); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
// sync systems with config
|
||||||
|
return h.syncSystemsWithConfig()
|
||||||
})
|
})
|
||||||
|
|
||||||
// serve web ui
|
// serve web ui
|
||||||
@@ -128,7 +127,11 @@ func (h *Hub) Run() {
|
|||||||
// delete old records once every hour
|
// delete old records once every hour
|
||||||
scheduler.MustAdd("delete old records", "8 * * * *", h.rm.DeleteOldRecords)
|
scheduler.MustAdd("delete old records", "8 * * * *", h.rm.DeleteOldRecords)
|
||||||
// create longer records every 10 minutes
|
// create longer records every 10 minutes
|
||||||
scheduler.MustAdd("create longer records", "*/10 * * * *", h.rm.CreateLongerRecords)
|
scheduler.MustAdd("create longer records", "*/10 * * * *", func() {
|
||||||
|
if systemStats, containerStats, err := h.getCollections(); err == nil {
|
||||||
|
h.rm.CreateLongerRecords([]*models.Collection{systemStats, containerStats})
|
||||||
|
}
|
||||||
|
})
|
||||||
scheduler.Start()
|
scheduler.Start()
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -153,6 +156,8 @@ func (h *Hub) Run() {
|
|||||||
})
|
})
|
||||||
// send test notification
|
// send test notification
|
||||||
e.Router.GET("/api/beszel/send-test-notification", h.am.SendTestNotification)
|
e.Router.GET("/api/beszel/send-test-notification", h.am.SendTestNotification)
|
||||||
|
// API endpoint to get config.yml content
|
||||||
|
e.Router.GET("/api/beszel/config-yaml", h.getYamlConfig)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -289,37 +294,61 @@ func (h *Hub) updateSystem(record *models.Record) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// update system record
|
// update system record
|
||||||
|
dao := h.app.Dao()
|
||||||
record.Set("status", "up")
|
record.Set("status", "up")
|
||||||
record.Set("info", systemData.Info)
|
record.Set("info", systemData.Info)
|
||||||
if err := h.app.Dao().SaveRecord(record); err != nil {
|
if err := dao.SaveRecord(record); err != nil {
|
||||||
h.app.Logger().Error("Failed to update record: ", "err", err.Error())
|
h.app.Logger().Error("Failed to update record: ", "err", err.Error())
|
||||||
}
|
}
|
||||||
// add new system_stats record
|
// add system_stats and container_stats records
|
||||||
system_stats, _ := h.app.Dao().FindCollectionByNameOrId("system_stats")
|
if systemStats, containerStats, err := h.getCollections(); err != nil {
|
||||||
systemStatsRecord := models.NewRecord(system_stats)
|
h.app.Logger().Error("Failed to get collections: ", "err", err.Error())
|
||||||
systemStatsRecord.Set("system", record.Id)
|
} else {
|
||||||
systemStatsRecord.Set("stats", systemData.Stats)
|
// add new system_stats record
|
||||||
systemStatsRecord.Set("type", "1m")
|
systemStatsRecord := models.NewRecord(systemStats)
|
||||||
if err := h.app.Dao().SaveRecord(systemStatsRecord); err != nil {
|
systemStatsRecord.Set("system", record.Id)
|
||||||
h.app.Logger().Error("Failed to save record: ", "err", err.Error())
|
systemStatsRecord.Set("stats", systemData.Stats)
|
||||||
}
|
systemStatsRecord.Set("type", "1m")
|
||||||
// add new container_stats record
|
if err := dao.SaveRecord(systemStatsRecord); err != nil {
|
||||||
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())
|
h.app.Logger().Error("Failed to save record: ", "err", err.Error())
|
||||||
}
|
}
|
||||||
|
// add new container_stats record
|
||||||
|
if len(systemData.Containers) > 0 {
|
||||||
|
containerStatsRecord := models.NewRecord(containerStats)
|
||||||
|
containerStatsRecord.Set("system", record.Id)
|
||||||
|
containerStatsRecord.Set("stats", systemData.Containers)
|
||||||
|
containerStatsRecord.Set("type", "1m")
|
||||||
|
if err := dao.SaveRecord(containerStatsRecord); err != nil {
|
||||||
|
h.app.Logger().Error("Failed to save record: ", "err", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// system info alerts (todo: extra fs alerts)
|
// system info alerts (todo: extra fs alerts)
|
||||||
if err := h.am.HandleSystemAlerts(record, systemData.Info, systemData.Stats.Temperatures, systemData.Stats.ExtraFs); err != nil {
|
if err := h.am.HandleSystemAlerts(record, systemData.Info, systemData.Stats.Temperatures, systemData.Stats.ExtraFs); err != nil {
|
||||||
h.app.Logger().Error("System alerts error", "err", err.Error())
|
h.app.Logger().Error("System alerts error", "err", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// return system_stats and container_stats collections
|
||||||
|
func (h *Hub) getCollections() (*models.Collection, *models.Collection, error) {
|
||||||
|
if h.systemStats == nil {
|
||||||
|
systemStats, err := h.app.Dao().FindCollectionByNameOrId("system_stats")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
h.systemStats = systemStats
|
||||||
|
}
|
||||||
|
if h.containerStats == nil {
|
||||||
|
containerStats, err := h.app.Dao().FindCollectionByNameOrId("container_stats")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
h.containerStats = containerStats
|
||||||
|
}
|
||||||
|
return h.systemStats, h.containerStats, nil
|
||||||
|
}
|
||||||
|
|
||||||
// set system to specified status and save record
|
// set system to specified status and save record
|
||||||
func (h *Hub) updateSystemStatus(record *models.Record, status string) {
|
func (h *Hub) updateSystemStatus(record *models.Record, status string) {
|
||||||
if record.GetString("status") != status {
|
if record.GetString("status") != status {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ type RecordDeletionData struct {
|
|||||||
retention time.Duration
|
retention time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordStats []*struct {
|
type RecordStats []struct {
|
||||||
Stats []byte `db:"stats"`
|
Stats []byte `db:"stats"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,9 +41,9 @@ func NewRecordManager(app *pocketbase.PocketBase) *RecordManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create longer records by averaging shorter records
|
// Create longer records by averaging shorter records
|
||||||
func (rm *RecordManager) CreateLongerRecords() {
|
func (rm *RecordManager) CreateLongerRecords(collections []*models.Collection) {
|
||||||
// start := time.Now()
|
// start := time.Now()
|
||||||
recordData := []LongerRecordData{
|
longerRecordData := []LongerRecordData{
|
||||||
{
|
{
|
||||||
shorterType: "1m",
|
shorterType: "1m",
|
||||||
// change to 9 from 10 to allow edge case timing or short pauses
|
// change to 9 from 10 to allow edge case timing or short pauses
|
||||||
@@ -78,17 +78,11 @@ func (rm *RecordManager) CreateLongerRecords() {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// need *models.Collection to create a new record with models.NewRecord
|
|
||||||
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
|
// loop through all active systems, time periods, and collections
|
||||||
for _, system := range activeSystems {
|
for _, system := range activeSystems {
|
||||||
// log.Println("processing system", system.GetString("name"))
|
// log.Println("processing system", system.GetString("name"))
|
||||||
for _, recordData := range recordData {
|
for i := range longerRecordData {
|
||||||
|
recordData := longerRecordData[i]
|
||||||
// log.Println("processing longer record type", recordData.longerType)
|
// 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
|
// 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)
|
longerRecordPeriod := time.Now().UTC().Add(recordData.longerTimeDuration + time.Minute)
|
||||||
@@ -112,14 +106,6 @@ func (rm *RecordManager) CreateLongerRecords() {
|
|||||||
// get shorter records from the past x minutes
|
// get shorter records from the past x minutes
|
||||||
var stats RecordStats
|
var stats RecordStats
|
||||||
|
|
||||||
// allShorterRecords, err := txDao.FindRecordsByExpr(
|
|
||||||
// collection,
|
|
||||||
// dbx.NewExp(
|
|
||||||
// "type = {:type} AND system = {:system} AND created > {:created}",
|
|
||||||
// dbx.Params{"type": recordData.shorterType, "system": system.Id, "created": shorterRecordPeriod},
|
|
||||||
// ),
|
|
||||||
// )
|
|
||||||
|
|
||||||
err := txDao.DB().
|
err := txDao.DB().
|
||||||
Select("stats").
|
Select("stats").
|
||||||
From(collection.Name).
|
From(collection.Name).
|
||||||
@@ -173,8 +159,8 @@ func (rm *RecordManager) AverageSystemStats(records RecordStats) system.Stats {
|
|||||||
tempCount := float64(0)
|
tempCount := float64(0)
|
||||||
|
|
||||||
var stats system.Stats
|
var stats system.Stats
|
||||||
for _, record := range records {
|
for i := range records {
|
||||||
json.Unmarshal(record.Stats, &stats)
|
json.Unmarshal(records[i].Stats, &stats)
|
||||||
sum.Cpu += stats.Cpu
|
sum.Cpu += stats.Cpu
|
||||||
sum.Mem += stats.Mem
|
sum.Mem += stats.Mem
|
||||||
sum.MemUsed += stats.MemUsed
|
sum.MemUsed += stats.MemUsed
|
||||||
@@ -276,13 +262,14 @@ func (rm *RecordManager) AverageContainerStats(records RecordStats) []container.
|
|||||||
count := float64(len(records))
|
count := float64(len(records))
|
||||||
|
|
||||||
var containerStats []container.Stats
|
var containerStats []container.Stats
|
||||||
for _, record := range records {
|
for i := range records {
|
||||||
// Reset the slice length to 0, but keep the capacity
|
// Reset the slice length to 0, but keep the capacity
|
||||||
containerStats = containerStats[:0]
|
containerStats = containerStats[:0]
|
||||||
if err := json.Unmarshal(record.Stats, &containerStats); err != nil {
|
if err := json.Unmarshal(records[i].Stats, &containerStats); err != nil {
|
||||||
return []container.Stats{}
|
return []container.Stats{}
|
||||||
}
|
}
|
||||||
for _, stat := range containerStats {
|
for i := range containerStats {
|
||||||
|
stat := containerStats[i]
|
||||||
if _, ok := sums[stat.Name]; !ok {
|
if _, ok := sums[stat.Name]; !ok {
|
||||||
sums[stat.Name] = &container.Stats{Name: stat.Name}
|
sums[stat.Name] = &container.Stats{Name: stat.Name}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
119
beszel/site/package-lock.json
generated
119
beszel/site/package-lock.json
generated
@@ -8,9 +8,11 @@
|
|||||||
"name": "beszel",
|
"name": "beszel",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@henrygd/queue": "^1.0.7",
|
||||||
"@nanostores/react": "^0.7.3",
|
"@nanostores/react": "^0.7.3",
|
||||||
"@nanostores/router": "^0.15.1",
|
"@nanostores/router": "^0.11.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.2",
|
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||||
|
"@radix-ui/react-checkbox": "^1.1.2",
|
||||||
"@radix-ui/react-dialog": "^1.1.2",
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
@@ -19,6 +21,7 @@
|
|||||||
"@radix-ui/react-slider": "^1.2.1",
|
"@radix-ui/react-slider": "^1.2.1",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@radix-ui/react-switch": "^1.1.1",
|
"@radix-ui/react-switch": "^1.1.1",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.1",
|
||||||
"@radix-ui/react-toast": "^1.2.2",
|
"@radix-ui/react-toast": "^1.2.2",
|
||||||
"@radix-ui/react-tooltip": "^1.1.3",
|
"@radix-ui/react-tooltip": "^1.1.3",
|
||||||
"@tanstack/react-table": "^8.20.5",
|
"@tanstack/react-table": "^8.20.5",
|
||||||
@@ -28,7 +31,7 @@
|
|||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"d3-time": "^3.1.0",
|
"d3-time": "^3.1.0",
|
||||||
"lucide-react": "^0.452.0",
|
"lucide-react": "^0.452.0",
|
||||||
"nanostores": "^0.10.3",
|
"nanostores": "^0.11.3",
|
||||||
"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",
|
||||||
@@ -730,6 +733,12 @@
|
|||||||
"integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==",
|
"integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@henrygd/queue": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@henrygd/queue/-/queue-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-Jmt/iO6yDlz9UYGILkm/Qzi/ckkEiTNZcqDvt3QFLE4OThPeiCj6tKsynHFm/ppl8RumWXAx1dZPBPiRPaaGig==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
@@ -815,9 +824,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nanostores/router": {
|
"node_modules/@nanostores/router": {
|
||||||
"version": "0.15.1",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@nanostores/router/-/router-0.15.1.tgz",
|
"resolved": "https://registry.npmjs.org/@nanostores/router/-/router-0.11.0.tgz",
|
||||||
"integrity": "sha512-uEDiHPjkHGSrqEE4NRuqOLepR6lFmfLQ90c4sLLvwSNiWTY3MxejkD7tALmpj9cN4fmLnqt54r51tCNEHJL+mw==",
|
"integrity": "sha512-QlcneDqXVIsQL3agOS59d9gJQ/9M3qyVOWVttARL5Xvpmovtq91oOYcQxKbLq9i7iitGs5yDJmabe/O3QjWddQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -826,10 +835,10 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.0.0 || >=20.0.0"
|
"node": "^16.0.0 || ^18.0.0 || >=20.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"nanostores": "^0.9.0 || ^0.10.0 || ^0.11.0"
|
"nanostores": "^0.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
@@ -955,6 +964,51 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-checkbox": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-/i0fl686zaJbDQLNKrkCbMyDm6FQMt4jg323k7HuqitoANm9sE23Ql8yOK3Wusk34HSLKDChhMux05FnP6KUkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.0",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-presence": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||||
|
"@radix-ui/react-use-previous": "1.1.0",
|
||||||
|
"@radix-ui/react-use-size": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-context": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-collection": {
|
"node_modules/@radix-ui/react-collection": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
|
||||||
@@ -1609,6 +1663,51 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-3GBUDmP2DvzmtYLMsHmpA1GtR46ZDZ+OreXM/N+kkQJOPIgytFWWTfDQmBQKBvaFS0Vno0FktdbVzN28KGrMdw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.0",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-direction": "1.1.0",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-presence": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-roving-focus": "1.1.0",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-context": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-toast": {
|
"node_modules/@radix-ui/react-toast": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.2.tgz",
|
||||||
@@ -3944,9 +4043,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nanostores": {
|
"node_modules/nanostores": {
|
||||||
"version": "0.10.3",
|
"version": "0.11.3",
|
||||||
"resolved": "https://registry.npmjs.org/nanostores/-/nanostores-0.10.3.tgz",
|
"resolved": "https://registry.npmjs.org/nanostores/-/nanostores-0.11.3.tgz",
|
||||||
"integrity": "sha512-Nii8O1XqmawqSCf9o2aWqVxhKRN01+iue9/VEd1TiJCr9VT5XxgPFbF1Edl1XN6pwJcZRsl8Ki+z01yb/T/C2g==",
|
"integrity": "sha512-TUes3xKIX33re4QzdxwZ6tdbodjmn3tWXCEc1uokiEmo14sI1EaGYNs2k3bU2pyyGNmBqFGAVl6jAGWd06AVIg==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
|
|||||||
@@ -9,9 +9,11 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@henrygd/queue": "^1.0.7",
|
||||||
"@nanostores/react": "^0.7.3",
|
"@nanostores/react": "^0.7.3",
|
||||||
"@nanostores/router": "^0.15.1",
|
"@nanostores/router": "^0.11.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.2",
|
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||||
|
"@radix-ui/react-checkbox": "^1.1.2",
|
||||||
"@radix-ui/react-dialog": "^1.1.2",
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
"@radix-ui/react-slider": "^1.2.1",
|
"@radix-ui/react-slider": "^1.2.1",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@radix-ui/react-switch": "^1.1.1",
|
"@radix-ui/react-switch": "^1.1.1",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.1",
|
||||||
"@radix-ui/react-toast": "^1.2.2",
|
"@radix-ui/react-toast": "^1.2.2",
|
||||||
"@radix-ui/react-tooltip": "^1.1.3",
|
"@radix-ui/react-tooltip": "^1.1.3",
|
||||||
"@tanstack/react-table": "^8.20.5",
|
"@tanstack/react-table": "^8.20.5",
|
||||||
@@ -29,7 +32,7 @@
|
|||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"d3-time": "^3.1.0",
|
"d3-time": "^3.1.0",
|
||||||
"lucide-react": "^0.452.0",
|
"lucide-react": "^0.452.0",
|
||||||
"nanostores": "^0.10.3",
|
"nanostores": "^0.11.3",
|
||||||
"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",
|
||||||
@@ -47,5 +50,10 @@
|
|||||||
"tailwindcss": "^3.4.14",
|
"tailwindcss": "^3.4.14",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
"vite": "^5.4.9"
|
"vite": "^5.4.9"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"@nanostores/router": {
|
||||||
|
"nanostores": "^0.11.3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
127
beszel/site/src/components/alerts/alert-button.tsx
Normal file
127
beszel/site/src/components/alerts/alert-button.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { memo, useState } from 'react'
|
||||||
|
import { useStore } from '@nanostores/react'
|
||||||
|
import { $alerts, $systems } from '@/lib/stores'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { BellIcon, GlobeIcon, ServerIcon } from 'lucide-react'
|
||||||
|
import { alertInfo, cn } from '@/lib/utils'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { AlertRecord, SystemRecord } from '@/types'
|
||||||
|
import { Link } from '../router'
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
|
import { Checkbox } from '../ui/checkbox'
|
||||||
|
import { SystemAlert, SystemAlertGlobal } from './alerts-system'
|
||||||
|
|
||||||
|
export default memo(function AlertsButton({ system }: { system: SystemRecord }) {
|
||||||
|
const alerts = useStore($alerts)
|
||||||
|
const [opened, setOpened] = useState(false)
|
||||||
|
|
||||||
|
const systemAlerts = alerts.filter((alert) => alert.system === system.id) as AlertRecord[]
|
||||||
|
const active = systemAlerts.length > 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
aria-label="Alerts"
|
||||||
|
data-nolink
|
||||||
|
onClick={() => setOpened(true)}
|
||||||
|
>
|
||||||
|
<BellIcon
|
||||||
|
className={cn('h-[1.2em] w-[1.2em] pointer-events-none', {
|
||||||
|
'fill-primary': active,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-full overflow-auto max-w-[35rem]">
|
||||||
|
{opened && <TheContent data={{ system, alerts, systemAlerts }} />}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
function TheContent({
|
||||||
|
data: { system, alerts, systemAlerts },
|
||||||
|
}: {
|
||||||
|
data: { system: SystemRecord; alerts: AlertRecord[]; systemAlerts: AlertRecord[] }
|
||||||
|
}) {
|
||||||
|
const [overwriteExisting, setOverwriteExisting] = useState<boolean | 'indeterminate'>(false)
|
||||||
|
const systems = $systems.get()
|
||||||
|
|
||||||
|
const data = Object.keys(alertInfo).map((key) => {
|
||||||
|
const alert = alertInfo[key as keyof typeof alertInfo]
|
||||||
|
return {
|
||||||
|
key: key as keyof typeof alertInfo,
|
||||||
|
alert,
|
||||||
|
system,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-xl">Alerts</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
See{' '}
|
||||||
|
<Link href="/settings/notifications" className="link">
|
||||||
|
notification settings
|
||||||
|
</Link>{' '}
|
||||||
|
to configure how you receive alerts.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<Tabs defaultValue="system">
|
||||||
|
<TabsList className="mb-1 -mt-0.5">
|
||||||
|
<TabsTrigger value="system">
|
||||||
|
<ServerIcon className="mr-2 h-3.5 w-3.5" />
|
||||||
|
{system.name}
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="global">
|
||||||
|
<GlobeIcon className="mr-1.5 h-3.5 w-3.5" />
|
||||||
|
All systems
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="system">
|
||||||
|
<div className="grid gap-3">
|
||||||
|
{data.map((d) => (
|
||||||
|
<SystemAlert key={d.key} system={system} data={d} systemAlerts={systemAlerts} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="global">
|
||||||
|
<label
|
||||||
|
htmlFor="ovw"
|
||||||
|
className="mb-3 flex gap-2 items-center justify-center cursor-pointer border rounded-sm py-3 px-4 border-destructive text-destructive font-semibold text-sm"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id="ovw"
|
||||||
|
className="text-destructive border-destructive data-[state=checked]:bg-destructive"
|
||||||
|
checked={overwriteExisting}
|
||||||
|
onCheckedChange={setOverwriteExisting}
|
||||||
|
/>
|
||||||
|
Overwrite existing alerts
|
||||||
|
</label>
|
||||||
|
<div className="grid gap-3">
|
||||||
|
{data.map((d) => (
|
||||||
|
<SystemAlertGlobal
|
||||||
|
key={d.key}
|
||||||
|
data={d}
|
||||||
|
overwrite={overwriteExisting}
|
||||||
|
alerts={alerts}
|
||||||
|
systems={systems}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
246
beszel/site/src/components/alerts/alerts-system.tsx
Normal file
246
beszel/site/src/components/alerts/alerts-system.tsx
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import { pb } from '@/lib/stores'
|
||||||
|
import { alertInfo, cn } from '@/lib/utils'
|
||||||
|
import { Switch } from '@/components/ui/switch'
|
||||||
|
import { AlertRecord, SystemRecord } from '@/types'
|
||||||
|
import { lazy, Suspense, useRef, useState } from 'react'
|
||||||
|
import { toast } from '../ui/use-toast'
|
||||||
|
import { RecordOptions } from 'pocketbase'
|
||||||
|
import { newQueue, Queue } from '@henrygd/queue'
|
||||||
|
|
||||||
|
interface AlertData {
|
||||||
|
checked?: boolean
|
||||||
|
val?: number
|
||||||
|
min?: number
|
||||||
|
updateAlert?: (checked: boolean, value: number, min: number) => void
|
||||||
|
key: keyof typeof alertInfo
|
||||||
|
alert: (typeof alertInfo)[keyof typeof alertInfo]
|
||||||
|
system: SystemRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
const Slider = lazy(() => import('@/components/ui/slider'))
|
||||||
|
|
||||||
|
let queue: Queue
|
||||||
|
|
||||||
|
const failedUpdateToast = () =>
|
||||||
|
toast({
|
||||||
|
title: 'Failed to update alert',
|
||||||
|
description: 'Please check logs for more details.',
|
||||||
|
variant: 'destructive',
|
||||||
|
})
|
||||||
|
|
||||||
|
export function SystemAlert({
|
||||||
|
system,
|
||||||
|
systemAlerts,
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
system: SystemRecord
|
||||||
|
systemAlerts: AlertRecord[]
|
||||||
|
data: AlertData
|
||||||
|
}) {
|
||||||
|
const alert = systemAlerts.find((alert) => alert.name === data.key)
|
||||||
|
|
||||||
|
data.updateAlert = async (checked: boolean, value: number, min: number) => {
|
||||||
|
try {
|
||||||
|
if (alert && !checked) {
|
||||||
|
await pb.collection('alerts').delete(alert.id)
|
||||||
|
} else if (alert && checked) {
|
||||||
|
await pb.collection('alerts').update(alert.id, { value, min, triggered: false })
|
||||||
|
} else if (checked) {
|
||||||
|
pb.collection('alerts').create({
|
||||||
|
system: system.id,
|
||||||
|
user: pb.authStore.model!.id,
|
||||||
|
name: data.key,
|
||||||
|
value: value,
|
||||||
|
min: min,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
failedUpdateToast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alert) {
|
||||||
|
data.checked = true
|
||||||
|
data.val = alert.value
|
||||||
|
data.min = alert.min || 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return <AlertContent data={data} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SystemAlertGlobal({
|
||||||
|
data,
|
||||||
|
overwrite,
|
||||||
|
alerts,
|
||||||
|
systems,
|
||||||
|
}: {
|
||||||
|
data: AlertData
|
||||||
|
overwrite: boolean | 'indeterminate'
|
||||||
|
alerts: AlertRecord[]
|
||||||
|
systems: SystemRecord[]
|
||||||
|
}) {
|
||||||
|
const systemsWithExistingAlerts = useRef<{ set: Set<string>; populatedSet: boolean }>({
|
||||||
|
set: new Set(),
|
||||||
|
populatedSet: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
data.checked = false
|
||||||
|
data.val = data.min = 0
|
||||||
|
|
||||||
|
data.updateAlert = (checked: boolean, value: number, min: number) => {
|
||||||
|
if (!queue) {
|
||||||
|
queue = newQueue(5)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { set, populatedSet } = systemsWithExistingAlerts.current
|
||||||
|
|
||||||
|
// if overwrite checked, make sure all alerts will be overwritten
|
||||||
|
if (overwrite) {
|
||||||
|
set.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
const recordData: Partial<AlertRecord> = {
|
||||||
|
value,
|
||||||
|
min,
|
||||||
|
triggered: false,
|
||||||
|
}
|
||||||
|
for (let system of systems) {
|
||||||
|
// if overwrite is false and system is in set (alert existed), skip
|
||||||
|
if (!overwrite && set.has(system.id)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// find matching existing alert
|
||||||
|
const existingAlert = alerts.find(
|
||||||
|
(alert) => alert.system === system.id && data.key === alert.name
|
||||||
|
)
|
||||||
|
// if first run, add system to set (alert already existed when global panel was opened)
|
||||||
|
if (existingAlert && !populatedSet && !overwrite) {
|
||||||
|
set.add(system.id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const requestOptions: RecordOptions = {
|
||||||
|
requestKey: system.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
// checked - make sure alert is created or updated
|
||||||
|
if (checked) {
|
||||||
|
if (existingAlert) {
|
||||||
|
// console.log('updating', system.name)
|
||||||
|
queue
|
||||||
|
.add(() => pb.collection('alerts').update(existingAlert.id, recordData, requestOptions))
|
||||||
|
.catch(failedUpdateToast)
|
||||||
|
} else {
|
||||||
|
// console.log('creating', system.name)
|
||||||
|
queue
|
||||||
|
.add(() =>
|
||||||
|
pb.collection('alerts').create(
|
||||||
|
{
|
||||||
|
system: system.id,
|
||||||
|
user: pb.authStore.model!.id,
|
||||||
|
name: data.key,
|
||||||
|
...recordData,
|
||||||
|
},
|
||||||
|
requestOptions
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.catch(failedUpdateToast)
|
||||||
|
}
|
||||||
|
} else if (existingAlert) {
|
||||||
|
// console.log('deleting', system.name)
|
||||||
|
queue.add(() => pb.collection('alerts').delete(existingAlert.id)).catch(failedUpdateToast)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
systemsWithExistingAlerts.current.populatedSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return <AlertContent data={data} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertContent({ data }: { data: AlertData }) {
|
||||||
|
const { key } = data
|
||||||
|
|
||||||
|
const hasSliders = !('single' in data.alert)
|
||||||
|
|
||||||
|
const [checked, setChecked] = useState(data.checked || false)
|
||||||
|
const [min, setMin] = useState(data.min || (hasSliders ? 10 : 0))
|
||||||
|
const [value, setValue] = useState(data.val || (hasSliders ? 80 : 0))
|
||||||
|
|
||||||
|
const showSliders = checked && hasSliders
|
||||||
|
|
||||||
|
const newMin = useRef(min)
|
||||||
|
const newValue = useRef(value)
|
||||||
|
|
||||||
|
const Icon = alertInfo[key].icon
|
||||||
|
|
||||||
|
const updateAlert = (c?: boolean) =>
|
||||||
|
data.updateAlert?.(c ?? checked, newValue.current, newMin.current)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-muted-foreground/15 hover:border-muted-foreground/20 transition-colors duration-100 group">
|
||||||
|
<label
|
||||||
|
htmlFor={`s${key}`}
|
||||||
|
className={cn('flex flex-row items-center justify-between gap-4 cursor-pointer p-4', {
|
||||||
|
'pb-0': showSliders,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="grid gap-1 select-none">
|
||||||
|
<p className="font-semibold flex gap-3 items-center capitalize">
|
||||||
|
<Icon className="h-4 w-4 opacity-85" /> {data.alert.name}
|
||||||
|
</p>
|
||||||
|
{!showSliders && (
|
||||||
|
<span className="block text-sm text-muted-foreground">{data.alert.desc}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id={`s${key}`}
|
||||||
|
checked={checked}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setChecked(checked)
|
||||||
|
updateAlert(checked)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{showSliders && (
|
||||||
|
<div className="grid sm:grid-cols-2 mt-1.5 gap-5 px-4 pb-5 tabular-nums text-muted-foreground">
|
||||||
|
<Suspense fallback={<div className="h-10" />}>
|
||||||
|
<div>
|
||||||
|
<p id={`v${key}`} className="text-sm block h-8">
|
||||||
|
Average exceeds{' '}
|
||||||
|
<strong className="text-foreground">
|
||||||
|
{value}
|
||||||
|
{data.alert.unit}
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Slider
|
||||||
|
aria-labelledby={`v${key}`}
|
||||||
|
defaultValue={[value]}
|
||||||
|
onValueCommit={(val) => (newValue.current = val[0]) && updateAlert()}
|
||||||
|
onValueChange={(val) => setValue(val[0])}
|
||||||
|
min={1}
|
||||||
|
max={99}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p id={`t${key}`} className="text-sm block h-8">
|
||||||
|
For <strong className="text-foreground">{min}</strong> minute
|
||||||
|
{min > 1 && 's'}
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Slider
|
||||||
|
aria-labelledby={`v${key}`}
|
||||||
|
defaultValue={[min]}
|
||||||
|
onValueCommit={(val) => (newMin.current = val[0]) && updateAlert()}
|
||||||
|
onValueChange={(val) => setMin(val[0])}
|
||||||
|
min={1}
|
||||||
|
max={60}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
import { Area, AreaChart, CartesianGrid, YAxis } from 'recharts'
|
||||||
|
|
||||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from '@/components/ui/chart'
|
||||||
import {
|
import {
|
||||||
useYAxisWidth,
|
useYAxisWidth,
|
||||||
chartTimeData,
|
|
||||||
cn,
|
cn,
|
||||||
formatShortDate,
|
formatShortDate,
|
||||||
toFixedWithoutTrailingZeros,
|
toFixedWithoutTrailingZeros,
|
||||||
@@ -88,18 +87,7 @@ export default memo(function AreaChartDefault({
|
|||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
/>
|
/>
|
||||||
<XAxis
|
{xAxis(chartData)}
|
||||||
dataKey="created"
|
|
||||||
domain={chartData.domain}
|
|
||||||
ticks={chartData.ticks}
|
|
||||||
allowDataOverflow
|
|
||||||
type="number"
|
|
||||||
scale="time"
|
|
||||||
minTickGap={30}
|
|
||||||
tickMargin={8}
|
|
||||||
axisLine={false}
|
|
||||||
tickFormatter={chartTimeData[chartTime].format}
|
|
||||||
/>
|
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
animationEasing="ease-out"
|
animationEasing="ease-out"
|
||||||
animationDuration={150}
|
animationDuration={150}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
import { Area, AreaChart, CartesianGrid, YAxis } from 'recharts'
|
||||||
import {
|
import {
|
||||||
ChartConfig,
|
ChartConfig,
|
||||||
ChartContainer,
|
ChartContainer,
|
||||||
ChartTooltip,
|
ChartTooltip,
|
||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
|
xAxis,
|
||||||
} from '@/components/ui/chart'
|
} from '@/components/ui/chart'
|
||||||
import { memo, useMemo } from 'react'
|
import { memo, useMemo } from 'react'
|
||||||
import {
|
import {
|
||||||
useYAxisWidth,
|
useYAxisWidth,
|
||||||
chartTimeData,
|
|
||||||
cn,
|
cn,
|
||||||
formatShortDate,
|
formatShortDate,
|
||||||
decimalString,
|
decimalString,
|
||||||
@@ -37,7 +37,7 @@ export default memo(function ContainerChart({
|
|||||||
const filter = useStore($containerFilter)
|
const filter = useStore($containerFilter)
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||||
|
|
||||||
const { containerData, ticks, domain, chartTime } = chartData
|
const { containerData } = chartData
|
||||||
|
|
||||||
const isNetChart = chartName === 'net'
|
const isNetChart = chartName === 'net'
|
||||||
|
|
||||||
@@ -153,18 +153,7 @@ export default memo(function ContainerChart({
|
|||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
/>
|
/>
|
||||||
<XAxis
|
{xAxis(chartData)}
|
||||||
dataKey="created"
|
|
||||||
domain={domain}
|
|
||||||
allowDataOverflow
|
|
||||||
ticks={ticks}
|
|
||||||
type="number"
|
|
||||||
scale="time"
|
|
||||||
minTickGap={35}
|
|
||||||
tickMargin={8}
|
|
||||||
axisLine={false}
|
|
||||||
tickFormatter={chartTimeData[chartTime].format}
|
|
||||||
/>
|
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
animationEasing="ease-out"
|
animationEasing="ease-out"
|
||||||
animationDuration={150}
|
animationDuration={150}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
import { Area, AreaChart, CartesianGrid, YAxis } from 'recharts'
|
||||||
|
|
||||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from '@/components/ui/chart'
|
||||||
import {
|
import {
|
||||||
useYAxisWidth,
|
useYAxisWidth,
|
||||||
chartTimeData,
|
|
||||||
cn,
|
cn,
|
||||||
formatShortDate,
|
formatShortDate,
|
||||||
decimalString,
|
decimalString,
|
||||||
@@ -25,8 +24,6 @@ export default memo(function DiskChart({
|
|||||||
}) {
|
}) {
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||||
|
|
||||||
// console.log('rendered at', new Date())
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
@@ -49,18 +46,7 @@ export default memo(function DiskChart({
|
|||||||
return updateYAxisWidth(toFixedFloat(v, 2) + u)
|
return updateYAxisWidth(toFixedFloat(v, 2) + u)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<XAxis
|
{xAxis(chartData)}
|
||||||
dataKey="created"
|
|
||||||
domain={chartData.domain}
|
|
||||||
ticks={chartData.ticks}
|
|
||||||
allowDataOverflow
|
|
||||||
type="number"
|
|
||||||
scale="time"
|
|
||||||
minTickGap={30}
|
|
||||||
tickMargin={8}
|
|
||||||
axisLine={false}
|
|
||||||
tickFormatter={chartTimeData[chartData.chartTime].format}
|
|
||||||
/>
|
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
animationEasing="ease-out"
|
animationEasing="ease-out"
|
||||||
animationDuration={150}
|
animationDuration={150}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
import { Area, AreaChart, CartesianGrid, YAxis } from 'recharts'
|
||||||
|
|
||||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from '@/components/ui/chart'
|
||||||
import {
|
import {
|
||||||
useYAxisWidth,
|
useYAxisWidth,
|
||||||
chartTimeData,
|
|
||||||
cn,
|
cn,
|
||||||
toFixedFloat,
|
toFixedFloat,
|
||||||
decimalString,
|
decimalString,
|
||||||
@@ -45,18 +44,7 @@ export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<XAxis
|
{xAxis(chartData)}
|
||||||
dataKey="created"
|
|
||||||
domain={chartData.domain}
|
|
||||||
ticks={chartData.ticks}
|
|
||||||
allowDataOverflow
|
|
||||||
type="number"
|
|
||||||
scale="time"
|
|
||||||
minTickGap={30}
|
|
||||||
tickMargin={8}
|
|
||||||
axisLine={false}
|
|
||||||
tickFormatter={chartTimeData[chartData.chartTime].format}
|
|
||||||
/>
|
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
// cursor={false}
|
// cursor={false}
|
||||||
animationEasing="ease-out"
|
animationEasing="ease-out"
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
import { Area, AreaChart, CartesianGrid, YAxis } from 'recharts'
|
||||||
|
|
||||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
import { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from '@/components/ui/chart'
|
||||||
import {
|
import {
|
||||||
useYAxisWidth,
|
useYAxisWidth,
|
||||||
chartTimeData,
|
|
||||||
cn,
|
cn,
|
||||||
formatShortDate,
|
formatShortDate,
|
||||||
toFixedWithoutTrailingZeros,
|
toFixedWithoutTrailingZeros,
|
||||||
@@ -36,18 +35,7 @@ export default memo(function SwapChart({ chartData }: { chartData: ChartData })
|
|||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickFormatter={(value) => updateYAxisWidth(value + ' GB')}
|
tickFormatter={(value) => updateYAxisWidth(value + ' GB')}
|
||||||
/>
|
/>
|
||||||
<XAxis
|
{xAxis(chartData)}
|
||||||
dataKey="created"
|
|
||||||
domain={chartData.domain}
|
|
||||||
ticks={chartData.ticks}
|
|
||||||
allowDataOverflow
|
|
||||||
type="number"
|
|
||||||
scale="time"
|
|
||||||
minTickGap={30}
|
|
||||||
tickMargin={8}
|
|
||||||
axisLine={false}
|
|
||||||
tickFormatter={chartTimeData[chartData.chartTime].format}
|
|
||||||
/>
|
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
animationEasing="ease-out"
|
animationEasing="ease-out"
|
||||||
animationDuration={150}
|
animationDuration={150}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from 'recharts'
|
import { CartesianGrid, Line, LineChart, YAxis } from 'recharts'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChartContainer,
|
ChartContainer,
|
||||||
@@ -6,10 +6,10 @@ import {
|
|||||||
ChartLegendContent,
|
ChartLegendContent,
|
||||||
ChartTooltip,
|
ChartTooltip,
|
||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
|
xAxis,
|
||||||
} from '@/components/ui/chart'
|
} from '@/components/ui/chart'
|
||||||
import {
|
import {
|
||||||
useYAxisWidth,
|
useYAxisWidth,
|
||||||
chartTimeData,
|
|
||||||
cn,
|
cn,
|
||||||
formatShortDate,
|
formatShortDate,
|
||||||
toFixedWithoutTrailingZeros,
|
toFixedWithoutTrailingZeros,
|
||||||
@@ -70,18 +70,7 @@ export default memo(function TemperatureChart({ chartData }: { chartData: ChartD
|
|||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
/>
|
/>
|
||||||
<XAxis
|
{xAxis(chartData)}
|
||||||
dataKey="created"
|
|
||||||
domain={chartData.domain}
|
|
||||||
ticks={chartData.ticks}
|
|
||||||
allowDataOverflow
|
|
||||||
type="number"
|
|
||||||
scale="time"
|
|
||||||
minTickGap={30}
|
|
||||||
tickMargin={8}
|
|
||||||
axisLine={false}
|
|
||||||
tickFormatter={chartTimeData[chartData.chartTime].format}
|
|
||||||
/>
|
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
animationEasing="ease-out"
|
animationEasing="ease-out"
|
||||||
animationDuration={150}
|
animationDuration={150}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen grid items-center py-12">
|
<div className="min-h-svh grid items-center py-12">
|
||||||
<div className="grid gap-5 w-full px-4 mx-auto" style={{ maxWidth: '22em' }}>
|
<div className="grid gap-5 w-full px-4 mx-auto" style={{ maxWidth: '22em' }}>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="mb-3">
|
<h1 className="mb-3">
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export default function () {
|
|||||||
className="hover:-translate-y-[1px] duration-200 bg-transparent border-foreground/10 hover:shadow-md shadow-black"
|
className="hover:-translate-y-[1px] duration-200 bg-transparent border-foreground/10 hover:shadow-md shadow-black"
|
||||||
>
|
>
|
||||||
<info.icon className="h-4 w-4" />
|
<info.icon className="h-4 w-4" />
|
||||||
<AlertTitle className="mb-2">
|
<AlertTitle>
|
||||||
{alert.sysname} {info.name}
|
{alert.sysname} {info.name}
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
|
|||||||
93
beszel/site/src/components/routes/settings/config-yaml.tsx
Normal file
93
beszel/site/src/components/routes/settings/config-yaml.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { isAdmin } from '@/lib/utils'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { redirectPage } from '@nanostores/router'
|
||||||
|
import { $router } from '@/components/router'
|
||||||
|
import { AlertCircleIcon, FileSlidersIcon, LoaderCircleIcon } from 'lucide-react'
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||||
|
import { pb } from '@/lib/stores'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { toast } from '@/components/ui/use-toast'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
export default function ConfigYaml() {
|
||||||
|
const [configContent, setConfigContent] = useState<string>('')
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
const ButtonIcon = isLoading ? LoaderCircleIcon : FileSlidersIcon
|
||||||
|
|
||||||
|
async function fetchConfig() {
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
const { config } = await pb.send<{ config: string }>('/api/beszel/config-yaml', {})
|
||||||
|
setConfigContent(config)
|
||||||
|
} catch (error: any) {
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: error.message,
|
||||||
|
variant: 'destructive',
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAdmin()) {
|
||||||
|
redirectPage($router, 'settings', { name: 'general' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-medium mb-2">YAML Configuration</h3>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
Export your current systems configuration.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Separator className="my-4" />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed my-1">
|
||||||
|
Systems may be managed in a{' '}
|
||||||
|
<code className="bg-muted rounded-sm px-1 text-primary">config.yml</code> file inside
|
||||||
|
your data directory.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
On each restart, systems in the database will be updated to match the systems defined in
|
||||||
|
the file.
|
||||||
|
</p>
|
||||||
|
<Alert className="my-4 border-destructive text-destructive w-auto table md:pr-6">
|
||||||
|
<AlertCircleIcon className="h-4 w-4 stroke-destructive" />
|
||||||
|
<AlertTitle>Caution - potential data loss</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
<p>
|
||||||
|
Existing systems not defined in <code>config.yml</code> will be deleted. Please make
|
||||||
|
regular backups.
|
||||||
|
</p>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
{configContent && (
|
||||||
|
<Textarea
|
||||||
|
autoFocus
|
||||||
|
defaultValue={configContent}
|
||||||
|
spellCheck="false"
|
||||||
|
rows={Math.min(25, configContent.split('\n').length)}
|
||||||
|
className="font-mono whitespace-pre"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Separator className="my-5" />
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="mt-2 flex items-center gap-1"
|
||||||
|
onClick={fetchConfig}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<ButtonIcon className={clsx('h-4 w-4 mr-0.5', isLoading && 'animate-spin')} />
|
||||||
|
Export configuration
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,12 +5,14 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
|||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { $router } from '@/components/router.tsx'
|
import { $router } from '@/components/router.tsx'
|
||||||
import { redirectPage } from '@nanostores/router'
|
import { redirectPage } from '@nanostores/router'
|
||||||
import { BellIcon, SettingsIcon } from 'lucide-react'
|
import { BellIcon, FileSlidersIcon, SettingsIcon } from 'lucide-react'
|
||||||
import { $userSettings, pb } from '@/lib/stores.ts'
|
import { $userSettings, pb } from '@/lib/stores.ts'
|
||||||
import { toast } from '@/components/ui/use-toast.ts'
|
import { toast } from '@/components/ui/use-toast.ts'
|
||||||
import { UserSettings } from '@/types.js'
|
import { UserSettings } from '@/types.js'
|
||||||
import General from './general.tsx'
|
import General from './general.tsx'
|
||||||
import Notifications from './notifications.tsx'
|
import Notifications from './notifications.tsx'
|
||||||
|
import ConfigYaml from './config-yaml.tsx'
|
||||||
|
import { isAdmin } from '@/lib/utils.ts'
|
||||||
|
|
||||||
const sidebarNavItems = [
|
const sidebarNavItems = [
|
||||||
{
|
{
|
||||||
@@ -25,6 +27,14 @@ const sidebarNavItems = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (isAdmin()) {
|
||||||
|
sidebarNavItems.push({
|
||||||
|
title: 'YAML Config',
|
||||||
|
href: '/settings/config',
|
||||||
|
icon: FileSlidersIcon,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function saveSettings(newSettings: Partial<UserSettings>) {
|
export async function saveSettings(newSettings: Partial<UserSettings>) {
|
||||||
try {
|
try {
|
||||||
// get fresh copy of settings
|
// get fresh copy of settings
|
||||||
@@ -94,5 +104,7 @@ function SettingsContent({ name }: { name: string }) {
|
|||||||
return <General userSettings={userSettings} />
|
return <General userSettings={userSettings} />
|
||||||
case 'notifications':
|
case 'notifications':
|
||||||
return <Notifications userSettings={userSettings} />
|
return <Notifications userSettings={userSettings} />
|
||||||
|
case 'config':
|
||||||
|
return <ConfigYaml />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
SystemRecord,
|
SystemRecord,
|
||||||
SystemStatsRecord,
|
SystemStatsRecord,
|
||||||
} from '@/types'
|
} from '@/types'
|
||||||
import React, { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Card, CardHeader, CardTitle, CardDescription } from '../ui/card'
|
import { Card, CardHeader, CardTitle, CardDescription } from '../ui/card'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import Spinner from '../spinner'
|
import Spinner from '../spinner'
|
||||||
@@ -155,7 +155,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
|||||||
return () => {
|
return () => {
|
||||||
pb.collection('systems').unsubscribe(system.id)
|
pb.collection('systems').unsubscribe(system.id)
|
||||||
}
|
}
|
||||||
}, [system])
|
}, [system.id])
|
||||||
|
|
||||||
const chartData: ChartData = useMemo(() => {
|
const chartData: ChartData = useMemo(() => {
|
||||||
const lastCreated = Math.max(
|
const lastCreated = Math.max(
|
||||||
@@ -612,7 +612,7 @@ function ChartCard({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<div className="pl-0 w-[calc(100%-1.6em)] h-52 relative">
|
<div className="pl-0 w-[calc(100%-1.6em)] h-52 relative">
|
||||||
{<Spinner />}
|
{<Spinner />}
|
||||||
{isIntersecting && <Suspense>{children}</Suspense>}
|
{isIntersecting && children}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ import { useEffect, useMemo, useState } from 'react'
|
|||||||
import { $hubVersion, $systems, pb } from '@/lib/stores'
|
import { $hubVersion, $systems, pb } from '@/lib/stores'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { cn, copyToClipboard, decimalString, isReadOnlyUser } from '@/lib/utils'
|
import { cn, copyToClipboard, decimalString, isReadOnlyUser } from '@/lib/utils'
|
||||||
import AlertsButton from '../table-alerts'
|
import AlertsButton from '../alerts/alert-button'
|
||||||
import { navigate } from '../router'
|
import { navigate } from '../router'
|
||||||
import { EthernetIcon } from '../ui/icons'
|
import { EthernetIcon } from '../ui/icons'
|
||||||
|
|
||||||
@@ -260,7 +260,7 @@ export default function SystemsTable({ filter }: { filter?: string }) {
|
|||||||
<AlertDialogTitle>Are you sure you want to delete {name}?</AlertDialogTitle>
|
<AlertDialogTitle>Are you sure you want to delete {name}?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
This action cannot be undone. This will permanently delete all current records
|
This action cannot be undone. This will permanently delete all current records
|
||||||
for <code className={'bg-muted rounded-sm px-1'}>{name}</code> from the
|
for <code className="bg-muted rounded-sm px-1">{name}</code> from the
|
||||||
database.
|
database.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
|
|||||||
@@ -1,273 +0,0 @@
|
|||||||
import { $alerts, pb } from '@/lib/stores'
|
|
||||||
import { useStore } from '@nanostores/react'
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogTrigger,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { BellIcon, ServerIcon } from 'lucide-react'
|
|
||||||
import { alertInfo, cn } from '@/lib/utils'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Switch } from '@/components/ui/switch'
|
|
||||||
import { AlertRecord, SystemRecord } from '@/types'
|
|
||||||
import { lazy, Suspense, useMemo, useState } from 'react'
|
|
||||||
import { toast } from './ui/use-toast'
|
|
||||||
import { Link } from './router'
|
|
||||||
|
|
||||||
const Slider = lazy(() => import('./ui/slider'))
|
|
||||||
|
|
||||||
const failedUpdateToast = () =>
|
|
||||||
toast({
|
|
||||||
title: 'Failed to update alert',
|
|
||||||
description: 'Please check logs for more details.',
|
|
||||||
variant: 'destructive',
|
|
||||||
})
|
|
||||||
|
|
||||||
export default function AlertsButton({ system }: { system: SystemRecord }) {
|
|
||||||
const alerts = useStore($alerts)
|
|
||||||
const [opened, setOpened] = useState(false)
|
|
||||||
|
|
||||||
const systemAlerts = alerts.filter((alert) => alert.system === system.id) as AlertRecord[]
|
|
||||||
|
|
||||||
const active = systemAlerts.length > 0
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size={'icon'}
|
|
||||||
aria-label="Alerts"
|
|
||||||
data-nolink
|
|
||||||
onClick={() => setOpened(true)}
|
|
||||||
>
|
|
||||||
<BellIcon
|
|
||||||
className={cn('h-[1.2em] w-[1.2em] pointer-events-none', {
|
|
||||||
'fill-foreground': active,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent
|
|
||||||
className="max-h-full overflow-auto max-w-[35rem]"
|
|
||||||
// onCloseAutoFocus={() => setOpened(false)}
|
|
||||||
>
|
|
||||||
{opened && (
|
|
||||||
<>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="text-xl">{system.name} alerts</DialogTitle>
|
|
||||||
<DialogDescription className="mb-1">
|
|
||||||
See{' '}
|
|
||||||
<Link href="/settings/notifications" className="link">
|
|
||||||
notification settings
|
|
||||||
</Link>{' '}
|
|
||||||
to configure how you receive alerts.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="grid gap-3">
|
|
||||||
<AlertStatus system={system} alerts={systemAlerts} />
|
|
||||||
{Object.keys(alertInfo).map((key) => {
|
|
||||||
const alert = alertInfo[key as keyof typeof alertInfo]
|
|
||||||
return (
|
|
||||||
<AlertWithSlider
|
|
||||||
key={key}
|
|
||||||
system={system}
|
|
||||||
alerts={systemAlerts}
|
|
||||||
name={key}
|
|
||||||
title={alert.name}
|
|
||||||
description={alert.desc}
|
|
||||||
unit={alert.unit}
|
|
||||||
Icon={alert.icon}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertStatus({ system, alerts }: { system: SystemRecord; alerts: AlertRecord[] }) {
|
|
||||||
const [pendingChange, setPendingChange] = useState(false)
|
|
||||||
|
|
||||||
const alert = alerts.find((alert) => alert.name === 'Status')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<label
|
|
||||||
htmlFor="alert-status"
|
|
||||||
className="flex flex-row items-center justify-between gap-4 rounded-lg border border-muted-foreground/15 hover:border-muted-foreground/20 transition-colors duration-100 p-4 cursor-pointer"
|
|
||||||
>
|
|
||||||
<div className="grid gap-1 select-none">
|
|
||||||
<p className="font-semibold flex gap-3 items-center">
|
|
||||||
<ServerIcon className="h-4 w-4 opacity-85" /> System status
|
|
||||||
</p>
|
|
||||||
<span className="block text-sm text-muted-foreground">
|
|
||||||
Triggers when status switches between up and down.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
id="alert-status"
|
|
||||||
className={cn('transition-opacity', pendingChange && 'opacity-40')}
|
|
||||||
checked={!!alert}
|
|
||||||
value={!!alert ? 'on' : 'off'}
|
|
||||||
onCheckedChange={async (active) => {
|
|
||||||
if (pendingChange) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setPendingChange(true)
|
|
||||||
try {
|
|
||||||
if (!active && alert) {
|
|
||||||
await pb.collection('alerts').delete(alert.id)
|
|
||||||
} else if (active) {
|
|
||||||
pb.collection('alerts').create({
|
|
||||||
system: system.id,
|
|
||||||
user: pb.authStore.model!.id,
|
|
||||||
name: 'Status',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
failedUpdateToast()
|
|
||||||
} finally {
|
|
||||||
setPendingChange(false)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertWithSlider({
|
|
||||||
system,
|
|
||||||
alerts,
|
|
||||||
name,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
unit = '%',
|
|
||||||
max = 99,
|
|
||||||
Icon,
|
|
||||||
}: {
|
|
||||||
system: SystemRecord
|
|
||||||
alerts: AlertRecord[]
|
|
||||||
name: string
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
unit?: string
|
|
||||||
max?: number
|
|
||||||
Icon: React.FC<React.SVGProps<SVGSVGElement>>
|
|
||||||
}) {
|
|
||||||
const [pendingChange, setPendingChange] = useState(false)
|
|
||||||
const [liveValue, setLiveValue] = useState(80)
|
|
||||||
const [liveMinutes, setLiveMinutes] = useState(10)
|
|
||||||
|
|
||||||
const key = name.replaceAll(' ', '-')
|
|
||||||
|
|
||||||
const alert = useMemo(() => {
|
|
||||||
const alert = alerts.find((alert) => alert.name === name)
|
|
||||||
if (alert) {
|
|
||||||
setLiveValue(alert.value)
|
|
||||||
setLiveMinutes(alert.min || 1)
|
|
||||||
}
|
|
||||||
return alert
|
|
||||||
}, [alerts])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-lg border border-muted-foreground/15 hover:border-muted-foreground/20 transition-colors duration-100 group">
|
|
||||||
<label
|
|
||||||
htmlFor={`s${key}`}
|
|
||||||
className={cn('flex flex-row items-center justify-between gap-4 cursor-pointer p-4', {
|
|
||||||
'pb-0': !!alert,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div className="grid gap-1 select-none">
|
|
||||||
<p className="font-semibold flex gap-3 items-center">
|
|
||||||
<Icon className="h-4 w-4 opacity-85" /> {title}
|
|
||||||
</p>
|
|
||||||
{!alert && <span className="block text-sm text-muted-foreground">{description}</span>}
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
id={`s${key}`}
|
|
||||||
className={cn('transition-opacity', pendingChange && 'opacity-40')}
|
|
||||||
checked={!!alert}
|
|
||||||
value={!!alert ? 'on' : 'off'}
|
|
||||||
onCheckedChange={async (active) => {
|
|
||||||
if (pendingChange) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setPendingChange(true)
|
|
||||||
try {
|
|
||||||
if (!active && alert) {
|
|
||||||
await pb.collection('alerts').delete(alert.id)
|
|
||||||
} else if (active) {
|
|
||||||
pb.collection('alerts').create({
|
|
||||||
system: system.id,
|
|
||||||
user: pb.authStore.model!.id,
|
|
||||||
name,
|
|
||||||
value: liveValue,
|
|
||||||
min: liveMinutes,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
failedUpdateToast()
|
|
||||||
} finally {
|
|
||||||
setPendingChange(false)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
{alert && (
|
|
||||||
<div className="grid sm:grid-cols-2 mt-1.5 gap-5 px-4 pb-5 tabular-nums text-muted-foreground">
|
|
||||||
<Suspense fallback={<div className="h-10" />}>
|
|
||||||
<div>
|
|
||||||
<p id={`v${key}`} className="text-sm block h-8">
|
|
||||||
Average exceeds{' '}
|
|
||||||
<strong className="text-foreground">
|
|
||||||
{liveValue}
|
|
||||||
{unit}
|
|
||||||
</strong>
|
|
||||||
</p>
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<Slider
|
|
||||||
aria-labelledby={`v${key}`}
|
|
||||||
defaultValue={[liveValue]}
|
|
||||||
onValueCommit={(val) => {
|
|
||||||
pb.collection('alerts').update(alert.id, {
|
|
||||||
value: val[0],
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
onValueChange={(val) => setLiveValue(val[0])}
|
|
||||||
min={1}
|
|
||||||
max={max}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p id={`t${key}`} className="text-sm block h-8">
|
|
||||||
For <strong className="text-foreground">{liveMinutes}</strong> minute
|
|
||||||
{liveMinutes > 1 && 's'}
|
|
||||||
</p>
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<Slider
|
|
||||||
aria-labelledby={`v${key}`}
|
|
||||||
defaultValue={[liveMinutes]}
|
|
||||||
onValueCommit={(val) => {
|
|
||||||
pb.collection('alerts').update(alert.id, {
|
|
||||||
min: val[0],
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
onValueChange={(val) => setLiveMinutes(val[0])}
|
|
||||||
min={1}
|
|
||||||
max={60}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,59 +1,59 @@
|
|||||||
import * as React from "react"
|
import * as React from 'react'
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
// import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const alertVariants = cva(
|
// const alertVariants = cva(
|
||||||
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
// "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||||
{
|
// {
|
||||||
variants: {
|
// variants: {
|
||||||
variant: {
|
// variant: {
|
||||||
default: "bg-background text-foreground",
|
// default: "bg-background text-foreground",
|
||||||
destructive:
|
// destructive:
|
||||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
// "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
defaultVariants: {
|
// defaultVariants: {
|
||||||
variant: "default",
|
// variant: "default",
|
||||||
},
|
// },
|
||||||
}
|
// }
|
||||||
)
|
// )
|
||||||
|
|
||||||
const Alert = React.forwardRef<
|
const Alert = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
// React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||||
>(({ className, variant, ...props }, ref) => (
|
// >(({ className, variant, ...props }, ref) => (
|
||||||
<div
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
ref={ref}
|
|
||||||
role="alert"
|
|
||||||
className={cn(alertVariants({ variant }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
Alert.displayName = "Alert"
|
|
||||||
|
|
||||||
const AlertTitle = React.forwardRef<
|
|
||||||
HTMLParagraphElement,
|
|
||||||
React.HTMLAttributes<HTMLHeadingElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<h5
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
role="alert"
|
||||||
{...props}
|
className={cn(
|
||||||
/>
|
'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground bg-background text-foreground',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
))
|
))
|
||||||
AlertTitle.displayName = "AlertTitle"
|
Alert.displayName = 'Alert'
|
||||||
|
|
||||||
|
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<h5
|
||||||
|
ref={ref}
|
||||||
|
className={cn('mb-1 -mt-0.5 font-medium leading-tight tracking-tight', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AlertTitle.displayName = 'AlertTitle'
|
||||||
|
|
||||||
const AlertDescription = React.forwardRef<
|
const AlertDescription = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div
|
<div ref={ref} className={cn('text-sm [&_p]:leading-relaxed', className)} {...props} />
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
))
|
||||||
AlertDescription.displayName = "AlertDescription"
|
AlertDescription.displayName = 'AlertDescription'
|
||||||
|
|
||||||
export { Alert, AlertTitle, AlertDescription }
|
export { Alert, AlertTitle, AlertDescription }
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import * as RechartsPrimitive from 'recharts'
|
import * as RechartsPrimitive from 'recharts'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { chartTimeData, cn } from '@/lib/utils'
|
||||||
|
import { ChartData } from '@/types'
|
||||||
|
|
||||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||||
const THEMES = { light: '', dark: '.dark' } as const
|
const THEMES = { light: '', dark: '.dark' } as const
|
||||||
@@ -331,11 +332,34 @@ function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key:
|
|||||||
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]
|
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cachedAxis: JSX.Element
|
||||||
|
const xAxis = function ({ domain, ticks, chartTime }: ChartData) {
|
||||||
|
if (cachedAxis && domain[0] === cachedAxis.props.domain[0]) {
|
||||||
|
return cachedAxis
|
||||||
|
}
|
||||||
|
cachedAxis = (
|
||||||
|
<RechartsPrimitive.XAxis
|
||||||
|
dataKey="created"
|
||||||
|
domain={domain}
|
||||||
|
ticks={ticks}
|
||||||
|
allowDataOverflow
|
||||||
|
type="number"
|
||||||
|
scale="time"
|
||||||
|
minTickGap={15}
|
||||||
|
tickMargin={8}
|
||||||
|
axisLine={false}
|
||||||
|
tickFormatter={chartTimeData[chartTime].format}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
return cachedAxis
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ChartContainer,
|
ChartContainer,
|
||||||
ChartTooltip,
|
ChartTooltip,
|
||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
ChartLegend,
|
ChartLegend,
|
||||||
ChartLegendContent,
|
ChartLegendContent,
|
||||||
|
xAxis,
|
||||||
// ChartStyle,
|
// ChartStyle,
|
||||||
}
|
}
|
||||||
|
|||||||
26
beszel/site/src/components/ui/checkbox.tsx
Normal file
26
beszel/site/src/components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
|
||||||
|
import { Check } from 'lucide-react'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const Checkbox = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'peer h-4 w-4 shrink-0 rounded-[.3em] border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator className={cn('flex items-center justify-center text-current')}>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
))
|
||||||
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Checkbox }
|
||||||
53
beszel/site/src/components/ui/tabs.tsx
Normal file
53
beszel/site/src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||||
@@ -1,112 +1,107 @@
|
|||||||
import * as React from "react"
|
import * as React from 'react'
|
||||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
import * as ToastPrimitives from '@radix-ui/react-toast'
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
import { X } from "lucide-react"
|
import { X } from 'lucide-react'
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const ToastProvider = ToastPrimitives.Provider
|
const ToastProvider = ToastPrimitives.Provider
|
||||||
|
|
||||||
const ToastViewport = React.forwardRef<
|
const ToastViewport = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Viewport
|
<ToastPrimitives.Viewport
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
'fixed top-0 z-[100] flex max-h-dvh w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||||
|
|
||||||
const toastVariants = cva(
|
const toastVariants = cva(
|
||||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "border bg-background text-foreground",
|
default: 'border bg-background text-foreground',
|
||||||
destructive:
|
destructive:
|
||||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
'destructive group border-destructive bg-destructive text-destructive-foreground',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: 'default',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const Toast = React.forwardRef<
|
const Toast = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>
|
||||||
VariantProps<typeof toastVariants>
|
|
||||||
>(({ className, variant, ...props }, ref) => {
|
>(({ className, variant, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<ToastPrimitives.Root
|
<ToastPrimitives.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(toastVariants({ variant }), className)}
|
className={cn(toastVariants({ variant }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
Toast.displayName = ToastPrimitives.Root.displayName
|
Toast.displayName = ToastPrimitives.Root.displayName
|
||||||
|
|
||||||
const ToastAction = React.forwardRef<
|
const ToastAction = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Action
|
<ToastPrimitives.Action
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||||
|
|
||||||
const ToastClose = React.forwardRef<
|
const ToastClose = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Close
|
<ToastPrimitives.Close
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
'absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
toast-close=""
|
toast-close=""
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</ToastPrimitives.Close>
|
</ToastPrimitives.Close>
|
||||||
))
|
))
|
||||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||||
|
|
||||||
const ToastTitle = React.forwardRef<
|
const ToastTitle = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Title
|
<ToastPrimitives.Title ref={ref} className={cn('text-sm font-semibold', className)} {...props} />
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm font-semibold", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
))
|
||||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||||
|
|
||||||
const ToastDescription = React.forwardRef<
|
const ToastDescription = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Description
|
<ToastPrimitives.Description
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("text-sm opacity-90", className)}
|
className={cn('text-sm opacity-90', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||||
|
|
||||||
@@ -115,13 +110,13 @@ type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
|||||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||||
|
|
||||||
export {
|
export {
|
||||||
type ToastProps,
|
type ToastProps,
|
||||||
type ToastActionElement,
|
type ToastActionElement,
|
||||||
ToastProvider,
|
ToastProvider,
|
||||||
ToastViewport,
|
ToastViewport,
|
||||||
Toast,
|
Toast,
|
||||||
ToastTitle,
|
ToastTitle,
|
||||||
ToastDescription,
|
ToastDescription,
|
||||||
ToastClose,
|
ToastClose,
|
||||||
ToastAction,
|
ToastAction,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
--muted-foreground: 240 5.03% 64.9%;
|
--muted-foreground: 240 5.03% 64.9%;
|
||||||
--accent: 240 3.7% 15.88%;
|
--accent: 240 3.7% 15.88%;
|
||||||
--accent-foreground: 0 0% 98.04%;
|
--accent-foreground: 0 0% 98.04%;
|
||||||
--destructive: 0 56.48% 42.35%;
|
--destructive: 0 59% 46%;
|
||||||
--destructive-foreground: 0 0% 98.04%;
|
--destructive-foreground: 0 0% 98.04%;
|
||||||
--border: 240 2.86% 12%;
|
--border: 240 2.86% 12%;
|
||||||
--input: 240 3.7% 15.88%;
|
--input: 240 3.7% 15.88%;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ 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 { CpuIcon, HardDriveIcon, MemoryStickIcon } from 'lucide-react'
|
import { CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from 'lucide-react'
|
||||||
import { EthernetIcon, ThermometerIcon } from '@/components/ui/icons'
|
import { EthernetIcon, ThermometerIcon } from '@/components/ui/icons'
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
@@ -102,8 +102,9 @@ export const formatDay = (timestamp: string) => {
|
|||||||
return dayFormatter.format(new Date(timestamp))
|
return dayFormatter.format(new Date(timestamp))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateFavicon = (newIcon: string) =>
|
export const updateFavicon = (newIcon: string) => {
|
||||||
((document.querySelector("link[rel='icon']") as HTMLLinkElement).href = `/static/${newIcon}`)
|
;(document.querySelector("link[rel='icon']") as HTMLLinkElement).href = `/static/${newIcon}`
|
||||||
|
}
|
||||||
|
|
||||||
export const isAdmin = () => pb.authStore.model?.role === 'admin'
|
export const isAdmin = () => pb.authStore.model?.role === 'admin'
|
||||||
export const isReadOnlyUser = () => pb.authStore.model?.role === 'readonly'
|
export const isReadOnlyUser = () => pb.authStore.model?.role === 'readonly'
|
||||||
@@ -299,6 +300,13 @@ export const getSizeAndUnit = (n: number, isGigabytes = true) => {
|
|||||||
export const chartMargin = { top: 12 }
|
export const chartMargin = { top: 12 }
|
||||||
|
|
||||||
export const alertInfo = {
|
export const alertInfo = {
|
||||||
|
Status: {
|
||||||
|
name: 'Status',
|
||||||
|
unit: '',
|
||||||
|
icon: ServerIcon,
|
||||||
|
desc: 'Triggers when status switches between up and down.',
|
||||||
|
single: true,
|
||||||
|
},
|
||||||
CPU: {
|
CPU: {
|
||||||
name: 'CPU usage',
|
name: 'CPU usage',
|
||||||
unit: '%',
|
unit: '%',
|
||||||
@@ -306,25 +314,25 @@ export const alertInfo = {
|
|||||||
desc: 'Triggers when CPU usage exceeds a threshold.',
|
desc: 'Triggers when CPU usage exceeds a threshold.',
|
||||||
},
|
},
|
||||||
Memory: {
|
Memory: {
|
||||||
name: 'Memory usage',
|
name: 'memory usage',
|
||||||
unit: '%',
|
unit: '%',
|
||||||
icon: MemoryStickIcon,
|
icon: MemoryStickIcon,
|
||||||
desc: 'Triggers when memory usage exceeds a threshold.',
|
desc: 'Triggers when memory usage exceeds a threshold.',
|
||||||
},
|
},
|
||||||
Disk: {
|
Disk: {
|
||||||
name: 'Disk usage',
|
name: 'disk usage',
|
||||||
unit: '%',
|
unit: '%',
|
||||||
icon: HardDriveIcon,
|
icon: HardDriveIcon,
|
||||||
desc: 'Triggers when usage of any disk exceeds a threshold.',
|
desc: 'Triggers when usage of any disk exceeds a threshold.',
|
||||||
},
|
},
|
||||||
Bandwidth: {
|
Bandwidth: {
|
||||||
name: 'Bandwidth',
|
name: 'bandwidth',
|
||||||
unit: ' MB/s',
|
unit: ' MB/s',
|
||||||
icon: EthernetIcon,
|
icon: EthernetIcon,
|
||||||
desc: 'Triggers when combined up/down exceeds a threshold.',
|
desc: 'Triggers when combined up/down exceeds a threshold.',
|
||||||
},
|
},
|
||||||
Temperature: {
|
Temperature: {
|
||||||
name: 'Temperature',
|
name: 'temperature',
|
||||||
unit: '°C',
|
unit: '°C',
|
||||||
icon: ThermometerIcon,
|
icon: ThermometerIcon,
|
||||||
desc: 'Triggers when any sensor exceeds a threshold.',
|
desc: 'Triggers when any sensor exceeds a threshold.',
|
||||||
|
|||||||
@@ -73,29 +73,27 @@ const App = () => {
|
|||||||
updateUserSettings()
|
updateUserSettings()
|
||||||
// get alerts after system list is loaded
|
// get alerts after system list is loaded
|
||||||
updateSystemList().then(updateAlerts)
|
updateSystemList().then(updateAlerts)
|
||||||
|
|
||||||
|
return () => updateFavicon('favicon.svg')
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// update favicon
|
// update favicon
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authenticated || !systems.length) {
|
if (!systems.length || !authenticated) {
|
||||||
updateFavicon('favicon.svg')
|
updateFavicon('favicon.svg')
|
||||||
} else {
|
} else {
|
||||||
let up = false
|
let up = false
|
||||||
for (const system of systems) {
|
for (const system of systems) {
|
||||||
if (system.status === 'down') {
|
if (system.status === 'down') {
|
||||||
updateFavicon('favicon-red.svg')
|
updateFavicon('favicon-red.svg')
|
||||||
return () => updateFavicon('favicon.svg')
|
return
|
||||||
} else if (system.status === 'up') {
|
} else if (system.status === 'up') {
|
||||||
up = true
|
up = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateFavicon(up ? 'favicon-green.svg' : 'favicon.svg')
|
updateFavicon(up ? 'favicon-green.svg' : 'favicon.svg')
|
||||||
return () => updateFavicon('favicon.svg')
|
|
||||||
}
|
}
|
||||||
return () => {
|
}, [systems])
|
||||||
updateFavicon('favicon.svg')
|
|
||||||
}
|
|
||||||
}, [authenticated, systems])
|
|
||||||
|
|
||||||
if (!page) {
|
if (!page) {
|
||||||
return <h1 className="text-3xl text-center my-14">404</h1>
|
return <h1 className="text-3xl text-center my-14">404</h1>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package beszel
|
package beszel
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Version = "0.6.0"
|
Version = "0.6.2"
|
||||||
AppName = "beszel"
|
AppName = "beszel"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user