mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-25 15:06:17 +01:00
Compare commits
359 Commits
v0.9.0
...
bce37e70a8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bce37e70a8 | ||
|
|
bb48305573 | ||
|
|
44d76502cd | ||
|
|
a6ca5dd6db | ||
|
|
b99330bc5a | ||
|
|
99f4313551 | ||
|
|
b574e6f43a | ||
|
|
40da67605e | ||
|
|
b4732202a2 | ||
|
|
2f50943ee4 | ||
|
|
0f7e936082 | ||
|
|
343e277f6a | ||
|
|
2edaff5a06 | ||
|
|
63cd60ddc1 | ||
|
|
6f1bd5f7c3 | ||
|
|
6d6a0c9d6d | ||
|
|
3533b57147 | ||
|
|
667bef63f0 | ||
|
|
fe60651a80 | ||
|
|
7a52e4b4b0 | ||
|
|
922e2b31e7 | ||
|
|
0aebedfe33 | ||
|
|
ce497dcb26 | ||
|
|
2bd08df641 | ||
|
|
c7c2f566c3 | ||
|
|
1ca2b71460 | ||
|
|
422895204b | ||
|
|
8a796bab7d | ||
|
|
03ed830a02 | ||
|
|
025a80dedf | ||
|
|
e68a74429e | ||
|
|
8eac0f1deb | ||
|
|
9e3a429fa9 | ||
|
|
bb35175808 | ||
|
|
9d95dd818a | ||
|
|
afd7385a4b | ||
|
|
c0737f8696 | ||
|
|
039639d5db | ||
|
|
ab7a18a79c | ||
|
|
6726702a9f | ||
|
|
7cdee9cb5e | ||
|
|
c583214578 | ||
|
|
9fb023d54b | ||
|
|
492485b347 | ||
|
|
c9d4115268 | ||
|
|
7c814f24cf | ||
|
|
30d89ad537 | ||
|
|
5e02b76f93 | ||
|
|
f82c4d7bdf | ||
|
|
04f4221ab1 | ||
|
|
6c231eb02f | ||
|
|
017365bb55 | ||
|
|
824149927e | ||
|
|
4c8f2452a8 | ||
|
|
196cbd7f78 | ||
|
|
3c9c5e7938 | ||
|
|
67a35ec7ea | ||
|
|
3a3516d05e | ||
|
|
cf22a44e03 | ||
|
|
fe3c40d528 | ||
|
|
efa74665db | ||
|
|
49d94321fd | ||
|
|
250aa61322 | ||
|
|
39cf17d24b | ||
|
|
a2ea3d104e | ||
|
|
ab0f5c765f | ||
|
|
c6a4903b51 | ||
|
|
2e14315e3b | ||
|
|
da3543017d | ||
|
|
e2b9144cef | ||
|
|
d897325933 | ||
|
|
3382ff89e7 | ||
|
|
d07dd668e6 | ||
|
|
652c6f3303 | ||
|
|
05134cb354 | ||
|
|
397fdfd68b | ||
|
|
278e066c6f | ||
|
|
6573a599f6 | ||
|
|
059575dc83 | ||
|
|
40f55f6959 | ||
|
|
e586aeb17a | ||
|
|
dc3f2fdb51 | ||
|
|
a8bcb2022b | ||
|
|
9db2daa640 | ||
|
|
0364c59ae2 | ||
|
|
741c83d476 | ||
|
|
f061352ddc | ||
|
|
fa77ddfffe | ||
|
|
27f4a648d1 | ||
|
|
d0ff1ccc16 | ||
|
|
8edb928e50 | ||
|
|
97e31dd5a9 | ||
|
|
181bdab303 | ||
|
|
30756bb079 | ||
|
|
a0540821db | ||
|
|
1779a056c3 | ||
|
|
258c70f2d8 | ||
|
|
ea50c711b2 | ||
|
|
b30d957b38 | ||
|
|
e6abfc6a5d | ||
|
|
7d356d6edb | ||
|
|
30766df9fa | ||
|
|
eaf992f3f1 | ||
|
|
27b81aa489 | ||
|
|
5a903f472e | ||
|
|
4985474b0f | ||
|
|
0632db3c19 | ||
|
|
6980a4ef86 | ||
|
|
ee22bf8bcb | ||
|
|
ae4c402b99 | ||
|
|
7281094dc3 | ||
|
|
699e81eb1c | ||
|
|
a6065b6a19 | ||
|
|
b278c92e54 | ||
|
|
83d41ed1f9 | ||
|
|
7fa39a120f | ||
|
|
4b59748c0c | ||
|
|
ab2ef5bcfc | ||
|
|
2524b0d359 | ||
|
|
f84b159309 | ||
|
|
d5e5f8678a | ||
|
|
c91e0a9faa | ||
|
|
c6a6a353a5 | ||
|
|
7d1f8f5cae | ||
|
|
64a5ef4dfe | ||
|
|
030d2b6684 | ||
|
|
938e87db8b | ||
|
|
31ed898a58 | ||
|
|
1b8ad81046 | ||
|
|
ff3874491c | ||
|
|
fe81553c6a | ||
|
|
6e8b3f6265 | ||
|
|
9c87911a7f | ||
|
|
082cbb36e7 | ||
|
|
8347cf0e5c | ||
|
|
abfafae0d9 | ||
|
|
6496f35915 | ||
|
|
7fee3da2e8 | ||
|
|
443e203d9c | ||
|
|
24875b5733 | ||
|
|
66eaf45c79 | ||
|
|
2b9fb8b1ec | ||
|
|
06e0ae4733 | ||
|
|
4cc0bfeb0f | ||
|
|
320d0e5d97 | ||
|
|
67953bb6af | ||
|
|
123c57ded9 | ||
|
|
a7991ca184 | ||
|
|
1b01410533 | ||
|
|
1687919f31 | ||
|
|
6046dbb727 | ||
|
|
2505b16faa | ||
|
|
c4352e65fb | ||
|
|
7b2e9ccdcb | ||
|
|
75a87cddbf | ||
|
|
bda7b9b48d | ||
|
|
65d6ec1918 | ||
|
|
0e16fca07f | ||
|
|
b7e574f379 | ||
|
|
113f1833e1 | ||
|
|
a0d3e60d29 | ||
|
|
df7f12bfec | ||
|
|
02e88d99e5 | ||
|
|
a11ceb36d1 | ||
|
|
d3a373338d | ||
|
|
e1ef05da8c | ||
|
|
c01bfd9332 | ||
|
|
209e82aed2 | ||
|
|
c66e740d52 | ||
|
|
551ae49c2e | ||
|
|
906daa6f2a | ||
|
|
49e55943c1 | ||
|
|
9e75cbc1ef | ||
|
|
79190a2c51 | ||
|
|
32960c7c35 | ||
|
|
cdff974a8a | ||
|
|
20a6ef129c | ||
|
|
d1e5310f83 | ||
|
|
152173eab5 | ||
|
|
c2aec69638 | ||
|
|
16c97fe77b | ||
|
|
bed5bc2791 | ||
|
|
424c7aceff | ||
|
|
8eb101e714 | ||
|
|
f2e69e2a80 | ||
|
|
41b4dbce98 | ||
|
|
c3432fc7b5 | ||
|
|
b167244e28 | ||
|
|
bcca6a5a9d | ||
|
|
6b661b4878 | ||
|
|
bc537edb73 | ||
|
|
561a3e8aaf | ||
|
|
f173ea37da | ||
|
|
0d9f2ba06a | ||
|
|
4e772e6008 | ||
|
|
b959d97502 | ||
|
|
735cbe2cf0 | ||
|
|
0f5a470495 | ||
|
|
a42e99370e | ||
|
|
abfae78af1 | ||
|
|
e36c40c4a9 | ||
|
|
bf7b2ae598 | ||
|
|
f71ee4b058 | ||
|
|
f2466eb37d | ||
|
|
7244c7130b | ||
|
|
f2e84a9d3e | ||
|
|
04a1ee5e4e | ||
|
|
9b83088897 | ||
|
|
13b30aa255 | ||
|
|
2bd25e9e8d | ||
|
|
4fe192bf28 | ||
|
|
2056ae285f | ||
|
|
a02e7a0a69 | ||
|
|
b1a9e90034 | ||
|
|
20916fab3e | ||
|
|
0c777cca72 | ||
|
|
44e30ad429 | ||
|
|
9e30786dda | ||
|
|
1f677773e7 | ||
|
|
882289da91 | ||
|
|
fbbc4eff27 | ||
|
|
b78231f677 | ||
|
|
332e2d14a9 | ||
|
|
e088b88c84 | ||
|
|
09840f95d9 | ||
|
|
de03bb658a | ||
|
|
bcadfeb729 | ||
|
|
ec582a9171 | ||
|
|
0c3eaefc90 | ||
|
|
d6b5866f90 | ||
|
|
2ba629e4b4 | ||
|
|
6e9341b7ff | ||
|
|
89499a341b | ||
|
|
9c88845798 | ||
|
|
c9f65f63e6 | ||
|
|
996620c8e0 | ||
|
|
626c1358d9 | ||
|
|
19dd39b7db | ||
|
|
d482b50e31 | ||
|
|
f61ec49c76 | ||
|
|
2b43ba3cbe | ||
|
|
b2b1a0b6ea | ||
|
|
b11d0aae61 | ||
|
|
2b73d8845a | ||
|
|
41e3e3d760 | ||
|
|
c22b57ce67 | ||
|
|
23bee0aa7c | ||
|
|
0c2629f57e | ||
|
|
a4b689e8f1 | ||
|
|
0c5841133b | ||
|
|
78a645fa05 | ||
|
|
f1208a9f00 | ||
|
|
aeb5f1424b | ||
|
|
4a1fb513c5 | ||
|
|
e5a66cc156 | ||
|
|
aef3b3e610 | ||
|
|
1d13ecd8ec | ||
|
|
6404895a47 | ||
|
|
ba7db28e80 | ||
|
|
6b41a98338 | ||
|
|
b958e9eefe | ||
|
|
baf56fe83b | ||
|
|
96f9128d1a | ||
|
|
1fb60e05d7 | ||
|
|
a9a9a932a6 | ||
|
|
e0a1a49a8f | ||
|
|
79eb42d04d | ||
|
|
25b70af196 | ||
|
|
c12b27afb5 | ||
|
|
7485f79071 | ||
|
|
d170e7a00d | ||
|
|
1a6a2a64f2 | ||
|
|
646b899851 | ||
|
|
821e2e3a78 | ||
|
|
9be3fcb8ca | ||
|
|
f271b5a56c | ||
|
|
4f80a58929 | ||
|
|
2ab2cc83de | ||
|
|
3376a97bea | ||
|
|
0c54f95546 | ||
|
|
5ea6eb08a1 | ||
|
|
6b2a9463ca | ||
|
|
a94cfff965 | ||
|
|
1f69937572 | ||
|
|
aa3de511b9 | ||
|
|
3afab00937 | ||
|
|
e6054058b9 | ||
|
|
31d52d5e15 | ||
|
|
44d930a700 | ||
|
|
d7ada1b1c5 | ||
|
|
1daf35406a | ||
|
|
2216e40f04 | ||
|
|
f4480c7aa7 | ||
|
|
5b478c11eb | ||
|
|
58085bf300 | ||
|
|
ce171cf375 | ||
|
|
e689f547ef | ||
|
|
5a8e8c1512 | ||
|
|
ff5eb07716 | ||
|
|
fdbbbc77b0 | ||
|
|
20cba1b695 | ||
|
|
207d58a07e | ||
|
|
0759a3607c | ||
|
|
0b4742d064 | ||
|
|
4557f18195 | ||
|
|
83668e5727 | ||
|
|
120aff0d18 | ||
|
|
7170b24160 | ||
|
|
3441b39a02 | ||
|
|
31d306f8be | ||
|
|
76347f25e5 | ||
|
|
c157f38957 | ||
|
|
d185dfdef8 | ||
|
|
319a9895b0 | ||
|
|
68dae3967d | ||
|
|
0a331524cc | ||
|
|
5b625db57c | ||
|
|
0943e01b71 | ||
|
|
cfda7d0740 | ||
|
|
4e3d198b7b | ||
|
|
a6a9719565 | ||
|
|
ddb4f1c8f8 | ||
|
|
55d13c551a | ||
|
|
fef30b1750 | ||
|
|
0cee9e4e4b | ||
|
|
90378d09a3 | ||
|
|
7adf7ef549 | ||
|
|
ee6a456b66 | ||
|
|
4789f48ad0 | ||
|
|
ea098fd61c | ||
|
|
e7c214799a | ||
|
|
9cabc103e5 | ||
|
|
be955e0122 | ||
|
|
8a0f2d61a8 | ||
|
|
48ed4abc02 | ||
|
|
e8c680bda7 | ||
|
|
37c7a32c10 | ||
|
|
1c4533f1f2 | ||
|
|
5d88599c9a | ||
|
|
73427306d1 | ||
|
|
f24a7313d6 | ||
|
|
ad55d1ca88 | ||
|
|
9c669d8833 | ||
|
|
81fa4f16d6 | ||
|
|
40cc1a875e | ||
|
|
1ac165d7d3 | ||
|
|
9619e6cf89 | ||
|
|
fc31cefd4c | ||
|
|
5fd9010b39 | ||
|
|
c2e3dd5ab1 | ||
|
|
8e531e6b3c | ||
|
|
527e6b57d5 | ||
|
|
245fa538e9 | ||
|
|
e14a851398 | ||
|
|
0c9bc47a3a | ||
|
|
19b4477a75 | ||
|
|
558d051c42 | ||
|
|
9c8528bae1 | ||
|
|
229ef19376 |
61
.github/DISCUSSION_TEMPLATE/support.yml
vendored
Normal file
61
.github/DISCUSSION_TEMPLATE/support.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Before opening a discussion:
|
||||
|
||||
- Check the [common issues guide](https://beszel.dev/guide/common-issues).
|
||||
- Search existing [issues](https://github.com/henrygd/beszel/issues) and [discussions](https://github.com/henrygd/beszel/discussions) (including closed).
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: A clear and concise description of the issue or question. If applicable, add screenshots to help explain your problem.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: system
|
||||
attributes:
|
||||
label: OS / Architecture
|
||||
placeholder: linux/amd64 (agent), freebsd/arm64 (hub)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Beszel version
|
||||
placeholder: 0.9.1
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: install-method
|
||||
attributes:
|
||||
label: Installation method
|
||||
options:
|
||||
- Docker
|
||||
- Binary
|
||||
- Nix
|
||||
- Unraid
|
||||
- Coolify
|
||||
- Other (please describe above)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: config
|
||||
attributes:
|
||||
label: Configuration
|
||||
description: Please provide any relevant service configuration
|
||||
render: yaml
|
||||
- type: textarea
|
||||
id: hub-logs
|
||||
attributes:
|
||||
label: Hub Logs
|
||||
description: Check the logs page in PocketBase (`/_/#/logs`) for relevant errors (copy JSON).
|
||||
render: json
|
||||
- type: textarea
|
||||
id: agent-logs
|
||||
attributes:
|
||||
label: Agent Logs
|
||||
description: Please provide any logs from the agent, if relevant. Use `LOG_LEVEL=debug` for more info.
|
||||
render: shell
|
||||
91
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
91
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
name: 🐛 Bug report
|
||||
description: Report a new bug or issue.
|
||||
title: '[Bug]: '
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Thanks for taking the time to fill out this bug report!
|
||||
|
||||
- For more general support, please [start a support thread](https://github.com/henrygd/beszel/discussions/new?category=support).
|
||||
- To request a change or feature, use the [feature request form](https://github.com/henrygd/beszel/issues/new?template=feature_request.yml).
|
||||
- Please do not submit bugs that are specific to ZFS. We plan to add integration with ZFS utilities in the near future.
|
||||
|
||||
### Before submitting a bug report:
|
||||
|
||||
- Check the [common issues guide](https://beszel.dev/guide/common-issues).
|
||||
- Search existing [issues](https://github.com/henrygd/beszel/issues) and [discussions](https://github.com/henrygd/beszel/discussions) (including closed).
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Explain the issue you experienced clearly and concisely.
|
||||
placeholder: I went to the coffee pot and it was empty.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: In a perfect world, what should have happened?
|
||||
placeholder: When I got to the coffee pot, it should have been full.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps-to-reproduce
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Describe how to reproduce the issue in repeatable steps.
|
||||
placeholder: |
|
||||
1. Go to the coffee pot.
|
||||
2. Make more coffee.
|
||||
3. Pour it into a cup.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: system
|
||||
attributes:
|
||||
label: OS / Architecture
|
||||
placeholder: linux/amd64 (agent), freebsd/arm64 (hub)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Beszel version
|
||||
placeholder: 0.9.1
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: install-method
|
||||
attributes:
|
||||
label: Installation method
|
||||
default: 0
|
||||
options:
|
||||
- Docker
|
||||
- Binary
|
||||
- Nix
|
||||
- Unraid
|
||||
- Coolify
|
||||
- Other (please describe above)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: config
|
||||
attributes:
|
||||
label: Configuration
|
||||
description: Please provide any relevant service configuration
|
||||
render: yaml
|
||||
- type: textarea
|
||||
id: hub-logs
|
||||
attributes:
|
||||
label: Hub Logs
|
||||
description: Check the logs page in PocketBase (`/_/#/logs`) for relevant errors (copy JSON).
|
||||
render: json
|
||||
- type: textarea
|
||||
id: agent-logs
|
||||
attributes:
|
||||
label: Agent Logs
|
||||
description: Please provide any logs from the agent, if relevant. Use `LOG_LEVEL=debug` for more info.
|
||||
render: shell
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 💬 Support and questions
|
||||
url: https://github.com/henrygd/beszel/discussions
|
||||
about: Ask and answer questions here.
|
||||
- name: ℹ️ View the Common Issues page
|
||||
url: https://beszel.dev/guide/common-issues
|
||||
about: Find information about commonly encountered problems.
|
||||
18
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
18
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: 🚀 Feature request
|
||||
description: Request a new feature or change.
|
||||
title: "[Feature]: "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Before submitting, please search existing [issues](https://github.com/henrygd/beszel/issues) and [discussions](https://github.com/henrygd/beszel/discussions) (including closed).
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the feature you would like to see
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe how you would like to see this feature implemented
|
||||
validations:
|
||||
required: true
|
||||
1
.github/funding.yml
vendored
Normal file
1
.github/funding.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
buy_me_a_coffee: henrygd
|
||||
23
.github/workflows/docker-images.yml
vendored
23
.github/workflows/docker-images.yml
vendored
@@ -15,9 +15,27 @@ jobs:
|
||||
- image: henrygd/beszel
|
||||
context: ./beszel
|
||||
dockerfile: ./beszel/dockerfile_Hub
|
||||
registry: docker.io
|
||||
username_secret: DOCKERHUB_USERNAME
|
||||
password_secret: DOCKERHUB_TOKEN
|
||||
- image: henrygd/beszel-agent
|
||||
context: ./beszel
|
||||
dockerfile: ./beszel/dockerfile_Agent
|
||||
registry: docker.io
|
||||
username_secret: DOCKERHUB_USERNAME
|
||||
password_secret: DOCKERHUB_TOKEN
|
||||
- image: ghcr.io/${{ github.repository }}/beszel
|
||||
context: ./beszel
|
||||
dockerfile: ./beszel/dockerfile_Hub
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password_secret: GITHUB_TOKEN
|
||||
- image: ghcr.io/${{ github.repository }}/beszel-agent
|
||||
context: ./beszel
|
||||
dockerfile: ./beszel/dockerfile_Agent
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password_secret: GITHUB_TOKEN
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -57,8 +75,9 @@ jobs:
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
username: ${{ matrix.username || secrets[matrix.username_secret] }}
|
||||
password: ${{ secrets[matrix.password_secret] }}
|
||||
registry: ${{ matrix.registry }}
|
||||
|
||||
# Build and push Docker image with Buildx (don't push on PR)
|
||||
# https://github.com/docker/build-push-action
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -14,4 +14,5 @@ node_modules
|
||||
beszel/build
|
||||
*timestamp*
|
||||
.swc
|
||||
beszel/site/src/locales/**/*.ts
|
||||
beszel/site/src/locales/**/*.ts
|
||||
*.bak
|
||||
@@ -29,14 +29,22 @@ builds:
|
||||
- linux
|
||||
- darwin
|
||||
- freebsd
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
- arm
|
||||
- mips64
|
||||
- riscv64
|
||||
ignore:
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: darwin
|
||||
goarch: riscv64
|
||||
- goos: windows
|
||||
goarch: riscv64
|
||||
|
||||
archives:
|
||||
- id: beszel
|
||||
@@ -47,6 +55,10 @@ archives:
|
||||
{{ .Binary }}_
|
||||
{{- .Os }}_
|
||||
{{- .Arch }}
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
|
||||
- id: beszel-agent
|
||||
format: tar.gz
|
||||
builds:
|
||||
@@ -55,10 +67,52 @@ archives:
|
||||
{{ .Binary }}_
|
||||
{{- .Os }}_
|
||||
{{- .Arch }}
|
||||
# use zip for windows archives
|
||||
# format_overrides:
|
||||
# - goos: windows
|
||||
# format: zip
|
||||
|
||||
nfpms:
|
||||
- id: beszel-agent
|
||||
package_name: beszel-agent
|
||||
description: |-
|
||||
Agent for Beszel
|
||||
Beszel is a lightweight server monitoring platform that includes Docker
|
||||
statistics, historical data, and alert functions. It has a friendly web
|
||||
interface, simple configuration, and is ready to use out of the box.
|
||||
It supports automatic backup, multi-user, OAuth authentication, and
|
||||
API access.
|
||||
maintainer: henrygd <hank@henrygd.me>
|
||||
section: net
|
||||
builds:
|
||||
- beszel-agent
|
||||
formats:
|
||||
- deb
|
||||
# don't think this is needed with CGO_ENABLED=0
|
||||
# dependencies:
|
||||
# - libc6
|
||||
contents:
|
||||
- src: ../supplemental/debian/beszel-agent.service
|
||||
dst: lib/systemd/system/beszel-agent.service
|
||||
packager: deb
|
||||
- src: ../supplemental/debian/copyright
|
||||
dst: usr/share/doc/beszel-agent/copyright
|
||||
packager: deb
|
||||
- src: ../supplemental/debian/lintian-overrides
|
||||
dst: usr/share/lintian/overrides/beszel-agent
|
||||
packager: deb
|
||||
scripts:
|
||||
postinstall: ../supplemental/debian/postinstall.sh
|
||||
preremove: ../supplemental/debian/prerm.sh
|
||||
postremove: ../supplemental/debian/postrm.sh
|
||||
deb:
|
||||
predepends:
|
||||
- adduser
|
||||
- debconf
|
||||
scripts:
|
||||
templates: ../supplemental/debian/templates
|
||||
# Currently broken due to a bug in goreleaser
|
||||
# https://github.com/goreleaser/goreleaser/issues/5487
|
||||
#config: ../supplemental/debian/config.sh
|
||||
|
||||
release:
|
||||
draft: true
|
||||
|
||||
changelog:
|
||||
disable: true
|
||||
|
||||
@@ -27,10 +27,10 @@ build-web-ui:
|
||||
fi
|
||||
|
||||
build-agent: tidy
|
||||
CGO_ENABLED=0 GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel-agent_$(OS)_$(ARCH) -ldflags "-w -s" beszel/cmd/agent
|
||||
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel-agent_$(OS)_$(ARCH) -ldflags "-w -s" beszel/cmd/agent
|
||||
|
||||
build-hub: tidy $(if $(filter false,$(SKIP_WEB)),build-web-ui)
|
||||
CGO_ENABLED=0 GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel_$(OS)_$(ARCH) -ldflags "-w -s" beszel/cmd/hub
|
||||
GOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel_$(OS)_$(ARCH) -ldflags "-w -s" beszel/cmd/hub
|
||||
|
||||
build: build-agent build-hub
|
||||
|
||||
@@ -48,6 +48,7 @@ dev-server: generate-locales
|
||||
cd ./site && npm run dev; \
|
||||
fi
|
||||
|
||||
dev-hub: export ENV=dev
|
||||
dev-hub:
|
||||
mkdir -p ./site/dist && touch ./site/dist/index.html
|
||||
@if command -v entr >/dev/null 2>&1; then \
|
||||
|
||||
@@ -3,39 +3,134 @@ package main
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/agent"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// handle flags / subcommands
|
||||
if len(os.Args) > 1 {
|
||||
switch os.Args[1] {
|
||||
case "-v":
|
||||
fmt.Println(beszel.AppName+"-agent", beszel.Version)
|
||||
case "update":
|
||||
agent.Update()
|
||||
}
|
||||
// cli options
|
||||
type cmdOptions struct {
|
||||
key string // key is the public key(s) for SSH authentication.
|
||||
addr string // addr is the address or port to listen on.
|
||||
}
|
||||
|
||||
// parseFlags parses the command line flags and populates the config struct.
|
||||
func (opts *cmdOptions) parseFlags() {
|
||||
flag.StringVar(&opts.key, "key", "", "Public key(s) for SSH authentication")
|
||||
flag.StringVar(&opts.addr, "addr", "", "Address or port to listen on")
|
||||
|
||||
flag.Usage = func() {
|
||||
fmt.Printf("Usage: %s [options] [subcommand]\n", os.Args[0])
|
||||
fmt.Println("\nOptions:")
|
||||
flag.PrintDefaults()
|
||||
fmt.Println("\nSubcommands:")
|
||||
fmt.Println(" version Display the version")
|
||||
fmt.Println(" help Display this help message")
|
||||
fmt.Println(" update Update the agent to the latest version")
|
||||
}
|
||||
}
|
||||
|
||||
// handleSubcommand handles subcommands such as version, help, and update.
|
||||
// It returns true if a subcommand was handled, false otherwise.
|
||||
func handleSubcommand() bool {
|
||||
if len(os.Args) <= 1 {
|
||||
return false
|
||||
}
|
||||
switch os.Args[1] {
|
||||
case "version", "-v":
|
||||
fmt.Println(beszel.AppName+"-agent", beszel.Version)
|
||||
os.Exit(0)
|
||||
case "help":
|
||||
flag.Usage()
|
||||
os.Exit(0)
|
||||
case "update":
|
||||
agent.Update()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
var pubKey []byte
|
||||
if pubKeyEnv, exists := os.LookupEnv("KEY"); exists {
|
||||
pubKey = []byte(pubKeyEnv)
|
||||
} else {
|
||||
log.Fatal("KEY environment variable is not set")
|
||||
}
|
||||
|
||||
addr := ":45876"
|
||||
if portEnvVar, exists := os.LookupEnv("PORT"); exists {
|
||||
// allow passing an address in the form of "127.0.0.1:45876"
|
||||
if !strings.Contains(portEnvVar, ":") {
|
||||
portEnvVar = ":" + portEnvVar
|
||||
}
|
||||
addr = portEnvVar
|
||||
}
|
||||
|
||||
agent.NewAgent().Run(pubKey, addr)
|
||||
return false
|
||||
}
|
||||
|
||||
// loadPublicKeys loads the public keys from the command line flag, environment variable, or key file.
|
||||
func (opts *cmdOptions) loadPublicKeys() ([]ssh.PublicKey, error) {
|
||||
// Try command line flag first
|
||||
if opts.key != "" {
|
||||
return agent.ParseKeys(opts.key)
|
||||
}
|
||||
|
||||
// Try environment variable
|
||||
if key, ok := agent.GetEnv("KEY"); ok && key != "" {
|
||||
return agent.ParseKeys(key)
|
||||
}
|
||||
|
||||
// Try key file
|
||||
keyFile, ok := agent.GetEnv("KEY_FILE")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no key provided: must set -key flag, KEY env var, or KEY_FILE env var. Use 'beszel-agent help' for usage")
|
||||
}
|
||||
|
||||
pubKey, err := os.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read key file: %w", err)
|
||||
}
|
||||
return agent.ParseKeys(string(pubKey))
|
||||
}
|
||||
|
||||
// getAddress gets the address to listen on from the command line flag, environment variable, or default value.
|
||||
func (opts *cmdOptions) getAddress() string {
|
||||
// Try command line flag first
|
||||
if opts.addr != "" {
|
||||
return opts.addr
|
||||
}
|
||||
// Try environment variables
|
||||
if addr, ok := agent.GetEnv("ADDR"); ok && addr != "" {
|
||||
return addr
|
||||
}
|
||||
// Legacy PORT environment variable support
|
||||
if port, ok := agent.GetEnv("PORT"); ok && port != "" {
|
||||
return port
|
||||
}
|
||||
return ":45876"
|
||||
}
|
||||
|
||||
// getNetwork returns the network type to use for the server.
|
||||
func (opts *cmdOptions) getNetwork() string {
|
||||
if network, _ := agent.GetEnv("NETWORK"); network != "" {
|
||||
return network
|
||||
}
|
||||
if strings.HasPrefix(opts.addr, "/") {
|
||||
return "unix"
|
||||
}
|
||||
return "tcp"
|
||||
}
|
||||
|
||||
func main() {
|
||||
var opts cmdOptions
|
||||
opts.parseFlags()
|
||||
|
||||
if handleSubcommand() {
|
||||
return
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
|
||||
opts.addr = opts.getAddress()
|
||||
|
||||
var serverConfig agent.ServerOptions
|
||||
var err error
|
||||
serverConfig.Keys, err = opts.loadPublicKeys()
|
||||
if err != nil {
|
||||
log.Fatal("Failed to load public keys:", err)
|
||||
}
|
||||
|
||||
serverConfig.Addr = opts.addr
|
||||
serverConfig.Network = opts.getNetwork()
|
||||
|
||||
agent := agent.NewAgent()
|
||||
if err := agent.StartServer(serverConfig); err != nil {
|
||||
log.Fatal("Failed to start server:", err)
|
||||
}
|
||||
}
|
||||
|
||||
302
beszel/cmd/agent/agent_test.go
Normal file
302
beszel/cmd/agent/agent_test.go
Normal file
@@ -0,0 +1,302 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"flag"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func TestGetAddress(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts cmdOptions
|
||||
envVars map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "default port when no config",
|
||||
opts: cmdOptions{},
|
||||
expected: ":45876",
|
||||
},
|
||||
{
|
||||
name: "use address from flag",
|
||||
opts: cmdOptions{
|
||||
addr: "8080",
|
||||
},
|
||||
expected: "8080",
|
||||
},
|
||||
{
|
||||
name: "use unix socket from flag",
|
||||
opts: cmdOptions{
|
||||
addr: "/tmp/beszel.sock",
|
||||
},
|
||||
expected: "/tmp/beszel.sock",
|
||||
},
|
||||
{
|
||||
name: "use ADDR env var",
|
||||
opts: cmdOptions{},
|
||||
envVars: map[string]string{
|
||||
"ADDR": "1.2.3.4:9090",
|
||||
},
|
||||
expected: "1.2.3.4:9090",
|
||||
},
|
||||
{
|
||||
name: "use legacy PORT env var",
|
||||
opts: cmdOptions{},
|
||||
envVars: map[string]string{
|
||||
"PORT": "7070",
|
||||
},
|
||||
expected: "7070",
|
||||
},
|
||||
{
|
||||
name: "use unix socket from env var",
|
||||
opts: cmdOptions{
|
||||
addr: "",
|
||||
},
|
||||
envVars: map[string]string{
|
||||
"ADDR": "/tmp/beszel.sock",
|
||||
},
|
||||
expected: "/tmp/beszel.sock",
|
||||
},
|
||||
{
|
||||
name: "flag takes precedence over env vars",
|
||||
opts: cmdOptions{
|
||||
addr: ":8080",
|
||||
},
|
||||
envVars: map[string]string{
|
||||
"ADDR": ":9090",
|
||||
"PORT": "7070",
|
||||
},
|
||||
expected: ":8080",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Setup environment
|
||||
for k, v := range tt.envVars {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
|
||||
addr := tt.opts.getAddress()
|
||||
assert.Equal(t, tt.expected, addr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPublicKeys(t *testing.T) {
|
||||
// Generate a test key
|
||||
_, priv, err := ed25519.GenerateKey(nil)
|
||||
require.NoError(t, err)
|
||||
signer, err := ssh.NewSignerFromKey(priv)
|
||||
require.NoError(t, err)
|
||||
pubKey := ssh.MarshalAuthorizedKey(signer.PublicKey())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
opts cmdOptions
|
||||
envVars map[string]string
|
||||
setupFiles map[string][]byte
|
||||
wantErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "load key from flag",
|
||||
opts: cmdOptions{
|
||||
key: string(pubKey),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "load key from env var",
|
||||
envVars: map[string]string{
|
||||
"KEY": string(pubKey),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "load key from file",
|
||||
envVars: map[string]string{
|
||||
"KEY_FILE": "testkey.pub",
|
||||
},
|
||||
setupFiles: map[string][]byte{
|
||||
"testkey.pub": pubKey,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error when no key provided",
|
||||
wantErr: true,
|
||||
errContains: "no key provided",
|
||||
},
|
||||
{
|
||||
name: "error on invalid key file",
|
||||
envVars: map[string]string{
|
||||
"KEY_FILE": "nonexistent.pub",
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "failed to read key file",
|
||||
},
|
||||
{
|
||||
name: "error on invalid key data",
|
||||
opts: cmdOptions{
|
||||
key: "invalid-key-data",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create a temporary directory for test files
|
||||
if len(tt.setupFiles) > 0 {
|
||||
tmpDir := t.TempDir()
|
||||
for name, content := range tt.setupFiles {
|
||||
path := filepath.Join(tmpDir, name)
|
||||
err := os.WriteFile(path, content, 0600)
|
||||
require.NoError(t, err)
|
||||
if tt.envVars != nil {
|
||||
tt.envVars["KEY_FILE"] = path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set up environment
|
||||
for k, v := range tt.envVars {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
|
||||
keys, err := tt.opts.loadPublicKeys()
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
if tt.errContains != "" {
|
||||
assert.Contains(t, err.Error(), tt.errContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, keys, 1)
|
||||
assert.Equal(t, signer.PublicKey().Type(), keys[0].Type())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNetwork(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts cmdOptions
|
||||
envVars map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "NETWORK env var",
|
||||
envVars: map[string]string{
|
||||
"NETWORK": "tcp4",
|
||||
},
|
||||
expected: "tcp4",
|
||||
},
|
||||
{
|
||||
name: "only port",
|
||||
opts: cmdOptions{addr: "8080"},
|
||||
expected: "tcp",
|
||||
},
|
||||
{
|
||||
name: "ipv4 address",
|
||||
opts: cmdOptions{addr: "1.2.3.4:8080"},
|
||||
expected: "tcp",
|
||||
},
|
||||
{
|
||||
name: "ipv6 address",
|
||||
opts: cmdOptions{addr: "[2001:db8::1]:8080"},
|
||||
expected: "tcp",
|
||||
},
|
||||
{
|
||||
name: "unix network",
|
||||
opts: cmdOptions{addr: "/tmp/beszel.sock"},
|
||||
expected: "unix",
|
||||
},
|
||||
{
|
||||
name: "env var network",
|
||||
opts: cmdOptions{addr: ":8080"},
|
||||
envVars: map[string]string{"NETWORK": "tcp4"},
|
||||
expected: "tcp4",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Setup environment
|
||||
for k, v := range tt.envVars {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
network := tt.opts.getNetwork()
|
||||
assert.Equal(t, tt.expected, network)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFlags(t *testing.T) {
|
||||
// Save original command line arguments and restore after test
|
||||
oldArgs := os.Args
|
||||
defer func() {
|
||||
os.Args = oldArgs
|
||||
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
expected cmdOptions
|
||||
}{
|
||||
{
|
||||
name: "no flags",
|
||||
args: []string{"cmd"},
|
||||
expected: cmdOptions{
|
||||
key: "",
|
||||
addr: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "key flag only",
|
||||
args: []string{"cmd", "-key", "testkey"},
|
||||
expected: cmdOptions{
|
||||
key: "testkey",
|
||||
addr: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addr flag only",
|
||||
args: []string{"cmd", "-addr", ":8080"},
|
||||
expected: cmdOptions{
|
||||
key: "",
|
||||
addr: ":8080",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "both flags",
|
||||
args: []string{"cmd", "-key", "testkey", "-addr", ":8080"},
|
||||
expected: cmdOptions{
|
||||
key: "testkey",
|
||||
addr: ":8080",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Reset flags for each test
|
||||
flag.CommandLine = flag.NewFlagSet(tt.args[0], flag.ExitOnError)
|
||||
os.Args = tt.args
|
||||
|
||||
var opts cmdOptions
|
||||
opts.parseFlags()
|
||||
flag.Parse()
|
||||
|
||||
assert.Equal(t, tt.expected, opts)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"beszel"
|
||||
"beszel/internal/hub"
|
||||
|
||||
_ "beszel/migrations"
|
||||
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := pocketbase.NewWithConfig(pocketbase.Config{
|
||||
DefaultDataDir: beszel.AppName + "_data",
|
||||
})
|
||||
app.RootCmd.Version = beszel.Version
|
||||
app.RootCmd.Use = beszel.AppName
|
||||
app.RootCmd.Short = ""
|
||||
|
||||
// add update command
|
||||
app.RootCmd.AddCommand(&cobra.Command{
|
||||
Use: "update",
|
||||
Short: "Update " + beszel.AppName + " to the latest version",
|
||||
Run: hub.Update,
|
||||
})
|
||||
|
||||
hub.NewHub(app).Run()
|
||||
hub.NewHub().Run()
|
||||
}
|
||||
|
||||
104
beszel/go.mod
104
beszel/go.mod
@@ -1,77 +1,72 @@
|
||||
module beszel
|
||||
|
||||
go 1.23
|
||||
|
||||
toolchain go1.23.2
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/blang/semver v3.5.1+incompatible
|
||||
github.com/containrrr/shoutrrr v0.8.0
|
||||
github.com/gliderlabs/ssh v0.3.8
|
||||
github.com/goccy/go-json v0.10.4
|
||||
github.com/goccy/go-json v0.10.5
|
||||
github.com/pocketbase/dbx v1.11.0
|
||||
github.com/pocketbase/pocketbase v0.23.9
|
||||
github.com/pocketbase/pocketbase v0.25.0
|
||||
github.com/rhysd/go-github-selfupdate v1.2.3
|
||||
github.com/shirou/gopsutil/v4 v4.24.11
|
||||
github.com/spf13/cast v1.7.0
|
||||
github.com/shirou/gopsutil/v4 v4.25.1
|
||||
github.com/spf13/cast v1.7.1
|
||||
github.com/spf13/cobra v1.8.1
|
||||
golang.org/x/crypto v0.31.0
|
||||
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/crypto v0.32.0
|
||||
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.32.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.28.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.47 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.43 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 // 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.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.71.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.2 // indirect
|
||||
github.com/aws/smithy-go v1.22.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.59 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.28 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.59 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.75.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.14 // indirect
|
||||
github.com/aws/smithy-go v1.22.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/disintegration/imaging v1.6.2 // indirect
|
||||
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ebitengine/purego v0.8.1 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.7 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/ganigeorgiev/fexpr v0.4.1 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/go-github/v30 v30.1.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/tcnksm/go-gitconfig v0.1.2 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||
github.com/tklauser/numcpus v0.9.0 // indirect
|
||||
@@ -79,23 +74,20 @@ require (
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
gocloud.dev v0.40.0 // indirect
|
||||
golang.org/x/image v0.23.0 // indirect
|
||||
golang.org/x/net v0.32.0 // indirect
|
||||
golang.org/x/oauth2 v0.24.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/term v0.27.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/image v0.24.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/oauth2 v0.26.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/api v0.212.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 // indirect
|
||||
google.golang.org/grpc v1.69.0 // indirect
|
||||
google.golang.org/protobuf v1.36.0 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20241213165251-3bc300f6d0c9 // indirect
|
||||
modernc.org/libc v1.61.4 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/sqlite v1.34.2 // indirect
|
||||
modernc.org/strutil v1.2.0 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
google.golang.org/api v0.220.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489 // indirect
|
||||
google.golang.org/grpc v1.70.0 // indirect
|
||||
google.golang.org/protobuf v1.36.4 // indirect
|
||||
modernc.org/libc v1.55.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.8.2 // indirect
|
||||
modernc.org/sqlite v1.34.5 // indirect
|
||||
)
|
||||
|
||||
294
beszel/go.sum
294
beszel/go.sum
@@ -1,10 +1,10 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
|
||||
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
|
||||
cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs=
|
||||
cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8=
|
||||
cloud.google.com/go/auth v0.14.1 h1:AwoJbzUdxA/whv1qj3TLKwh3XX5sikny2fc40wUl+h0=
|
||||
cloud.google.com/go/auth v0.14.1/go.mod h1:4JHUxlGXisL0AW8kXPtUF6ztuOksyfUQNFjfsOCXkPM=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
|
||||
cloud.google.com/go/compute v1.14.0 h1:hfm2+FfxVmnRlh6LpB7cg1ZNU+5edAHmW679JePztk0=
|
||||
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
|
||||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||
@@ -14,11 +14,7 @@ cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyX
|
||||
cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
|
||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||
@@ -26,44 +22,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/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
|
||||
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.32.6 h1:7BokKRgRPuGmKkFMhEg/jSul+tB9VvXhcViILtfG8b4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.32.6/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.28.6 h1:D89IKtGrs/I3QXOLNTH93NJYtDhm8SYa9Q5CsPShmyo=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.28.6/go.mod h1:GDzxJ5wyyFSCoLkS+UhGB0dArhb9mI+Co4dHtoTxbko=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.47 h1:48bA+3/fCdi2yAwVt+3COvmatZ6jUDNkDTIsqDiMUdw=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.47/go.mod h1:+KdckOejLW3Ks3b0E3b5rHsr2f9yuORBum0WPnE5o5w=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21 h1:AmoU1pziydclFT/xRV+xXE/Vb8fttJCLRPv8oAkprc0=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21/go.mod h1:AjUdLYe4Tgs6kpH4Bv7uMZo7pottoyHMn4eTcIcneaY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.43 h1:iLdpkYZ4cXIQMO7ud+cqMWR1xK5ESbt1rvN77tRi1BY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.43/go.mod h1:OgbsKPAswXDd5kxnR4vZov69p3oYjbvUyIRBAAV0y9o=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 h1:s/fF4+yDQDoElYhfIVvSNyeCydfbuTKzhxSXDXCPasU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25/go.mod h1:IgPfDv5jqFIzQSNbUEMoitNooSMXjRSDkhXv8jiROvU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 h1:ZntTCl5EsYnhN/IygQEUugpdwbhdkom9uHcbCftiGgA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25/go.mod h1:DBdPrgeocww+CSl1C8cEV8PN1mHMBhuCDLpXezyvWkE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.25 h1:r67ps7oHCYnflpgDy2LZU0MAQtQbYIOqNNnqGO6xQkE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.25/go.mod h1:GrGY+Q4fIokYLtjCVB/aFfCVL6hhGUFl8inD18fDalE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.6 h1:HCpPsWqmYQieU7SS6E9HXfdAMSud0pteVXieJmcpIRI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.6/go.mod h1:ngUiVRCco++u+soRRVBIvBZxSMMvOVMXA4PJ36JLfSw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6 h1:50+XsN70RS7dwJ2CkVNXzj7U2L1HKP8nqTd3XWEXBN4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6/go.mod h1:WqgLmwY7so32kG01zD8CPTJWVWM+TzJoOVHwTg4aPug=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.6 h1:BbGDtTi0T1DYlmjBiCr/le3wzhA37O8QTC5/Ab8+EXk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.6/go.mod h1:hLMJt7Q8ePgViKupeymbqI0la+t9/iYFBjxQCFwuAwI=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.71.0 h1:nyuzXooUNJexRT0Oy0UQY6AhOzxPxhtt4DcBIHyCnmw=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.71.0/go.mod h1:sT/iQz8JK3u/5gZkT+Hmr7GzVZehUMkRZpOaAwYXeGY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.7 h1:rLnYAfXQ3YAccocshIH5mzNNwZBkBo+bP6EhIxak6Hw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.7/go.mod h1:ZHtuQJ6t9A/+YDuxOLnbryAmITtr8UysSny3qcyvJTc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6 h1:JnhTZR3PiYDNKlXy50/pNeix9aGMo6lLpXwJ1mw8MD4=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6/go.mod h1:URronUEGfXZN1VpdktPSD1EkAL9mfrV+2F4sjH38qOY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.2 h1:s4074ZO1Hk8qv65GqNXqDjmkf4HSQqJukaLuuW0TpDA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.2/go.mod h1:mVggCnIWoM09jP71Wh+ea7+5gAp53q+49wDFs1SW5z8=
|
||||
github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
|
||||
github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.1 h1:iTDl5U6oAhkNPba0e1t1hrwAo02ZMqbrGq4k5JBWM5E=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.1/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 h1:zAxi9p3wsZMIaVCdoiQp2uZ9k1LsZvmAnoTBeZPXom0=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8/go.mod h1:3XkePX5dSaxveLAYY7nsbsZZrKxCyEuE5pM4ziFxyGg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.6 h1:fqgqEKK5HaZVWLQoLiC9Q+xDlSp+1LYidp6ybGE2OGg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.6/go.mod h1:Ft+WLODzDQmCTHDvqAH1JfC2xxbZ0MxpZAcJqmE1LTQ=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.59 h1:9btwmrt//Q6JcSdgJOLI98sdr5p7tssS9yAsGe8aKP4=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.59/go.mod h1:NM8fM6ovI3zak23UISdWidyZuI1ghNe2xjzUZAyT+08=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.28 h1:KwsodFKVQTlI5EyhRSugALzsV6mG/SGrdjlMXSZSdso=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.28/go.mod h1:EY3APf9MzygVhKuPXAc5H+MkGb8k/DOSQjWS0LgkKqI=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.59 h1:5Vsrfdlf9KQP3leGX1dD7VwZq/3HAerEFoXAII4t6zo=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.59/go.mod h1:7XTNs3NYApJjkx6A2Fk9qq23qBuBnIU58k3fKC2Fr1I=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32 h1:BjUcr3X3K0wZPGFg2bxOWW3VPN8rkE3/61zhP+IHviA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32/go.mod h1:80+OGC/bgzzFFTUmcuwD0lb4YutwQeKLFpmt6hoWapU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32 h1:m1GeXHVMJsRsUAqG6HjZWx9dj7F5TR+cF1bjyfYyBd4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32/go.mod h1:IitoQxGfaKdVLNg0hD8/DXmAqNy0H4K2H2Sf91ti8sI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.32 h1:OIHj/nAhVzIXGzbAE+4XmZ8FPvro3THr6NlqErJc3wY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.32/go.mod h1:LiBEsDo34OJXqdDlRGsilhlIiXR7DL+6Cx2f4p1EgzI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.6 h1:cCBJaT7EeEojpJ4s7wTDbhZlHVJOgNHN7iw6qVurGaw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.6/go.mod h1:WYH1ABybY7JK9TITPnk6ZlP7gQB8psI4c9qDmMsnLSA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13 h1:SYVGSFQHlchIcy6e7x12bsrxClCXSP5et8cqVhL8cuw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13/go.mod h1:kizuDaLX37bG5WZaoxGPQR/LNFXpxp0vsUnqfkWXfNE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.13 h1:OBsrtam3rk8NfBEq7OLOMm5HtQ9Yyw32X4UQMya/wjw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.13/go.mod h1:3U4gFA5pmoCOja7aq4nSaIAGbaOHv2Yl2ug018cmC+Q=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.75.4 h1:DJYjOvNgC30JAcDCRmtQHoYK4trc7XetDXRTEAReGKA=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.75.4/go.mod h1:KuLNrwYJFaC2AVZ+CVVc12k9NyqwgWsoNNHjwqF6QNk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.15 h1:/eE3DogBjYlvlbhd2ssWyeuovWunHLxfgw3s/OJa4GQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.15/go.mod h1:2PCJYpi7EKeA5SkStAmZlF6fi0uUABuhtF8ILHjGc3Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.14 h1:M/zwXiL2iXUrHputuXgmO94TVNmcenPHxgLXLutodKE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.14/go.mod h1:RVwIw3y/IqxC2YEXSIkAzRDdEU1iRabDPaYjpGCbCGQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.14 h1:TzeR06UCMUq+KA3bDkujxK1GVGy+G8qQN/QVYzGLkQE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.14/go.mod h1:dspXf/oYWGWo6DEvj98wpaTeqt5+DMidZD0A9BYTizc=
|
||||
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
|
||||
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
@@ -72,8 +68,6 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX
|
||||
github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec=
|
||||
github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
|
||||
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
@@ -84,8 +78,8 @@ github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCO
|
||||
github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
|
||||
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
@@ -97,8 +91,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/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
|
||||
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/ganigeorgiev/fexpr v0.4.1 h1:hpUgbUEEWIZhSDBtf4M9aUNfQQ0BZkGRaMePy7Gcx5k=
|
||||
github.com/ganigeorgiev/fexpr v0.4.1/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
@@ -117,10 +111,10 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
||||
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
@@ -154,8 +148,8 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
|
||||
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
|
||||
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
@@ -163,12 +157,8 @@ github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
|
||||
github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
|
||||
github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPqMNIe8o=
|
||||
github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
|
||||
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
|
||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
|
||||
@@ -178,8 +168,6 @@ github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nu
|
||||
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
@@ -189,16 +177,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
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/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/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
|
||||
@@ -213,8 +195,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/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU=
|
||||
github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
|
||||
github.com/pocketbase/pocketbase v0.23.9 h1:0P3BaMTUO8QzyamYqd/OpPM4L7zmu6HrmDGFQmX+eu4=
|
||||
github.com/pocketbase/pocketbase v0.23.9/go.mod h1:8qIx1v60b+YES3e8H4J2QQF48J0uiydPhRi4ZHlKNjk=
|
||||
github.com/pocketbase/pocketbase v0.25.0 h1:/4YQq1hd0muvhzbERyUTVNh88N0BCj5diqK0jtLN6k8=
|
||||
github.com/pocketbase/pocketbase v0.25.0/go.mod h1:tOtOv7f3vJhAiyUluIwV9JPuKeknZRQ9F6uJE3W/ntI=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
@@ -225,24 +207,24 @@ github.com/rhysd/go-github-selfupdate v1.2.3/go.mod h1:mp/N8zj6jFfBQy/XMYoWsmfzx
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8=
|
||||
github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8=
|
||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
|
||||
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
|
||||
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
||||
@@ -252,43 +234,42 @@ github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS
|
||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
|
||||
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
|
||||
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
|
||||
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
|
||||
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
|
||||
go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
|
||||
go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
|
||||
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
|
||||
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
|
||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
|
||||
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
|
||||
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||
gocloud.dev v0.40.0 h1:f8LgP+4WDqOG/RXoUcyLpeIAGOcAbZrZbDQCUee10ng=
|
||||
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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo=
|
||||
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
|
||||
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
|
||||
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -298,70 +279,53 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
|
||||
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
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.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
|
||||
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
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/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
google.golang.org/api v0.212.0 h1:BcRj3MJfHF3FYD29rk7u9kuu1SyfGqfHcA0hSwKqkHg=
|
||||
google.golang.org/api v0.212.0/go.mod h1:gICpLlpp12/E8mycRMzgy3SQ9cFh2XnVJ6vJi/kQbvI=
|
||||
google.golang.org/api v0.220.0 h1:3oMI4gdBgB72WFVwE1nerDD8W3HUOS4kypK6rRLbGns=
|
||||
google.golang.org/api v0.220.0/go.mod h1:26ZAlY6aN/8WgpCzjPNy18QpYaz7Zgg1h0qe1GkZEmY=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@@ -371,17 +335,17 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988 h1:CT2Thj5AuPV9phrYMtzX11k+XkzMGfRAet42PmoTATM=
|
||||
google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988/go.mod h1:7uvplUBj4RjHAxIZ//98LzOvrQ04JBkaixRmCMI29hc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 h1:Z7FRVJPSMaHQxD0uXU8WdgFh8PseLM8Q8NzhnpMrBhQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489 h1:5bKytslY8ViY0Cj/ewmRtrWHW64bNF03cAatUUFCdFI=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.69.0 h1:quSiOM1GJPmPH5XtU+BCoVXcDVJJAzNcoyfC2cCjGkI=
|
||||
google.golang.org/grpc v1.69.0/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
||||
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
|
||||
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -391,8 +355,8 @@ 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.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.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ=
|
||||
google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
|
||||
google.golang.org/protobuf v1.36.4/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 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -405,28 +369,26 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
modernc.org/cc/v4 v4.23.1 h1:WqJoPL3x4cUufQVHkXpXX7ThFJ1C4ik80i2eXEXbhD8=
|
||||
modernc.org/cc/v4 v4.23.1/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||
modernc.org/ccgo/v4 v4.23.1 h1:N49a7JiWGWV7lkPE4yYcvjkBGZQi93/JabRYjdWmJXc=
|
||||
modernc.org/ccgo/v4 v4.23.1/go.mod h1:JoIUegEIfutvoWV/BBfDFpPpfR2nc3U0jKucGcbmwDU=
|
||||
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
||||
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
|
||||
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
|
||||
modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||
modernc.org/gc/v3 v3.0.0-20241213165251-3bc300f6d0c9 h1:ovz6yUKX71igz2yvk4NpiCL5fvdjZAI+DhuDEGx1xyU=
|
||||
modernc.org/gc/v3 v3.0.0-20241213165251-3bc300f6d0c9/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||
modernc.org/libc v1.61.4 h1:wVyqEx6tlltte9lPTjq0kDAdtdM9c4JH8rU6M1ZVawA=
|
||||
modernc.org/libc v1.61.4/go.mod h1:VfXVuM/Shh5XsMNrh3C6OkfL78G3loa4ZC/Ljv9k7xc=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
||||
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
|
||||
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
|
||||
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
|
||||
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||
modernc.org/sqlite v1.34.2 h1:J9n76TPsfYYkFkZ9Uy1QphILYifiVEwwOT7yP5b++2Y=
|
||||
modernc.org/sqlite v1.34.2/go.mod h1:dnR723UrTtjKpoHCAMN0Q/gZ9MT4r+iRvIBb9umWFkU=
|
||||
modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
|
||||
modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -8,11 +8,13 @@ import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/common"
|
||||
)
|
||||
|
||||
type Agent struct {
|
||||
sync.Mutex // Used to lock agent while collecting data
|
||||
debug bool // true if LOG_LEVEL is set to debug
|
||||
zfs bool // true if system has arcstats
|
||||
memCalc string // Memory calculation formula
|
||||
@@ -28,19 +30,16 @@ type Agent struct {
|
||||
}
|
||||
|
||||
func NewAgent() *Agent {
|
||||
return &Agent{
|
||||
sensorsContext: context.Background(),
|
||||
memCalc: os.Getenv("MEM_CALC"),
|
||||
fsStats: make(map[string]*system.FsStats),
|
||||
agent := &Agent{
|
||||
fsStats: make(map[string]*system.FsStats),
|
||||
}
|
||||
}
|
||||
agent.memCalc, _ = GetEnv("MEM_CALC")
|
||||
|
||||
func (a *Agent) Run(pubKey []byte, addr string) {
|
||||
// Set up slog with a log level determined by the LOG_LEVEL env var
|
||||
if logLevelStr, exists := os.LookupEnv("LOG_LEVEL"); exists {
|
||||
if logLevelStr, exists := GetEnv("LOG_LEVEL"); exists {
|
||||
switch strings.ToLower(logLevelStr) {
|
||||
case "debug":
|
||||
a.debug = true
|
||||
agent.debug = true
|
||||
slog.SetLogLoggerLevel(slog.LevelDebug)
|
||||
case "warn":
|
||||
slog.SetLogLoggerLevel(slog.LevelWarn)
|
||||
@@ -52,45 +51,58 @@ func (a *Agent) Run(pubKey []byte, addr string) {
|
||||
slog.Debug(beszel.Version)
|
||||
|
||||
// Set sensors context (allows overriding sys location for sensors)
|
||||
if sysSensors, exists := os.LookupEnv("SYS_SENSORS"); exists {
|
||||
if sysSensors, exists := GetEnv("SYS_SENSORS"); exists {
|
||||
slog.Info("SYS_SENSORS", "path", sysSensors)
|
||||
a.sensorsContext = context.WithValue(a.sensorsContext,
|
||||
agent.sensorsContext = context.WithValue(agent.sensorsContext,
|
||||
common.EnvKey, common.EnvMap{common.HostSysEnvKey: sysSensors},
|
||||
)
|
||||
} else {
|
||||
agent.sensorsContext = context.Background()
|
||||
}
|
||||
|
||||
// Set sensors whitelist
|
||||
if sensors, exists := os.LookupEnv("SENSORS"); exists {
|
||||
a.sensorsWhitelist = make(map[string]struct{})
|
||||
if sensors, exists := GetEnv("SENSORS"); exists {
|
||||
agent.sensorsWhitelist = make(map[string]struct{})
|
||||
for _, sensor := range strings.Split(sensors, ",") {
|
||||
if sensor != "" {
|
||||
a.sensorsWhitelist[sensor] = struct{}{}
|
||||
agent.sensorsWhitelist[sensor] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// initialize system info / docker manager
|
||||
a.initializeSystemInfo()
|
||||
a.initializeDiskInfo()
|
||||
a.initializeNetIoStats()
|
||||
a.dockerManager = newDockerManager(a)
|
||||
agent.initializeSystemInfo()
|
||||
agent.initializeDiskInfo()
|
||||
agent.initializeNetIoStats()
|
||||
agent.dockerManager = newDockerManager(agent)
|
||||
|
||||
// initialize GPU manager
|
||||
if gm, err := NewGPUManager(); err != nil {
|
||||
slog.Debug("GPU", "err", err)
|
||||
} else {
|
||||
a.gpuManager = gm
|
||||
agent.gpuManager = gm
|
||||
}
|
||||
|
||||
// if debugging, print stats
|
||||
if a.debug {
|
||||
slog.Debug("Stats", "data", a.gatherStats())
|
||||
if agent.debug {
|
||||
slog.Debug("Stats", "data", agent.gatherStats())
|
||||
}
|
||||
|
||||
a.startServer(pubKey, addr)
|
||||
return agent
|
||||
}
|
||||
|
||||
// GetEnv retrieves an environment variable with a "BESZEL_AGENT_" prefix, or falls back to the unprefixed key.
|
||||
func GetEnv(key string) (value string, exists bool) {
|
||||
if value, exists = os.LookupEnv("BESZEL_AGENT_" + key); exists {
|
||||
return value, exists
|
||||
}
|
||||
// Fallback to the old unprefixed key
|
||||
return os.LookupEnv(key)
|
||||
}
|
||||
|
||||
func (a *Agent) gatherStats() system.CombinedData {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
slog.Debug("Getting stats")
|
||||
systemData := system.CombinedData{
|
||||
Stats: a.getSystemStats(),
|
||||
|
||||
@@ -3,18 +3,17 @@ package agent
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
)
|
||||
|
||||
// Sets up the filesystems to monitor for disk usage and I/O.
|
||||
func (a *Agent) initializeDiskInfo() {
|
||||
filesystem := os.Getenv("FILESYSTEM")
|
||||
filesystem, _ := GetEnv("FILESYSTEM")
|
||||
efPath := "/extra-filesystems"
|
||||
hasRoot := false
|
||||
|
||||
@@ -79,7 +78,7 @@ func (a *Agent) initializeDiskInfo() {
|
||||
}
|
||||
|
||||
// Add EXTRA_FILESYSTEMS env var values to fsStats
|
||||
if extraFilesystems, exists := os.LookupEnv("EXTRA_FILESYSTEMS"); exists {
|
||||
if extraFilesystems, exists := GetEnv("EXTRA_FILESYSTEMS"); exists {
|
||||
for _, fs := range strings.Split(extraFilesystems, ",") {
|
||||
found := false
|
||||
for _, p := range partitions {
|
||||
|
||||
@@ -208,7 +208,7 @@ func (dm *dockerManager) deleteContainerStatsSync(id string) {
|
||||
|
||||
// Creates a new http client for Docker or Podman API
|
||||
func newDockerManager(a *Agent) *dockerManager {
|
||||
dockerHost, exists := os.LookupEnv("DOCKER_HOST")
|
||||
dockerHost, exists := GetEnv("DOCKER_HOST")
|
||||
if exists {
|
||||
slog.Info("DOCKER_HOST", "host", dockerHost)
|
||||
} else {
|
||||
@@ -242,7 +242,7 @@ func newDockerManager(a *Agent) *dockerManager {
|
||||
|
||||
// configurable timeout
|
||||
timeout := time.Millisecond * 2100
|
||||
if t, set := os.LookupEnv("DOCKER_TIMEOUT"); set {
|
||||
if t, set := GetEnv("DOCKER_TIMEOUT"); set {
|
||||
timeout, err = time.ParseDuration(t)
|
||||
if err != nil {
|
||||
slog.Error(err.Error())
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -16,28 +17,31 @@ import (
|
||||
|
||||
// GPUManager manages data collection for GPUs (either Nvidia or AMD)
|
||||
type GPUManager struct {
|
||||
sync.Mutex
|
||||
nvidiaSmi bool
|
||||
rocmSmi bool
|
||||
tegrastats bool
|
||||
GpuDataMap map[string]*system.GPUData
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// RocmSmiJson represents the JSON structure of rocm-smi output
|
||||
type RocmSmiJson struct {
|
||||
ID string `json:"Device ID"`
|
||||
Name string `json:"Card series"`
|
||||
Temperature string `json:"Temperature (Sensor edge) (C)"`
|
||||
MemoryUsed string `json:"VRAM Total Used Memory (B)"`
|
||||
MemoryTotal string `json:"VRAM Total Memory (B)"`
|
||||
Usage string `json:"GPU use (%)"`
|
||||
Power string `json:"Current Socket Graphics Package Power (W)"`
|
||||
ID string `json:"GUID"`
|
||||
Name string `json:"Card series"`
|
||||
Temperature string `json:"Temperature (Sensor edge) (C)"`
|
||||
MemoryUsed string `json:"VRAM Total Used Memory (B)"`
|
||||
MemoryTotal string `json:"VRAM Total Memory (B)"`
|
||||
Usage string `json:"GPU use (%)"`
|
||||
PowerPackage string `json:"Average Graphics Package Power (W)"`
|
||||
PowerSocket string `json:"Current Socket Graphics Package Power (W)"`
|
||||
}
|
||||
|
||||
// gpuCollector defines a collector for a specific GPU management utility (nvidia-smi or rocm-smi)
|
||||
type gpuCollector struct {
|
||||
name string
|
||||
cmd *exec.Cmd
|
||||
parse func([]byte) bool // returns true if valid data was found
|
||||
name string
|
||||
cmdArgs []string
|
||||
parse func([]byte) bool // returns true if valid data was found
|
||||
buf []byte
|
||||
}
|
||||
|
||||
var errNoValidData = fmt.Errorf("no valid GPU data found") // Error for missing data
|
||||
@@ -60,71 +64,118 @@ func (c *gpuCollector) start() {
|
||||
|
||||
// collect executes the command, parses output with the assigned parser function
|
||||
func (c *gpuCollector) collect() error {
|
||||
stdout, err := c.cmd.StdoutPipe()
|
||||
cmd := exec.Command(c.name, c.cmdArgs...)
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.cmd.Start(); err != nil {
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
buf := make([]byte, 0, 8*1024)
|
||||
scanner.Buffer(buf, bufio.MaxScanTokenSize)
|
||||
|
||||
hasValidData := false
|
||||
for scanner.Scan() {
|
||||
if c.parse(scanner.Bytes()) {
|
||||
hasValidData = true
|
||||
}
|
||||
if c.buf == nil {
|
||||
c.buf = make([]byte, 0, 4*1024)
|
||||
}
|
||||
scanner.Buffer(c.buf, bufio.MaxScanTokenSize)
|
||||
|
||||
if !hasValidData {
|
||||
return errNoValidData
|
||||
for scanner.Scan() {
|
||||
hasValidData := c.parse(scanner.Bytes())
|
||||
if !hasValidData {
|
||||
return errNoValidData
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return fmt.Errorf("scanner error: %w", err)
|
||||
}
|
||||
return c.cmd.Wait()
|
||||
return cmd.Wait()
|
||||
}
|
||||
|
||||
// getJetsonParser returns a function to parse the output of tegrastats and update the GPUData map
|
||||
func (gm *GPUManager) getJetsonParser() func(output []byte) bool {
|
||||
// use closure to avoid recompiling the regex
|
||||
ramPattern := regexp.MustCompile(`RAM (\d+)/(\d+)MB`)
|
||||
gr3dPattern := regexp.MustCompile(`GR3D_FREQ (\d+)%`)
|
||||
tempPattern := regexp.MustCompile(`tj@(\d+\.?\d*)C`)
|
||||
// Orin Nano / NX do not have GPU specific power monitor
|
||||
// TODO: Maybe use VDD_IN for Nano / NX and add a total system power chart
|
||||
powerPattern := regexp.MustCompile(`(GPU_SOC|CPU_GPU_CV) (\d+)mW`)
|
||||
|
||||
return func(output []byte) bool {
|
||||
gm.Lock()
|
||||
defer gm.Unlock()
|
||||
// we get gpu name from the intitial run of nvidia-smi, so return if it hasn't been initialized
|
||||
gpuData, ok := gm.GpuDataMap["0"]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
data := string(output)
|
||||
// Parse RAM usage
|
||||
ramMatches := ramPattern.FindStringSubmatch(data)
|
||||
if ramMatches != nil {
|
||||
gpuData.MemoryUsed, _ = strconv.ParseFloat(ramMatches[1], 64)
|
||||
gpuData.MemoryTotal, _ = strconv.ParseFloat(ramMatches[2], 64)
|
||||
}
|
||||
// Parse GR3D (GPU) usage
|
||||
gr3dMatches := gr3dPattern.FindStringSubmatch(data)
|
||||
if gr3dMatches != nil {
|
||||
gpuData.Usage, _ = strconv.ParseFloat(gr3dMatches[1], 64)
|
||||
}
|
||||
// Parse temperature
|
||||
tempMatches := tempPattern.FindStringSubmatch(data)
|
||||
if tempMatches != nil {
|
||||
gpuData.Temperature, _ = strconv.ParseFloat(tempMatches[1], 64)
|
||||
}
|
||||
// Parse power usage
|
||||
powerMatches := powerPattern.FindStringSubmatch(data)
|
||||
if powerMatches != nil {
|
||||
power, _ := strconv.ParseFloat(powerMatches[2], 64)
|
||||
gpuData.Power = power / 1000
|
||||
}
|
||||
gpuData.Count++
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// parseNvidiaData parses the output of nvidia-smi and updates the GPUData map
|
||||
func (gm *GPUManager) parseNvidiaData(output []byte) bool {
|
||||
fields := strings.Split(string(output), ", ")
|
||||
if len(fields) < 7 {
|
||||
return false
|
||||
}
|
||||
gm.mutex.Lock()
|
||||
defer gm.mutex.Unlock()
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
if line != "" {
|
||||
fields := strings.Split(line, ", ")
|
||||
if len(fields) >= 7 {
|
||||
id := fields[0]
|
||||
temp, _ := strconv.ParseFloat(fields[2], 64)
|
||||
memoryUsage, _ := strconv.ParseFloat(fields[3], 64)
|
||||
totalMemory, _ := strconv.ParseFloat(fields[4], 64)
|
||||
usage, _ := strconv.ParseFloat(fields[5], 64)
|
||||
power, _ := strconv.ParseFloat(fields[6], 64)
|
||||
// add gpu if not exists
|
||||
if _, ok := gm.GpuDataMap[id]; !ok {
|
||||
name := strings.TrimPrefix(fields[1], "NVIDIA ")
|
||||
gm.GpuDataMap[id] = &system.GPUData{Name: strings.TrimSuffix(name, " Laptop GPU")}
|
||||
}
|
||||
// update gpu data
|
||||
gpu := gm.GpuDataMap[id]
|
||||
gpu.Temperature = temp
|
||||
gpu.MemoryUsed = memoryUsage / 1.024
|
||||
gpu.MemoryTotal = totalMemory / 1.024
|
||||
gpu.Usage += usage
|
||||
gpu.Power += power
|
||||
gpu.Count++
|
||||
gm.Lock()
|
||||
defer gm.Unlock()
|
||||
var valid bool
|
||||
for line := range strings.Lines(string(output)) {
|
||||
fields := strings.Split(strings.TrimSpace(line), ", ")
|
||||
if len(fields) < 7 {
|
||||
continue
|
||||
}
|
||||
valid = true
|
||||
id := fields[0]
|
||||
temp, _ := strconv.ParseFloat(fields[2], 64)
|
||||
memoryUsage, _ := strconv.ParseFloat(fields[3], 64)
|
||||
totalMemory, _ := strconv.ParseFloat(fields[4], 64)
|
||||
usage, _ := strconv.ParseFloat(fields[5], 64)
|
||||
power, _ := strconv.ParseFloat(fields[6], 64)
|
||||
// add gpu if not exists
|
||||
if _, ok := gm.GpuDataMap[id]; !ok {
|
||||
name := strings.TrimPrefix(fields[1], "NVIDIA ")
|
||||
gm.GpuDataMap[id] = &system.GPUData{Name: strings.TrimSuffix(name, " Laptop GPU")}
|
||||
// check if tegrastats is active - if so we will only use nvidia-smi to get gpu name
|
||||
// - nvidia-smi does not provide metrics for tegra / jetson devices
|
||||
// this will end the nvidia-smi collector
|
||||
if gm.tegrastats {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// update gpu data
|
||||
gpu := gm.GpuDataMap[id]
|
||||
gpu.Temperature = temp
|
||||
gpu.MemoryUsed = memoryUsage / 1.024
|
||||
gpu.MemoryTotal = totalMemory / 1.024
|
||||
gpu.Usage += usage
|
||||
gpu.Power += power
|
||||
gpu.Count++
|
||||
}
|
||||
return true
|
||||
return valid
|
||||
}
|
||||
|
||||
// parseAmdData parses the output of rocm-smi and updates the GPUData map
|
||||
@@ -133,24 +184,26 @@ func (gm *GPUManager) parseAmdData(output []byte) bool {
|
||||
if err := json.Unmarshal(output, &rocmSmiInfo); err != nil || len(rocmSmiInfo) == 0 {
|
||||
return false
|
||||
}
|
||||
gm.mutex.Lock()
|
||||
defer gm.mutex.Unlock()
|
||||
gm.Lock()
|
||||
defer gm.Unlock()
|
||||
for _, v := range rocmSmiInfo {
|
||||
temp, _ := strconv.ParseFloat(v.Temperature, 64)
|
||||
var power float64
|
||||
if v.PowerPackage != "" {
|
||||
power, _ = strconv.ParseFloat(v.PowerPackage, 64)
|
||||
} else {
|
||||
power, _ = strconv.ParseFloat(v.PowerSocket, 64)
|
||||
}
|
||||
memoryUsage, _ := strconv.ParseFloat(v.MemoryUsed, 64)
|
||||
totalMemory, _ := strconv.ParseFloat(v.MemoryTotal, 64)
|
||||
usage, _ := strconv.ParseFloat(v.Usage, 64)
|
||||
power, _ := strconv.ParseFloat(v.Power, 64)
|
||||
memoryUsage = bytesToMegabytes(memoryUsage)
|
||||
totalMemory = bytesToMegabytes(totalMemory)
|
||||
|
||||
if _, ok := gm.GpuDataMap[v.ID]; !ok {
|
||||
gm.GpuDataMap[v.ID] = &system.GPUData{Name: v.Name}
|
||||
}
|
||||
gpu := gm.GpuDataMap[v.ID]
|
||||
gpu.Temperature = temp
|
||||
gpu.MemoryUsed = memoryUsage
|
||||
gpu.MemoryTotal = totalMemory
|
||||
gpu.Temperature, _ = strconv.ParseFloat(v.Temperature, 64)
|
||||
gpu.MemoryUsed = bytesToMegabytes(memoryUsage)
|
||||
gpu.MemoryTotal = bytesToMegabytes(totalMemory)
|
||||
gpu.Usage += usage
|
||||
gpu.Power += power
|
||||
gpu.Count++
|
||||
@@ -160,8 +213,15 @@ func (gm *GPUManager) parseAmdData(output []byte) bool {
|
||||
|
||||
// sums and resets the current GPU utilization data since the last update
|
||||
func (gm *GPUManager) GetCurrentData() map[string]system.GPUData {
|
||||
gm.mutex.Lock()
|
||||
defer gm.mutex.Unlock()
|
||||
gm.Lock()
|
||||
defer gm.Unlock()
|
||||
|
||||
// check for GPUs with the same name
|
||||
nameCounts := make(map[string]int)
|
||||
for _, gpu := range gm.GpuDataMap {
|
||||
nameCounts[gpu.Name]++
|
||||
}
|
||||
|
||||
// copy / reset the data
|
||||
gpuData := make(map[string]system.GPUData, len(gm.GpuDataMap))
|
||||
for id, gpu := range gm.GpuDataMap {
|
||||
@@ -171,48 +231,71 @@ func (gm *GPUManager) GetCurrentData() map[string]system.GPUData {
|
||||
gpu.MemoryTotal = twoDecimals(gpu.MemoryTotal)
|
||||
gpu.Usage = twoDecimals(gpu.Usage / gpu.Count)
|
||||
gpu.Power = twoDecimals(gpu.Power / gpu.Count)
|
||||
gpuData[id] = *gpu
|
||||
// reset the count
|
||||
gpu.Count = 1
|
||||
// dereference to avoid overwriting anything else
|
||||
gpuCopy := *gpu
|
||||
// append id to the name if there are multiple GPUs with the same name
|
||||
if nameCounts[gpu.Name] > 1 {
|
||||
gpuCopy.Name = fmt.Sprintf("%s %s", gpu.Name, id)
|
||||
}
|
||||
gpuData[id] = gpuCopy
|
||||
}
|
||||
return gpuData
|
||||
}
|
||||
|
||||
// detectGPUs returns the GPU brand (nvidia or amd) or an error if none is found
|
||||
// todo: make sure there's actually a GPU, not just if the command exists
|
||||
// detectGPUs checks for the presence of GPU management tools (nvidia-smi, rocm-smi, tegrastats)
|
||||
// in the system path. It sets the corresponding flags in the GPUManager struct if any of these
|
||||
// tools are found. If none of the tools are found, it returns an error indicating that no GPU
|
||||
// management tools are available.
|
||||
func (gm *GPUManager) detectGPUs() error {
|
||||
if err := exec.Command("nvidia-smi").Run(); err == nil {
|
||||
if _, err := exec.LookPath("nvidia-smi"); err == nil {
|
||||
gm.nvidiaSmi = true
|
||||
}
|
||||
if err := exec.Command("rocm-smi").Run(); err == nil {
|
||||
if _, err := exec.LookPath("rocm-smi"); err == nil {
|
||||
gm.rocmSmi = true
|
||||
}
|
||||
if gm.nvidiaSmi || gm.rocmSmi {
|
||||
if _, err := exec.LookPath("tegrastats"); err == nil {
|
||||
gm.tegrastats = true
|
||||
}
|
||||
if gm.nvidiaSmi || gm.rocmSmi || gm.tegrastats {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("no GPU found - install nvidia-smi or rocm-smi")
|
||||
return fmt.Errorf("no GPU found - install nvidia-smi, rocm-smi, or tegrastats")
|
||||
}
|
||||
|
||||
// startCollector starts the appropriate GPU data collector based on the command
|
||||
func (gm *GPUManager) startCollector(command string) {
|
||||
collector := gpuCollector{
|
||||
name: command,
|
||||
}
|
||||
switch command {
|
||||
case "nvidia-smi":
|
||||
nvidia := gpuCollector{
|
||||
name: "nvidia-smi",
|
||||
cmd: exec.Command("nvidia-smi", "-l", "4",
|
||||
"--query-gpu=index,name,temperature.gpu,memory.used,memory.total,utilization.gpu,power.draw",
|
||||
"--format=csv,noheader,nounits"),
|
||||
parse: gm.parseNvidiaData,
|
||||
}
|
||||
go nvidia.start()
|
||||
collector.cmdArgs = []string{"-l", "4",
|
||||
"--query-gpu=index,name,temperature.gpu,memory.used,memory.total,utilization.gpu,power.draw",
|
||||
"--format=csv,noheader,nounits"}
|
||||
collector.parse = gm.parseNvidiaData
|
||||
go collector.start()
|
||||
case "tegrastats":
|
||||
collector.cmdArgs = []string{"--interval", "3000"}
|
||||
collector.parse = gm.getJetsonParser()
|
||||
go collector.start()
|
||||
case "rocm-smi":
|
||||
amdCollector := gpuCollector{
|
||||
name: "rocm-smi",
|
||||
cmd: exec.Command("/bin/sh", "-c",
|
||||
"while true; do rocm-smi --showid --showtemp --showuse --showpower --showproductname --showmeminfo vram --json; sleep 4.3; done"),
|
||||
parse: gm.parseAmdData,
|
||||
}
|
||||
go amdCollector.start()
|
||||
collector.cmdArgs = []string{"--showid", "--showtemp", "--showuse", "--showpower", "--showproductname", "--showmeminfo", "vram", "--json"}
|
||||
collector.parse = gm.parseAmdData
|
||||
go func() {
|
||||
failures := 0
|
||||
for {
|
||||
if err := collector.collect(); err != nil {
|
||||
failures++
|
||||
if failures > 5 {
|
||||
break
|
||||
}
|
||||
slog.Warn("Error collecting AMD GPU data", "err", err)
|
||||
}
|
||||
time.Sleep(4300 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +305,7 @@ func NewGPUManager() (*GPUManager, error) {
|
||||
if err := gm.detectGPUs(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gm.GpuDataMap = make(map[string]*system.GPUData, 1)
|
||||
gm.GpuDataMap = make(map[string]*system.GPUData)
|
||||
|
||||
if gm.nvidiaSmi {
|
||||
gm.startCollector("nvidia-smi")
|
||||
@@ -230,6 +313,9 @@ func NewGPUManager() (*GPUManager, error) {
|
||||
if gm.rocmSmi {
|
||||
gm.startCollector("rocm-smi")
|
||||
}
|
||||
if gm.tegrastats {
|
||||
gm.startCollector("tegrastats")
|
||||
}
|
||||
|
||||
return &gm, nil
|
||||
}
|
||||
|
||||
525
beszel/internal/agent/gpu_test.go
Normal file
525
beszel/internal/agent/gpu_test.go
Normal file
@@ -0,0 +1,525 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseNvidiaData(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantData map[string]system.GPUData
|
||||
wantValid bool
|
||||
}{
|
||||
{
|
||||
name: "valid multi-gpu data",
|
||||
input: "0, NVIDIA GeForce RTX 3050 Ti Laptop GPU, 48, 12, 4096, 26.3, 12.73\n1, NVIDIA A100-PCIE-40GB, 38, 74, 40960, [N/A], 36.79",
|
||||
wantData: map[string]system.GPUData{
|
||||
"0": {
|
||||
Name: "GeForce RTX 3050 Ti",
|
||||
Temperature: 48.0,
|
||||
MemoryUsed: 12.0 / 1.024,
|
||||
MemoryTotal: 4096.0 / 1.024,
|
||||
Usage: 26.3,
|
||||
Power: 12.73,
|
||||
Count: 1,
|
||||
},
|
||||
"1": {
|
||||
Name: "A100-PCIE-40GB",
|
||||
Temperature: 38.0,
|
||||
MemoryUsed: 74.0 / 1.024,
|
||||
MemoryTotal: 40960.0 / 1.024,
|
||||
Usage: 0.0,
|
||||
Power: 36.79,
|
||||
Count: 1,
|
||||
},
|
||||
},
|
||||
wantValid: true,
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
wantData: map[string]system.GPUData{},
|
||||
wantValid: false,
|
||||
},
|
||||
{
|
||||
name: "malformed data",
|
||||
input: "bad, data, here",
|
||||
wantData: map[string]system.GPUData{},
|
||||
wantValid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gm := &GPUManager{
|
||||
GpuDataMap: make(map[string]*system.GPUData),
|
||||
}
|
||||
valid := gm.parseNvidiaData([]byte(tt.input))
|
||||
assert.Equal(t, tt.wantValid, valid)
|
||||
|
||||
if tt.wantValid {
|
||||
for id, want := range tt.wantData {
|
||||
got := gm.GpuDataMap[id]
|
||||
require.NotNil(t, got)
|
||||
assert.Equal(t, want.Name, got.Name)
|
||||
assert.InDelta(t, want.Temperature, got.Temperature, 0.01)
|
||||
assert.InDelta(t, want.MemoryUsed, got.MemoryUsed, 0.01)
|
||||
assert.InDelta(t, want.MemoryTotal, got.MemoryTotal, 0.01)
|
||||
assert.InDelta(t, want.Usage, got.Usage, 0.01)
|
||||
assert.InDelta(t, want.Power, got.Power, 0.01)
|
||||
assert.Equal(t, want.Count, got.Count)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAmdData(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantData map[string]system.GPUData
|
||||
wantValid bool
|
||||
}{
|
||||
{
|
||||
name: "valid single gpu data",
|
||||
input: `{
|
||||
"card0": {
|
||||
"GUID": "34756",
|
||||
"Temperature (Sensor edge) (C)": "47.0",
|
||||
"Current Socket Graphics Package Power (W)": "9.215",
|
||||
"GPU use (%)": "0",
|
||||
"VRAM Total Memory (B)": "536870912",
|
||||
"VRAM Total Used Memory (B)": "482263040",
|
||||
"Card Series": "Rembrandt [Radeon 680M]"
|
||||
}
|
||||
}`,
|
||||
wantData: map[string]system.GPUData{
|
||||
"34756": {
|
||||
Name: "Rembrandt [Radeon 680M]",
|
||||
Temperature: 47.0,
|
||||
MemoryUsed: 482263040.0 / (1024 * 1024),
|
||||
MemoryTotal: 536870912.0 / (1024 * 1024),
|
||||
Usage: 0.0,
|
||||
Power: 9.215,
|
||||
Count: 1,
|
||||
},
|
||||
},
|
||||
wantValid: true,
|
||||
},
|
||||
{
|
||||
name: "valid multi gpu data",
|
||||
input: `{
|
||||
"card0": {
|
||||
"GUID": "34756",
|
||||
"Temperature (Sensor edge) (C)": "47.0",
|
||||
"Current Socket Graphics Package Power (W)": "9.215",
|
||||
"GPU use (%)": "0",
|
||||
"VRAM Total Memory (B)": "536870912",
|
||||
"VRAM Total Used Memory (B)": "482263040",
|
||||
"Card Series": "Rembrandt [Radeon 680M]"
|
||||
},
|
||||
"card1": {
|
||||
"GUID": "38294",
|
||||
"Temperature (Sensor edge) (C)": "49.0",
|
||||
"Temperature (Sensor junction) (C)": "49.0",
|
||||
"Temperature (Sensor memory) (C)": "62.0",
|
||||
"Average Graphics Package Power (W)": "19.0",
|
||||
"GPU use (%)": "20.3",
|
||||
"VRAM Total Memory (B)": "25753026560",
|
||||
"VRAM Total Used Memory (B)": "794341376",
|
||||
"Card Series": "Navi 31 [Radeon RX 7900 XT]"
|
||||
}
|
||||
}`,
|
||||
wantData: map[string]system.GPUData{
|
||||
"34756": {
|
||||
Name: "Rembrandt [Radeon 680M]",
|
||||
Temperature: 47.0,
|
||||
MemoryUsed: 482263040.0 / (1024 * 1024),
|
||||
MemoryTotal: 536870912.0 / (1024 * 1024),
|
||||
Usage: 0.0,
|
||||
Power: 9.215,
|
||||
Count: 1,
|
||||
},
|
||||
"38294": {
|
||||
Name: "Navi 31 [Radeon RX 7900 XT]",
|
||||
Temperature: 49.0,
|
||||
MemoryUsed: 794341376.0 / (1024 * 1024),
|
||||
MemoryTotal: 25753026560.0 / (1024 * 1024),
|
||||
Usage: 20.3,
|
||||
Power: 19.0,
|
||||
Count: 1,
|
||||
},
|
||||
},
|
||||
wantValid: true,
|
||||
},
|
||||
{
|
||||
name: "invalid json",
|
||||
input: "{bad json",
|
||||
},
|
||||
{
|
||||
name: "invalid json",
|
||||
input: "{bad json",
|
||||
wantData: map[string]system.GPUData{},
|
||||
wantValid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gm := &GPUManager{
|
||||
GpuDataMap: make(map[string]*system.GPUData),
|
||||
}
|
||||
valid := gm.parseAmdData([]byte(tt.input))
|
||||
assert.Equal(t, tt.wantValid, valid)
|
||||
|
||||
if tt.wantValid {
|
||||
for id, want := range tt.wantData {
|
||||
got := gm.GpuDataMap[id]
|
||||
require.NotNil(t, got)
|
||||
assert.Equal(t, want.Name, got.Name)
|
||||
assert.InDelta(t, want.Temperature, got.Temperature, 0.01)
|
||||
assert.InDelta(t, want.MemoryUsed, got.MemoryUsed, 0.01)
|
||||
assert.InDelta(t, want.MemoryTotal, got.MemoryTotal, 0.01)
|
||||
assert.InDelta(t, want.Usage, got.Usage, 0.01)
|
||||
assert.InDelta(t, want.Power, got.Power, 0.01)
|
||||
assert.Equal(t, want.Count, got.Count)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseJetsonData(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
gm *GPUManager
|
||||
wantMetrics *system.GPUData
|
||||
}{
|
||||
{
|
||||
name: "valid data",
|
||||
input: "RAM 4300/30698MB GR3D_FREQ 45% tj@52.468C VDD_GPU_SOC 2171mW",
|
||||
wantMetrics: &system.GPUData{
|
||||
Name: "Jetson",
|
||||
MemoryUsed: 4300.0,
|
||||
MemoryTotal: 30698.0,
|
||||
Usage: 45.0,
|
||||
Temperature: 52.468,
|
||||
Power: 2.171,
|
||||
Count: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing temperature",
|
||||
input: "RAM 4300/30698MB GR3D_FREQ 45% VDD_GPU_SOC 2171mW",
|
||||
wantMetrics: &system.GPUData{
|
||||
Name: "Jetson",
|
||||
MemoryUsed: 4300.0,
|
||||
MemoryTotal: 30698.0,
|
||||
Usage: 45.0,
|
||||
Power: 2.171,
|
||||
Count: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no gpu defined by nvidia-smi",
|
||||
input: "RAM 4300/30698MB GR3D_FREQ 45% VDD_GPU_SOC 2171mW",
|
||||
gm: &GPUManager{
|
||||
GpuDataMap: map[string]*system.GPUData{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.gm != nil {
|
||||
// should return if no gpu set by nvidia-smi
|
||||
assert.Empty(t, tt.gm.GpuDataMap)
|
||||
return
|
||||
}
|
||||
tt.gm = &GPUManager{
|
||||
GpuDataMap: map[string]*system.GPUData{
|
||||
"0": {Name: "Jetson"},
|
||||
},
|
||||
}
|
||||
parser := tt.gm.getJetsonParser()
|
||||
valid := parser([]byte(tt.input))
|
||||
assert.Equal(t, true, valid)
|
||||
|
||||
got := tt.gm.GpuDataMap["0"]
|
||||
require.NotNil(t, got)
|
||||
assert.Equal(t, tt.wantMetrics.Name, got.Name)
|
||||
assert.InDelta(t, tt.wantMetrics.MemoryUsed, got.MemoryUsed, 0.01)
|
||||
assert.InDelta(t, tt.wantMetrics.MemoryTotal, got.MemoryTotal, 0.01)
|
||||
assert.InDelta(t, tt.wantMetrics.Usage, got.Usage, 0.01)
|
||||
if tt.wantMetrics.Temperature > 0 {
|
||||
assert.InDelta(t, tt.wantMetrics.Temperature, got.Temperature, 0.01)
|
||||
}
|
||||
assert.InDelta(t, tt.wantMetrics.Power, got.Power, 0.01)
|
||||
assert.Equal(t, tt.wantMetrics.Count, got.Count)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCurrentData(t *testing.T) {
|
||||
gm := &GPUManager{
|
||||
GpuDataMap: map[string]*system.GPUData{
|
||||
"0": {
|
||||
Name: "GPU1",
|
||||
Temperature: 50,
|
||||
MemoryUsed: 2048,
|
||||
MemoryTotal: 4096,
|
||||
Usage: 100, // 100 over 2 counts = 50 avg
|
||||
Power: 200, // 200 over 2 counts = 100 avg
|
||||
Count: 2,
|
||||
},
|
||||
"1": {
|
||||
Name: "GPU1",
|
||||
Temperature: 60,
|
||||
MemoryUsed: 3072,
|
||||
MemoryTotal: 8192,
|
||||
Usage: 30,
|
||||
Power: 60,
|
||||
Count: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := gm.GetCurrentData()
|
||||
|
||||
// Verify name disambiguation
|
||||
assert.Equal(t, "GPU1 0", result["0"].Name)
|
||||
assert.Equal(t, "GPU1 1", result["1"].Name)
|
||||
|
||||
// Check averaged values
|
||||
assert.InDelta(t, 50.0, result["0"].Usage, 0.01)
|
||||
assert.InDelta(t, 100.0, result["0"].Power, 0.01)
|
||||
assert.InDelta(t, 30.0, result["1"].Usage, 0.01)
|
||||
assert.InDelta(t, 60.0, result["1"].Power, 0.01)
|
||||
|
||||
// Verify reset counts
|
||||
assert.Equal(t, float64(1), gm.GpuDataMap["0"].Count)
|
||||
assert.Equal(t, float64(1), gm.GpuDataMap["1"].Count)
|
||||
}
|
||||
|
||||
func TestDetectGPUs(t *testing.T) {
|
||||
// Save original PATH
|
||||
origPath := os.Getenv("PATH")
|
||||
defer os.Setenv("PATH", origPath)
|
||||
|
||||
// Set up temp dir with the commands
|
||||
tempDir := t.TempDir()
|
||||
os.Setenv("PATH", tempDir)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
setupCommands func() error
|
||||
wantNvidiaSmi bool
|
||||
wantRocmSmi bool
|
||||
wantTegrastats bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "nvidia-smi not available",
|
||||
setupCommands: func() error {
|
||||
return nil
|
||||
},
|
||||
wantNvidiaSmi: false,
|
||||
wantRocmSmi: false,
|
||||
wantTegrastats: false,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "nvidia-smi available",
|
||||
setupCommands: func() error {
|
||||
path := filepath.Join(tempDir, "nvidia-smi")
|
||||
script := `#!/bin/sh
|
||||
echo "test"`
|
||||
if err := os.WriteFile(path, []byte(script), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
wantNvidiaSmi: true,
|
||||
wantTegrastats: false,
|
||||
wantRocmSmi: false,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "rocm-smi available",
|
||||
setupCommands: func() error {
|
||||
path := filepath.Join(tempDir, "rocm-smi")
|
||||
script := `#!/bin/sh
|
||||
echo "test"`
|
||||
if err := os.WriteFile(path, []byte(script), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
wantNvidiaSmi: true,
|
||||
wantRocmSmi: true,
|
||||
wantTegrastats: false,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "tegrastats available",
|
||||
setupCommands: func() error {
|
||||
path := filepath.Join(tempDir, "tegrastats")
|
||||
script := `#!/bin/sh
|
||||
echo "test"`
|
||||
if err := os.WriteFile(path, []byte(script), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
wantNvidiaSmi: true,
|
||||
wantRocmSmi: true,
|
||||
wantTegrastats: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "no gpu tools available",
|
||||
setupCommands: func() error {
|
||||
os.Setenv("PATH", "")
|
||||
return nil
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.setupCommands(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
gm := &GPUManager{}
|
||||
err := gm.detectGPUs()
|
||||
|
||||
t.Logf("nvidiaSmi: %v, rocmSmi: %v, tegrastats: %v", gm.nvidiaSmi, gm.rocmSmi, gm.tegrastats)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantNvidiaSmi, gm.nvidiaSmi)
|
||||
assert.Equal(t, tt.wantRocmSmi, gm.rocmSmi)
|
||||
assert.Equal(t, tt.wantTegrastats, gm.tegrastats)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartCollector(t *testing.T) {
|
||||
// Save original PATH
|
||||
origPath := os.Getenv("PATH")
|
||||
defer os.Setenv("PATH", origPath)
|
||||
|
||||
// Set up temp dir with the commands
|
||||
dir := t.TempDir()
|
||||
os.Setenv("PATH", dir)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
command string
|
||||
setup func(t *testing.T) error
|
||||
validate func(t *testing.T, gm *GPUManager)
|
||||
gm *GPUManager
|
||||
}{
|
||||
{
|
||||
name: "nvidia-smi collector",
|
||||
command: "nvidia-smi",
|
||||
setup: func(t *testing.T) error {
|
||||
path := filepath.Join(dir, "nvidia-smi")
|
||||
script := `#!/bin/sh
|
||||
echo "0, NVIDIA Test GPU, 50, 1024, 4096, 25, 100"`
|
||||
if err := os.WriteFile(path, []byte(script), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
validate: func(t *testing.T, gm *GPUManager) {
|
||||
gpu, exists := gm.GpuDataMap["0"]
|
||||
assert.True(t, exists)
|
||||
if exists {
|
||||
assert.Equal(t, "Test GPU", gpu.Name)
|
||||
assert.Equal(t, 50.0, gpu.Temperature)
|
||||
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rocm-smi collector",
|
||||
command: "rocm-smi",
|
||||
setup: func(t *testing.T) error {
|
||||
path := filepath.Join(dir, "rocm-smi")
|
||||
script := `#!/bin/sh
|
||||
echo '{"card0": {"Temperature (Sensor edge) (C)": "49.0", "Current Socket Graphics Package Power (W)": "28.159", "GPU use (%)": "0", "VRAM Total Memory (B)": "536870912", "VRAM Total Used Memory (B)": "445550592", "Card Series": "Rembrandt [Radeon 680M]", "Card Model": "0x1681", "Card Vendor": "Advanced Micro Devices, Inc. [AMD/ATI]", "Card SKU": "REMBRANDT", "Subsystem ID": "0x8a22", "Device Rev": "0xc8", "Node ID": "1", "GUID": "34756", "GFX Version": "gfx1035"}}'`
|
||||
if err := os.WriteFile(path, []byte(script), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
validate: func(t *testing.T, gm *GPUManager) {
|
||||
gpu, exists := gm.GpuDataMap["34756"]
|
||||
assert.True(t, exists)
|
||||
if exists {
|
||||
assert.Equal(t, "Rembrandt [Radeon 680M]", gpu.Name)
|
||||
assert.InDelta(t, 49.0, gpu.Temperature, 0.01)
|
||||
assert.InDelta(t, 28.159, gpu.Power, 0.01)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tegrastats collector",
|
||||
command: "tegrastats",
|
||||
setup: func(t *testing.T) error {
|
||||
path := filepath.Join(dir, "tegrastats")
|
||||
script := `#!/bin/sh
|
||||
echo "RAM 1024/4096MB GR3D_FREQ 80% tj@70C VDD_GPU_SOC 1000mW"`
|
||||
if err := os.WriteFile(path, []byte(script), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
validate: func(t *testing.T, gm *GPUManager) {
|
||||
gpu, exists := gm.GpuDataMap["0"]
|
||||
assert.True(t, exists)
|
||||
if exists {
|
||||
assert.InDelta(t, 70.0, gpu.Temperature, 0.1)
|
||||
}
|
||||
},
|
||||
gm: &GPUManager{
|
||||
GpuDataMap: map[string]*system.GPUData{
|
||||
"0": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.setup(t); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tt.gm == nil {
|
||||
tt.gm = &GPUManager{
|
||||
GpuDataMap: make(map[string]*system.GPUData),
|
||||
}
|
||||
}
|
||||
tt.gm.startCollector(tt.command)
|
||||
time.Sleep(50 * time.Millisecond) // Give collector time to run
|
||||
tt.validate(t, tt.gm)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package agent
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -15,7 +14,7 @@ func (a *Agent) initializeNetIoStats() {
|
||||
|
||||
// map of network interface names passed in via NICS env var
|
||||
var nicsMap map[string]struct{}
|
||||
nics, nicsEnvExists := os.LookupEnv("NICS")
|
||||
nics, nicsEnvExists := GetEnv("NICS")
|
||||
if nicsEnvExists {
|
||||
nicsMap = make(map[string]struct{}, 0)
|
||||
for _, nic := range strings.Split(nics, ",") {
|
||||
|
||||
@@ -2,33 +2,96 @@ package agent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
sshServer "github.com/gliderlabs/ssh"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func (a *Agent) startServer(pubKey []byte, addr string) {
|
||||
type ServerOptions struct {
|
||||
Addr string
|
||||
Network string
|
||||
Keys []ssh.PublicKey
|
||||
}
|
||||
|
||||
func (a *Agent) StartServer(opts ServerOptions) error {
|
||||
sshServer.Handle(a.handleSession)
|
||||
|
||||
slog.Info("Starting SSH server", "address", addr)
|
||||
if err := sshServer.ListenAndServe(addr, nil, sshServer.NoPty(),
|
||||
sshServer.PublicKeyAuth(func(ctx sshServer.Context, key sshServer.PublicKey) bool {
|
||||
allowed, _, _, _, _ := sshServer.ParseAuthorizedKey(pubKey)
|
||||
return sshServer.KeysEqual(key, allowed)
|
||||
}),
|
||||
); err != nil {
|
||||
slog.Error("Error starting SSH server", "err", err)
|
||||
os.Exit(1)
|
||||
slog.Info("Starting SSH server", "addr", opts.Addr, "network", opts.Network)
|
||||
|
||||
switch opts.Network {
|
||||
case "unix":
|
||||
// remove existing socket file if it exists
|
||||
if err := os.Remove(opts.Addr); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
// prefix with : if only port was provided
|
||||
if !strings.Contains(opts.Addr, ":") {
|
||||
opts.Addr = ":" + opts.Addr
|
||||
}
|
||||
}
|
||||
|
||||
// Listen on the address
|
||||
ln, err := net.Listen(opts.Network, opts.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
// Start SSH server on the listener
|
||||
err = sshServer.Serve(ln, nil, sshServer.NoPty(),
|
||||
sshServer.PublicKeyAuth(func(ctx sshServer.Context, key sshServer.PublicKey) bool {
|
||||
for _, pubKey := range opts.Keys {
|
||||
if sshServer.KeysEqual(key, pubKey) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Agent) handleSession(s sshServer.Session) {
|
||||
// slog.Debug("connection", "remoteaddr", s.RemoteAddr(), "user", s.User())
|
||||
stats := a.gatherStats()
|
||||
if err := json.NewEncoder(s).Encode(stats); err != nil {
|
||||
slog.Error("Error encoding stats", "err", err)
|
||||
slog.Error("Error encoding stats", "err", err, "stats", stats)
|
||||
s.Exit(1)
|
||||
return
|
||||
}
|
||||
s.Exit(0)
|
||||
}
|
||||
|
||||
// ParseKeys parses a string containing SSH public keys in authorized_keys format.
|
||||
// It returns a slice of ssh.PublicKey and an error if any key fails to parse.
|
||||
func ParseKeys(input string) ([]ssh.PublicKey, error) {
|
||||
var parsedKeys []ssh.PublicKey
|
||||
|
||||
for line := range strings.Lines(input) {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Skip empty lines or comments
|
||||
if len(line) == 0 || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse the key
|
||||
parsedKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(line))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse key: %s, error: %w", line, err)
|
||||
}
|
||||
|
||||
// Append the parsed key to the list
|
||||
parsedKeys = append(parsedKeys, parsedKey)
|
||||
}
|
||||
|
||||
return parsedKeys, nil
|
||||
}
|
||||
|
||||
289
beszel/internal/agent/server_test.go
Normal file
289
beszel/internal/agent/server_test.go
Normal file
@@ -0,0 +1,289 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func TestStartServer(t *testing.T) {
|
||||
// Generate a test key pair
|
||||
pubKey, privKey, err := ed25519.GenerateKey(nil)
|
||||
require.NoError(t, err)
|
||||
signer, err := ssh.NewSignerFromKey(privKey)
|
||||
require.NoError(t, err)
|
||||
sshPubKey, err := ssh.NewPublicKey(pubKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate a different key pair for bad key test
|
||||
badPubKey, badPrivKey, err := ed25519.GenerateKey(nil)
|
||||
require.NoError(t, err)
|
||||
badSigner, err := ssh.NewSignerFromKey(badPrivKey)
|
||||
require.NoError(t, err)
|
||||
sshBadPubKey, err := ssh.NewPublicKey(badPubKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
socketFile := filepath.Join(t.TempDir(), "beszel-test.sock")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config ServerOptions
|
||||
wantErr bool
|
||||
errContains string
|
||||
setup func() error
|
||||
cleanup func() error
|
||||
}{
|
||||
{
|
||||
name: "tcp port only",
|
||||
config: ServerOptions{
|
||||
Network: "tcp",
|
||||
Addr: "45987",
|
||||
Keys: []ssh.PublicKey{sshPubKey},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tcp with ipv4",
|
||||
config: ServerOptions{
|
||||
Network: "tcp4",
|
||||
Addr: "127.0.0.1:45988",
|
||||
Keys: []ssh.PublicKey{sshPubKey},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tcp with ipv6",
|
||||
config: ServerOptions{
|
||||
Network: "tcp6",
|
||||
Addr: "[::1]:45989",
|
||||
Keys: []ssh.PublicKey{sshPubKey},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unix socket",
|
||||
config: ServerOptions{
|
||||
Network: "unix",
|
||||
Addr: socketFile,
|
||||
Keys: []ssh.PublicKey{sshPubKey},
|
||||
},
|
||||
setup: func() error {
|
||||
// Create a socket file that should be removed
|
||||
f, err := os.Create(socketFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return f.Close()
|
||||
},
|
||||
cleanup: func() error {
|
||||
return os.Remove(socketFile)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bad key should fail",
|
||||
config: ServerOptions{
|
||||
Network: "tcp",
|
||||
Addr: "45987",
|
||||
Keys: []ssh.PublicKey{sshBadPubKey},
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "ssh: handshake failed",
|
||||
},
|
||||
{
|
||||
name: "good key still good",
|
||||
config: ServerOptions{
|
||||
Network: "tcp",
|
||||
Addr: "45987",
|
||||
Keys: []ssh.PublicKey{sshPubKey},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.setup != nil {
|
||||
err := tt.setup()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
if tt.cleanup != nil {
|
||||
defer tt.cleanup()
|
||||
}
|
||||
|
||||
agent := NewAgent()
|
||||
|
||||
// Start server in a goroutine since it blocks
|
||||
errChan := make(chan error, 1)
|
||||
go func() {
|
||||
errChan <- agent.StartServer(tt.config)
|
||||
}()
|
||||
|
||||
// Add a short delay to allow the server to start
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Try to connect to verify server is running
|
||||
var client *ssh.Client
|
||||
var err error
|
||||
|
||||
// Choose the appropriate signer based on the test case
|
||||
testSigner := signer
|
||||
if tt.name == "bad key should fail" {
|
||||
testSigner = badSigner
|
||||
}
|
||||
|
||||
sshClientConfig := &ssh.ClientConfig{
|
||||
User: "a",
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.PublicKeys(testSigner),
|
||||
},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
Timeout: 4 * time.Second,
|
||||
}
|
||||
|
||||
switch tt.config.Network {
|
||||
case "unix":
|
||||
client, err = ssh.Dial("unix", tt.config.Addr, sshClientConfig)
|
||||
default:
|
||||
if !strings.Contains(tt.config.Addr, ":") {
|
||||
tt.config.Addr = ":" + tt.config.Addr
|
||||
}
|
||||
client, err = ssh.Dial("tcp", tt.config.Addr, sshClientConfig)
|
||||
}
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
if tt.errContains != "" {
|
||||
assert.Contains(t, err.Error(), tt.errContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, client)
|
||||
client.Close()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////
|
||||
//////////////////// ParseKeys Tests ////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////
|
||||
|
||||
// Helper function to generate a temporary file with content
|
||||
func createTempFile(content string) (string, error) {
|
||||
tmpFile, err := os.CreateTemp("", "ssh_keys_*.txt")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
defer tmpFile.Close()
|
||||
|
||||
if _, err := tmpFile.WriteString(content); err != nil {
|
||||
return "", fmt.Errorf("failed to write to temp file: %w", err)
|
||||
}
|
||||
|
||||
return tmpFile.Name(), nil
|
||||
}
|
||||
|
||||
// Test case 1: String with a single SSH key
|
||||
func TestParseSingleKeyFromString(t *testing.T) {
|
||||
input := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKCBM91kukN7hbvFKtbpEeo2JXjCcNxXcdBH7V7ADMBo"
|
||||
keys, err := ParseKeys(input)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
if len(keys) != 1 {
|
||||
t.Fatalf("Expected 1 key, got %d keys", len(keys))
|
||||
}
|
||||
if keys[0].Type() != "ssh-ed25519" {
|
||||
t.Fatalf("Expected key type 'ssh-ed25519', got '%s'", keys[0].Type())
|
||||
}
|
||||
}
|
||||
|
||||
// Test case 2: String with multiple SSH keys
|
||||
func TestParseMultipleKeysFromString(t *testing.T) {
|
||||
input := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKCBM91kukN7hbvFKtbpEeo2JXjCcNxXcdBH7V7ADMBo\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJDMtAOQfxDlCxe+A5lVbUY/DHxK1LAF2Z3AV0FYv36D \n #comment\n ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJDMtAOQfxDlCxe+A5lVbUY/DHxK1LAF2Z3AV0FYv36D"
|
||||
keys, err := ParseKeys(input)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
if len(keys) != 3 {
|
||||
t.Fatalf("Expected 3 keys, got %d keys", len(keys))
|
||||
}
|
||||
if keys[0].Type() != "ssh-ed25519" || keys[1].Type() != "ssh-ed25519" || keys[2].Type() != "ssh-ed25519" {
|
||||
t.Fatalf("Unexpected key types: %s, %s, %s", keys[0].Type(), keys[1].Type(), keys[2].Type())
|
||||
}
|
||||
}
|
||||
|
||||
// Test case 3: File with a single SSH key
|
||||
func TestParseSingleKeyFromFile(t *testing.T) {
|
||||
content := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKCBM91kukN7hbvFKtbpEeo2JXjCcNxXcdBH7V7ADMBo"
|
||||
filePath, err := createTempFile(content)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(filePath) // Clean up the file after the test
|
||||
|
||||
// Read the file content
|
||||
fileContent, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read temp file: %v", err)
|
||||
}
|
||||
|
||||
// Parse the keys
|
||||
keys, err := ParseKeys(string(fileContent))
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
if len(keys) != 1 {
|
||||
t.Fatalf("Expected 1 key, got %d keys", len(keys))
|
||||
}
|
||||
if keys[0].Type() != "ssh-ed25519" {
|
||||
t.Fatalf("Expected key type 'ssh-ed25519', got '%s'", keys[0].Type())
|
||||
}
|
||||
}
|
||||
|
||||
// Test case 4: File with multiple SSH keys
|
||||
func TestParseMultipleKeysFromFile(t *testing.T) {
|
||||
content := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKCBM91kukN7hbvFKtbpEeo2JXjCcNxXcdBH7V7ADMBo\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJDMtAOQfxDlCxe+A5lVbUY/DHxK1LAF2Z3AV0FYv36D \n #comment\n ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJDMtAOQfxDlCxe+A5lVbUY/DHxK1LAF2Z3AV0FYv36D"
|
||||
filePath, err := createTempFile(content)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp file: %v", err)
|
||||
}
|
||||
// defer os.Remove(filePath) // Clean up the file after the test
|
||||
|
||||
// Read the file content
|
||||
fileContent, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read temp file: %v", err)
|
||||
}
|
||||
|
||||
// Parse the keys
|
||||
keys, err := ParseKeys(string(fileContent))
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
if len(keys) != 3 {
|
||||
t.Fatalf("Expected 3 keys, got %d keys", len(keys))
|
||||
}
|
||||
if keys[0].Type() != "ssh-ed25519" || keys[1].Type() != "ssh-ed25519" || keys[2].Type() != "ssh-ed25519" {
|
||||
t.Fatalf("Unexpected key types: %s, %s, %s", keys[0].Type(), keys[1].Type(), keys[2].Type())
|
||||
}
|
||||
}
|
||||
|
||||
// Test case 5: Invalid SSH key input
|
||||
func TestParseInvalidKey(t *testing.T) {
|
||||
input := "invalid-key-data"
|
||||
_, err := ParseKeys(input)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected an error for invalid key, got nil")
|
||||
}
|
||||
expectedErrMsg := "failed to parse key"
|
||||
if !strings.Contains(err.Error(), expectedErrMsg) {
|
||||
t.Fatalf("Expected error message to contain '%s', got: %v", expectedErrMsg, err)
|
||||
}
|
||||
}
|
||||
@@ -138,6 +138,13 @@ func (a *Agent) getSystemStats() system.Stats {
|
||||
}
|
||||
|
||||
// network stats
|
||||
if len(a.netInterfaces) == 0 {
|
||||
// if no network interfaces, initialize again
|
||||
// this is a fix if agent started before network is online (#466)
|
||||
// maybe refactor this in the future to not cache interface names at all so we
|
||||
// don't miss an interface that's been added after agent started in any circumstance
|
||||
a.initializeNetIoStats()
|
||||
}
|
||||
if netIO, err := psutilNet.IOCounters(true); err == nil {
|
||||
secondsElapsed := time.Since(a.netIoStats.Time).Seconds()
|
||||
a.netIoStats.Time = time.Now()
|
||||
@@ -178,44 +185,19 @@ func (a *Agent) getSystemStats() system.Stats {
|
||||
}
|
||||
|
||||
// temperatures (skip if sensors whitelist is set to empty string)
|
||||
if a.sensorsWhitelist != nil && len(a.sensorsWhitelist) == 0 {
|
||||
slog.Debug("Skipping temperature collection")
|
||||
} else {
|
||||
temps, err := sensors.TemperaturesWithContext(a.sensorsContext)
|
||||
if err != nil {
|
||||
slog.Debug("Sensor error", "err", err)
|
||||
}
|
||||
slog.Debug("Temperature", "sensors", temps)
|
||||
if len(temps) > 0 {
|
||||
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
|
||||
// (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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
err = a.updateTemperatures(&systemStats)
|
||||
if err != nil {
|
||||
slog.Error("Error getting temperatures", "err", err)
|
||||
}
|
||||
|
||||
// GPU data
|
||||
if a.gpuManager != nil {
|
||||
// reset high gpu percent
|
||||
a.systemInfo.GpuPct = 0
|
||||
// get current GPU data
|
||||
if gpuData := a.gpuManager.GetCurrentData(); len(gpuData) > 0 {
|
||||
systemStats.GPUData = gpuData
|
||||
|
||||
// add temperatures
|
||||
if systemStats.Temperatures == nil {
|
||||
systemStats.Temperatures = make(map[string]float64, len(gpuData))
|
||||
@@ -224,6 +206,8 @@ func (a *Agent) getSystemStats() system.Stats {
|
||||
if gpu.Temperature > 0 {
|
||||
systemStats.Temperatures[gpu.Name] = gpu.Temperature
|
||||
}
|
||||
// update high gpu percent for dashboard
|
||||
a.systemInfo.GpuPct = max(a.systemInfo.GpuPct, gpu.Usage)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,6 +223,60 @@ func (a *Agent) getSystemStats() system.Stats {
|
||||
return systemStats
|
||||
}
|
||||
|
||||
func (a *Agent) updateTemperatures(systemStats *system.Stats) error {
|
||||
// skip if sensors whitelist is set to empty string
|
||||
if a.sensorsWhitelist != nil && len(a.sensorsWhitelist) == 0 {
|
||||
slog.Debug("Skipping temperature collection")
|
||||
return nil
|
||||
}
|
||||
|
||||
primarySensor, primarySensorIsDefined := GetEnv("PRIMARY_SENSOR")
|
||||
|
||||
// reset high temp
|
||||
a.systemInfo.DashboardTemp = 0
|
||||
|
||||
// get sensor data
|
||||
temps, err := sensors.TemperaturesWithContext(a.sensorsContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
slog.Debug("Temperature", "sensors", temps)
|
||||
|
||||
// return if no sensors
|
||||
if len(temps) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
systemStats.Temperatures = make(map[string]float64, len(temps))
|
||||
for i, sensor := range temps {
|
||||
// skip if temperature is unreasonable
|
||||
if sensor.Temperature <= 0 || sensor.Temperature >= 200 {
|
||||
continue
|
||||
}
|
||||
sensorName := sensor.SensorKey
|
||||
if _, ok := systemStats.Temperatures[sensorName]; ok {
|
||||
// if key already exists, append int to key
|
||||
sensorName = sensorName + "_" + strconv.Itoa(i)
|
||||
}
|
||||
// skip if not in whitelist
|
||||
if a.sensorsWhitelist != nil {
|
||||
if _, nameInWhitelist := a.sensorsWhitelist[sensorName]; !nameInWhitelist {
|
||||
continue
|
||||
}
|
||||
}
|
||||
// set dashboard temperature
|
||||
if primarySensorIsDefined {
|
||||
if sensorName == primarySensor {
|
||||
a.systemInfo.DashboardTemp = sensor.Temperature
|
||||
}
|
||||
} else {
|
||||
a.systemInfo.DashboardTemp = max(a.systemInfo.DashboardTemp, sensor.Temperature)
|
||||
}
|
||||
systemStats.Temperatures[sensorName] = twoDecimals(sensor.Temperature)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns the size of the ZFS ARC memory cache in bytes
|
||||
func getARCSize() (uint64, error) {
|
||||
file, err := os.Open("/proc/spl/kstat/zfs/arcstats")
|
||||
|
||||
@@ -2,26 +2,24 @@
|
||||
package alerts
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/shoutrrr"
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/mailer"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
type AlertManager struct {
|
||||
app *pocketbase.PocketBase
|
||||
app core.App
|
||||
alertQueue chan alertTask
|
||||
stopChan chan struct{}
|
||||
pendingAlerts sync.Map
|
||||
}
|
||||
|
||||
type AlertMessageData struct {
|
||||
@@ -61,351 +59,43 @@ type SystemAlertData struct {
|
||||
descriptor string // override descriptor in notification body (for temp sensor, disk partition, etc)
|
||||
}
|
||||
|
||||
func NewAlertManager(app *pocketbase.PocketBase) *AlertManager {
|
||||
return &AlertManager{
|
||||
app: app,
|
||||
}
|
||||
// notification services that support title param
|
||||
var supportsTitle = map[string]struct{}{
|
||||
"bark": {},
|
||||
"discord": {},
|
||||
"gotify": {},
|
||||
"ifttt": {},
|
||||
"join": {},
|
||||
"matrix": {},
|
||||
"ntfy": {},
|
||||
"opsgenie": {},
|
||||
"pushbullet": {},
|
||||
"pushover": {},
|
||||
"slack": {},
|
||||
"teams": {},
|
||||
"telegram": {},
|
||||
"zulip": {},
|
||||
}
|
||||
|
||||
func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, systemInfo system.Info, temperatures map[string]float64, extraFs map[string]*system.FsStats) error {
|
||||
// start := time.Now()
|
||||
// defer func() {
|
||||
// log.Println("alert stats took", time.Since(start))
|
||||
// }()
|
||||
alertRecords, err := am.app.FindAllRecords("alerts",
|
||||
dbx.NewExp("system={:system}", dbx.Params{"system": systemRecord.Id}),
|
||||
)
|
||||
if err != nil || len(alertRecords) == 0 {
|
||||
// log.Println("no alerts found for system")
|
||||
return nil
|
||||
// NewAlertManager creates a new AlertManager instance.
|
||||
func NewAlertManager(app core.App) *AlertManager {
|
||||
am := &AlertManager{
|
||||
app: app,
|
||||
alertQueue: make(chan alertTask),
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
var validAlerts []SystemAlertData
|
||||
now := systemRecord.GetDateTime("updated").Time().UTC()
|
||||
oldestTime := now
|
||||
|
||||
for _, alertRecord := range alertRecords {
|
||||
name := alertRecord.GetString("name")
|
||||
var val float64
|
||||
unit := "%"
|
||||
|
||||
switch name {
|
||||
case "CPU":
|
||||
val = systemInfo.Cpu
|
||||
case "Memory":
|
||||
val = systemInfo.MemPct
|
||||
case "Bandwidth":
|
||||
val = systemInfo.Bandwidth
|
||||
unit = " MB/s"
|
||||
case "Disk":
|
||||
maxUsedPct := systemInfo.DiskPct
|
||||
for _, fs := range extraFs {
|
||||
usedPct := fs.DiskUsed / fs.DiskTotal * 100
|
||||
if usedPct > maxUsedPct {
|
||||
maxUsedPct = usedPct
|
||||
}
|
||||
}
|
||||
val = maxUsedPct
|
||||
case "Temperature":
|
||||
if temperatures == nil {
|
||||
continue
|
||||
}
|
||||
for _, temp := range temperatures {
|
||||
if temp > val {
|
||||
val = temp
|
||||
}
|
||||
}
|
||||
unit = "°C"
|
||||
}
|
||||
|
||||
triggered := alertRecord.GetBool("triggered")
|
||||
threshold := alertRecord.GetFloat("value")
|
||||
|
||||
// CONTINUE
|
||||
// IF alert is not triggered and curValue is less than threshold
|
||||
// OR alert is triggered and curValue is greater than threshold
|
||||
if (!triggered && val <= threshold) || (triggered && val > threshold) {
|
||||
// log.Printf("Skipping alert %s: val %f | threshold %f | triggered %v\n", name, val, threshold, triggered)
|
||||
continue
|
||||
}
|
||||
|
||||
min := max(1, cast.ToUint8(alertRecord.Get("min")))
|
||||
// add time to alert time to make sure it's slighty after record creation
|
||||
time := now.Add(-time.Duration(min) * time.Minute)
|
||||
if time.Before(oldestTime) {
|
||||
oldestTime = time
|
||||
}
|
||||
|
||||
validAlerts = append(validAlerts, SystemAlertData{
|
||||
systemRecord: systemRecord,
|
||||
alertRecord: alertRecord,
|
||||
name: name,
|
||||
unit: unit,
|
||||
val: val,
|
||||
threshold: threshold,
|
||||
triggered: triggered,
|
||||
time: time,
|
||||
min: min,
|
||||
})
|
||||
}
|
||||
|
||||
systemStats := []struct {
|
||||
Stats []byte `db:"stats"`
|
||||
Created types.DateTime `db:"created"`
|
||||
}{}
|
||||
|
||||
err = am.app.DB().
|
||||
Select("stats", "created").
|
||||
From("system_stats").
|
||||
Where(dbx.NewExp(
|
||||
"system={:system} AND type='1m' AND created > {:created}",
|
||||
dbx.Params{
|
||||
"system": systemRecord.Id,
|
||||
// subtract some time to give us a bit of buffer
|
||||
"created": oldestTime.Add(-time.Second * 90),
|
||||
},
|
||||
)).
|
||||
OrderBy("created").
|
||||
All(&systemStats)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get oldest record creation time from first record in the slice
|
||||
oldestRecordTime := systemStats[0].Created.Time()
|
||||
// log.Println("oldestRecordTime", oldestRecordTime.String())
|
||||
|
||||
// delete from validAlerts if time is older than oldestRecord
|
||||
for i := 0; i < len(validAlerts); i++ {
|
||||
if validAlerts[i].time.Before(oldestRecordTime) {
|
||||
// log.Println("deleting alert - time is older than oldestRecord", validAlerts[i].name, oldestRecordTime, validAlerts[i].time)
|
||||
validAlerts = append(validAlerts[:i], validAlerts[i+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(validAlerts) == 0 {
|
||||
// log.Println("no valid alerts found")
|
||||
return nil
|
||||
}
|
||||
|
||||
var stats SystemAlertStats
|
||||
|
||||
// we can skip the latest systemStats record since it's the current value
|
||||
for i := 0; i < len(systemStats); i++ {
|
||||
stat := systemStats[i]
|
||||
// subtract 10 seconds to give a small time buffer
|
||||
systemStatsCreation := stat.Created.Time().Add(-time.Second * 10)
|
||||
if err := json.Unmarshal(stat.Stats, &stats); err != nil {
|
||||
return err
|
||||
}
|
||||
// log.Println("stats", stats)
|
||||
for j := range validAlerts {
|
||||
alert := &validAlerts[j]
|
||||
// reset alert val on first iteration
|
||||
if i == 0 {
|
||||
alert.val = 0
|
||||
}
|
||||
// continue if system_stats is older than alert time range
|
||||
if systemStatsCreation.Before(alert.time) {
|
||||
continue
|
||||
}
|
||||
// add to alert value
|
||||
switch alert.name {
|
||||
case "CPU":
|
||||
alert.val += stats.Cpu
|
||||
case "Memory":
|
||||
alert.val += stats.Mem
|
||||
case "Bandwidth":
|
||||
alert.val += stats.NetSent + stats.NetRecv
|
||||
case "Disk":
|
||||
if alert.mapSums == nil {
|
||||
alert.mapSums = make(map[string]float32, len(extraFs)+1)
|
||||
}
|
||||
// add root disk
|
||||
if _, ok := alert.mapSums["root"]; !ok {
|
||||
alert.mapSums["root"] = 0.0
|
||||
}
|
||||
alert.mapSums["root"] += float32(stats.Disk)
|
||||
// add extra disks
|
||||
for key, fs := range extraFs {
|
||||
if _, ok := alert.mapSums[key]; !ok {
|
||||
alert.mapSums[key] = 0.0
|
||||
}
|
||||
alert.mapSums[key] += float32(fs.DiskUsed / fs.DiskTotal * 100)
|
||||
}
|
||||
case "Temperature":
|
||||
if alert.mapSums == nil {
|
||||
alert.mapSums = make(map[string]float32, len(stats.Temperatures))
|
||||
}
|
||||
for key, temp := range stats.Temperatures {
|
||||
if _, ok := alert.mapSums[key]; !ok {
|
||||
alert.mapSums[key] = float32(0)
|
||||
}
|
||||
alert.mapSums[key] += temp
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
alert.count++
|
||||
}
|
||||
}
|
||||
// sum up vals for each alert
|
||||
for _, alert := range validAlerts {
|
||||
switch alert.name {
|
||||
case "Disk":
|
||||
maxPct := float32(0)
|
||||
for key, value := range alert.mapSums {
|
||||
sumPct := float32(value)
|
||||
if sumPct > maxPct {
|
||||
maxPct = sumPct
|
||||
alert.descriptor = fmt.Sprintf("Usage of %s", key)
|
||||
}
|
||||
}
|
||||
alert.val = float64(maxPct / float32(alert.count))
|
||||
case "Temperature":
|
||||
maxTemp := float32(0)
|
||||
for key, value := range alert.mapSums {
|
||||
sumTemp := float32(value) / float32(alert.count)
|
||||
if sumTemp > maxTemp {
|
||||
maxTemp = sumTemp
|
||||
alert.descriptor = fmt.Sprintf("Highest sensor %s", key)
|
||||
}
|
||||
}
|
||||
alert.val = float64(maxTemp)
|
||||
default:
|
||||
alert.val = alert.val / float64(alert.count)
|
||||
}
|
||||
minCount := float32(alert.min) / 1.2
|
||||
// log.Println("alert", alert.name, "val", alert.val, "threshold", alert.threshold, "triggered", alert.triggered)
|
||||
// log.Printf("%s: val %f | count %d | min-count %f | threshold %f\n", alert.name, alert.val, alert.count, minCount, alert.threshold)
|
||||
// pass through alert if count is greater than or equal to minCount
|
||||
if float32(alert.count) >= minCount {
|
||||
if !alert.triggered && alert.val > alert.threshold {
|
||||
alert.triggered = true
|
||||
go am.sendSystemAlert(alert)
|
||||
} else if alert.triggered && alert.val <= alert.threshold {
|
||||
alert.triggered = false
|
||||
go am.sendSystemAlert(alert)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
go am.startWorker()
|
||||
return am
|
||||
}
|
||||
|
||||
func (am *AlertManager) sendSystemAlert(alert SystemAlertData) {
|
||||
// log.Printf("Sending alert %s: val %f | count %d | threshold %f\n", alert.name, alert.val, alert.count, alert.threshold)
|
||||
systemName := alert.systemRecord.GetString("name")
|
||||
|
||||
// change Disk to Disk usage
|
||||
if alert.name == "Disk" {
|
||||
alert.name += " usage"
|
||||
}
|
||||
|
||||
// make title alert name lowercase if not CPU
|
||||
titleAlertName := alert.name
|
||||
if titleAlertName != "CPU" {
|
||||
titleAlertName = strings.ToLower(titleAlertName)
|
||||
}
|
||||
|
||||
var subject string
|
||||
if alert.triggered {
|
||||
subject = fmt.Sprintf("%s %s above threshold", systemName, titleAlertName)
|
||||
} else {
|
||||
subject = fmt.Sprintf("%s %s below threshold", systemName, titleAlertName)
|
||||
}
|
||||
minutesLabel := "minute"
|
||||
if alert.min > 1 {
|
||||
minutesLabel += "s"
|
||||
}
|
||||
if alert.descriptor == "" {
|
||||
alert.descriptor = alert.name
|
||||
}
|
||||
body := fmt.Sprintf("%s averaged %.2f%s for the previous %v %s.", alert.descriptor, alert.val, alert.unit, alert.min, minutesLabel)
|
||||
|
||||
alert.alertRecord.Set("triggered", alert.triggered)
|
||||
if err := am.app.Save(alert.alertRecord); err != nil {
|
||||
// app.Logger().Error("failed to save alert record", "err", err.Error())
|
||||
return
|
||||
}
|
||||
// expand the user relation and send the alert
|
||||
if errs := am.app.ExpandRecord(alert.alertRecord, []string{"user"}, nil); len(errs) > 0 {
|
||||
// app.Logger().Error("failed to expand user relation", "errs", errs)
|
||||
return
|
||||
}
|
||||
if user := alert.alertRecord.ExpandedOne("user"); user != nil {
|
||||
am.sendAlert(AlertMessageData{
|
||||
UserID: user.Id,
|
||||
Title: subject,
|
||||
Message: body,
|
||||
Link: am.app.Settings().Meta.AppURL + "/system/" + url.PathEscape(systemName),
|
||||
LinkText: "View " + systemName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// todo: allow x minutes downtime before sending alert
|
||||
func (am *AlertManager) HandleStatusAlerts(newStatus string, oldSystemRecord *core.Record) error {
|
||||
var alertStatus string
|
||||
switch newStatus {
|
||||
case "up":
|
||||
if oldSystemRecord.GetString("status") == "down" {
|
||||
alertStatus = "up"
|
||||
}
|
||||
case "down":
|
||||
if oldSystemRecord.GetString("status") == "up" {
|
||||
alertStatus = "down"
|
||||
}
|
||||
}
|
||||
if alertStatus == "" {
|
||||
return nil
|
||||
}
|
||||
// check if use
|
||||
alertRecords, err := am.app.FindAllRecords("alerts",
|
||||
dbx.HashExp{
|
||||
"system": oldSystemRecord.Id,
|
||||
"name": "Status",
|
||||
},
|
||||
)
|
||||
if err != nil || len(alertRecords) == 0 {
|
||||
// log.Println("no alerts found for system")
|
||||
return nil
|
||||
}
|
||||
for _, alertRecord := range alertRecords {
|
||||
// expand the user relation
|
||||
if errs := am.app.ExpandRecord(alertRecord, []string{"user"}, nil); len(errs) > 0 {
|
||||
return fmt.Errorf("failed to expand: %v", errs)
|
||||
}
|
||||
user := alertRecord.ExpandedOne("user")
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
emoji := "\U0001F534"
|
||||
if alertStatus == "up" {
|
||||
emoji = "\u2705"
|
||||
}
|
||||
// send alert
|
||||
systemName := oldSystemRecord.GetString("name")
|
||||
am.sendAlert(AlertMessageData{
|
||||
UserID: user.Id,
|
||||
Title: fmt.Sprintf("Connection to %s is %s %v", systemName, alertStatus, emoji),
|
||||
Message: fmt.Sprintf("Connection to %s is %s", systemName, alertStatus),
|
||||
Link: am.app.Settings().Meta.AppURL + "/system/" + url.PathEscape(systemName),
|
||||
LinkText: "View " + systemName,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *AlertManager) sendAlert(data AlertMessageData) {
|
||||
func (am *AlertManager) SendAlert(data AlertMessageData) error {
|
||||
// get user settings
|
||||
record, err := am.app.FindFirstRecordByFilter(
|
||||
"user_settings", "user={:user}",
|
||||
dbx.Params{"user": data.UserID},
|
||||
)
|
||||
if err != nil {
|
||||
am.app.Logger().Error("Failed to get user settings", "err", err.Error())
|
||||
return
|
||||
return err
|
||||
}
|
||||
// unmarshal user settings
|
||||
userAlertSettings := UserNotificationSettings{
|
||||
@@ -423,8 +113,7 @@ func (am *AlertManager) sendAlert(data AlertMessageData) {
|
||||
}
|
||||
// send alerts via email
|
||||
if len(userAlertSettings.Emails) == 0 {
|
||||
// log.Println("No email addresses found")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
addresses := []mail.Address{}
|
||||
for _, email := range userAlertSettings.Emails {
|
||||
@@ -439,18 +128,16 @@ func (am *AlertManager) sendAlert(data AlertMessageData) {
|
||||
Name: am.app.Settings().Meta.SenderName,
|
||||
},
|
||||
}
|
||||
if err := am.app.NewMailClient().Send(&message); err != nil {
|
||||
am.app.Logger().Error("Failed to send alert: ", "err", err.Error())
|
||||
} else {
|
||||
am.app.Logger().Info("Sent email alert", "to", message.To, "subj", message.Subject)
|
||||
err = am.app.NewMailClient().Send(&message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
am.app.Logger().Info("Sent email alert", "to", message.To, "subj", message.Subject)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendShoutrrrAlert sends an alert via a Shoutrrr URL
|
||||
func (am *AlertManager) SendShoutrrrAlert(notificationUrl, title, message, link, linkText string) error {
|
||||
// services that support title param
|
||||
supportsTitle := []string{"bark", "discord", "gotify", "ifttt", "join", "matrix", "ntfy", "opsgenie", "pushbullet", "pushover", "slack", "teams", "telegram", "zulip"}
|
||||
|
||||
// Parse the URL
|
||||
parsedURL, err := url.Parse(notificationUrl)
|
||||
if err != nil {
|
||||
@@ -460,7 +147,7 @@ func (am *AlertManager) SendShoutrrrAlert(notificationUrl, title, message, link,
|
||||
queryParams := parsedURL.Query()
|
||||
|
||||
// Add title
|
||||
if sliceContains(supportsTitle, scheme) {
|
||||
if _, ok := supportsTitle[scheme]; ok {
|
||||
queryParams.Add("title", title)
|
||||
} else if scheme == "mattermost" {
|
||||
// use markdown title for mattermost
|
||||
@@ -501,16 +188,6 @@ func (am *AlertManager) SendShoutrrrAlert(notificationUrl, title, message, link,
|
||||
return nil
|
||||
}
|
||||
|
||||
// Contains checks if a string is present in a slice of strings
|
||||
func sliceContains(slice []string, item string) bool {
|
||||
for _, v := range slice {
|
||||
if v == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (am *AlertManager) SendTestNotification(e *core.RequestEvent) error {
|
||||
info, _ := e.RequestInfo()
|
||||
if info.Auth == nil {
|
||||
|
||||
175
beszel/internal/alerts/alerts_status.go
Normal file
175
beszel/internal/alerts/alerts_status.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package alerts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
type alertTask struct {
|
||||
action string // "schedule" or "cancel"
|
||||
systemName string
|
||||
alertRecord *core.Record
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
type alertInfo struct {
|
||||
systemName string
|
||||
alertRecord *core.Record
|
||||
expireTime time.Time
|
||||
}
|
||||
|
||||
// startWorker is a long-running goroutine that processes alert tasks
|
||||
// every x seconds. It must be running to process status alerts.
|
||||
func (am *AlertManager) startWorker() {
|
||||
// no special reason for 13 seconds
|
||||
tick := time.Tick(13 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-am.stopChan:
|
||||
return
|
||||
case task := <-am.alertQueue:
|
||||
switch task.action {
|
||||
case "schedule":
|
||||
am.pendingAlerts.Store(task.alertRecord.Id, &alertInfo{
|
||||
systemName: task.systemName,
|
||||
alertRecord: task.alertRecord,
|
||||
expireTime: time.Now().Add(task.delay),
|
||||
})
|
||||
case "cancel":
|
||||
am.pendingAlerts.Delete(task.alertRecord.Id)
|
||||
}
|
||||
case <-tick:
|
||||
// Check for expired alerts every tick
|
||||
now := time.Now()
|
||||
for key, value := range am.pendingAlerts.Range {
|
||||
info := value.(*alertInfo)
|
||||
if now.After(info.expireTime) {
|
||||
// Downtime delay has passed, process alert
|
||||
am.sendStatusAlert("down", info.systemName, info.alertRecord)
|
||||
am.pendingAlerts.Delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// StopWorker shuts down the AlertManager.worker goroutine
|
||||
func (am *AlertManager) StopWorker() {
|
||||
close(am.stopChan)
|
||||
}
|
||||
|
||||
// HandleStatusAlerts manages the logic when system status changes.
|
||||
func (am *AlertManager) HandleStatusAlerts(newStatus string, oldSystemRecord *core.Record) error {
|
||||
switch newStatus {
|
||||
case "up":
|
||||
if oldSystemRecord.GetString("status") != "down" {
|
||||
return nil
|
||||
}
|
||||
case "down":
|
||||
if oldSystemRecord.GetString("status") != "up" {
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
alertRecords, err := am.getSystemStatusAlerts(oldSystemRecord.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(alertRecords) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
systemName := oldSystemRecord.GetString("name")
|
||||
if newStatus == "down" {
|
||||
am.handleSystemDown(systemName, alertRecords)
|
||||
} else {
|
||||
am.handleSystemUp(systemName, alertRecords)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSystemStatusAlerts retrieves all "Status" alert records for a given system ID.
|
||||
func (am *AlertManager) getSystemStatusAlerts(systemID string) ([]*core.Record, error) {
|
||||
alertRecords, err := am.app.FindAllRecords("alerts", dbx.HashExp{
|
||||
"system": systemID,
|
||||
"name": "Status",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return alertRecords, nil
|
||||
}
|
||||
|
||||
// Schedules delayed "down" alerts for each alert record.
|
||||
func (am *AlertManager) handleSystemDown(systemName string, alertRecords []*core.Record) {
|
||||
for _, alertRecord := range alertRecords {
|
||||
// Continue if alert is already scheduled
|
||||
if _, exists := am.pendingAlerts.Load(alertRecord.Id); exists {
|
||||
continue
|
||||
}
|
||||
// Schedule by adding to queue
|
||||
min := max(1, alertRecord.GetInt("min"))
|
||||
am.alertQueue <- alertTask{
|
||||
action: "schedule",
|
||||
systemName: systemName,
|
||||
alertRecord: alertRecord,
|
||||
delay: time.Duration(min) * time.Minute,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleSystemUp manages the logic when a system status changes to "up".
|
||||
// It cancels any pending alerts and sends "up" alerts.
|
||||
func (am *AlertManager) handleSystemUp(systemName string, alertRecords []*core.Record) {
|
||||
for _, alertRecord := range alertRecords {
|
||||
alertRecordID := alertRecord.Id
|
||||
// If alert exists for record, delete and continue (down alert not sent)
|
||||
if _, exists := am.pendingAlerts.Load(alertRecordID); exists {
|
||||
am.alertQueue <- alertTask{
|
||||
action: "cancel",
|
||||
alertRecord: alertRecord,
|
||||
}
|
||||
continue
|
||||
}
|
||||
// No alert scheduled for this record, send "up" alert
|
||||
if err := am.sendStatusAlert("up", systemName, alertRecord); err != nil {
|
||||
am.app.Logger().Error("Failed to send alert", "err", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sendStatusAlert sends a status alert ("up" or "down") to the users associated with the alert records.
|
||||
func (am *AlertManager) sendStatusAlert(alertStatus string, systemName string, alertRecord *core.Record) error {
|
||||
var emoji string
|
||||
if alertStatus == "up" {
|
||||
emoji = "\u2705" // Green checkmark emoji
|
||||
} else {
|
||||
emoji = "\U0001F534" // Red alert emoji
|
||||
}
|
||||
|
||||
title := fmt.Sprintf("Connection to %s is %s %v", systemName, alertStatus, emoji)
|
||||
message := strings.TrimSuffix(title, emoji)
|
||||
|
||||
if errs := am.app.ExpandRecord(alertRecord, []string{"user"}, nil); len(errs) > 0 {
|
||||
return errs["user"]
|
||||
}
|
||||
user := alertRecord.ExpandedOne("user")
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return am.SendAlert(AlertMessageData{
|
||||
UserID: user.Id,
|
||||
Title: title,
|
||||
Message: message,
|
||||
Link: am.app.Settings().Meta.AppURL + "/system/" + url.PathEscape(systemName),
|
||||
LinkText: "View " + systemName,
|
||||
})
|
||||
}
|
||||
288
beszel/internal/alerts/alerts_system.go
Normal file
288
beszel/internal/alerts/alerts_system.go
Normal file
@@ -0,0 +1,288 @@
|
||||
package alerts
|
||||
|
||||
import (
|
||||
"beszel/internal/entities/system"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, systemInfo system.Info, temperatures map[string]float64, extraFs map[string]*system.FsStats) error {
|
||||
alertRecords, err := am.app.FindAllRecords("alerts",
|
||||
dbx.NewExp("system={:system}", dbx.Params{"system": systemRecord.Id}),
|
||||
)
|
||||
if err != nil || len(alertRecords) == 0 {
|
||||
// log.Println("no alerts found for system")
|
||||
return nil
|
||||
}
|
||||
|
||||
var validAlerts []SystemAlertData
|
||||
now := systemRecord.GetDateTime("updated").Time().UTC()
|
||||
oldestTime := now
|
||||
|
||||
for _, alertRecord := range alertRecords {
|
||||
name := alertRecord.GetString("name")
|
||||
var val float64
|
||||
unit := "%"
|
||||
|
||||
switch name {
|
||||
case "CPU":
|
||||
val = systemInfo.Cpu
|
||||
case "Memory":
|
||||
val = systemInfo.MemPct
|
||||
case "Bandwidth":
|
||||
val = systemInfo.Bandwidth
|
||||
unit = " MB/s"
|
||||
case "Disk":
|
||||
maxUsedPct := systemInfo.DiskPct
|
||||
for _, fs := range extraFs {
|
||||
usedPct := fs.DiskUsed / fs.DiskTotal * 100
|
||||
if usedPct > maxUsedPct {
|
||||
maxUsedPct = usedPct
|
||||
}
|
||||
}
|
||||
val = maxUsedPct
|
||||
case "Temperature":
|
||||
if temperatures == nil {
|
||||
continue
|
||||
}
|
||||
for _, temp := range temperatures {
|
||||
if temp > val {
|
||||
val = temp
|
||||
}
|
||||
}
|
||||
unit = "°C"
|
||||
}
|
||||
|
||||
triggered := alertRecord.GetBool("triggered")
|
||||
threshold := alertRecord.GetFloat("value")
|
||||
|
||||
// CONTINUE
|
||||
// IF alert is not triggered and curValue is less than threshold
|
||||
// OR alert is triggered and curValue is greater than threshold
|
||||
if (!triggered && val <= threshold) || (triggered && val > threshold) {
|
||||
// log.Printf("Skipping alert %s: val %f | threshold %f | triggered %v\n", name, val, threshold, triggered)
|
||||
continue
|
||||
}
|
||||
|
||||
min := max(1, cast.ToUint8(alertRecord.Get("min")))
|
||||
// add time to alert time to make sure it's slighty after record creation
|
||||
time := now.Add(-time.Duration(min) * time.Minute)
|
||||
if time.Before(oldestTime) {
|
||||
oldestTime = time
|
||||
}
|
||||
|
||||
validAlerts = append(validAlerts, SystemAlertData{
|
||||
systemRecord: systemRecord,
|
||||
alertRecord: alertRecord,
|
||||
name: name,
|
||||
unit: unit,
|
||||
val: val,
|
||||
threshold: threshold,
|
||||
triggered: triggered,
|
||||
time: time,
|
||||
min: min,
|
||||
})
|
||||
}
|
||||
|
||||
systemStats := []struct {
|
||||
Stats []byte `db:"stats"`
|
||||
Created types.DateTime `db:"created"`
|
||||
}{}
|
||||
|
||||
err = am.app.DB().
|
||||
Select("stats", "created").
|
||||
From("system_stats").
|
||||
Where(dbx.NewExp(
|
||||
"system={:system} AND type='1m' AND created > {:created}",
|
||||
dbx.Params{
|
||||
"system": systemRecord.Id,
|
||||
// subtract some time to give us a bit of buffer
|
||||
"created": oldestTime.Add(-time.Second * 90),
|
||||
},
|
||||
)).
|
||||
OrderBy("created").
|
||||
All(&systemStats)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get oldest record creation time from first record in the slice
|
||||
oldestRecordTime := systemStats[0].Created.Time()
|
||||
// log.Println("oldestRecordTime", oldestRecordTime.String())
|
||||
|
||||
// delete from validAlerts if time is older than oldestRecord
|
||||
for i := range validAlerts {
|
||||
if validAlerts[i].time.Before(oldestRecordTime) {
|
||||
// log.Println("deleting alert - time is older than oldestRecord", validAlerts[i].name, oldestRecordTime, validAlerts[i].time)
|
||||
validAlerts = slices.Delete(validAlerts, i, i+1)
|
||||
}
|
||||
}
|
||||
|
||||
if len(validAlerts) == 0 {
|
||||
// log.Println("no valid alerts found")
|
||||
return nil
|
||||
}
|
||||
|
||||
var stats SystemAlertStats
|
||||
|
||||
// we can skip the latest systemStats record since it's the current value
|
||||
for i := range systemStats {
|
||||
stat := systemStats[i]
|
||||
// subtract 10 seconds to give a small time buffer
|
||||
systemStatsCreation := stat.Created.Time().Add(-time.Second * 10)
|
||||
if err := json.Unmarshal(stat.Stats, &stats); err != nil {
|
||||
return err
|
||||
}
|
||||
// log.Println("stats", stats)
|
||||
for j := range validAlerts {
|
||||
alert := &validAlerts[j]
|
||||
// reset alert val on first iteration
|
||||
if i == 0 {
|
||||
alert.val = 0
|
||||
}
|
||||
// continue if system_stats is older than alert time range
|
||||
if systemStatsCreation.Before(alert.time) {
|
||||
continue
|
||||
}
|
||||
// add to alert value
|
||||
switch alert.name {
|
||||
case "CPU":
|
||||
alert.val += stats.Cpu
|
||||
case "Memory":
|
||||
alert.val += stats.Mem
|
||||
case "Bandwidth":
|
||||
alert.val += stats.NetSent + stats.NetRecv
|
||||
case "Disk":
|
||||
if alert.mapSums == nil {
|
||||
alert.mapSums = make(map[string]float32, len(extraFs)+1)
|
||||
}
|
||||
// add root disk
|
||||
if _, ok := alert.mapSums["root"]; !ok {
|
||||
alert.mapSums["root"] = 0.0
|
||||
}
|
||||
alert.mapSums["root"] += float32(stats.Disk)
|
||||
// add extra disks
|
||||
for key, fs := range extraFs {
|
||||
if _, ok := alert.mapSums[key]; !ok {
|
||||
alert.mapSums[key] = 0.0
|
||||
}
|
||||
alert.mapSums[key] += float32(fs.DiskUsed / fs.DiskTotal * 100)
|
||||
}
|
||||
case "Temperature":
|
||||
if alert.mapSums == nil {
|
||||
alert.mapSums = make(map[string]float32, len(stats.Temperatures))
|
||||
}
|
||||
for key, temp := range stats.Temperatures {
|
||||
if _, ok := alert.mapSums[key]; !ok {
|
||||
alert.mapSums[key] = float32(0)
|
||||
}
|
||||
alert.mapSums[key] += temp
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
alert.count++
|
||||
}
|
||||
}
|
||||
// sum up vals for each alert
|
||||
for _, alert := range validAlerts {
|
||||
switch alert.name {
|
||||
case "Disk":
|
||||
maxPct := float32(0)
|
||||
for key, value := range alert.mapSums {
|
||||
sumPct := float32(value)
|
||||
if sumPct > maxPct {
|
||||
maxPct = sumPct
|
||||
alert.descriptor = fmt.Sprintf("Usage of %s", key)
|
||||
}
|
||||
}
|
||||
alert.val = float64(maxPct / float32(alert.count))
|
||||
case "Temperature":
|
||||
maxTemp := float32(0)
|
||||
for key, value := range alert.mapSums {
|
||||
sumTemp := float32(value) / float32(alert.count)
|
||||
if sumTemp > maxTemp {
|
||||
maxTemp = sumTemp
|
||||
alert.descriptor = fmt.Sprintf("Highest sensor %s", key)
|
||||
}
|
||||
}
|
||||
alert.val = float64(maxTemp)
|
||||
default:
|
||||
alert.val = alert.val / float64(alert.count)
|
||||
}
|
||||
minCount := float32(alert.min) / 1.2
|
||||
// log.Println("alert", alert.name, "val", alert.val, "threshold", alert.threshold, "triggered", alert.triggered)
|
||||
// log.Printf("%s: val %f | count %d | min-count %f | threshold %f\n", alert.name, alert.val, alert.count, minCount, alert.threshold)
|
||||
// pass through alert if count is greater than or equal to minCount
|
||||
if float32(alert.count) >= minCount {
|
||||
if !alert.triggered && alert.val > alert.threshold {
|
||||
alert.triggered = true
|
||||
go am.sendSystemAlert(alert)
|
||||
} else if alert.triggered && alert.val <= alert.threshold {
|
||||
alert.triggered = false
|
||||
go am.sendSystemAlert(alert)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *AlertManager) sendSystemAlert(alert SystemAlertData) {
|
||||
// log.Printf("Sending alert %s: val %f | count %d | threshold %f\n", alert.name, alert.val, alert.count, alert.threshold)
|
||||
systemName := alert.systemRecord.GetString("name")
|
||||
|
||||
// change Disk to Disk usage
|
||||
if alert.name == "Disk" {
|
||||
alert.name += " usage"
|
||||
}
|
||||
|
||||
// make title alert name lowercase if not CPU
|
||||
titleAlertName := alert.name
|
||||
if titleAlertName != "CPU" {
|
||||
titleAlertName = strings.ToLower(titleAlertName)
|
||||
}
|
||||
|
||||
var subject string
|
||||
if alert.triggered {
|
||||
subject = fmt.Sprintf("%s %s above threshold", systemName, titleAlertName)
|
||||
} else {
|
||||
subject = fmt.Sprintf("%s %s below threshold", systemName, titleAlertName)
|
||||
}
|
||||
minutesLabel := "minute"
|
||||
if alert.min > 1 {
|
||||
minutesLabel += "s"
|
||||
}
|
||||
if alert.descriptor == "" {
|
||||
alert.descriptor = alert.name
|
||||
}
|
||||
body := fmt.Sprintf("%s averaged %.2f%s for the previous %v %s.", alert.descriptor, alert.val, alert.unit, alert.min, minutesLabel)
|
||||
|
||||
alert.alertRecord.Set("triggered", alert.triggered)
|
||||
if err := am.app.Save(alert.alertRecord); err != nil {
|
||||
// app.Logger().Error("failed to save alert record", "err", err.Error())
|
||||
return
|
||||
}
|
||||
// expand the user relation and send the alert
|
||||
if errs := am.app.ExpandRecord(alert.alertRecord, []string{"user"}, nil); len(errs) > 0 {
|
||||
// app.Logger().Error("failed to expand user relation", "errs", errs)
|
||||
return
|
||||
}
|
||||
if user := alert.alertRecord.ExpandedOne("user"); user != nil {
|
||||
am.SendAlert(AlertMessageData{
|
||||
UserID: user.Id,
|
||||
Title: subject,
|
||||
Message: body,
|
||||
Link: am.app.Settings().Meta.AppURL + "/system/" + url.PathEscape(systemName),
|
||||
LinkText: "View " + systemName,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,8 @@ type Info struct {
|
||||
Bandwidth float64 `json:"b"`
|
||||
AgentVersion string `json:"v"`
|
||||
Podman bool `json:"p,omitempty"`
|
||||
GpuPct float64 `json:"g,omitempty"`
|
||||
DashboardTemp float64 `json:"dt,omitempty"`
|
||||
}
|
||||
|
||||
// Final data structure to return to the hub
|
||||
|
||||
@@ -28,7 +28,7 @@ type SystemConfig struct {
|
||||
|
||||
// Syncs systems with the config.yml file
|
||||
func (h *Hub) syncSystemsWithConfig() error {
|
||||
configPath := filepath.Join(h.app.DataDir(), "config.yml")
|
||||
configPath := filepath.Join(h.DataDir(), "config.yml")
|
||||
configData, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -49,7 +49,7 @@ func (h *Hub) syncSystemsWithConfig() error {
|
||||
|
||||
// Create a map of email to user ID
|
||||
userEmailToID := make(map[string]string)
|
||||
users, err := h.app.FindAllRecords("users", dbx.NewExp("id != ''"))
|
||||
users, err := h.FindAllRecords("users", dbx.NewExp("id != ''"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -84,7 +84,7 @@ func (h *Hub) syncSystemsWithConfig() error {
|
||||
}
|
||||
|
||||
// Get existing systems
|
||||
existingSystems, err := h.app.FindAllRecords("systems", dbx.NewExp("id != ''"))
|
||||
existingSystems, err := h.FindAllRecords("systems", dbx.NewExp("id != ''"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -104,13 +104,13 @@ func (h *Hub) syncSystemsWithConfig() error {
|
||||
existingSystem.Set("name", sysConfig.Name)
|
||||
existingSystem.Set("users", sysConfig.Users)
|
||||
existingSystem.Set("port", sysConfig.Port)
|
||||
if err := h.app.Save(existingSystem); err != nil {
|
||||
if err := h.Save(existingSystem); err != nil {
|
||||
return err
|
||||
}
|
||||
delete(existingSystemsMap, key)
|
||||
} else {
|
||||
// Create new system
|
||||
systemsCollection, err := h.app.FindCollectionByNameOrId("systems")
|
||||
systemsCollection, err := h.FindCollectionByNameOrId("systems")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find systems collection: %v", err)
|
||||
}
|
||||
@@ -121,7 +121,7 @@ func (h *Hub) syncSystemsWithConfig() error {
|
||||
newSystem.Set("users", sysConfig.Users)
|
||||
newSystem.Set("info", system.Info{})
|
||||
newSystem.Set("status", "pending")
|
||||
if err := h.app.Save(newSystem); err != nil {
|
||||
if err := h.Save(newSystem); err != nil {
|
||||
return fmt.Errorf("failed to create new system: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,7 @@ func (h *Hub) syncSystemsWithConfig() error {
|
||||
|
||||
// Delete systems not in config
|
||||
for _, system := range existingSystemsMap {
|
||||
if err := h.app.Delete(system); err != nil {
|
||||
if err := h.Delete(system); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -141,7 +141,7 @@ func (h *Hub) syncSystemsWithConfig() error {
|
||||
// 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.FindRecordsByFilter("systems", "id != ''", "name", -1, 0)
|
||||
systems, err := h.FindRecordsByFilter("systems", "id != ''", "name", -1, 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -194,7 +194,7 @@ func (h *Hub) generateConfigYAML() (string, error) {
|
||||
|
||||
// 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.FindRecordsByIds("users", userIDs)
|
||||
users, err := h.FindRecordsByIds("users", userIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
"beszel/internal/records"
|
||||
"beszel/internal/users"
|
||||
"beszel/site"
|
||||
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
@@ -28,72 +27,101 @@ import (
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/plugins/migratecmd"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type Hub struct {
|
||||
app *pocketbase.PocketBase
|
||||
systemConnections sync.Map
|
||||
sshClientConfig *ssh.ClientConfig
|
||||
pubKey string
|
||||
am *alerts.AlertManager
|
||||
um *users.UserManager
|
||||
rm *records.RecordManager
|
||||
systemStats *core.Collection
|
||||
containerStats *core.Collection
|
||||
*pocketbase.PocketBase
|
||||
sshClientConfig *ssh.ClientConfig
|
||||
pubKey string
|
||||
am *alerts.AlertManager
|
||||
um *users.UserManager
|
||||
rm *records.RecordManager
|
||||
systemStats *core.Collection
|
||||
containerStats *core.Collection
|
||||
appURL string
|
||||
}
|
||||
|
||||
func NewHub(app *pocketbase.PocketBase) *Hub {
|
||||
return &Hub{
|
||||
app: app,
|
||||
am: alerts.NewAlertManager(app),
|
||||
um: users.NewUserManager(app),
|
||||
rm: records.NewRecordManager(app),
|
||||
// NewHub creates a new Hub instance with default configuration
|
||||
func NewHub() *Hub {
|
||||
var hub Hub
|
||||
hub.PocketBase = pocketbase.NewWithConfig(pocketbase.Config{
|
||||
DefaultDataDir: beszel.AppName + "_data",
|
||||
})
|
||||
|
||||
hub.RootCmd.Version = beszel.Version
|
||||
hub.RootCmd.Use = beszel.AppName
|
||||
hub.RootCmd.Short = ""
|
||||
// add update command
|
||||
hub.RootCmd.AddCommand(&cobra.Command{
|
||||
Use: "update",
|
||||
Short: "Update " + beszel.AppName + " to the latest version",
|
||||
Run: Update,
|
||||
})
|
||||
|
||||
hub.am = alerts.NewAlertManager(hub)
|
||||
hub.um = users.NewUserManager(hub)
|
||||
hub.rm = records.NewRecordManager(hub)
|
||||
hub.appURL, _ = GetEnv("APP_URL")
|
||||
return &hub
|
||||
}
|
||||
|
||||
// GetEnv retrieves an environment variable with a "BESZEL_HUB_" prefix, or falls back to the unprefixed key.
|
||||
func GetEnv(key string) (value string, exists bool) {
|
||||
if value, exists = os.LookupEnv("BESZEL_HUB_" + key); exists {
|
||||
return value, exists
|
||||
}
|
||||
// Fallback to the old unprefixed key
|
||||
return os.LookupEnv(key)
|
||||
}
|
||||
|
||||
func (h *Hub) Run() {
|
||||
// loosely check if it was executed using "go run"
|
||||
isGoRun := strings.HasPrefix(os.Args[0], os.TempDir())
|
||||
isDev := os.Getenv("ENV") == "dev"
|
||||
|
||||
// enable auto creation of migration files when making collection changes in the Admin UI
|
||||
migratecmd.MustRegister(h.app, h.app.RootCmd, migratecmd.Config{
|
||||
// (the isGoRun check is to enable it only during development)
|
||||
Automigrate: isGoRun,
|
||||
migratecmd.MustRegister(h, h.RootCmd, migratecmd.Config{
|
||||
// (the isDev check is to enable it only during development)
|
||||
Automigrate: isDev,
|
||||
Dir: "../../migrations",
|
||||
})
|
||||
|
||||
// initial setup
|
||||
h.app.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
||||
h.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
||||
// create ssh client config
|
||||
err := h.createSSHClientConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// set general settings
|
||||
settings := h.app.Settings()
|
||||
settings := h.Settings()
|
||||
// batch requests (for global alerts)
|
||||
settings.Batch.Enabled = true
|
||||
// set URL if BASE_URL env is set
|
||||
if h.appURL != "" {
|
||||
settings.Meta.AppURL = h.appURL
|
||||
}
|
||||
// set auth settings
|
||||
usersCollection, err := h.app.FindCollectionByNameOrId("users")
|
||||
usersCollection, err := h.FindCollectionByNameOrId("users")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// disable email auth if DISABLE_PASSWORD_AUTH env var is set
|
||||
usersCollection.PasswordAuth.Enabled = os.Getenv("DISABLE_PASSWORD_AUTH") != "true"
|
||||
disablePasswordAuth, _ := GetEnv("DISABLE_PASSWORD_AUTH")
|
||||
usersCollection.PasswordAuth.Enabled = disablePasswordAuth != "true"
|
||||
usersCollection.PasswordAuth.IdentityFields = []string{"email"}
|
||||
// disable oauth if no providers are configured (todo: remove this in post 0.9.0 release)
|
||||
if usersCollection.OAuth2.Enabled {
|
||||
usersCollection.OAuth2.Enabled = len(usersCollection.OAuth2.Providers) > 0
|
||||
}
|
||||
// allow oauth user creation if USER_CREATION is set
|
||||
if os.Getenv("USER_CREATION") == "true" {
|
||||
if userCreation, _ := GetEnv("USER_CREATION"); userCreation == "true" {
|
||||
cr := "@request.context = 'oauth2'"
|
||||
usersCollection.CreateRule = &cr
|
||||
} else {
|
||||
usersCollection.CreateRule = nil
|
||||
}
|
||||
if err := h.app.Save(usersCollection); err != nil {
|
||||
if err := h.Save(usersCollection); err != nil {
|
||||
return err
|
||||
}
|
||||
// sync systems with config
|
||||
@@ -102,40 +130,60 @@ func (h *Hub) Run() {
|
||||
})
|
||||
|
||||
// serve web ui
|
||||
h.app.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
||||
switch isGoRun {
|
||||
h.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
||||
switch isDev {
|
||||
case true:
|
||||
proxy := httputil.NewSingleHostReverseProxy(&url.URL{
|
||||
Scheme: "http",
|
||||
Host: "localhost:5173",
|
||||
})
|
||||
se.Router.Any("/", func(e *core.RequestEvent) error {
|
||||
se.Router.GET("/{path...}", func(e *core.RequestEvent) error {
|
||||
proxy.ServeHTTP(e.Response, e.Request)
|
||||
return nil
|
||||
})
|
||||
default:
|
||||
csp, cspExists := os.LookupEnv("CSP")
|
||||
se.Router.Any("/{path...}", func(e *core.RequestEvent) error {
|
||||
// parse app url
|
||||
parsedURL, err := url.Parse(h.appURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// fix base paths in html if using subpath
|
||||
basePath := strings.TrimSuffix(parsedURL.Path, "/") + "/"
|
||||
indexFile, _ := fs.ReadFile(site.DistDirFS, "index.html")
|
||||
indexContent := strings.ReplaceAll(string(indexFile), "./", basePath)
|
||||
// set up static asset serving
|
||||
staticPaths := [2]string{"/static/", "/assets/"}
|
||||
serveStatic := apis.Static(site.DistDirFS, false)
|
||||
// get CSP configuration
|
||||
csp, cspExists := GetEnv("CSP")
|
||||
// add route
|
||||
se.Router.GET("/{path...}", func(e *core.RequestEvent) error {
|
||||
// serve static assets if path is in staticPaths
|
||||
for i := range staticPaths {
|
||||
if strings.Contains(e.Request.URL.Path, staticPaths[i]) {
|
||||
e.Response.Header().Set("Cache-Control", "public, max-age=2592000")
|
||||
return serveStatic(e)
|
||||
}
|
||||
}
|
||||
if cspExists {
|
||||
e.Response.Header().Del("X-Frame-Options")
|
||||
e.Response.Header().Set("Content-Security-Policy", csp)
|
||||
}
|
||||
indexFallback := !strings.HasPrefix(e.Request.URL.Path, "/static/")
|
||||
return apis.Static(site.DistDirFS, indexFallback)(e)
|
||||
return e.HTML(http.StatusOK, indexContent)
|
||||
})
|
||||
}
|
||||
return se.Next()
|
||||
})
|
||||
|
||||
// set up scheduled jobs / ticker for system updates
|
||||
h.app.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
||||
h.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
||||
// 15 second ticker for system updates
|
||||
go h.startSystemUpdateTicker()
|
||||
// set up cron jobs
|
||||
// delete old records once every hour
|
||||
h.app.Cron().MustAdd("delete old records", "8 * * * *", h.rm.DeleteOldRecords)
|
||||
h.Cron().MustAdd("delete old records", "8 * * * *", h.rm.DeleteOldRecords)
|
||||
// create longer records every 10 minutes
|
||||
h.app.Cron().MustAdd("create longer records", "*/10 * * * *", func() {
|
||||
h.Cron().MustAdd("create longer records", "*/10 * * * *", func() {
|
||||
if systemStats, containerStats, err := h.getCollections(); err == nil {
|
||||
h.rm.CreateLongerRecords([]*core.Collection{systemStats, containerStats})
|
||||
}
|
||||
@@ -144,7 +192,7 @@ func (h *Hub) Run() {
|
||||
})
|
||||
|
||||
// custom api routes
|
||||
h.app.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
||||
h.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
||||
// returns public key
|
||||
se.Router.GET("/api/beszel/getkey", func(e *core.RequestEvent) error {
|
||||
info, _ := e.RequestInfo()
|
||||
@@ -155,7 +203,7 @@ func (h *Hub) Run() {
|
||||
})
|
||||
// check if first time setup on login page
|
||||
se.Router.GET("/api/beszel/first-run", func(e *core.RequestEvent) error {
|
||||
total, err := h.app.CountRecords("users")
|
||||
total, err := h.CountRecords("users")
|
||||
return e.JSON(http.StatusOK, map[string]bool{"firstRun": err == nil && total == 0})
|
||||
})
|
||||
// send test notification
|
||||
@@ -163,31 +211,31 @@ func (h *Hub) Run() {
|
||||
// API endpoint to get config.yml content
|
||||
se.Router.GET("/api/beszel/config-yaml", h.getYamlConfig)
|
||||
// create first user endpoint only needed if no users exist
|
||||
if totalUsers, _ := h.app.CountRecords("users"); totalUsers == 0 {
|
||||
if totalUsers, _ := h.CountRecords("users"); totalUsers == 0 {
|
||||
se.Router.POST("/api/beszel/create-user", h.um.CreateFirstUser)
|
||||
}
|
||||
return se.Next()
|
||||
})
|
||||
|
||||
// system creation defaults
|
||||
h.app.OnRecordCreate("systems").BindFunc(func(e *core.RecordEvent) error {
|
||||
h.OnRecordCreate("systems").BindFunc(func(e *core.RecordEvent) error {
|
||||
e.Record.Set("info", system.Info{})
|
||||
e.Record.Set("status", "pending")
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
// immediately create connection for new systems
|
||||
h.app.OnRecordAfterCreateSuccess("systems").BindFunc(func(e *core.RecordEvent) error {
|
||||
h.OnRecordAfterCreateSuccess("systems").BindFunc(func(e *core.RecordEvent) error {
|
||||
go h.updateSystem(e.Record)
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
// handle default values for user / user_settings creation
|
||||
h.app.OnRecordCreate("users").BindFunc(h.um.InitializeUserRole)
|
||||
h.app.OnRecordCreate("user_settings").BindFunc(h.um.InitializeUserSettings)
|
||||
h.OnRecordCreate("users").BindFunc(h.um.InitializeUserRole)
|
||||
h.OnRecordCreate("user_settings").BindFunc(h.um.InitializeUserSettings)
|
||||
|
||||
// empty info for systems that are paused
|
||||
h.app.OnRecordUpdate("systems").BindFunc(func(e *core.RecordEvent) error {
|
||||
h.OnRecordUpdate("systems").BindFunc(func(e *core.RecordEvent) error {
|
||||
if e.Record.GetString("status") == "paused" {
|
||||
e.Record.Set("info", system.Info{})
|
||||
}
|
||||
@@ -195,13 +243,13 @@ func (h *Hub) Run() {
|
||||
})
|
||||
|
||||
// do things after a systems record is updated
|
||||
h.app.OnRecordAfterUpdateSuccess("systems").BindFunc(func(e *core.RecordEvent) error {
|
||||
h.OnRecordAfterUpdateSuccess("systems").BindFunc(func(e *core.RecordEvent) error {
|
||||
newRecord := e.Record.Fresh()
|
||||
oldRecord := newRecord.Original()
|
||||
newStatus := newRecord.GetString("status")
|
||||
|
||||
// if system is disconnected and connection exists, remove it
|
||||
if newStatus == "down" || newStatus == "paused" {
|
||||
// if system is not up and connection exists, remove it
|
||||
if newStatus != "up" {
|
||||
h.deleteSystemConnection(newRecord)
|
||||
}
|
||||
|
||||
@@ -210,31 +258,30 @@ func (h *Hub) Run() {
|
||||
go h.updateSystem(newRecord)
|
||||
} else {
|
||||
h.am.HandleStatusAlerts(newStatus, oldRecord)
|
||||
|
||||
}
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
// if system is deleted, close connection
|
||||
h.app.OnRecordAfterDeleteSuccess("systems").BindFunc(func(e *core.RecordEvent) error {
|
||||
h.OnRecordAfterDeleteSuccess("systems").BindFunc(func(e *core.RecordEvent) error {
|
||||
h.deleteSystemConnection(e.Record)
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
if err := h.app.Start(); err != nil {
|
||||
if err := h.Start(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) startSystemUpdateTicker() {
|
||||
ticker := time.NewTicker(15 * time.Second)
|
||||
for range ticker.C {
|
||||
c := time.Tick(15 * time.Second)
|
||||
for range c {
|
||||
h.updateSystems()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) updateSystems() {
|
||||
records, err := h.app.FindRecordsByFilter(
|
||||
records, err := h.FindRecordsByFilter(
|
||||
"2hz5ncl8tizk5nx", // systems collection
|
||||
"status != 'paused'", // filter
|
||||
"updated", // sort
|
||||
@@ -243,7 +290,7 @@ func (h *Hub) updateSystems() {
|
||||
)
|
||||
// log.Println("records", len(records))
|
||||
if err != nil || len(records) == 0 {
|
||||
// h.app.Logger().Error("Failed to query systems")
|
||||
// h.Logger().Error("Failed to query systems")
|
||||
return
|
||||
}
|
||||
fiftySecondsAgo := time.Now().UTC().Add(-50 * time.Second)
|
||||
@@ -268,52 +315,52 @@ func (h *Hub) updateSystem(record *core.Record) {
|
||||
var err error
|
||||
|
||||
// check if system connection exists
|
||||
if existingClient, ok := h.systemConnections.Load(record.Id); ok {
|
||||
if existingClient, ok := h.Store().GetOk(record.Id); ok {
|
||||
client = existingClient.(*ssh.Client)
|
||||
} else {
|
||||
// create system connection
|
||||
client, err = h.createSystemConnection(record)
|
||||
if err != nil {
|
||||
if record.GetString("status") != "down" {
|
||||
h.app.Logger().Error("Failed to connect:", "err", err.Error(), "system", record.GetString("host"), "port", record.GetString("port"))
|
||||
h.Logger().Error("Failed to connect:", "err", err.Error(), "system", record.GetString("host"), "port", record.GetString("port"))
|
||||
h.updateSystemStatus(record, "down")
|
||||
}
|
||||
return
|
||||
}
|
||||
h.systemConnections.Store(record.Id, client)
|
||||
h.Store().Set(record.Id, client)
|
||||
}
|
||||
// get system stats from agent
|
||||
var systemData system.CombinedData
|
||||
if err := h.requestJsonFromAgent(client, &systemData); err != nil {
|
||||
if err.Error() == "bad client" {
|
||||
// if previous connection was closed, try again
|
||||
h.app.Logger().Error("Existing SSH connection closed. Retrying...", "host", record.GetString("host"), "port", record.GetString("port"))
|
||||
h.Logger().Error("Existing SSH connection closed. Retrying...", "host", record.GetString("host"), "port", record.GetString("port"))
|
||||
h.deleteSystemConnection(record)
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
h.updateSystem(record)
|
||||
return
|
||||
}
|
||||
h.app.Logger().Error("Failed to get system stats: ", "err", err.Error())
|
||||
h.Logger().Error("Failed to get system stats: ", "err", err.Error())
|
||||
h.updateSystemStatus(record, "down")
|
||||
return
|
||||
}
|
||||
// update system record
|
||||
record.Set("status", "up")
|
||||
record.Set("info", systemData.Info)
|
||||
if err := h.app.SaveNoValidate(record); err != nil {
|
||||
h.app.Logger().Error("Failed to update record: ", "err", err.Error())
|
||||
if err := h.SaveNoValidate(record); err != nil {
|
||||
h.Logger().Error("Failed to update record: ", "err", err.Error())
|
||||
}
|
||||
// add system_stats and container_stats records
|
||||
if systemStats, containerStats, err := h.getCollections(); err != nil {
|
||||
h.app.Logger().Error("Failed to get collections: ", "err", err.Error())
|
||||
h.Logger().Error("Failed to get collections: ", "err", err.Error())
|
||||
} else {
|
||||
// add new system_stats record
|
||||
systemStatsRecord := core.NewRecord(systemStats)
|
||||
systemStatsRecord.Set("system", record.Id)
|
||||
systemStatsRecord.Set("stats", systemData.Stats)
|
||||
systemStatsRecord.Set("type", "1m")
|
||||
if err := h.app.SaveNoValidate(systemStatsRecord); err != nil {
|
||||
h.app.Logger().Error("Failed to save record: ", "err", err.Error())
|
||||
if err := h.SaveNoValidate(systemStatsRecord); err != nil {
|
||||
h.Logger().Error("Failed to save record: ", "err", err.Error())
|
||||
}
|
||||
// add new container_stats record
|
||||
if len(systemData.Containers) > 0 {
|
||||
@@ -321,29 +368,29 @@ func (h *Hub) updateSystem(record *core.Record) {
|
||||
containerStatsRecord.Set("system", record.Id)
|
||||
containerStatsRecord.Set("stats", systemData.Containers)
|
||||
containerStatsRecord.Set("type", "1m")
|
||||
if err := h.app.SaveNoValidate(containerStatsRecord); err != nil {
|
||||
h.app.Logger().Error("Failed to save record: ", "err", err.Error())
|
||||
if err := h.SaveNoValidate(containerStatsRecord); err != nil {
|
||||
h.Logger().Error("Failed to save record: ", "err", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// system info alerts
|
||||
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.Logger().Error("System alerts error", "err", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// return system_stats and container_stats collections
|
||||
func (h *Hub) getCollections() (*core.Collection, *core.Collection, error) {
|
||||
if h.systemStats == nil {
|
||||
systemStats, err := h.app.FindCollectionByNameOrId("system_stats")
|
||||
systemStats, err := h.FindCollectionByNameOrId("system_stats")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
h.systemStats = systemStats
|
||||
}
|
||||
if h.containerStats == nil {
|
||||
containerStats, err := h.app.FindCollectionByNameOrId("container_stats")
|
||||
containerStats, err := h.FindCollectionByNameOrId("container_stats")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -356,24 +403,31 @@ func (h *Hub) getCollections() (*core.Collection, *core.Collection, error) {
|
||||
func (h *Hub) updateSystemStatus(record *core.Record, status string) {
|
||||
if record.Fresh().GetString("status") != status {
|
||||
record.Set("status", status)
|
||||
if err := h.app.SaveNoValidate(record); err != nil {
|
||||
h.app.Logger().Error("Failed to update record: ", "err", err.Error())
|
||||
if err := h.SaveNoValidate(record); err != nil {
|
||||
h.Logger().Error("Failed to update record: ", "err", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// delete system connection from map and close connection
|
||||
func (h *Hub) deleteSystemConnection(record *core.Record) {
|
||||
if client, ok := h.systemConnections.Load(record.Id); ok {
|
||||
if client, ok := h.Store().GetOk(record.Id); ok {
|
||||
if sshClient := client.(*ssh.Client); sshClient != nil {
|
||||
sshClient.Close()
|
||||
}
|
||||
h.systemConnections.Delete(record.Id)
|
||||
h.Store().Remove(record.Id)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) createSystemConnection(record *core.Record) (*ssh.Client, error) {
|
||||
client, err := ssh.Dial("tcp", net.JoinHostPort(record.GetString("host"), record.GetString("port")), h.sshClientConfig)
|
||||
network := "tcp"
|
||||
host := record.GetString("host")
|
||||
if strings.HasPrefix(host, "/") {
|
||||
network = "unix"
|
||||
} else {
|
||||
host = net.JoinHostPort(host, record.GetString("port"))
|
||||
}
|
||||
client, err := ssh.Dial(network, host, h.sshClientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -383,7 +437,7 @@ func (h *Hub) createSystemConnection(record *core.Record) (*ssh.Client, error) {
|
||||
func (h *Hub) createSSHClientConfig() error {
|
||||
key, err := h.getSSHKey()
|
||||
if err != nil {
|
||||
h.app.Logger().Error("Failed to get SSH key: ", "err", err.Error())
|
||||
h.Logger().Error("Failed to get SSH key: ", "err", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -460,11 +514,11 @@ func newSessionWithTimeout(client *ssh.Client, timeout time.Duration) (*ssh.Sess
|
||||
}
|
||||
|
||||
func (h *Hub) getSSHKey() ([]byte, error) {
|
||||
dataDir := h.app.DataDir()
|
||||
dataDir := h.DataDir()
|
||||
// check if the key pair already exists
|
||||
existingKey, err := os.ReadFile(dataDir + "/id_ed25519")
|
||||
if err == nil {
|
||||
if pubKey, err := os.ReadFile(h.app.DataDir() + "/id_ed25519.pub"); err == nil {
|
||||
if pubKey, err := os.ReadFile(h.DataDir() + "/id_ed25519.pub"); err == nil {
|
||||
h.pubKey = strings.TrimSuffix(string(pubKey), "\n")
|
||||
}
|
||||
// return existing private key
|
||||
@@ -474,27 +528,27 @@ func (h *Hub) getSSHKey() ([]byte, error) {
|
||||
// Generate the Ed25519 key pair
|
||||
pubKey, privKey, err := ed25519.GenerateKey(nil)
|
||||
if err != nil {
|
||||
// h.app.Logger().Error("Error generating key pair:", "err", err.Error())
|
||||
// h.Logger().Error("Error generating key pair:", "err", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the private key in OpenSSH format
|
||||
privKeyBytes, err := ssh.MarshalPrivateKey(privKey, "")
|
||||
if err != nil {
|
||||
// h.app.Logger().Error("Error marshaling private key:", "err", err.Error())
|
||||
// h.Logger().Error("Error marshaling private key:", "err", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Save the private key to a file
|
||||
privateFile, err := os.Create(dataDir + "/id_ed25519")
|
||||
if err != nil {
|
||||
// h.app.Logger().Error("Error creating private key file:", "err", err.Error())
|
||||
// h.Logger().Error("Error creating private key file:", "err", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
defer privateFile.Close()
|
||||
|
||||
if err := pem.Encode(privateFile, privKeyBytes); err != nil {
|
||||
// h.app.Logger().Error("Error writing private key to file:", "err", err.Error())
|
||||
// h.Logger().Error("Error writing private key to file:", "err", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -518,9 +572,9 @@ func (h *Hub) getSSHKey() ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h.app.Logger().Info("ed25519 SSH key pair generated successfully.")
|
||||
h.app.Logger().Info("Private key saved to: " + dataDir + "/id_ed25519")
|
||||
h.app.Logger().Info("Public key saved to: " + dataDir + "/id_ed25519.pub")
|
||||
h.Logger().Info("ed25519 SSH key pair generated successfully.")
|
||||
h.Logger().Info("Private key saved to: " + dataDir + "/id_ed25519")
|
||||
h.Logger().Info("Public key saved to: " + dataDir + "/id_ed25519.pub")
|
||||
|
||||
existingKey, err = os.ReadFile(dataDir + "/id_ed25519")
|
||||
if err == nil {
|
||||
|
||||
@@ -10,13 +10,12 @@ import (
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
type RecordManager struct {
|
||||
app *pocketbase.PocketBase
|
||||
app core.App
|
||||
}
|
||||
|
||||
type LongerRecordData struct {
|
||||
@@ -35,7 +34,7 @@ type RecordStats []struct {
|
||||
Stats []byte `db:"stats"`
|
||||
}
|
||||
|
||||
func NewRecordManager(app *pocketbase.PocketBase) *RecordManager {
|
||||
func NewRecordManager(app core.App) *RecordManager {
|
||||
return &RecordManager{app}
|
||||
}
|
||||
|
||||
@@ -155,6 +154,7 @@ func (rm *RecordManager) AverageSystemStats(records RecordStats) system.Stats {
|
||||
|
||||
var stats system.Stats
|
||||
for i := range records {
|
||||
stats = system.Stats{} // Zero the struct before unmarshalling
|
||||
json.Unmarshal(records[i].Stats, &stats)
|
||||
sum.Cpu += stats.Cpu
|
||||
sum.Mem += stats.Mem
|
||||
|
||||
@@ -5,14 +5,12 @@ import (
|
||||
"beszel/migrations"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
type UserManager struct {
|
||||
app *pocketbase.PocketBase
|
||||
app core.App
|
||||
}
|
||||
|
||||
type UserSettings struct {
|
||||
@@ -22,7 +20,7 @@ type UserSettings struct {
|
||||
// Language string `json:"lang"`
|
||||
}
|
||||
|
||||
func NewUserManager(app *pocketbase.PocketBase) *UserManager {
|
||||
func NewUserManager(app core.App) *UserManager {
|
||||
return &UserManager{
|
||||
app: app,
|
||||
}
|
||||
@@ -98,9 +96,6 @@ func (um *UserManager) CreateFirstUser(e *core.RequestEvent) error {
|
||||
user.SetPassword(data.Password)
|
||||
user.Set("role", "admin")
|
||||
user.Set("verified", true)
|
||||
if username := strings.Split(data.Email, "@")[0]; len(username) > 2 {
|
||||
user.Set("username", username)
|
||||
}
|
||||
if err := um.app.Save(user); err != nil {
|
||||
return e.JSON(http.StatusInternalServerError, map[string]string{"err": err.Error()})
|
||||
}
|
||||
|
||||
98
beszel/migrations/1738624382_updated_users.go
Normal file
98
beszel/migrations/1738624382_updated_users.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("_pb_users_auth_")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"indexes": [
|
||||
"CREATE UNIQUE INDEX ` + "`" + `__pb_users_auth__email_idx` + "`" + ` ON ` + "`" + `users` + "`" + ` (` + "`" + `email` + "`" + `) WHERE ` + "`" + `email` + "`" + ` != ''",
|
||||
"CREATE UNIQUE INDEX ` + "`" + `__pb_users_auth__tokenKey_idx` + "`" + ` ON ` + "`" + `users` + "`" + ` (` + "`" + `tokenKey` + "`" + `)"
|
||||
]
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("text4166911607")
|
||||
|
||||
// update field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(3, []byte(`{
|
||||
"exceptDomains": null,
|
||||
"hidden": false,
|
||||
"id": "email3885137012",
|
||||
"name": "email",
|
||||
"onlyDomains": null,
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "email"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("_pb_users_auth_")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"indexes": [
|
||||
"CREATE UNIQUE INDEX ` + "`" + `__pb_users_auth__username_idx` + "`" + ` ON ` + "`" + `users` + "`" + ` (username COLLATE NOCASE)",
|
||||
"CREATE UNIQUE INDEX ` + "`" + `__pb_users_auth__email_idx` + "`" + ` ON ` + "`" + `users` + "`" + ` (` + "`" + `email` + "`" + `) WHERE ` + "`" + `email` + "`" + ` != ''",
|
||||
"CREATE UNIQUE INDEX ` + "`" + `__pb_users_auth__tokenKey_idx` + "`" + ` ON ` + "`" + `users` + "`" + ` (` + "`" + `tokenKey` + "`" + `)"
|
||||
]
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{
|
||||
"autogeneratePattern": "users[0-9]{6}",
|
||||
"hidden": false,
|
||||
"id": "text4166911607",
|
||||
"max": 150,
|
||||
"min": 3,
|
||||
"name": "username",
|
||||
"pattern": "^[\\w][\\w\\.\\-]*$",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(3, []byte(`{
|
||||
"exceptDomains": null,
|
||||
"hidden": false,
|
||||
"id": "email3885137012",
|
||||
"name": "email",
|
||||
"onlyDomains": null,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": true,
|
||||
"type": "email"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package migrations
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -16,6 +15,7 @@ func init() {
|
||||
settings := app.Settings()
|
||||
settings.Meta.AppName = "Beszel"
|
||||
settings.Meta.HideControls = true
|
||||
settings.Logs.MinLevel = 4
|
||||
if err := app.Save(settings); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -23,7 +23,7 @@ func init() {
|
||||
collection, _ := app.FindCollectionByNameOrId(core.CollectionNameSuperusers)
|
||||
user := core.NewRecord(collection)
|
||||
user.SetEmail(TempAdminEmail)
|
||||
user.SetPassword(security.RandomString(12))
|
||||
user.SetRandomPassword()
|
||||
return app.Save(user)
|
||||
}, nil)
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,10 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
||||
<link rel="manifest" href="./static/manifest.json" />
|
||||
<link rel="icon" type="image/svg+xml" href="./static/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Beszel</title>
|
||||
<script>window.BASE_PATH = "%BASE_URL%"</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -4,23 +4,30 @@ const config: LinguiConfig = {
|
||||
locales: [
|
||||
"en",
|
||||
"ar",
|
||||
"bg",
|
||||
"cs",
|
||||
"da",
|
||||
"de",
|
||||
"es",
|
||||
"fa",
|
||||
"fr",
|
||||
"hr",
|
||||
"hu",
|
||||
"it",
|
||||
"is",
|
||||
"ja",
|
||||
"ko",
|
||||
"nl",
|
||||
"no",
|
||||
"pl",
|
||||
"pt",
|
||||
"tr",
|
||||
"ru",
|
||||
"sl",
|
||||
"sv",
|
||||
"uk",
|
||||
"vi",
|
||||
"zh",
|
||||
"zh-CN",
|
||||
"zh-HK",
|
||||
],
|
||||
|
||||
422
beszel/site/package-lock.json
generated
422
beszel/site/package-lock.json
generated
@@ -14,20 +14,20 @@
|
||||
"@lingui/react": "^4.14.1",
|
||||
"@nanostores/react": "^0.7.3",
|
||||
"@nanostores/router": "^0.11.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||
"@radix-ui/react-checkbox": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-direction": "^1.1.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-select": "^2.1.4",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slider": "^1.2.2",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@radix-ui/react-toast": "^1.2.4",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slider": "^1.2.3",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-toast": "^1.2.6",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -35,11 +35,11 @@
|
||||
"d3-time": "^3.1.0",
|
||||
"lucide-react": "^0.452.0",
|
||||
"nanostores": "^0.11.3",
|
||||
"pocketbase": "^0.22.1",
|
||||
"pocketbase": "^0.25.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"recharts": "^2.15.0",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"recharts": "^2.15.1",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"valibot": "^0.36.0"
|
||||
},
|
||||
@@ -47,16 +47,16 @@
|
||||
"@lingui/cli": "^4.14.1",
|
||||
"@lingui/swc-plugin": "^4.1.0",
|
||||
"@lingui/vite-plugin": "^4.14.1",
|
||||
"@types/bun": "^1.1.14",
|
||||
"@types/react": "^18.3.17",
|
||||
"@types/bun": "^1.2.2",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@vitejs/plugin-react-swc": "^3.7.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss": "^8.5.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-rtl": "^0.9.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^5.4.11"
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^5.4.14"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/linux-arm64": "^0.21.5"
|
||||
@@ -1079,22 +1079,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.6.8",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz",
|
||||
"integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==",
|
||||
"version": "1.6.9",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
|
||||
"integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.8"
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.6.12",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz",
|
||||
"integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==",
|
||||
"version": "1.6.13",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
|
||||
"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.6.0",
|
||||
"@floating-ui/utils": "^0.2.8"
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
@@ -1111,9 +1111,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
|
||||
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==",
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
|
||||
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@henrygd/queue": {
|
||||
@@ -2246,17 +2246,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-alert-dialog": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.4.tgz",
|
||||
"integrity": "sha512-A6Kh23qZDLy3PSU4bh2UJZznOrUdHImIXqF8YtUa6CN73f8EOO9XlXSCd9IHyPvIquTaa/kwaSWzZTtUvgXVGw==",
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.6.tgz",
|
||||
"integrity": "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-dialog": "1.1.4",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-slot": "1.1.1"
|
||||
"@radix-ui/react-dialog": "1.1.6",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-slot": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -2274,12 +2274,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-arrow": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz",
|
||||
"integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz",
|
||||
"integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.1"
|
||||
"@radix-ui/react-primitive": "2.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -2297,16 +2297,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-checkbox": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.3.tgz",
|
||||
"integrity": "sha512-HD7/ocp8f1B3e6OHygH0n7ZKjONkhciy1Nh0yuBgObqThc3oyx+vuMfFHKAknXRHHWVE9XvXStxJFyjUmB8PIw==",
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.4.tgz",
|
||||
"integrity": "sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@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"
|
||||
@@ -2327,15 +2327,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz",
|
||||
"integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz",
|
||||
"integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-slot": "1.1.1"
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-slot": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -2383,25 +2383,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz",
|
||||
"integrity": "sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==",
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz",
|
||||
"integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.3",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.5",
|
||||
"@radix-ui/react-focus-guards": "1.1.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.2",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-portal": "1.1.3",
|
||||
"@radix-ui/react-portal": "1.1.4",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-slot": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-slot": "1.1.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"aria-hidden": "^1.1.1",
|
||||
"react-remove-scroll": "^2.6.1"
|
||||
"aria-hidden": "^1.2.4",
|
||||
"react-remove-scroll": "^2.6.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -2434,14 +2434,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz",
|
||||
"integrity": "sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==",
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
|
||||
"integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-escape-keydown": "1.1.0"
|
||||
},
|
||||
@@ -2461,17 +2461,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dropdown-menu": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.4.tgz",
|
||||
"integrity": "sha512-iXU1Ab5ecM+yEepGAWK8ZhMyKX4ubFdCNtol4sT9D0OVErG9PNElfx3TQhjw7n7BC5nFVz68/5//clWy+8TXzA==",
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.6.tgz",
|
||||
"integrity": "sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-menu": "2.1.4",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-menu": "2.1.6",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -2505,13 +2505,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-scope": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz",
|
||||
"integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz",
|
||||
"integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -2548,12 +2548,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-label": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.1.tgz",
|
||||
"integrity": "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==",
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.2.tgz",
|
||||
"integrity": "sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.1"
|
||||
"@radix-ui/react-primitive": "2.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -2571,29 +2571,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-menu": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.4.tgz",
|
||||
"integrity": "sha512-BnOgVoL6YYdHAG6DtXONaR29Eq4nvbi8rutrV/xlr3RQCMMb3yqP85Qiw/3NReozrSW+4dfLkK+rc1hb4wPU/A==",
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.6.tgz",
|
||||
"integrity": "sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-collection": "1.1.1",
|
||||
"@radix-ui/react-collection": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.3",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.5",
|
||||
"@radix-ui/react-focus-guards": "1.1.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.2",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-popper": "1.2.1",
|
||||
"@radix-ui/react-portal": "1.1.3",
|
||||
"@radix-ui/react-popper": "1.2.2",
|
||||
"@radix-ui/react-portal": "1.1.4",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-roving-focus": "1.1.1",
|
||||
"@radix-ui/react-slot": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-roving-focus": "1.1.2",
|
||||
"@radix-ui/react-slot": "1.1.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"aria-hidden": "^1.1.1",
|
||||
"react-remove-scroll": "^2.6.1"
|
||||
"aria-hidden": "^1.2.4",
|
||||
"react-remove-scroll": "^2.6.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -2611,16 +2611,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popper": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz",
|
||||
"integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz",
|
||||
"integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.0.0",
|
||||
"@radix-ui/react-arrow": "1.1.1",
|
||||
"@radix-ui/react-arrow": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||
"@radix-ui/react-use-rect": "1.1.0",
|
||||
@@ -2643,12 +2643,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz",
|
||||
"integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==",
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
|
||||
"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -2691,12 +2691,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
|
||||
"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
|
||||
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.1.1"
|
||||
"@radix-ui/react-slot": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -2714,18 +2714,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-roving-focus": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz",
|
||||
"integrity": "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz",
|
||||
"integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-collection": "1.1.1",
|
||||
"@radix-ui/react-collection": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||
},
|
||||
@@ -2745,32 +2745,32 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-select": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.4.tgz",
|
||||
"integrity": "sha512-pOkb2u8KgO47j/h7AylCj7dJsm69BXcjkrvTqMptFqsE2i0p8lHkfgneXKjAgPzBMivnoMyt8o4KiV4wYzDdyQ==",
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz",
|
||||
"integrity": "sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/number": "1.1.0",
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-collection": "1.1.1",
|
||||
"@radix-ui/react-collection": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.3",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.5",
|
||||
"@radix-ui/react-focus-guards": "1.1.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.2",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-popper": "1.2.1",
|
||||
"@radix-ui/react-portal": "1.1.3",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-slot": "1.1.1",
|
||||
"@radix-ui/react-popper": "1.2.2",
|
||||
"@radix-ui/react-portal": "1.1.4",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-slot": "1.1.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||
"@radix-ui/react-use-previous": "1.1.0",
|
||||
"@radix-ui/react-visually-hidden": "1.1.1",
|
||||
"aria-hidden": "^1.1.1",
|
||||
"react-remove-scroll": "^2.6.1"
|
||||
"@radix-ui/react-visually-hidden": "1.1.2",
|
||||
"aria-hidden": "^1.2.4",
|
||||
"react-remove-scroll": "^2.6.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -2788,12 +2788,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-separator": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.1.tgz",
|
||||
"integrity": "sha512-RRiNRSrD8iUiXriq/Y5n4/3iE8HzqgLHsusUSg5jVpU2+3tqcUFPJXHDymwEypunc2sWxDUS3UC+rkZRlHedsw==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.2.tgz",
|
||||
"integrity": "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.1"
|
||||
"@radix-ui/react-primitive": "2.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -2811,18 +2811,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.2.tgz",
|
||||
"integrity": "sha512-sNlU06ii1/ZcbHf8I9En54ZPW0Vil/yPVg4vQMcFNjrIx51jsHbFl1HYHQvCIWJSr1q0ZmA+iIs/ZTv8h7HHSA==",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.3.tgz",
|
||||
"integrity": "sha512-nNrLAWLjGESnhqBqcCNW4w2nn7LxudyMzeB6VgdyAnFLC6kfQgnAjSL2v6UkQTnDctJBlxrmxfplWS4iYjdUTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/number": "1.1.0",
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-collection": "1.1.1",
|
||||
"@radix-ui/react-collection": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||
"@radix-ui/react-use-previous": "1.1.0",
|
||||
@@ -2844,9 +2844,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
||||
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
@@ -2862,15 +2862,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.2.tgz",
|
||||
"integrity": "sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g==",
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.3.tgz",
|
||||
"integrity": "sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@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"
|
||||
@@ -2891,9 +2891,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.2.tgz",
|
||||
"integrity": "sha512-9u/tQJMcC2aGq7KXpGivMm1mgq7oRJKXphDwdypPd/j21j/2znamPU8WkXgnhUaTrSFNIt8XhOyCAupg8/GbwQ==",
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz",
|
||||
"integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
@@ -2901,8 +2901,8 @@
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-roving-focus": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-roving-focus": "1.1.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -2921,23 +2921,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toast": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.4.tgz",
|
||||
"integrity": "sha512-Sch9idFJHJTMH9YNpxxESqABcAFweJG4tKv+0zo0m5XBvUSL8FM5xKcJLFLXononpePs8IclyX1KieL5SDUNgA==",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz",
|
||||
"integrity": "sha512-gN4dpuIVKEgpLn1z5FhzT9mYRUitbfZq9XqN/7kkBMUgFTzTG8x/KszWJugJXHcwxckY8xcKDZPz7kG3o6DsUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-collection": "1.1.1",
|
||||
"@radix-ui/react-collection": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.3",
|
||||
"@radix-ui/react-portal": "1.1.3",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.5",
|
||||
"@radix-ui/react-portal": "1.1.4",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||
"@radix-ui/react-visually-hidden": "1.1.1"
|
||||
"@radix-ui/react-visually-hidden": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -2955,23 +2955,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.6.tgz",
|
||||
"integrity": "sha512-TLB5D8QLExS1uDn7+wH/bjEmRurNMTzNrtq7IjaS4kjion9NtzsTGkvR5+i7yc9q01Pi2KMM2cN3f8UG4IvvXA==",
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz",
|
||||
"integrity": "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.3",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.5",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-popper": "1.2.1",
|
||||
"@radix-ui/react-portal": "1.1.3",
|
||||
"@radix-ui/react-popper": "1.2.2",
|
||||
"@radix-ui/react-portal": "1.1.4",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-slot": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-slot": "1.1.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"@radix-ui/react-visually-hidden": "1.1.1"
|
||||
"@radix-ui/react-visually-hidden": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -3106,12 +3106,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-visually-hidden": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz",
|
||||
"integrity": "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz",
|
||||
"integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.1"
|
||||
"@radix-ui/react-primitive": "2.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -3624,13 +3624,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/bun": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.1.14.tgz",
|
||||
"integrity": "sha512-opVYiFGtO2af0dnWBdZWlioLBoxSdDO5qokaazLhq8XQtGZbY4pY3/JxY8Zdf/hEwGubbp7ErZXoN1+h2yesxA==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.2.2.tgz",
|
||||
"integrity": "sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bun-types": "1.1.37"
|
||||
"bun-types": "1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
@@ -3750,9 +3750,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.17.tgz",
|
||||
"integrity": "sha512-opAQ5no6LqJNo9TqnxBKsgnkIYHozW9KSTlFVoSUJYh1Fl/sswkEoqIugRSm7tbh6pABtYjGAjW+GOS23j8qbw==",
|
||||
"version": "18.3.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
|
||||
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3771,9 +3771,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
|
||||
"integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==",
|
||||
"version": "8.5.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz",
|
||||
"integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4100,13 +4100,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bun-types": {
|
||||
"version": "1.1.37",
|
||||
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.1.37.tgz",
|
||||
"integrity": "sha512-C65lv6eBr3LPJWFZ2gswyrGZ82ljnH8flVE03xeXxKhi2ZGtFiO4isRKTKnitbSqtRAcaqYSR6djt1whI66AbA==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.2.2.tgz",
|
||||
"integrity": "sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "~20.12.8",
|
||||
"@types/node": "*",
|
||||
"@types/ws": "~8.5.10"
|
||||
}
|
||||
},
|
||||
@@ -4724,9 +4724,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fast-equals": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz",
|
||||
"integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==",
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz",
|
||||
"integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -6167,9 +6167,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pocketbase": {
|
||||
"version": "0.22.1",
|
||||
"resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.22.1.tgz",
|
||||
"integrity": "sha512-hnzpDcIx5kbkUJXz6IkiTox6R4nb706u4rPSMy2FYCaI2OqBnFxbY0qwWBSqx1ERz/Xi61Uguf17Gb9bWZ8+kw==",
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.25.1.tgz",
|
||||
"integrity": "sha512-2IH0KLI/qMNR/E17C7BGWX2FxW7Tead+igLHOWZ45P56v/NyVT18Jnmddeft+3qWWGL1Hog2F8bc4orWV/+Fcg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pofile": {
|
||||
@@ -6180,9 +6180,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.49",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
|
||||
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
|
||||
"version": "8.5.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
|
||||
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -6199,7 +6199,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.7",
|
||||
"nanoid": "^3.3.8",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -6460,16 +6460,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-remove-scroll": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz",
|
||||
"integrity": "sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==",
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz",
|
||||
"integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-remove-scroll-bar": "^2.3.7",
|
||||
"react-style-singleton": "^2.2.1",
|
||||
"react-style-singleton": "^2.2.3",
|
||||
"tslib": "^2.1.0",
|
||||
"use-callback-ref": "^1.3.3",
|
||||
"use-sidecar": "^1.1.2"
|
||||
"use-sidecar": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -6507,9 +6507,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-smooth": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz",
|
||||
"integrity": "sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
|
||||
"integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-equals": "^5.0.1",
|
||||
@@ -6517,8 +6517,8 @@
|
||||
"react-transition-group": "^4.4.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
@@ -6596,16 +6596,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "2.15.0",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.0.tgz",
|
||||
"integrity": "sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==",
|
||||
"version": "2.15.1",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz",
|
||||
"integrity": "sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^2.0.0",
|
||||
"eventemitter3": "^4.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"react-is": "^18.3.1",
|
||||
"react-smooth": "^4.0.0",
|
||||
"react-smooth": "^4.0.4",
|
||||
"recharts-scale": "^0.4.4",
|
||||
"tiny-invariant": "^1.3.1",
|
||||
"victory-vendor": "^36.6.8"
|
||||
@@ -7034,9 +7034,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.5.tgz",
|
||||
"integrity": "sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
|
||||
"integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -7200,9 +7200,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
|
||||
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -7343,9 +7343,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
|
||||
"integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
|
||||
"version": "5.4.14",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
|
||||
"integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -17,20 +17,20 @@
|
||||
"@lingui/react": "^4.14.1",
|
||||
"@nanostores/react": "^0.7.3",
|
||||
"@nanostores/router": "^0.11.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||
"@radix-ui/react-checkbox": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-direction": "^1.1.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-select": "^2.1.4",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slider": "^1.2.2",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@radix-ui/react-toast": "^1.2.4",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slider": "^1.2.3",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-toast": "^1.2.6",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -38,11 +38,11 @@
|
||||
"d3-time": "^3.1.0",
|
||||
"lucide-react": "^0.452.0",
|
||||
"nanostores": "^0.11.3",
|
||||
"pocketbase": "^0.22.1",
|
||||
"pocketbase": "^0.25.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"recharts": "^2.15.0",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"recharts": "^2.15.1",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"valibot": "^0.36.0"
|
||||
},
|
||||
@@ -50,16 +50,16 @@
|
||||
"@lingui/cli": "^4.14.1",
|
||||
"@lingui/swc-plugin": "^4.1.0",
|
||||
"@lingui/vite-plugin": "^4.14.1",
|
||||
"@types/bun": "^1.1.14",
|
||||
"@types/react": "^18.3.17",
|
||||
"@types/bun": "^1.2.2",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@vitejs/plugin-react-swc": "^3.7.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss": "^8.5.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-rtl": "^0.9.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^5.4.11"
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^5.4.14"
|
||||
},
|
||||
"overrides": {
|
||||
"@nanostores/router": {
|
||||
|
||||
BIN
beszel/site/public/static/icon.png
Normal file
BIN
beszel/site/public/static/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
|
Before Width: | Height: | Size: 257 B |
14
beszel/site/public/static/manifest.json
Normal file
14
beszel/site/public/static/manifest.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "Beszel",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icon.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": "../",
|
||||
"display": "standalone",
|
||||
"background_color": "#202225",
|
||||
"theme_color": "#202225"
|
||||
}
|
||||
@@ -8,63 +8,27 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { $publicKey, pb } from "@/lib/stores"
|
||||
import { Copy, PlusIcon } from "lucide-react"
|
||||
import { useState, useRef, MutableRefObject } from "react"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { cn, copyToClipboard, isReadOnlyUser } from "@/lib/utils"
|
||||
import { navigate } from "./router"
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { i18n } from "@lingui/core"
|
||||
import { t, Trans } from "@lingui/macro"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { ChevronDownIcon, Copy, PlusIcon } from "lucide-react"
|
||||
import { memo, useRef, useState } from "react"
|
||||
import { basePath, navigate } from "./router"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu"
|
||||
import { SystemRecord } from "@/types"
|
||||
|
||||
export function AddSystemButton({ className }: { className?: string }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const port = useRef() as MutableRefObject<HTMLInputElement>
|
||||
const publicKey = useStore($publicKey)
|
||||
|
||||
function copyDockerCompose(port: string) {
|
||||
copyToClipboard(`services:
|
||||
beszel-agent:
|
||||
image: "henrygd/beszel-agent"
|
||||
container_name: "beszel-agent"
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
# monitor other disks / partitions by mounting a folder in /extra-filesystems
|
||||
# - /mnt/disk/.beszel:/extra-filesystems/sda1:ro
|
||||
environment:
|
||||
PORT: ${port}
|
||||
KEY: "${publicKey}"`)
|
||||
}
|
||||
|
||||
function copyInstallCommand(port: string) {
|
||||
let cmd = `curl -sL https://raw.githubusercontent.com/henrygd/beszel/main/supplemental/scripts/install-agent.sh -o install-agent.sh && chmod +x install-agent.sh && ./install-agent.sh -p ${port} -k "${publicKey}"`
|
||||
// add china mirrors flag if zh-CN
|
||||
if ((i18n.locale + navigator.language).includes("zh-CN")) {
|
||||
cmd += ` --china-mirrors`
|
||||
}
|
||||
copyToClipboard(cmd)
|
||||
}
|
||||
|
||||
async function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
const formData = new FormData(e.target as HTMLFormElement)
|
||||
const data = Object.fromEntries(formData) as Record<string, any>
|
||||
data.users = pb.authStore.record!.id
|
||||
try {
|
||||
setOpen(false)
|
||||
await pb.collection("systems").create(data)
|
||||
navigate("/")
|
||||
// console.log(record)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
let opened = useRef(false)
|
||||
if (open) {
|
||||
opened.current = true
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -80,106 +44,230 @@ export function AddSystemButton({ className }: { className?: string }) {
|
||||
</Trans>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="w-[90%] sm:max-w-[440px] rounded-lg">
|
||||
<Tabs defaultValue="docker">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="mb-2">
|
||||
<Trans>Add New System</Trans>
|
||||
</DialogTitle>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="docker">Docker</TabsTrigger>
|
||||
<TabsTrigger value="binary">
|
||||
<Trans>Binary</Trans>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</DialogHeader>
|
||||
{/* Docker */}
|
||||
<TabsContent value="docker">
|
||||
<DialogDescription className="mb-4 leading-normal">
|
||||
<Trans>
|
||||
The agent must be running on the system to connect. Copy the
|
||||
<code className="bg-muted px-1 rounded-sm leading-3">docker-compose.yml</code> for the agent below.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</TabsContent>
|
||||
{/* Binary */}
|
||||
<TabsContent value="binary">
|
||||
<DialogDescription className="mb-4 leading-normal">
|
||||
<Trans>
|
||||
The agent must be running on the system to connect. Copy the installation command for the agent below.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</TabsContent>
|
||||
<form onSubmit={handleSubmit as any}>
|
||||
<div className="grid xs:grid-cols-[auto_1fr] gap-y-3 gap-x-4 items-center mt-1 mb-4">
|
||||
<Label htmlFor="name" className="xs:text-end">
|
||||
<Trans>Name</Trans>
|
||||
</Label>
|
||||
<Input id="name" name="name" className="" required />
|
||||
<Label htmlFor="host" className="xs:text-end">
|
||||
<Trans>Host / IP</Trans>
|
||||
</Label>
|
||||
<Input id="host" name="host" className="" required />
|
||||
<Label htmlFor="port" className="xs:text-end">
|
||||
<Trans>Port</Trans>
|
||||
</Label>
|
||||
<Input ref={port} name="port" id="port" defaultValue="45876" className="" required />
|
||||
<Label htmlFor="pkey" className="xs:text-end whitespace-pre">
|
||||
<Trans comment="Use 'Key' if your language requires many more characters">Public Key</Trans>
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input readOnly id="pkey" value={publicKey} className="" required></Input>
|
||||
<div
|
||||
className={
|
||||
"h-6 w-24 bg-gradient-to-r rtl:bg-gradient-to-l from-transparent to-background to-65% absolute top-2 end-1 pointer-events-none"
|
||||
}
|
||||
></div>
|
||||
<TooltipProvider delayDuration={100}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant={"link"}
|
||||
className="absolute end-0 top-0"
|
||||
onClick={() => copyToClipboard(publicKey)}
|
||||
>
|
||||
<Copy className="h-4 w-4 " />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
<Trans>Click to copy</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
{/* Docker */}
|
||||
<TabsContent value="docker">
|
||||
<DialogFooter className="flex justify-end gap-2 sm:w-[calc(100%+20px)] sm:-ms-[20px]">
|
||||
<Button type="button" variant={"ghost"} onClick={() => copyDockerCompose(port.current.value)}>
|
||||
<Trans>Copy</Trans> docker compose
|
||||
</Button>
|
||||
<Button>
|
||||
<Trans>Add system</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</TabsContent>
|
||||
{/* Binary */}
|
||||
<TabsContent value="binary">
|
||||
<DialogFooter className="flex justify-end gap-2 sm:w-[calc(100%+20px)] sm:-ms-[20px]">
|
||||
<Button type="button" variant={"ghost"} onClick={() => copyInstallCommand(port.current.value)}>
|
||||
<Trans>Copy Linux command</Trans>
|
||||
</Button>
|
||||
<Button>
|
||||
<Trans>Add system</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</TabsContent>
|
||||
</form>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
{opened.current && <SystemDialog setOpen={setOpen} />}
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function copyDockerCompose(port = "45876", publicKey: string) {
|
||||
copyToClipboard(`services:
|
||||
beszel-agent:
|
||||
image: "henrygd/beszel-agent"
|
||||
container_name: "beszel-agent"
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
# monitor other disks / partitions by mounting a folder in /extra-filesystems
|
||||
# - /mnt/disk/.beszel:/extra-filesystems/sda1:ro
|
||||
environment:
|
||||
PORT: ${port}
|
||||
KEY: "${publicKey}"`)
|
||||
}
|
||||
|
||||
function copyDockerRun(port = "45876", publicKey: string) {
|
||||
copyToClipboard(
|
||||
`docker run -d --name beszel-agent --network host --restart unless-stopped -v /var/run/docker.sock:/var/run/docker.sock:ro -e KEY="${publicKey}" -e PORT=${port} henrygd/beszel-agent:latest`
|
||||
)
|
||||
}
|
||||
|
||||
function copyInstallCommand(port = "45876", publicKey: string) {
|
||||
let cmd = `curl -sL https://raw.githubusercontent.com/henrygd/beszel/main/supplemental/scripts/install-agent.sh -o install-agent.sh && chmod +x install-agent.sh && ./install-agent.sh -p ${port} -k "${publicKey}"`
|
||||
// add china mirrors flag if zh-CN
|
||||
if ((i18n.locale + navigator.language).includes("zh-CN")) {
|
||||
cmd += ` --china-mirrors`
|
||||
}
|
||||
copyToClipboard(cmd)
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemDialog component for adding or editing a system.
|
||||
* @param {Object} props - The component props.
|
||||
* @param {function} props.setOpen - Function to set the open state of the dialog.
|
||||
* @param {SystemRecord} [props.system] - Optional system record for editing an existing system.
|
||||
*/
|
||||
export const SystemDialog = memo(({ setOpen, system }: { setOpen: (open: boolean) => void; system?: SystemRecord }) => {
|
||||
const publicKey = useStore($publicKey)
|
||||
const port = useRef<HTMLInputElement>(null)
|
||||
const [hostValue, setHostValue] = useState(system?.host ?? "")
|
||||
const isUnixSocket = hostValue.startsWith("/")
|
||||
|
||||
async function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
const formData = new FormData(e.target as HTMLFormElement)
|
||||
const data = Object.fromEntries(formData) as Record<string, any>
|
||||
data.users = pb.authStore.record!.id
|
||||
try {
|
||||
setOpen(false)
|
||||
if (system) {
|
||||
await pb.collection("systems").update(system.id, { ...data, status: "pending" })
|
||||
} else {
|
||||
await pb.collection("systems").create(data)
|
||||
}
|
||||
navigate(basePath)
|
||||
// console.log(record)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogContent
|
||||
className="w-[90%] sm:w-auto sm:ns-dialog max-w-full rounded-lg"
|
||||
onCloseAutoFocus={() => {
|
||||
setHostValue(system?.host ?? "")
|
||||
}}
|
||||
>
|
||||
<Tabs defaultValue="docker">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="mb-2">
|
||||
{system ? `${t`Edit`} ${system?.name}` : <Trans>Add New System</Trans>}
|
||||
</DialogTitle>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="docker">Docker</TabsTrigger>
|
||||
<TabsTrigger value="binary">
|
||||
<Trans>Binary</Trans>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</DialogHeader>
|
||||
{/* Docker (set tab index to prevent auto focusing content in edit system dialog) */}
|
||||
<TabsContent value="docker" tabIndex={-1}>
|
||||
<DialogDescription className="mb-4 leading-normal w-0 min-w-full">
|
||||
<Trans>
|
||||
The agent must be running on the system to connect. Copy the
|
||||
<code className="bg-muted px-1 rounded-sm leading-3">docker-compose.yml</code> for the agent below.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</TabsContent>
|
||||
{/* Binary */}
|
||||
<TabsContent value="binary">
|
||||
<DialogDescription className="mb-4 leading-normal w-0 min-w-full">
|
||||
<Trans>
|
||||
The agent must be running on the system to connect. Copy the installation command for the agent below.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</TabsContent>
|
||||
<form onSubmit={handleSubmit as any}>
|
||||
<div className="grid xs:grid-cols-[auto_1fr] gap-y-3 gap-x-4 items-center mt-1 mb-4">
|
||||
<Label htmlFor="name" className="xs:text-end">
|
||||
<Trans>Name</Trans>
|
||||
</Label>
|
||||
<Input id="name" name="name" defaultValue={system?.name} required />
|
||||
<Label htmlFor="host" className="xs:text-end">
|
||||
<Trans>Host / IP</Trans>
|
||||
</Label>
|
||||
<Input
|
||||
id="host"
|
||||
name="host"
|
||||
value={hostValue}
|
||||
required
|
||||
onChange={(e) => {
|
||||
setHostValue(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="port" className={cn("xs:text-end", isUnixSocket && "hidden")}>
|
||||
<Trans>Port</Trans>
|
||||
</Label>
|
||||
<Input
|
||||
ref={port}
|
||||
name="port"
|
||||
id="port"
|
||||
defaultValue={system?.port || "45876"}
|
||||
required={!isUnixSocket}
|
||||
className={cn(isUnixSocket && "hidden")}
|
||||
/>
|
||||
<Label htmlFor="pkey" className="xs:text-end whitespace-pre">
|
||||
<Trans comment="Use 'Key' if your language requires many more characters">Public Key</Trans>
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input readOnly id="pkey" value={publicKey} required></Input>
|
||||
<div
|
||||
className={
|
||||
"h-6 w-24 bg-gradient-to-r rtl:bg-gradient-to-l from-transparent to-background to-65% absolute top-2 end-1 pointer-events-none"
|
||||
}
|
||||
></div>
|
||||
<TooltipProvider delayDuration={100}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant={"link"}
|
||||
className="absolute end-0 top-0"
|
||||
onClick={() => copyToClipboard(publicKey)}
|
||||
>
|
||||
<Copy className="h-4 w-4 " />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
<Trans>Click to copy</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="flex justify-end gap-x-2 gap-y-3 flex-col mt-5">
|
||||
{/* Docker */}
|
||||
<TabsContent value="docker" className="contents">
|
||||
<CopyButton
|
||||
text={t`Copy` + " docker compose"}
|
||||
onClick={() => copyDockerCompose(isUnixSocket ? hostValue : port.current?.value, publicKey)}
|
||||
dropdownText={t`Copy` + " docker run"}
|
||||
dropdownOnClick={() => copyDockerRun(isUnixSocket ? hostValue : port.current?.value, publicKey)}
|
||||
/>
|
||||
</TabsContent>
|
||||
{/* Binary */}
|
||||
<TabsContent value="binary" className="contents">
|
||||
<CopyButton
|
||||
text={t`Copy Linux command`}
|
||||
onClick={() => copyInstallCommand(isUnixSocket ? hostValue : port.current?.value, publicKey)}
|
||||
dropdownText={t`Manual setup instructions`}
|
||||
dropdownUrl="https://beszel.dev/guide/agent-installation#binary"
|
||||
/>
|
||||
</TabsContent>
|
||||
{/* Save */}
|
||||
<Button>{system ? <Trans>Save system</Trans> : <Trans>Add system</Trans>}</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
)
|
||||
})
|
||||
|
||||
interface CopyButtonProps {
|
||||
text: string
|
||||
onClick: () => void
|
||||
dropdownText: string
|
||||
dropdownOnClick?: () => void
|
||||
dropdownUrl?: string
|
||||
}
|
||||
|
||||
const CopyButton = memo((props: CopyButtonProps) => {
|
||||
return (
|
||||
<div className="flex gap-0 rounded-lg">
|
||||
<Button type="button" variant="outline" onClick={props.onClick} className="rounded-e-none dark:border-e-0 grow">
|
||||
{props.text}
|
||||
</Button>
|
||||
<div className="w-px h-full bg-muted"></div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className={"px-2 rounded-s-none border-s-0"}>
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{props.dropdownUrl ? (
|
||||
<DropdownMenuItem asChild>
|
||||
<a href={props.dropdownUrl} target="_blank" rel="noopener noreferrer">
|
||||
{props.dropdownText}
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenuItem onClick={props.dropdownOnClick}>{props.dropdownText}</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -160,13 +160,11 @@ export function SystemAlertGlobal({
|
||||
function AlertContent({ data }: { data: AlertData }) {
|
||||
const { key } = data
|
||||
|
||||
const hasSliders = !("single" in data.alert)
|
||||
const singleDescription = data.alert.singleDesc?.()
|
||||
|
||||
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 [min, setMin] = useState(data.min || 10)
|
||||
const [value, setValue] = useState(data.val || (singleDescription ? 0 : 80))
|
||||
|
||||
const newMin = useRef(min)
|
||||
const newValue = useRef(value)
|
||||
@@ -180,14 +178,14 @@ function AlertContent({ data }: { data: AlertData }) {
|
||||
<label
|
||||
htmlFor={`s${key}`}
|
||||
className={cn("flex flex-row items-center justify-between gap-4 cursor-pointer p-4", {
|
||||
"pb-0": showSliders,
|
||||
"pb-0": checked,
|
||||
})}
|
||||
>
|
||||
<div className="grid gap-1 select-none">
|
||||
<p className="font-semibold flex gap-3 items-center">
|
||||
<Icon className="h-4 w-4 opacity-85" /> {data.alert.name()}
|
||||
</p>
|
||||
{!showSliders && <span className="block text-sm text-muted-foreground">{data.alert.desc()}</span>}
|
||||
{!checked && <span className="block text-sm text-muted-foreground">{data.alert.desc()}</span>}
|
||||
</div>
|
||||
<Switch
|
||||
id={`s${key}`}
|
||||
@@ -198,9 +196,10 @@ function AlertContent({ data }: { data: AlertData }) {
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
{showSliders && (
|
||||
{checked && (
|
||||
<div className="grid sm:grid-cols-2 mt-1.5 gap-5 px-4 pb-5 tabular-nums text-muted-foreground">
|
||||
<Suspense fallback={<div className="h-10" />}>
|
||||
{!singleDescription && (
|
||||
<div>
|
||||
<p id={`v${key}`} className="text-sm block h-8">
|
||||
<Trans>
|
||||
@@ -222,8 +221,12 @@ function AlertContent({ data }: { data: AlertData }) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p id={`t${key}`} className="text-sm block h-8">
|
||||
)}
|
||||
<div className={cn(singleDescription && "col-span-full lowercase")}>
|
||||
<p id={`t${key}`} className="text-sm block h-8 first-letter:uppercase">
|
||||
{singleDescription && (
|
||||
<>{singleDescription}{` `}</>
|
||||
)}
|
||||
<Trans>
|
||||
For <strong className="text-foreground">{min}</strong>{" "}
|
||||
<Plural value={min} one=" minute" other=" minutes" />
|
||||
|
||||
@@ -22,9 +22,10 @@ import {
|
||||
import { useEffect } from "react"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { $systems } from "@/lib/stores"
|
||||
import { isAdmin } from "@/lib/utils"
|
||||
import { navigate } from "./router"
|
||||
import { getHostDisplayValue, isAdmin } from "@/lib/utils"
|
||||
import { $router, basePath, navigate } from "./router"
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
|
||||
export default function CommandPalette({ open, setOpen }: { open: boolean; setOpen: (open: boolean) => void }) {
|
||||
const systems = useStore($systems)
|
||||
@@ -55,13 +56,13 @@ export default function CommandPalette({ open, setOpen }: { open: boolean; setOp
|
||||
<CommandItem
|
||||
key={system.id}
|
||||
onSelect={() => {
|
||||
navigate(`/system/${encodeURIComponent(system.name)}`)
|
||||
navigate(getPagePath($router, "system", { name: system.name }))
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<Server className="me-2 h-4 w-4" />
|
||||
<span>{system.name}</span>
|
||||
<CommandShortcut>{system.host}</CommandShortcut>
|
||||
<CommandShortcut>{getHostDisplayValue(system)}</CommandShortcut>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
@@ -72,7 +73,7 @@ export default function CommandPalette({ open, setOpen }: { open: boolean; setOp
|
||||
<CommandItem
|
||||
keywords={["home"]}
|
||||
onSelect={() => {
|
||||
navigate("/")
|
||||
navigate(basePath)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
@@ -86,7 +87,7 @@ export default function CommandPalette({ open, setOpen }: { open: boolean; setOp
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
navigate("/settings/general")
|
||||
navigate(getPagePath($router, "settings", { name: "general" }))
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
@@ -101,7 +102,7 @@ export default function CommandPalette({ open, setOpen }: { open: boolean; setOp
|
||||
<CommandItem
|
||||
keywords={["alerts"]}
|
||||
onSelect={() => {
|
||||
navigate("/settings/notifications")
|
||||
navigate(getPagePath($router, "settings", { name: "notifications" }))
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -7,14 +7,19 @@ import { $authenticated, pb } from "@/lib/stores"
|
||||
import * as v from "valibot"
|
||||
import { toast } from "../ui/use-toast"
|
||||
import { Dialog, DialogContent, DialogTrigger, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { useCallback, useState } from "react"
|
||||
import { AuthMethodsList, OAuth2AuthConfig } from "pocketbase"
|
||||
import { Link } from "../router"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { AuthMethodsList, AuthProviderInfo, OAuth2AuthConfig } from "pocketbase"
|
||||
import { $router, Link, prependBasePath } from "../router"
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
|
||||
const honeypot = v.literal("")
|
||||
const emailSchema = v.pipe(v.string(), v.email(t`Invalid email address.`))
|
||||
const passwordSchema = v.pipe(v.string(), v.minLength(8, t`Password must be at least 8 characters.`))
|
||||
const passwordSchema = v.pipe(
|
||||
v.string(),
|
||||
v.minLength(8, t`Password must be at least 8 characters.`),
|
||||
v.maxBytes(72, t`Password must be less than 72 bytes.`)
|
||||
)
|
||||
|
||||
const LoginSchema = v.looseObject({
|
||||
name: honeypot,
|
||||
@@ -113,9 +118,49 @@ export function UserAuthForm({
|
||||
return null
|
||||
}
|
||||
|
||||
const oauthEnabled = authMethods.oauth2.enabled && authMethods.oauth2.providers.length > 0
|
||||
const authProviders = authMethods.oauth2.providers ?? []
|
||||
const oauthEnabled = authMethods.oauth2.enabled && authProviders.length > 0
|
||||
const passwordEnabled = authMethods.password.enabled
|
||||
|
||||
function loginWithOauth(provider: AuthProviderInfo, forcePopup = false) {
|
||||
setIsOauthLoading(true)
|
||||
const oAuthOpts: OAuth2AuthConfig = {
|
||||
provider: provider.name,
|
||||
}
|
||||
// https://github.com/pocketbase/pocketbase/discussions/2429#discussioncomment-5943061
|
||||
if (forcePopup || navigator.userAgent.match(/iPhone|iPad|iPod/i)) {
|
||||
const authWindow = window.open()
|
||||
if (!authWindow) {
|
||||
setIsOauthLoading(false)
|
||||
toast({
|
||||
title: t`Error`,
|
||||
description: t`Please enable pop-ups for this site`,
|
||||
variant: "destructive",
|
||||
})
|
||||
return
|
||||
}
|
||||
oAuthOpts.urlCallback = (url) => {
|
||||
authWindow.location.href = url
|
||||
}
|
||||
}
|
||||
pb.collection("users")
|
||||
.authWithOAuth2(oAuthOpts)
|
||||
.then(() => {
|
||||
$authenticated.set(pb.authStore.isValid)
|
||||
})
|
||||
.catch(showLoginFaliedToast)
|
||||
.finally(() => {
|
||||
setIsOauthLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// auto login if password disabled and only one auth provider
|
||||
if (!passwordEnabled && authProviders.length === 1) {
|
||||
loginWithOauth(authProviders[0], true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={cn("grid gap-6", className)} {...props}>
|
||||
{passwordEnabled && (
|
||||
@@ -132,12 +177,12 @@ export function UserAuthForm({
|
||||
name="email"
|
||||
required
|
||||
placeholder="name@example.com"
|
||||
type="email"
|
||||
type="text"
|
||||
autoCapitalize="none"
|
||||
autoComplete="email"
|
||||
autoCorrect="off"
|
||||
disabled={isLoading || isOauthLoading}
|
||||
className="ps-9"
|
||||
className={cn("ps-9", errors?.email && "border-red-500")}
|
||||
/>
|
||||
{errors?.email && <p className="px-1 text-xs text-red-600">{errors.email}</p>}
|
||||
</div>
|
||||
@@ -154,7 +199,7 @@ export function UserAuthForm({
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
disabled={isLoading || isOauthLoading}
|
||||
className="ps-9 placeholder:lowercase"
|
||||
className={cn("ps-9", errors?.password && "border-red-500")}
|
||||
/>
|
||||
{errors?.password && <p className="px-1 text-xs text-red-600">{errors.password}</p>}
|
||||
</div>
|
||||
@@ -172,7 +217,7 @@ export function UserAuthForm({
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
disabled={isLoading || isOauthLoading}
|
||||
className="ps-9 placeholder:lowercase"
|
||||
className={cn("ps-9", errors?.password && "border-red-500")}
|
||||
/>
|
||||
{errors?.passwordConfirm && <p className="px-1 text-xs text-red-600">{errors.passwordConfirm}</p>}
|
||||
</div>
|
||||
@@ -218,37 +263,7 @@ export function UserAuthForm({
|
||||
"justify-self-center": !passwordEnabled,
|
||||
"px-5": !passwordEnabled,
|
||||
})}
|
||||
onClick={() => {
|
||||
setIsOauthLoading(true)
|
||||
const oAuthOpts: OAuth2AuthConfig = {
|
||||
provider: provider.name,
|
||||
}
|
||||
// https://github.com/pocketbase/pocketbase/discussions/2429#discussioncomment-5943061
|
||||
if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {
|
||||
const authWindow = window.open()
|
||||
if (!authWindow) {
|
||||
setIsOauthLoading(false)
|
||||
toast({
|
||||
title: t`Error`,
|
||||
description: t`Please enable pop-ups for this site`,
|
||||
variant: "destructive",
|
||||
})
|
||||
return
|
||||
}
|
||||
oAuthOpts.urlCallback = (url) => {
|
||||
authWindow.location.href = url
|
||||
}
|
||||
}
|
||||
pb.collection("users")
|
||||
.authWithOAuth2(oAuthOpts)
|
||||
.then(() => {
|
||||
$authenticated.set(pb.authStore.isValid)
|
||||
})
|
||||
.catch(showLoginFaliedToast)
|
||||
.finally(() => {
|
||||
setIsOauthLoading(false)
|
||||
})
|
||||
}}
|
||||
onClick={() => loginWithOauth(provider)}
|
||||
disabled={isLoading || isOauthLoading}
|
||||
>
|
||||
{isOauthLoading ? (
|
||||
@@ -256,11 +271,11 @@ export function UserAuthForm({
|
||||
) : (
|
||||
<img
|
||||
className="me-2 h-4 w-4 dark:brightness-0 dark:invert"
|
||||
src={`/_/images/oauth2/${provider.name}.svg`}
|
||||
src={prependBasePath(`/_/images/oauth2/${provider.name}.svg`)}
|
||||
alt=""
|
||||
onError={(e) => {
|
||||
e.currentTarget.src = "/static/lock.svg"
|
||||
}}
|
||||
// onError={(e) => {
|
||||
// e.currentTarget.src = "/static/lock.svg"
|
||||
// }}
|
||||
/>
|
||||
)}
|
||||
<span className="translate-y-[1px]">{provider.displayName}</span>
|
||||
@@ -274,7 +289,7 @@ export function UserAuthForm({
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<button type="button" className={cn(buttonVariants({ variant: "outline" }))}>
|
||||
<img className="me-2 h-4 w-4 dark:invert" src="/_/images/oauth2/github.svg" alt="" />
|
||||
<img className="me-2 h-4 w-4 dark:invert" src={prependBasePath("/_/images/oauth2/github.svg")} alt="" />
|
||||
<span className="translate-y-[1px]">GitHub</span>
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
@@ -307,7 +322,7 @@ export function UserAuthForm({
|
||||
|
||||
{passwordEnabled && !isFirstRun && (
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
href={getPagePath($router, "forgot_password")}
|
||||
className="text-sm mx-auto hover:text-brand underline underline-offset-4 opacity-70 hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<Trans>Forgot password?</Trans>
|
||||
|
||||
@@ -83,7 +83,7 @@ export default function ForgotPassword() {
|
||||
<Trans>Command line instructions</Trans>
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[33em]">
|
||||
<DialogContent className="max-w-[41em]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Command line instructions</Trans>
|
||||
@@ -98,7 +98,10 @@ export default function ForgotPassword() {
|
||||
<Trans>Then log into the backend and reset your user account password in the users table.</Trans>
|
||||
</p>
|
||||
<code className="bg-muted rounded-sm py-0.5 px-2.5 me-auto text-sm">
|
||||
beszel admin update youremail@example.com newpassword
|
||||
./beszel superuser upsert user@example.com password
|
||||
</code>
|
||||
<code className="bg-muted rounded-sm py-0.5 px-2.5 me-auto text-sm">
|
||||
docker exec beszel /beszel superuser upsert name@example.com password
|
||||
</code>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function () {
|
||||
const subtitle = useMemo(() => {
|
||||
if (isFirstRun) {
|
||||
return t`Please create an admin account`
|
||||
} else if (page?.path === "/forgot-password") {
|
||||
} else if (page?.route === "forgot_password") {
|
||||
return t`Enter email address to reset password`
|
||||
} else {
|
||||
return t`Please sign in to your account`
|
||||
@@ -59,7 +59,7 @@ export default function () {
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">{subtitle}</p>
|
||||
</div>
|
||||
{page?.path === "/forgot-password" ? (
|
||||
{page?.route === "forgot_password" ? (
|
||||
<ForgotPassword />
|
||||
) : (
|
||||
<UserAuthForm isFirstRun={isFirstRun} authMethods={authMethods} />
|
||||
|
||||
@@ -10,12 +10,12 @@ import {
|
||||
UserIcon,
|
||||
UsersIcon,
|
||||
} from "lucide-react"
|
||||
import { Link } from "./router"
|
||||
import { $router, basePath, Link, prependBasePath } from "./router"
|
||||
import { LangToggle } from "./lang-toggle"
|
||||
import { ModeToggle } from "./mode-toggle"
|
||||
import { Logo } from "./logo"
|
||||
import { pb } from "@/lib/stores"
|
||||
import { cn, isReadOnlyUser, isAdmin } from "@/lib/utils"
|
||||
import { cn, isReadOnlyUser, isAdmin, logOut } from "@/lib/utils"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { AddSystemButton } from "./add-system"
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
|
||||
const CommandPalette = lazy(() => import("./command-palette"))
|
||||
|
||||
@@ -35,7 +36,7 @@ const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0
|
||||
export default function Navbar() {
|
||||
return (
|
||||
<div className="flex items-center h-14 md:h-16 bg-card px-4 pe-3 sm:px-6 border border-border/60 bt-0 rounded-md my-4">
|
||||
<Link href="/" aria-label="Home" className="p-2 ps-0 me-3">
|
||||
<Link href={basePath} aria-label="Home" className="p-2 ps-0 me-3">
|
||||
<Logo className="h-[1.1rem] md:h-5 fill-foreground" />
|
||||
</Link>
|
||||
<SearchButton />
|
||||
@@ -44,7 +45,7 @@ export default function Navbar() {
|
||||
<LangToggle />
|
||||
<ModeToggle />
|
||||
<Link
|
||||
href="/settings/general"
|
||||
href={getPagePath($router, "settings", { name: "general" })}
|
||||
aria-label="Settings"
|
||||
className={cn("", buttonVariants({ variant: "ghost", size: "icon" }))}
|
||||
>
|
||||
@@ -63,7 +64,7 @@ export default function Navbar() {
|
||||
{isAdmin() && (
|
||||
<>
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/_/" target="_blank">
|
||||
<a href={prependBasePath("/_/")} target="_blank">
|
||||
<UsersIcon className="me-2.5 h-4 w-4" />
|
||||
<span>
|
||||
<Trans>Users</Trans>
|
||||
@@ -71,7 +72,7 @@ export default function Navbar() {
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/_/#/collections?collectionId=2hz5ncl8tizk5nx" target="_blank">
|
||||
<a href={prependBasePath("/_/#/collections?collection=systems")} target="_blank">
|
||||
<ServerIcon className="me-2.5 h-4 w-4" />
|
||||
<span>
|
||||
<Trans>Systems</Trans>
|
||||
@@ -79,7 +80,7 @@ export default function Navbar() {
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/_/#/logs" target="_blank">
|
||||
<a href={prependBasePath("/_/#/logs")} target="_blank">
|
||||
<LogsIcon className="me-2.5 h-4 w-4" />
|
||||
<span>
|
||||
<Trans>Logs</Trans>
|
||||
@@ -87,7 +88,7 @@ export default function Navbar() {
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/_/#/settings/backups" target="_blank">
|
||||
<a href={prependBasePath("/_/#/settings/backups")} target="_blank">
|
||||
<DatabaseBackupIcon className="me-2.5 h-4 w-4" />
|
||||
<span>
|
||||
<Trans>Backups</Trans>
|
||||
@@ -98,7 +99,7 @@ export default function Navbar() {
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuItem onSelect={() => pb.authStore.clear()}>
|
||||
<DropdownMenuItem onSelect={logOut}>
|
||||
<LogOutIcon className="me-2.5 h-4 w-4" />
|
||||
<span>
|
||||
<Trans>Log Out</Trans>
|
||||
|
||||
@@ -1,16 +1,36 @@
|
||||
import { createRouter } from "@nanostores/router"
|
||||
|
||||
export const $router = createRouter(
|
||||
{
|
||||
home: "/",
|
||||
server: "/system/:name",
|
||||
settings: "/settings/:name?",
|
||||
forgot_password: "/forgot-password",
|
||||
},
|
||||
{ links: false }
|
||||
)
|
||||
const routes = {
|
||||
home: "/",
|
||||
system: `/system/:name`,
|
||||
settings: `/settings/:name?`,
|
||||
forgot_password: `/forgot-password`,
|
||||
} as const
|
||||
|
||||
/** Navigate to url using router */
|
||||
/**
|
||||
* The base path of the application.
|
||||
* This is used to prepend the base path to all routes.
|
||||
*/
|
||||
export const basePath = window.BASE_PATH || ""
|
||||
|
||||
/**
|
||||
* Prepends the base path to the given path.
|
||||
* @param path The path to prepend the base path to.
|
||||
* @returns The path with the base path prepended.
|
||||
*/
|
||||
export const prependBasePath = (path: string) => (basePath + path).replaceAll("//", "/")
|
||||
|
||||
// prepend base path to routes
|
||||
for (const route in routes) {
|
||||
// @ts-ignore need as const above to get nanostores to parse types properly
|
||||
routes[route] = prependBasePath(routes[route])
|
||||
}
|
||||
|
||||
export const $router = createRouter(routes, { links: false })
|
||||
|
||||
/** Navigate to url using router
|
||||
* Base path is automatically prepended if serving from subpath
|
||||
*/
|
||||
export const navigate = (urlString: string) => {
|
||||
$router.open(urlString)
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ import { Separator } from "../ui/separator"
|
||||
import { alertInfo, updateRecordList, updateSystemList } from "@/lib/utils"
|
||||
import { AlertRecord, SystemRecord } from "@/types"
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||
import { Link } from "../router"
|
||||
import { $router, Link } from "../router"
|
||||
import { Plural, t, Trans } from "@lingui/macro"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
|
||||
const SystemsTable = lazy(() => import("../systems-table/systems-table"))
|
||||
|
||||
@@ -83,7 +84,7 @@ export default function Home() {
|
||||
</Trans>
|
||||
</AlertDescription>
|
||||
<Link
|
||||
href={`/system/${encodeURIComponent(alert.sysname!)}`}
|
||||
href={getPagePath($router, "system", { name: alert.sysname! })}
|
||||
className="absolute inset-0 w-full h-full"
|
||||
aria-label="View system"
|
||||
></Link>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { SidebarNav } from "./sidebar-nav.tsx"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { $router } from "@/components/router.tsx"
|
||||
import { redirectPage } from "@nanostores/router"
|
||||
import { getPagePath, redirectPage } from "@nanostores/router"
|
||||
import { BellIcon, FileSlidersIcon, SettingsIcon } from "lucide-react"
|
||||
import { $userSettings, pb } from "@/lib/stores.ts"
|
||||
import { toast } from "@/components/ui/use-toast.ts"
|
||||
@@ -49,17 +49,17 @@ export default function SettingsLayout() {
|
||||
const sidebarNavItems = [
|
||||
{
|
||||
title: _(t({ message: `General`, comment: "Context: General settings" })),
|
||||
href: "/settings/general",
|
||||
href: getPagePath($router, "settings", { name: "general" }),
|
||||
icon: SettingsIcon,
|
||||
},
|
||||
{
|
||||
title: t`Notifications`,
|
||||
href: "/settings/notifications",
|
||||
href: getPagePath($router, "settings", { name: "notifications" }),
|
||||
icon: BellIcon,
|
||||
},
|
||||
{
|
||||
title: t`YAML Config`,
|
||||
href: "/settings/config",
|
||||
href: getPagePath($router, "settings", { name: "config" }),
|
||||
icon: FileSlidersIcon,
|
||||
admin: true,
|
||||
},
|
||||
@@ -69,8 +69,8 @@ export default function SettingsLayout() {
|
||||
|
||||
useEffect(() => {
|
||||
document.title = t`Settings` + " / Beszel"
|
||||
// redirect to account page if no page is specified
|
||||
if (page?.path === "/settings") {
|
||||
// @ts-ignore redirect to account page if no page is specified
|
||||
if (!page?.params?.name) {
|
||||
redirectPage($router, "settings", { name: "general" })
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -13,6 +13,7 @@ import { saveSettings } from "./layout"
|
||||
import * as v from "valibot"
|
||||
import { isAdmin } from "@/lib/utils"
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
import { prependBasePath } from "@/components/router"
|
||||
|
||||
interface ShoutrrrUrlCardProps {
|
||||
url: string
|
||||
@@ -94,7 +95,7 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
<Trans>
|
||||
Please{" "}
|
||||
<a href="/_/#/settings/mail" className="link" target="_blank">
|
||||
<a href={prependBasePath("/_/#/settings/mail")} className="link" target="_blank">
|
||||
configure an SMTP server
|
||||
</a>{" "}
|
||||
to ensure alerts are delivered.
|
||||
|
||||
@@ -22,7 +22,7 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
|
||||
<>
|
||||
{/* Mobile View */}
|
||||
<div className="md:hidden">
|
||||
<Select onValueChange={(value: string) => navigate(value)} value={page?.path}>
|
||||
<Select onValueChange={navigate} value={page?.path}>
|
||||
<SelectTrigger className="w-full my-3.5">
|
||||
<SelectValue placeholder="Select page" />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { $systems, pb, $chartTime, $containerFilter, $userSettings, $direction } from "@/lib/stores"
|
||||
import { $systems, pb, $chartTime, $containerFilter, $userSettings, $direction, $maxValues } from "@/lib/stores"
|
||||
import { ChartData, ChartTimes, ContainerStatsRecord, GPUData, SystemRecord, SystemStatsRecord } from "@/types"
|
||||
import React, { lazy, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import React, { lazy, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { Card, CardHeader, CardTitle, CardDescription } from "../ui/card"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import Spinner from "../spinner"
|
||||
import { ClockArrowUp, CpuIcon, GlobeIcon, LayoutGridIcon, MonitorIcon, XIcon } from "lucide-react"
|
||||
import ChartTimeSelect from "../charts/chart-time-select"
|
||||
import { chartTimeData, cn, getPbTimestamp, getSizeAndUnit, toFixedFloat, useLocalStorage } from "@/lib/utils"
|
||||
import {
|
||||
chartTimeData,
|
||||
cn,
|
||||
getHostDisplayValue,
|
||||
getPbTimestamp,
|
||||
getSizeAndUnit,
|
||||
toFixedFloat,
|
||||
useLocalStorage,
|
||||
} from "@/lib/utils"
|
||||
import { Separator } from "../ui/separator"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"
|
||||
import { Button } from "../ui/button"
|
||||
@@ -98,10 +106,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
const { _ } = useLingui()
|
||||
const systems = useStore($systems)
|
||||
const chartTime = useStore($chartTime)
|
||||
/** Max CPU toggle value */
|
||||
const cpuMaxStore = useState(false)
|
||||
const bandwidthMaxStore = useState(false)
|
||||
const diskIoMaxStore = useState(false)
|
||||
const maxValues = useStore($maxValues)
|
||||
const [grid, setGrid] = useLocalStorage("grid", true)
|
||||
const [system, setSystem] = useState({} as SystemRecord)
|
||||
const [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])
|
||||
@@ -121,9 +126,6 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
setContainerData([])
|
||||
setContainerFilterBar(null)
|
||||
$containerFilter.set("")
|
||||
cpuMaxStore[1](false)
|
||||
bandwidthMaxStore[1](false)
|
||||
diskIoMaxStore[1](false)
|
||||
}
|
||||
}, [name])
|
||||
|
||||
@@ -250,7 +252,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
uptime = <Plural value={Math.trunc(system.info?.u / 86400)} one="# day" other="# days" />
|
||||
}
|
||||
return [
|
||||
{ value: system.host, Icon: GlobeIcon },
|
||||
{ value: getHostDisplayValue(system), Icon: GlobeIcon },
|
||||
{
|
||||
value: system.info.h,
|
||||
Icon: MonitorIcon,
|
||||
@@ -291,9 +293,21 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
return null
|
||||
}
|
||||
|
||||
// select field for switching between avg and max values
|
||||
const maxValSelect = isLongerChart ? <SelectAvgMax max={maxValues} /> : null
|
||||
|
||||
// if no data, show empty message
|
||||
const dataEmpty = !chartLoading && chartData.systemStats.length === 0
|
||||
const hasGpuData = Object.keys(systemStats.at(-1)?.stats.g ?? {}).length > 0
|
||||
const lastGpuVals = Object.values(systemStats.at(-1)?.stats.g ?? {})
|
||||
const hasGpuData = lastGpuVals.length > 0
|
||||
const hasGpuPowerData = lastGpuVals.some((gpu) => gpu.p !== undefined)
|
||||
|
||||
let translatedStatus: string = system.status
|
||||
if (system.status === "up") {
|
||||
translatedStatus = t({ message: "Up", comment: "Context: System is up" })
|
||||
} else if (system.status === "down") {
|
||||
translatedStatus = t({ message: "Down", comment: "Context: System is down" })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -321,7 +335,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
})}
|
||||
></span>
|
||||
</span>
|
||||
{system.status}
|
||||
{translatedStatus}
|
||||
</div>
|
||||
{systemInfo.map(({ value, label, Icon, hide }, i) => {
|
||||
if (hide || !value) {
|
||||
@@ -383,9 +397,9 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
grid={grid}
|
||||
title={_(t`CPU Usage`)}
|
||||
description={t`Average system-wide CPU utilization`}
|
||||
cornerEl={isLongerChart ? <SelectAvgMax store={cpuMaxStore} /> : null}
|
||||
cornerEl={maxValSelect}
|
||||
>
|
||||
<AreaChartDefault chartData={chartData} chartName="CPU Usage" maxToggled={cpuMaxStore[0]} unit="%" />
|
||||
<AreaChartDefault chartData={chartData} chartName="CPU Usage" maxToggled={maxValues} unit="%" />
|
||||
</ChartCard>
|
||||
|
||||
{containerFilterBar && (
|
||||
@@ -430,19 +444,19 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
grid={grid}
|
||||
title={t`Disk I/O`}
|
||||
description={t`Throughput of root filesystem`}
|
||||
cornerEl={isLongerChart ? <SelectAvgMax store={diskIoMaxStore} /> : null}
|
||||
cornerEl={maxValSelect}
|
||||
>
|
||||
<AreaChartDefault chartData={chartData} maxToggled={diskIoMaxStore[0]} chartName="dio" />
|
||||
<AreaChartDefault chartData={chartData} chartName="dio" maxToggled={maxValues} />
|
||||
</ChartCard>
|
||||
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
title={t`Bandwidth`}
|
||||
cornerEl={isLongerChart ? <SelectAvgMax store={bandwidthMaxStore} /> : null}
|
||||
cornerEl={maxValSelect}
|
||||
description={t`Network traffic of public interfaces`}
|
||||
>
|
||||
<AreaChartDefault chartData={chartData} maxToggled={bandwidthMaxStore[0]} chartName="bw" />
|
||||
<AreaChartDefault chartData={chartData} chartName="bw" maxToggled={maxValues} />
|
||||
</ChartCard>
|
||||
|
||||
{containerFilterBar && containerData.length > 0 && (
|
||||
@@ -489,7 +503,7 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
)}
|
||||
|
||||
{/* GPU power draw chart */}
|
||||
{hasGpuData && (
|
||||
{hasGpuPowerData && (
|
||||
<ChartCard
|
||||
empty={dataEmpty}
|
||||
grid={grid}
|
||||
@@ -562,13 +576,9 @@ export default function SystemDetail({ name }: { name: string }) {
|
||||
grid={grid}
|
||||
title={`${extraFsName} I/O`}
|
||||
description={t`Throughput of ${extraFsName}`}
|
||||
cornerEl={isLongerChart ? <SelectAvgMax store={diskIoMaxStore} /> : null}
|
||||
cornerEl={maxValSelect}
|
||||
>
|
||||
<AreaChartDefault
|
||||
chartData={chartData}
|
||||
maxToggled={diskIoMaxStore[0]}
|
||||
chartName={`efs.${extraFsName}`}
|
||||
/>
|
||||
<AreaChartDefault chartData={chartData} chartName={`efs.${extraFsName}`} maxToggled={maxValues} />
|
||||
</ChartCard>
|
||||
</div>
|
||||
)
|
||||
@@ -610,12 +620,10 @@ function ContainerFilterBar() {
|
||||
)
|
||||
}
|
||||
|
||||
function SelectAvgMax({ store }: { store: [boolean, React.Dispatch<React.SetStateAction<boolean>>] }) {
|
||||
const [max, setMax] = store
|
||||
const SelectAvgMax = memo(({ max }: { max: boolean }) => {
|
||||
const Icon = max ? ChartMax : ChartAverage
|
||||
|
||||
return (
|
||||
<Select value={max ? "max" : "avg"} onValueChange={(e) => setMax(e === "max")}>
|
||||
<Select value={max ? "max" : "avg"} onValueChange={(e) => $maxValues.set(e === "max")}>
|
||||
<SelectTrigger className="relative ps-10 pe-5">
|
||||
<Icon className="h-4 w-4 absolute start-4 top-1/2 -translate-y-1/2 opacity-85" />
|
||||
<SelectValue />
|
||||
@@ -630,7 +638,7 @@ function SelectAvgMax({ store }: { store: [boolean, React.Dispatch<React.SetStat
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
function ChartCard({
|
||||
title,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
VisibilityState,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
Column,
|
||||
HeaderContext,
|
||||
} from "@tanstack/react-table"
|
||||
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
@@ -37,7 +37,6 @@ import {
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
|
||||
import { SystemRecord } from "@/types"
|
||||
@@ -59,32 +58,39 @@ import {
|
||||
ArrowUpIcon,
|
||||
Settings2Icon,
|
||||
EyeIcon,
|
||||
PenBoxIcon,
|
||||
} from "lucide-react"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { memo, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { $hubVersion, $systems, pb } from "@/lib/stores"
|
||||
import { useStore } from "@nanostores/react"
|
||||
import { cn, copyToClipboard, decimalString, isReadOnlyUser, useLocalStorage } from "@/lib/utils"
|
||||
import AlertsButton from "../alerts/alert-button"
|
||||
import { Link, navigate } from "../router"
|
||||
import { EthernetIcon } from "../ui/icons"
|
||||
import { $router, Link, navigate } from "../router"
|
||||
import { EthernetIcon, GpuIcon, ThermometerIcon } from "../ui/icons"
|
||||
import { Trans, t } from "@lingui/macro"
|
||||
import { useLingui } from "@lingui/react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
|
||||
import { Input } from "../ui/input"
|
||||
import { ClassValue } from "clsx"
|
||||
import { getPagePath } from "@nanostores/router"
|
||||
import { SystemDialog } from "../add-system"
|
||||
import { Dialog } from "../ui/dialog"
|
||||
|
||||
type ViewMode = "table" | "grid"
|
||||
|
||||
function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
||||
const val = info.getValue() as number
|
||||
const val = (info.getValue() as number) || 0
|
||||
return (
|
||||
<div className="flex gap-2 items-center tabular-nums tracking-tight">
|
||||
<span className="min-w-[3.5em]">{decimalString(val, 1)}%</span>
|
||||
<span className="min-w-[3.3em]">{decimalString(val, 1)}%</span>
|
||||
<span className="grow min-w-10 block bg-muted h-[1em] relative rounded-sm overflow-hidden">
|
||||
<span
|
||||
className={cn(
|
||||
"absolute inset-0 w-full h-full origin-left",
|
||||
(val < 65 && "bg-green-500") || (val < 90 && "bg-yellow-500") || "bg-red-600"
|
||||
(info.row.original.status !== "up" && "bg-primary/30") ||
|
||||
(val < 65 && "bg-green-500") ||
|
||||
(val < 90 && "bg-yellow-500") ||
|
||||
"bg-red-600"
|
||||
)}
|
||||
style={{
|
||||
transform: `scalex(${val / 100})`,
|
||||
@@ -95,7 +101,8 @@ function CellFormatter(info: CellContext<SystemRecord, unknown>) {
|
||||
)
|
||||
}
|
||||
|
||||
function sortableHeader(column: Column<SystemRecord, unknown>, hideSortIcon = false) {
|
||||
function sortableHeader(context: HeaderContext<SystemRecord, unknown>) {
|
||||
const { column } = context
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -103,9 +110,10 @@ function sortableHeader(column: Column<SystemRecord, unknown>, hideSortIcon = fa
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
{column.columnDef?.icon && <column.columnDef.icon className="me-2 size-4" />}
|
||||
{column.columnDef.icon && <column.columnDef.icon className="me-2 size-4" />}
|
||||
{column.id}
|
||||
{!hideSortIcon && <ArrowUpDownIcon className="ms-2 size-4" />}
|
||||
{/* @ts-ignore */}
|
||||
{column.columnDef.hideSort || <ArrowUpDownIcon className="ms-2 size-4" />}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -150,7 +158,7 @@ export default function SystemsTable() {
|
||||
</Button>
|
||||
</span>
|
||||
),
|
||||
header: ({ column }) => sortableHeader(column),
|
||||
header: sortableHeader,
|
||||
},
|
||||
{
|
||||
accessorKey: "info.cpu",
|
||||
@@ -158,7 +166,7 @@ export default function SystemsTable() {
|
||||
invertSorting: true,
|
||||
cell: CellFormatter,
|
||||
icon: CpuIcon,
|
||||
header: ({ column }) => sortableHeader(column),
|
||||
header: sortableHeader,
|
||||
},
|
||||
{
|
||||
accessorKey: "info.mp",
|
||||
@@ -166,7 +174,7 @@ export default function SystemsTable() {
|
||||
invertSorting: true,
|
||||
cell: CellFormatter,
|
||||
icon: MemoryStickIcon,
|
||||
header: ({ column }) => sortableHeader(column),
|
||||
header: sortableHeader,
|
||||
},
|
||||
{
|
||||
accessorKey: "info.dp",
|
||||
@@ -174,15 +182,24 @@ export default function SystemsTable() {
|
||||
invertSorting: true,
|
||||
cell: CellFormatter,
|
||||
icon: HardDriveIcon,
|
||||
header: ({ column }) => sortableHeader(column),
|
||||
header: sortableHeader,
|
||||
},
|
||||
{
|
||||
accessorFn: (originalRow) => originalRow.info.g,
|
||||
id: "GPU",
|
||||
invertSorting: true,
|
||||
sortUndefined: -1,
|
||||
cell: CellFormatter,
|
||||
icon: GpuIcon,
|
||||
header: sortableHeader,
|
||||
},
|
||||
{
|
||||
accessorFn: (originalRow) => originalRow.info.b || 0,
|
||||
id: t`Net`,
|
||||
invertSorting: true,
|
||||
size: 115,
|
||||
size: 50,
|
||||
icon: EthernetIcon,
|
||||
header: ({ column }) => sortableHeader(column),
|
||||
header: sortableHeader,
|
||||
cell(info) {
|
||||
const val = info.getValue() as number
|
||||
return (
|
||||
@@ -196,18 +213,48 @@ export default function SystemsTable() {
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorFn: (originalRow) => originalRow.info.dt,
|
||||
id: t({
|
||||
message: "Temp",
|
||||
comment: "Temperature label in systems table",
|
||||
}),
|
||||
invertSorting: true,
|
||||
sortUndefined: -1,
|
||||
size: 50,
|
||||
hideSort: true,
|
||||
icon: ThermometerIcon,
|
||||
header: sortableHeader,
|
||||
cell(info) {
|
||||
const val = info.getValue() as number
|
||||
if (!val) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<span
|
||||
className={cn("tabular-nums whitespace-nowrap", {
|
||||
"ps-1.5": viewMode === "table",
|
||||
})}
|
||||
>
|
||||
{decimalString(val)} °C
|
||||
</span>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "info.v",
|
||||
id: t`Agent`,
|
||||
invertSorting: true,
|
||||
size: 50,
|
||||
icon: WifiIcon,
|
||||
header: ({ column }) => sortableHeader(column, true),
|
||||
hideSort: true,
|
||||
header: sortableHeader,
|
||||
cell(info) {
|
||||
const version = info.getValue() as string
|
||||
if (!version || !hubVersion) {
|
||||
return null
|
||||
}
|
||||
const system = info.row.original
|
||||
return (
|
||||
<span
|
||||
className={cn("flex gap-2 items-center md:pe-5 tabular-nums", {
|
||||
@@ -215,8 +262,12 @@ export default function SystemsTable() {
|
||||
})}
|
||||
>
|
||||
<IndicatorDot
|
||||
system={info.row.original}
|
||||
className={version === hubVersion ? "bg-green-500" : "bg-yellow-500"}
|
||||
system={system}
|
||||
className={
|
||||
(system.status !== "up" && "bg-primary/30") ||
|
||||
(version === hubVersion && "bg-green-500") ||
|
||||
"bg-yellow-500"
|
||||
}
|
||||
/>
|
||||
<span>{info.getValue() as string}</span>
|
||||
</span>
|
||||
@@ -225,7 +276,7 @@ export default function SystemsTable() {
|
||||
},
|
||||
{
|
||||
id: t({ message: "Actions", comment: "Table column" }),
|
||||
size: 120,
|
||||
size: 50,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex justify-end items-center gap-1">
|
||||
<AlertsButton system={row.original} />
|
||||
@@ -257,6 +308,8 @@ export default function SystemsTable() {
|
||||
},
|
||||
})
|
||||
|
||||
const rows = table.getRowModel().rows
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-5 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1">
|
||||
@@ -387,7 +440,7 @@ export default function SystemsTable() {
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
{rows.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.original.id}
|
||||
@@ -398,7 +451,7 @@ export default function SystemsTable() {
|
||||
onClick={(e) => {
|
||||
const target = e.target as HTMLElement
|
||||
if (!target.closest("[data-nolink]") && e.currentTarget.contains(target)) {
|
||||
navigate(`/system/${encodeURIComponent(row.original.name)}`)
|
||||
navigate(getPagePath($router, "system", { name: row.original.name }))
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -481,7 +534,7 @@ export default function SystemsTable() {
|
||||
})}
|
||||
</CardContent>
|
||||
<Link
|
||||
href={`/system/${encodeURIComponent(row.original.name)}`}
|
||||
href={getPagePath($router, "system", { name: row.original.name })}
|
||||
className="inset-0 absolute w-full h-full"
|
||||
>
|
||||
<span className="sr-only">{row.original.name}</span>
|
||||
@@ -516,11 +569,15 @@ function IndicatorDot({ system, className }: { system: SystemRecord; className?:
|
||||
)
|
||||
}
|
||||
|
||||
function ActionsButton({ system }: { system: SystemRecord }) {
|
||||
// const [opened, setOpened] = useState(false)
|
||||
const ActionsButton = memo(({ system }: { system: SystemRecord }) => {
|
||||
const [deleteOpen, setDeleteOpen] = useState(false)
|
||||
const [editOpen, setEditOpen] = useState(false)
|
||||
let editOpened = useRef(false)
|
||||
|
||||
const { id, status, host, name } = system
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size={"icon"} data-nolink>
|
||||
@@ -531,6 +588,17 @@ function ActionsButton({ system }: { system: SystemRecord }) {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{!isReadOnlyUser() && (
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
editOpened.current = true
|
||||
setEditOpen(true)
|
||||
}}
|
||||
>
|
||||
<PenBoxIcon className="me-2.5 size-4" />
|
||||
<Trans>Edit</Trans>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className={cn(isReadOnlyUser() && "hidden")}
|
||||
onClick={() => {
|
||||
@@ -556,38 +624,43 @@ function ActionsButton({ system }: { system: SystemRecord }) {
|
||||
<Trans>Copy host</Trans>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className={cn(isReadOnlyUser() && "hidden")} />
|
||||
<AlertDialogTrigger asChild>
|
||||
<DropdownMenuItem className={cn(isReadOnlyUser() && "hidden")}>
|
||||
<Trash2Icon className="me-2.5 size-4" />
|
||||
<Trans>Delete</Trans>
|
||||
</DropdownMenuItem>
|
||||
</AlertDialogTrigger>
|
||||
<DropdownMenuItem className={cn(isReadOnlyUser() && "hidden")} onSelect={() => setDeleteOpen(true)}>
|
||||
<Trash2Icon className="me-2.5 size-4" />
|
||||
<Trans>Delete</Trans>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans>Are you sure you want to delete {name}?</Trans>
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<Trans>
|
||||
This action cannot be undone. This will permanently delete all current records for {name} from the
|
||||
database.
|
||||
</Trans>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans>Cancel</Trans>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className={cn(buttonVariants({ variant: "destructive" }))}
|
||||
onClick={() => pb.collection("systems").delete(id)}
|
||||
>
|
||||
<Trans>Continue</Trans>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
{/* edit dialog */}
|
||||
<Dialog open={editOpen} onOpenChange={setEditOpen}>
|
||||
{editOpened.current && <SystemDialog system={system} setOpen={setEditOpen} />}
|
||||
</Dialog>
|
||||
{/* deletion dialog */}
|
||||
<AlertDialog open={deleteOpen} onOpenChange={(open) => setDeleteOpen(open)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans>Are you sure you want to delete {name}?</Trans>
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<Trans>
|
||||
This action cannot be undone. This will permanently delete all current records for {name} from the
|
||||
database.
|
||||
</Trans>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans>Cancel</Trans>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className={cn(buttonVariants({ variant: "destructive" }))}
|
||||
onClick={() => pb.collection("systems").delete(id)}
|
||||
>
|
||||
<Trans>Continue</Trans>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -63,3 +63,13 @@ export function ThermometerIcon(props: SVGProps<SVGSVGElement>) {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Huge icons (MIT)
|
||||
export function GpuIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" {...props} stroke="currentColor" fill="none" strokeWidth="2">
|
||||
<path d="M4 21V4.1a1.5 1.5 0 0 0-1.1-1L2 3m2 2h13c2.4 0 3.5 0 4.3.7s.7 2 .7 4.3v4.5c0 2.4 0 3.5-.7 4.3-.8.7-2 .7-4.3.7h-4.9a1.8 1.8 0 0 1-1.6-1c-.3-.6-1-1-1.6-1H4" />
|
||||
<path d="M19 11.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0m-11.5-3h2m-2 3h2m-2 3h2" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -78,9 +78,19 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.link {
|
||||
@apply text-primary font-medium underline-offset-4 hover:underline;
|
||||
}
|
||||
/* New system dialog width */
|
||||
.ns-dialog {
|
||||
min-width: 30.3rem;
|
||||
}
|
||||
:where(:lang(zh), :lang(zh-CN), :lang(ko)) .ns-dialog {
|
||||
min-width: 27.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.recharts-tooltip-wrapper {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { $direction } from "./stores"
|
||||
import { i18n } from "@lingui/core"
|
||||
import type { Messages } from "@lingui/core"
|
||||
import languages from "@/lib/languages"
|
||||
import { detect, fromUrl, fromStorage, fromNavigator } from "@lingui/detect-locale"
|
||||
import { detect, fromStorage, fromNavigator } from "@lingui/detect-locale"
|
||||
import { messages as enMessages } from "@/locales/en/en.ts"
|
||||
|
||||
// let locale = detect(fromUrl("lang"), fromStorage("lang"), fromNavigator(), "en")
|
||||
@@ -41,15 +41,10 @@ export async function dynamicActivate(locale: string) {
|
||||
if (locale?.startsWith("zh-")) {
|
||||
// map zh variants to zh-CN
|
||||
const zhVariantMap: Record<string, string> = {
|
||||
"zh-CN": "zh-CN",
|
||||
"zh-SG": "zh-CN",
|
||||
"zh-MY": "zh-CN",
|
||||
zh: "zh-CN",
|
||||
"zh-Hans": "zh-CN",
|
||||
"zh-HK": "zh-HK",
|
||||
"zh-TW": "zh-HK",
|
||||
"zh-MO": "zh-HK",
|
||||
"zh-Hant": "zh-HK",
|
||||
"zh-TW": "zh",
|
||||
"zh-MO": "zh",
|
||||
"zh-Hant": "zh",
|
||||
}
|
||||
dynamicActivate(zhVariantMap[locale] || "zh-CN")
|
||||
} else {
|
||||
|
||||
@@ -4,11 +4,21 @@ export default [
|
||||
label: "العربية",
|
||||
e: "🇵🇸",
|
||||
},
|
||||
{
|
||||
lang: "bg",
|
||||
label: "Български",
|
||||
e: "🇧🇬",
|
||||
},
|
||||
{
|
||||
lang: "cs",
|
||||
label: "Čeština",
|
||||
e: "🇨🇿",
|
||||
},
|
||||
{
|
||||
lang: "da",
|
||||
label: "Dansk",
|
||||
e: "🇩🇰",
|
||||
},
|
||||
{
|
||||
lang: "de",
|
||||
label: "Deutsch",
|
||||
@@ -39,6 +49,11 @@ export default [
|
||||
label: "Hrvatski",
|
||||
e: "🇭🇷",
|
||||
},
|
||||
{
|
||||
lang: "hu",
|
||||
label: "Magyar",
|
||||
e: "🇭🇺",
|
||||
},
|
||||
{
|
||||
lang: "it",
|
||||
label: "Italiano",
|
||||
@@ -59,6 +74,11 @@ export default [
|
||||
label: "Nederlands",
|
||||
e: "🇳🇱",
|
||||
},
|
||||
{
|
||||
lang: "no",
|
||||
label: "Norsk",
|
||||
e: "🇳🇴",
|
||||
},
|
||||
{
|
||||
lang: "pl",
|
||||
label: "Polski",
|
||||
@@ -79,6 +99,11 @@ export default [
|
||||
label: "Русский",
|
||||
e: "🇷🇺",
|
||||
},
|
||||
{
|
||||
lang: "sl",
|
||||
label: "Slovenščina",
|
||||
e: "🇸🇮",
|
||||
},
|
||||
{
|
||||
lang: "sv",
|
||||
label: "Svenska",
|
||||
@@ -104,4 +129,9 @@ export default [
|
||||
label: "繁體中文",
|
||||
e: "🇭🇰",
|
||||
},
|
||||
{
|
||||
lang: "zh",
|
||||
label: "繁體中文",
|
||||
e: "🇹🇼",
|
||||
},
|
||||
] as const
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import PocketBase from "pocketbase"
|
||||
import { atom, map, WritableAtom } from "nanostores"
|
||||
import { atom, map, PreinitializedWritableAtom } from "nanostores"
|
||||
import { AlertRecord, ChartTimes, SystemRecord, UserSettings } from "@/types"
|
||||
import { basePath } from "@/components/router"
|
||||
|
||||
/** PocketBase JS Client */
|
||||
export const pb = new PocketBase("/")
|
||||
export const pb = new PocketBase(basePath)
|
||||
|
||||
/** Store if user is authenticated */
|
||||
export const $authenticated = atom(pb.authStore.isValid)
|
||||
@@ -21,7 +22,10 @@ export const $publicKey = atom("")
|
||||
export const $hubVersion = atom("")
|
||||
|
||||
/** Chart time period */
|
||||
export const $chartTime = atom("1h") as WritableAtom<ChartTimes>
|
||||
export const $chartTime = atom("1h") as PreinitializedWritableAtom<ChartTimes>
|
||||
|
||||
/** Whether to display average or max chart values */
|
||||
export const $maxValues = atom(false)
|
||||
|
||||
/** User settings */
|
||||
export const $userSettings = map<UserSettings>({
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useEffect, useState } from "react"
|
||||
import { CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from "lucide-react"
|
||||
import { EthernetIcon, ThermometerIcon } from "@/components/ui/icons"
|
||||
import { t } from "@lingui/macro"
|
||||
import { prependBasePath } from "@/components/router"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
@@ -33,7 +34,7 @@ const verifyAuth = () => {
|
||||
pb.collection("users")
|
||||
.authRefresh()
|
||||
.catch(() => {
|
||||
pb.authStore.clear()
|
||||
logOut()
|
||||
toast({
|
||||
title: t`Failed to authenticate`,
|
||||
description: t`Please log in again`,
|
||||
@@ -42,15 +43,33 @@ const verifyAuth = () => {
|
||||
})
|
||||
}
|
||||
|
||||
export const updateSystemList = async () => {
|
||||
const records = await pb
|
||||
.collection<SystemRecord>("systems")
|
||||
.getFullList({ sort: "+name", fields: "id,name,host,info,status" })
|
||||
if (records.length) {
|
||||
$systems.set(records)
|
||||
} else {
|
||||
verifyAuth()
|
||||
export const updateSystemList = (() => {
|
||||
let isFetchingSystems = false
|
||||
return async () => {
|
||||
if (isFetchingSystems) {
|
||||
return
|
||||
}
|
||||
isFetchingSystems = true
|
||||
try {
|
||||
const records = await pb
|
||||
.collection<SystemRecord>("systems")
|
||||
.getFullList({ sort: "+name", fields: "id,name,host,port,info,status" })
|
||||
|
||||
if (records.length) {
|
||||
$systems.set(records)
|
||||
} else {
|
||||
verifyAuth()
|
||||
}
|
||||
} finally {
|
||||
isFetchingSystems = false
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
/** Logs the user out by clearing the auth store and unsubscribing from realtime updates. */
|
||||
export async function logOut() {
|
||||
pb.authStore.clear()
|
||||
pb.realtime.unsubscribe()
|
||||
}
|
||||
|
||||
export const updateAlerts = () => {
|
||||
@@ -88,7 +107,7 @@ export const formatDay = (timestamp: string) => {
|
||||
}
|
||||
|
||||
export const updateFavicon = (newIcon: string) => {
|
||||
;(document.querySelector("link[rel='icon']") as HTMLLinkElement).href = `/static/${newIcon}`
|
||||
;(document.querySelector("link[rel='icon']") as HTMLLinkElement).href = prependBasePath(`/static/${newIcon}`)
|
||||
}
|
||||
|
||||
export const isAdmin = () => pb.authStore.record?.role === "admin"
|
||||
@@ -283,7 +302,8 @@ export const alertInfo: Record<string, AlertInfo> = {
|
||||
unit: "",
|
||||
icon: ServerIcon,
|
||||
desc: () => t`Triggers when status switches between up and down`,
|
||||
single: true,
|
||||
/** "for x minutes" is appended to desc when only one value */
|
||||
singleDesc: () => t`System` + " " + t`Down`,
|
||||
},
|
||||
CPU: {
|
||||
name: () => t`CPU Usage`,
|
||||
@@ -317,3 +337,11 @@ export const alertInfo: Record<string, AlertInfo> = {
|
||||
desc: () => t`Triggers when any sensor exceeds a threshold`,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Retuns value of system host, truncating full path if socket.
|
||||
* @example
|
||||
* // Assuming system.host is "/var/run/beszel.sock"
|
||||
* const hostname = getHostDisplayValue(system) // hostname will be "beszel.sock"
|
||||
*/
|
||||
export const getHostDisplayValue = (system: SystemRecord): string => system.host.slice(system.host.lastIndexOf("/") + 1)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1114
beszel/site/src/locales/bg/bg.po
Normal file
1114
beszel/site/src/locales/bg/bg.po
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1114
beszel/site/src/locales/da/da.po
Normal file
1114
beszel/site/src/locales/da/da.po
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1114
beszel/site/src/locales/el/el.po
Normal file
1114
beszel/site/src/locales/el/el.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,60 +13,59 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/components/routes/system.tsx:250
|
||||
#: src/components/routes/system.tsx:252
|
||||
msgid "{0, plural, one {# day} other {# days}}"
|
||||
msgstr "{0, plural, one {# day} other {# days}}"
|
||||
|
||||
#: src/components/routes/system.tsx:248
|
||||
#: src/components/routes/system.tsx:250
|
||||
msgid "{hours, plural, one {# hour} other {# hours}}"
|
||||
msgstr "{hours, plural, one {# hour} other {# hours}}"
|
||||
|
||||
#: src/lib/utils.ts:139
|
||||
#: src/lib/utils.ts:158
|
||||
msgid "1 hour"
|
||||
msgstr "1 hour"
|
||||
|
||||
#: src/lib/utils.ts:162
|
||||
#: src/lib/utils.ts:181
|
||||
msgid "1 week"
|
||||
msgstr "1 week"
|
||||
|
||||
#: src/lib/utils.ts:147
|
||||
#: src/lib/utils.ts:166
|
||||
msgid "12 hours"
|
||||
msgstr "12 hours"
|
||||
|
||||
#: src/lib/utils.ts:155
|
||||
#: src/lib/utils.ts:174
|
||||
msgid "24 hours"
|
||||
msgstr "24 hours"
|
||||
|
||||
#: src/lib/utils.ts:170
|
||||
#: src/lib/utils.ts:189
|
||||
msgid "30 days"
|
||||
msgstr "30 days"
|
||||
|
||||
#. Table column
|
||||
#: src/components/systems-table/systems-table.tsx:227
|
||||
#: src/components/systems-table/systems-table.tsx:313
|
||||
#: src/components/systems-table/systems-table.tsx:455
|
||||
#: src/components/systems-table/systems-table.tsx:465
|
||||
#: src/components/systems-table/systems-table.tsx:278
|
||||
#: src/components/systems-table/systems-table.tsx:366
|
||||
#: src/components/systems-table/systems-table.tsx:508
|
||||
#: src/components/systems-table/systems-table.tsx:518
|
||||
msgid "Actions"
|
||||
msgstr "Actions"
|
||||
|
||||
#: src/components/routes/home.tsx:61
|
||||
#: src/components/routes/home.tsx:62
|
||||
msgid "Active Alerts"
|
||||
msgstr "Active Alerts"
|
||||
|
||||
#: src/components/add-system.tsx:78
|
||||
#: src/components/add-system.tsx:42
|
||||
msgid "Add <0>System</0>"
|
||||
msgstr "Add <0>System</0>"
|
||||
|
||||
#: src/components/add-system.tsx:87
|
||||
#: src/components/add-system.tsx:124
|
||||
msgid "Add New System"
|
||||
msgstr "Add New System"
|
||||
|
||||
#: src/components/add-system.tsx:165
|
||||
#: src/components/add-system.tsx:176
|
||||
#: src/components/add-system.tsx:230
|
||||
msgid "Add system"
|
||||
msgstr "Add system"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx:156
|
||||
#: src/components/routes/settings/notifications.tsx:157
|
||||
msgid "Add URL"
|
||||
msgstr "Add URL"
|
||||
|
||||
@@ -74,15 +73,15 @@ msgstr "Add URL"
|
||||
msgid "Adjust display options for charts."
|
||||
msgstr "Adjust display options for charts."
|
||||
|
||||
#: src/components/command-palette.tsx:132
|
||||
#: src/components/command-palette.tsx:145
|
||||
#: src/components/command-palette.tsx:159
|
||||
#: src/components/command-palette.tsx:173
|
||||
#: src/components/command-palette.tsx:188
|
||||
#: src/components/command-palette.tsx:133
|
||||
#: src/components/command-palette.tsx:146
|
||||
#: src/components/command-palette.tsx:160
|
||||
#: src/components/command-palette.tsx:174
|
||||
#: src/components/command-palette.tsx:189
|
||||
msgid "Admin"
|
||||
msgstr "Admin"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx:201
|
||||
#: src/components/systems-table/systems-table.tsx:246
|
||||
msgid "Agent"
|
||||
msgstr "Agent"
|
||||
|
||||
@@ -92,11 +91,11 @@ msgid "Alerts"
|
||||
msgstr "Alerts"
|
||||
|
||||
#: src/components/alerts/alert-button.tsx:88
|
||||
#: src/components/systems-table/systems-table.tsx:266
|
||||
#: src/components/systems-table/systems-table.tsx:319
|
||||
msgid "All Systems"
|
||||
msgstr "All Systems"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx:570
|
||||
#: src/components/systems-table/systems-table.tsx:642
|
||||
msgid "Are you sure you want to delete {name}?"
|
||||
msgstr "Are you sure you want to delete {name}?"
|
||||
|
||||
@@ -104,49 +103,49 @@ msgstr "Are you sure you want to delete {name}?"
|
||||
msgid "Automatic copy requires a secure context."
|
||||
msgstr "Automatic copy requires a secure context."
|
||||
|
||||
#: src/components/routes/system.tsx:625
|
||||
#: src/components/routes/system.tsx:633
|
||||
msgid "Average"
|
||||
msgstr "Average"
|
||||
|
||||
#: src/components/routes/system.tsx:396
|
||||
#: src/components/routes/system.tsx:410
|
||||
msgid "Average CPU utilization of containers"
|
||||
msgstr "Average CPU utilization of containers"
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx:206
|
||||
#: src/components/alerts/alerts-system.tsx:205
|
||||
msgid "Average exceeds <0>{value}{0}</0>"
|
||||
msgstr "Average exceeds <0>{value}{0}</0>"
|
||||
|
||||
#: src/components/routes/system.tsx:497
|
||||
#: src/components/routes/system.tsx:511
|
||||
msgid "Average power consumption of GPUs"
|
||||
msgstr "Average power consumption of GPUs"
|
||||
|
||||
#: src/components/routes/system.tsx:385
|
||||
#: src/components/routes/system.tsx:399
|
||||
msgid "Average system-wide CPU utilization"
|
||||
msgstr "Average system-wide CPU utilization"
|
||||
|
||||
#: src/components/routes/system.tsx:515
|
||||
#: src/components/routes/system.tsx:529
|
||||
msgid "Average utilization of {0}"
|
||||
msgstr "Average utilization of {0}"
|
||||
|
||||
#: src/components/command-palette.tsx:170
|
||||
#: src/components/navbar.tsx:93
|
||||
#: src/components/command-palette.tsx:171
|
||||
#: src/components/navbar.tsx:94
|
||||
msgid "Backups"
|
||||
msgstr "Backups"
|
||||
|
||||
#: src/components/routes/system.tsx:441
|
||||
#: src/lib/utils.ts:307
|
||||
#: src/components/routes/system.tsx:455
|
||||
#: src/lib/utils.ts:327
|
||||
msgid "Bandwidth"
|
||||
msgstr "Bandwidth"
|
||||
|
||||
#: src/components/login/auth-form.tsx:289
|
||||
#: src/components/login/auth-form.tsx:304
|
||||
msgid "Beszel supports OpenID Connect and many OAuth2 authentication providers."
|
||||
msgstr "Beszel supports OpenID Connect and many OAuth2 authentication providers."
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx:127
|
||||
#: src/components/routes/settings/notifications.tsx:128
|
||||
msgid "Beszel uses <0>Shoutrrr</0> to integrate with popular notification services."
|
||||
msgstr "Beszel uses <0>Shoutrrr</0> to integrate with popular notification services."
|
||||
|
||||
#: src/components/add-system.tsx:92
|
||||
#: src/components/add-system.tsx:129
|
||||
msgid "Binary"
|
||||
msgstr "Binary"
|
||||
|
||||
@@ -154,7 +153,7 @@ msgstr "Binary"
|
||||
msgid "Cache / Buffers"
|
||||
msgstr "Cache / Buffers"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx:581
|
||||
#: src/components/systems-table/systems-table.tsx:653
|
||||
msgid "Cancel"
|
||||
msgstr "Cancel"
|
||||
|
||||
@@ -178,11 +177,11 @@ msgstr "Check {email} for a reset link."
|
||||
msgid "Check logs for more details."
|
||||
msgstr "Check logs for more details."
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx:183
|
||||
#: src/components/routes/settings/notifications.tsx:184
|
||||
msgid "Check your notification service"
|
||||
msgstr "Check your notification service"
|
||||
|
||||
#: src/components/add-system.tsx:151
|
||||
#: src/components/add-system.tsx:203
|
||||
msgid "Click to copy"
|
||||
msgstr "Click to copy"
|
||||
|
||||
@@ -191,32 +190,33 @@ msgstr "Click to copy"
|
||||
msgid "Command line instructions"
|
||||
msgstr "Command line instructions"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx:77
|
||||
#: src/components/routes/settings/notifications.tsx:78
|
||||
msgid "Configure how you receive alert notifications."
|
||||
msgstr "Configure how you receive alert notifications."
|
||||
|
||||
#: src/components/login/auth-form.tsx:165
|
||||
#: src/components/login/auth-form.tsx:170
|
||||
#: src/components/login/auth-form.tsx:210
|
||||
#: src/components/login/auth-form.tsx:215
|
||||
msgid "Confirm password"
|
||||
msgstr "Confirm password"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx:587
|
||||
#: src/components/systems-table/systems-table.tsx:659
|
||||
msgid "Continue"
|
||||
msgstr "Continue"
|
||||
|
||||
#: src/lib/utils.ts:25
|
||||
#: src/lib/utils.ts:26
|
||||
msgid "Copied to clipboard"
|
||||
msgstr "Copied to clipboard"
|
||||
|
||||
#: src/components/add-system.tsx:162
|
||||
#: src/components/add-system.tsx:214
|
||||
#: src/components/add-system.tsx:216
|
||||
msgid "Copy"
|
||||
msgstr "Copy"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx:556
|
||||
#: src/components/systems-table/systems-table.tsx:624
|
||||
msgid "Copy host"
|
||||
msgstr "Copy host"
|
||||
|
||||
#: src/components/add-system.tsx:173
|
||||
#: src/components/add-system.tsx:223
|
||||
msgid "Copy Linux command"
|
||||
msgstr "Copy Linux command"
|
||||
|
||||
@@ -224,17 +224,17 @@ msgstr "Copy Linux command"
|
||||
msgid "Copy text"
|
||||
msgstr "Copy text"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx:157
|
||||
#: src/components/systems-table/systems-table.tsx:165
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
|
||||
#: src/components/charts/area-chart.tsx:56
|
||||
#: src/components/routes/system.tsx:384
|
||||
#: src/lib/utils.ts:289
|
||||
#: src/components/routes/system.tsx:398
|
||||
#: src/lib/utils.ts:309
|
||||
msgid "CPU Usage"
|
||||
msgstr "CPU Usage"
|
||||
|
||||
#: src/components/login/auth-form.tsx:191
|
||||
#: src/components/login/auth-form.tsx:236
|
||||
msgid "Create account"
|
||||
msgstr "Create account"
|
||||
|
||||
@@ -243,8 +243,8 @@ msgstr "Create account"
|
||||
msgid "Dark"
|
||||
msgstr "Dark"
|
||||
|
||||
#: src/components/command-palette.tsx:81
|
||||
#: src/components/routes/home.tsx:34
|
||||
#: src/components/command-palette.tsx:82
|
||||
#: src/components/routes/home.tsx:35
|
||||
msgid "Dashboard"
|
||||
msgstr "Dashboard"
|
||||
|
||||
@@ -252,50 +252,61 @@ msgstr "Dashboard"
|
||||
msgid "Default time period"
|
||||
msgstr "Default time period"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx:562
|
||||
#: src/components/systems-table/systems-table.tsx:629
|
||||
msgid "Delete"
|
||||
msgstr "Delete"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx:173
|
||||
#: src/components/systems-table/systems-table.tsx:181
|
||||
msgid "Disk"
|
||||
msgstr "Disk"
|
||||
|
||||
#: src/components/routes/system.tsx:431
|
||||
#: src/components/routes/system.tsx:445
|
||||
msgid "Disk I/O"
|
||||
msgstr "Disk I/O"
|
||||
|
||||
#: src/components/charts/disk-chart.tsx:79
|
||||
#: src/components/routes/system.tsx:424
|
||||
#: src/lib/utils.ts:301
|
||||
#: src/components/routes/system.tsx:438
|
||||
#: src/lib/utils.ts:321
|
||||
msgid "Disk Usage"
|
||||
msgstr "Disk Usage"
|
||||
|
||||
#: src/components/routes/system.tsx:552
|
||||
#: src/components/routes/system.tsx:566
|
||||
msgid "Disk usage of {extraFsName}"
|
||||
msgstr "Disk usage of {extraFsName}"
|
||||
|
||||
#: src/components/routes/system.tsx:395
|
||||
#: src/components/routes/system.tsx:409
|
||||
msgid "Docker CPU Usage"
|
||||
msgstr "Docker CPU Usage"
|
||||
|
||||
#: src/components/routes/system.tsx:416
|
||||
#: src/components/routes/system.tsx:430
|
||||
msgid "Docker Memory Usage"
|
||||
msgstr "Docker Memory Usage"
|
||||
|
||||
#: src/components/routes/system.tsx:457
|
||||
#: src/components/routes/system.tsx:471
|
||||
msgid "Docker Network I/O"
|
||||
msgstr "Docker Network I/O"
|
||||
|
||||
#: src/components/command-palette.tsx:124
|
||||
#: src/components/command-palette.tsx:125
|
||||
msgid "Documentation"
|
||||
msgstr "Documentation"
|
||||
|
||||
#: src/components/login/auth-form.tsx:128
|
||||
#. Context: System is down
|
||||
#: src/components/routes/system.tsx:309
|
||||
#: src/lib/utils.ts:306
|
||||
msgid "Down"
|
||||
msgstr "Down"
|
||||
|
||||
#: src/components/add-system.tsx:124
|
||||
#: src/components/systems-table/systems-table.tsx:599
|
||||
msgid "Edit"
|
||||
msgstr "Edit"
|
||||
|
||||
#: src/components/login/auth-form.tsx:173
|
||||
#: src/components/login/forgot-pass-form.tsx:53
|
||||
msgid "Email"
|
||||
msgstr "Email"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx:91
|
||||
#: src/components/routes/settings/notifications.tsx:92
|
||||
msgid "Email notifications"
|
||||
msgstr "Email notifications"
|
||||
|
||||
@@ -303,17 +314,17 @@ msgstr "Email notifications"
|
||||
msgid "Enter email address to reset password"
|
||||
msgstr "Enter email address to reset password"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx:111
|
||||
#: src/components/routes/settings/notifications.tsx:112
|
||||
msgid "Enter email address..."
|
||||
msgstr "Enter email address..."
|
||||
|
||||
#: src/components/login/auth-form.tsx:232
|
||||
#: src/components/login/auth-form.tsx:136
|
||||
#: src/components/routes/settings/config-yaml.tsx:28
|
||||
#: src/components/routes/settings/notifications.tsx:187
|
||||
#: src/components/routes/settings/notifications.tsx:188
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
|
||||
#: src/components/routes/home.tsx:80
|
||||
#: src/components/routes/home.tsx:81
|
||||
msgid "Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
msgstr "Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}"
|
||||
|
||||
@@ -329,16 +340,16 @@ msgstr "Export configuration"
|
||||
msgid "Export your current systems configuration."
|
||||
msgstr "Export your current systems configuration."
|
||||
|
||||
#: src/lib/utils.ts:38
|
||||
#: src/lib/utils.ts:39
|
||||
msgid "Failed to authenticate"
|
||||
msgstr "Failed to authenticate"
|
||||
|
||||
#: src/components/routes/settings/layout.tsx:39
|
||||
#: src/components/routes/settings/notifications.tsx:62
|
||||
#: src/components/routes/settings/notifications.tsx:63
|
||||
msgid "Failed to save settings"
|
||||
msgstr "Failed to save settings"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx:188
|
||||
#: src/components/routes/settings/notifications.tsx:189
|
||||
msgid "Failed to send test notification"
|
||||
msgstr "Failed to send test notification"
|
||||
|
||||
@@ -346,16 +357,16 @@ msgstr "Failed to send test notification"
|
||||
msgid "Failed to update alert"
|
||||
msgstr "Failed to update alert"
|
||||
|
||||
#: src/components/routes/system.tsx:596
|
||||
#: src/components/systems-table/systems-table.tsx:273
|
||||
#: src/components/routes/system.tsx:606
|
||||
#: src/components/systems-table/systems-table.tsx:326
|
||||
msgid "Filter..."
|
||||
msgstr "Filter..."
|
||||
|
||||
#: src/components/alerts/alerts-system.tsx:227
|
||||
#: src/components/alerts/alerts-system.tsx:230
|
||||
msgid "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
msgstr "For <0>{min}</0> {min, plural, one {minute} other {minutes}}"
|
||||
|
||||
#: src/components/login/auth-form.tsx:313
|
||||
#: src/components/login/auth-form.tsx:328
|
||||
msgid "Forgot password?"
|
||||
msgstr "Forgot password?"
|
||||
|
||||
@@ -365,15 +376,15 @@ msgstr "Forgot password?"
|
||||
msgid "General"
|
||||
msgstr "General"
|
||||
|
||||
#: src/components/routes/system.tsx:496
|
||||
#: src/components/routes/system.tsx:510
|
||||
msgid "GPU Power Draw"
|
||||
msgstr "GPU Power Draw"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx:300
|
||||
#: src/components/systems-table/systems-table.tsx:353
|
||||
msgid "Grid"
|
||||
msgstr "Grid"
|
||||
|
||||
#: src/components/add-system.tsx:120
|
||||
#: src/components/add-system.tsx:157
|
||||
msgid "Host / IP"
|
||||
msgstr "Host / IP"
|
||||
|
||||
@@ -381,12 +392,12 @@ msgstr "Host / IP"
|
||||
msgid "If you've lost the password to your admin account, you may reset it using the following command."
|
||||
msgstr "If you've lost the password to your admin account, you may reset it using the following command."
|
||||
|
||||
#: src/components/login/auth-form.tsx:16
|
||||
#: src/components/login/auth-form.tsx:17
|
||||
msgid "Invalid email address."
|
||||
msgstr "Invalid email address."
|
||||
|
||||
#. Linux kernel
|
||||
#: src/components/routes/system.tsx:262
|
||||
#: src/components/routes/system.tsx:264
|
||||
msgid "Kernel"
|
||||
msgstr "Kernel"
|
||||
|
||||
@@ -394,7 +405,7 @@ msgstr "Kernel"
|
||||
msgid "Language"
|
||||
msgstr "Language"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx:286
|
||||
#: src/components/systems-table/systems-table.tsx:339
|
||||
msgid "Layout"
|
||||
msgstr "Layout"
|
||||
|
||||
@@ -403,7 +414,7 @@ msgstr "Layout"
|
||||
msgid "Light"
|
||||
msgstr "Light"
|
||||
|
||||
#: src/components/navbar.tsx:104
|
||||
#: src/components/navbar.tsx:105
|
||||
msgid "Log Out"
|
||||
msgstr "Log Out"
|
||||
|
||||
@@ -411,17 +422,17 @@ msgstr "Log Out"
|
||||
msgid "Login"
|
||||
msgstr "Login"
|
||||
|
||||
#: src/components/login/auth-form.tsx:34
|
||||
#: src/components/login/auth-form.tsx:39
|
||||
#: src/components/login/forgot-pass-form.tsx:15
|
||||
msgid "Login attempt failed"
|
||||
msgstr "Login attempt failed"
|
||||
|
||||
#: src/components/command-palette.tsx:156
|
||||
#: src/components/navbar.tsx:85
|
||||
#: src/components/command-palette.tsx:157
|
||||
#: src/components/navbar.tsx:86
|
||||
msgid "Logs"
|
||||
msgstr "Logs"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx:80
|
||||
#: src/components/routes/settings/notifications.tsx:81
|
||||
msgid "Looking instead for where to create alerts? Click the bell <0/> icons in the systems table."
|
||||
msgstr "Looking instead for where to create alerts? Click the bell <0/> icons in the systems table."
|
||||
|
||||
@@ -429,56 +440,60 @@ msgstr "Looking instead for where to create alerts? Click the bell <0/> icons in
|
||||
msgid "Manage display and notification preferences."
|
||||
msgstr "Manage display and notification preferences."
|
||||
|
||||
#: src/components/add-system.tsx:225
|
||||
msgid "Manual setup instructions"
|
||||
msgstr "Manual setup instructions"
|
||||
|
||||
#. Chart select field. Please try to keep this short.
|
||||
#: src/components/routes/system.tsx:628
|
||||
#: src/components/routes/system.tsx:636
|
||||
msgid "Max 1 min"
|
||||
msgstr "Max 1 min"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx:165
|
||||
#: src/components/systems-table/systems-table.tsx:173
|
||||
msgid "Memory"
|
||||
msgstr "Memory"
|
||||
|
||||
#: src/components/routes/system.tsx:406
|
||||
#: src/lib/utils.ts:295
|
||||
#: src/components/routes/system.tsx:420
|
||||
#: src/lib/utils.ts:315
|
||||
msgid "Memory Usage"
|
||||
msgstr "Memory Usage"
|
||||
|
||||
#: src/components/routes/system.tsx:417
|
||||
#: src/components/routes/system.tsx:431
|
||||
msgid "Memory usage of docker containers"
|
||||
msgstr "Memory usage of docker containers"
|
||||
|
||||
#: src/components/add-system.tsx:116
|
||||
#: src/components/add-system.tsx:153
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx:181
|
||||
#: src/components/systems-table/systems-table.tsx:198
|
||||
msgid "Net"
|
||||
msgstr "Net"
|
||||
|
||||
#: src/components/routes/system.tsx:458
|
||||
#: src/components/routes/system.tsx:472
|
||||
msgid "Network traffic of docker containers"
|
||||
msgstr "Network traffic of docker containers"
|
||||
|
||||
#: src/components/routes/system.tsx:443
|
||||
#: src/components/routes/system.tsx:457
|
||||
msgid "Network traffic of public interfaces"
|
||||
msgstr "Network traffic of public interfaces"
|
||||
|
||||
#: src/components/command-palette.tsx:49
|
||||
#: src/components/command-palette.tsx:50
|
||||
msgid "No results found."
|
||||
msgstr "No results found."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx:421
|
||||
#: src/components/systems-table/systems-table.tsx:494
|
||||
#: src/components/systems-table/systems-table.tsx:474
|
||||
#: src/components/systems-table/systems-table.tsx:547
|
||||
msgid "No systems found."
|
||||
msgstr "No systems found."
|
||||
|
||||
#: src/components/command-palette.tsx:110
|
||||
#: src/components/command-palette.tsx:111
|
||||
#: src/components/routes/settings/layout.tsx:56
|
||||
#: src/components/routes/settings/notifications.tsx:74
|
||||
#: src/components/routes/settings/notifications.tsx:75
|
||||
msgid "Notifications"
|
||||
msgstr "Notifications"
|
||||
|
||||
#: src/components/login/auth-form.tsx:284
|
||||
#: src/components/login/auth-form.tsx:299
|
||||
msgid "OAuth 2 / OIDC support"
|
||||
msgstr "OAuth 2 / OIDC support"
|
||||
|
||||
@@ -486,11 +501,11 @@ msgstr "OAuth 2 / OIDC support"
|
||||
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
|
||||
msgstr "On each restart, systems in the database will be updated to match the systems defined in the file."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx:528
|
||||
#: src/components/systems-table/systems-table.tsx:585
|
||||
msgid "Open menu"
|
||||
msgstr "Open menu"
|
||||
|
||||
#: src/components/login/auth-form.tsx:203
|
||||
#: src/components/login/auth-form.tsx:248
|
||||
msgid "Or continue with"
|
||||
msgstr "Or continue with"
|
||||
|
||||
@@ -498,32 +513,36 @@ msgstr "Or continue with"
|
||||
msgid "Overwrite existing alerts"
|
||||
msgstr "Overwrite existing alerts"
|
||||
|
||||
#: src/components/command-palette.tsx:84
|
||||
#: src/components/command-palette.tsx:85
|
||||
msgid "Page"
|
||||
msgstr "Page"
|
||||
|
||||
#: src/components/command-palette.tsx:71
|
||||
#: src/components/command-palette.tsx:72
|
||||
msgid "Pages / Settings"
|
||||
msgstr "Pages / Settings"
|
||||
|
||||
#: src/components/login/auth-form.tsx:147
|
||||
#: src/components/login/auth-form.tsx:152
|
||||
#: src/components/login/auth-form.tsx:192
|
||||
#: src/components/login/auth-form.tsx:197
|
||||
msgid "Password"
|
||||
msgstr "Password"
|
||||
|
||||
#: src/components/login/auth-form.tsx:17
|
||||
#: src/components/login/auth-form.tsx:20
|
||||
msgid "Password must be at least 8 characters."
|
||||
msgstr "Password must be at least 8 characters."
|
||||
|
||||
#: src/components/login/auth-form.tsx:21
|
||||
msgid "Password must be less than 72 bytes."
|
||||
msgstr "Password must be less than 72 bytes."
|
||||
|
||||
#: src/components/login/forgot-pass-form.tsx:33
|
||||
msgid "Password reset request received"
|
||||
msgstr "Password reset request received"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx:550
|
||||
#: src/components/systems-table/systems-table.tsx:618
|
||||
msgid "Pause"
|
||||
msgstr "Pause"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx:95
|
||||
#: src/components/routes/settings/notifications.tsx:96
|
||||
msgid "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
msgstr "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
|
||||
@@ -531,7 +550,7 @@ msgstr "Please <0>configure an SMTP server</0> to ensure alerts are delivered."
|
||||
msgid "Please check logs for more details."
|
||||
msgstr "Please check logs for more details."
|
||||
|
||||
#: src/components/login/auth-form.tsx:35
|
||||
#: src/components/login/auth-form.tsx:40
|
||||
#: src/components/login/forgot-pass-form.tsx:16
|
||||
msgid "Please check your credentials and try again"
|
||||
msgstr "Please check your credentials and try again"
|
||||
@@ -540,15 +559,15 @@ msgstr "Please check your credentials and try again"
|
||||
msgid "Please create an admin account"
|
||||
msgstr "Please create an admin account"
|
||||
|
||||
#: src/components/login/auth-form.tsx:233
|
||||
#: src/components/login/auth-form.tsx:137
|
||||
msgid "Please enable pop-ups for this site"
|
||||
msgstr "Please enable pop-ups for this site"
|
||||
|
||||
#: src/lib/utils.ts:39
|
||||
#: src/lib/utils.ts:40
|
||||
msgid "Please log in again"
|
||||
msgstr "Please log in again"
|
||||
|
||||
#: src/components/login/auth-form.tsx:292
|
||||
#: src/components/login/auth-form.tsx:307
|
||||
msgid "Please see <0>the documentation</0> for instructions."
|
||||
msgstr "Please see <0>the documentation</0> for instructions."
|
||||
|
||||
@@ -556,12 +575,12 @@ msgstr "Please see <0>the documentation</0> for instructions."
|
||||
msgid "Please sign in to your account"
|
||||
msgstr "Please sign in to your account"
|
||||
|
||||
#: src/components/add-system.tsx:124
|
||||
#: src/components/add-system.tsx:169
|
||||
msgid "Port"
|
||||
msgstr "Port"
|
||||
|
||||
#: src/components/routes/system.tsx:407
|
||||
#: src/components/routes/system.tsx:523
|
||||
#: src/components/routes/system.tsx:421
|
||||
#: src/components/routes/system.tsx:537
|
||||
msgid "Precise utilization at the recorded time"
|
||||
msgstr "Precise utilization at the recorded time"
|
||||
|
||||
@@ -570,7 +589,7 @@ msgid "Preferred Language"
|
||||
msgstr "Preferred Language"
|
||||
|
||||
#. Use 'Key' if your language requires many more characters
|
||||
#: src/components/add-system.tsx:128
|
||||
#: src/components/add-system.tsx:180
|
||||
msgid "Public Key"
|
||||
msgstr "Public Key"
|
||||
|
||||
@@ -589,24 +608,28 @@ msgstr "Received"
|
||||
msgid "Reset Password"
|
||||
msgstr "Reset Password"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx:545
|
||||
#: src/components/systems-table/systems-table.tsx:613
|
||||
msgid "Resume"
|
||||
msgstr "Resume"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx:117
|
||||
#: src/components/routes/settings/notifications.tsx:118
|
||||
msgid "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
msgstr "Save address using enter key or comma. Leave blank to disable email notifications."
|
||||
|
||||
#: src/components/routes/settings/general.tsx:106
|
||||
#: src/components/routes/settings/notifications.tsx:167
|
||||
#: src/components/routes/settings/notifications.tsx:168
|
||||
msgid "Save Settings"
|
||||
msgstr "Save Settings"
|
||||
|
||||
#: src/components/navbar.tsx:133
|
||||
#: src/components/add-system.tsx:230
|
||||
msgid "Save system"
|
||||
msgstr "Save system"
|
||||
|
||||
#: src/components/navbar.tsx:134
|
||||
msgid "Search"
|
||||
msgstr "Search"
|
||||
|
||||
#: src/components/command-palette.tsx:46
|
||||
#: src/components/command-palette.tsx:47
|
||||
msgid "Search for systems or settings..."
|
||||
msgstr "Search for systems or settings..."
|
||||
|
||||
@@ -623,9 +646,9 @@ msgstr "Sent"
|
||||
msgid "Sets the default time range for charts when a system is viewed."
|
||||
msgstr "Sets the default time range for charts when a system is viewed."
|
||||
|
||||
#: src/components/command-palette.tsx:95
|
||||
#: src/components/command-palette.tsx:98
|
||||
#: src/components/command-palette.tsx:113
|
||||
#: src/components/command-palette.tsx:96
|
||||
#: src/components/command-palette.tsx:99
|
||||
#: src/components/command-palette.tsx:114
|
||||
#: src/components/routes/settings/layout.tsx:71
|
||||
#: src/components/routes/settings/layout.tsx:82
|
||||
msgid "Settings"
|
||||
@@ -635,40 +658,41 @@ msgstr "Settings"
|
||||
msgid "Settings saved"
|
||||
msgstr "Settings saved"
|
||||
|
||||
#: src/components/login/auth-form.tsx:191
|
||||
#: src/components/login/auth-form.tsx:236
|
||||
msgid "Sign in"
|
||||
msgstr "Sign in"
|
||||
|
||||
#: src/components/command-palette.tsx:185
|
||||
#: src/components/command-palette.tsx:186
|
||||
msgid "SMTP settings"
|
||||
msgstr "SMTP settings"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx:308
|
||||
#: src/components/systems-table/systems-table.tsx:361
|
||||
msgid "Sort By"
|
||||
msgstr "Sort By"
|
||||
|
||||
#: src/lib/utils.ts:282
|
||||
#: src/lib/utils.ts:301
|
||||
msgid "Status"
|
||||
msgstr "Status"
|
||||
|
||||
#: src/components/routes/system.tsx:473
|
||||
#: src/components/routes/system.tsx:487
|
||||
msgid "Swap space used by the system"
|
||||
msgstr "Swap space used by the system"
|
||||
|
||||
#: src/components/routes/system.tsx:472
|
||||
#: src/components/routes/system.tsx:486
|
||||
msgid "Swap Usage"
|
||||
msgstr "Swap Usage"
|
||||
|
||||
#. System theme
|
||||
#: src/components/mode-toggle.tsx:26
|
||||
#: src/components/systems-table/systems-table.tsx:117
|
||||
#: src/components/systems-table/systems-table.tsx:125
|
||||
#: src/components/systems-table/systems-table.tsx:136
|
||||
#: src/components/systems-table/systems-table.tsx:465
|
||||
#: src/components/systems-table/systems-table.tsx:133
|
||||
#: src/components/systems-table/systems-table.tsx:144
|
||||
#: src/components/systems-table/systems-table.tsx:518
|
||||
#: src/lib/utils.ts:306
|
||||
msgid "System"
|
||||
msgstr "System"
|
||||
|
||||
#: src/components/navbar.tsx:77
|
||||
#: src/components/navbar.tsx:78
|
||||
msgid "Systems"
|
||||
msgstr "Systems"
|
||||
|
||||
@@ -676,32 +700,37 @@ msgstr "Systems"
|
||||
msgid "Systems may be managed in a <0>config.yml</0> file inside your data directory."
|
||||
msgstr "Systems may be managed in a <0>config.yml</0> file inside your data directory."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx:296
|
||||
#: src/components/systems-table/systems-table.tsx:349
|
||||
msgid "Table"
|
||||
msgstr "Table"
|
||||
|
||||
#: src/components/routes/system.tsx:484
|
||||
#: src/lib/utils.ts:314
|
||||
#. Temperature label in systems table
|
||||
#: src/components/systems-table/systems-table.tsx:218
|
||||
msgid "Temp"
|
||||
msgstr "Temp"
|
||||
|
||||
#: src/components/routes/system.tsx:498
|
||||
#: src/lib/utils.ts:334
|
||||
msgid "Temperature"
|
||||
msgstr "Temperature"
|
||||
|
||||
#: src/components/routes/system.tsx:485
|
||||
#: src/components/routes/system.tsx:499
|
||||
msgid "Temperatures of system sensors"
|
||||
msgstr "Temperatures of system sensors"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx:211
|
||||
#: src/components/routes/settings/notifications.tsx:212
|
||||
msgid "Test <0>URL</0>"
|
||||
msgstr "Test <0>URL</0>"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx:182
|
||||
#: src/components/routes/settings/notifications.tsx:183
|
||||
msgid "Test notification sent"
|
||||
msgstr "Test notification sent"
|
||||
|
||||
#: src/components/add-system.tsx:108
|
||||
#: src/components/add-system.tsx:145
|
||||
msgid "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
msgstr "The agent must be running on the system to connect. Copy the installation command for the agent below."
|
||||
|
||||
#: src/components/add-system.tsx:99
|
||||
#: src/components/add-system.tsx:136
|
||||
msgid "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
msgstr "The agent must be running on the system to connect. Copy the<0>docker-compose.yml</0> for the agent below."
|
||||
|
||||
@@ -709,24 +738,24 @@ msgstr "The agent must be running on the system to connect. Copy the<0>docker-co
|
||||
msgid "Then log into the backend and reset your user account password in the users table."
|
||||
msgstr "Then log into the backend and reset your user account password in the users table."
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx:573
|
||||
#: src/components/systems-table/systems-table.tsx:645
|
||||
msgid "This action cannot be undone. This will permanently delete all current records for {name} from the database."
|
||||
msgstr "This action cannot be undone. This will permanently delete all current records for {name} from the database."
|
||||
|
||||
#: src/components/routes/system.tsx:564
|
||||
#: src/components/routes/system.tsx:578
|
||||
msgid "Throughput of {extraFsName}"
|
||||
msgstr "Throughput of {extraFsName}"
|
||||
|
||||
#: src/components/routes/system.tsx:432
|
||||
#: src/components/routes/system.tsx:446
|
||||
msgid "Throughput of root filesystem"
|
||||
msgstr "Throughput of root filesystem"
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx:106
|
||||
#: src/components/routes/settings/notifications.tsx:107
|
||||
msgid "To email(s)"
|
||||
msgstr "To email(s)"
|
||||
|
||||
#: src/components/routes/system.tsx:359
|
||||
#: src/components/routes/system.tsx:372
|
||||
#: src/components/routes/system.tsx:373
|
||||
#: src/components/routes/system.tsx:386
|
||||
msgid "Toggle grid"
|
||||
msgstr "Toggle grid"
|
||||
|
||||
@@ -734,45 +763,50 @@ msgstr "Toggle grid"
|
||||
msgid "Toggle theme"
|
||||
msgstr "Toggle theme"
|
||||
|
||||
#: src/lib/utils.ts:317
|
||||
#: src/lib/utils.ts:337
|
||||
msgid "Triggers when any sensor exceeds a threshold"
|
||||
msgstr "Triggers when any sensor exceeds a threshold"
|
||||
|
||||
#: src/lib/utils.ts:310
|
||||
#: src/lib/utils.ts:330
|
||||
msgid "Triggers when combined up/down exceeds a threshold"
|
||||
msgstr "Triggers when combined up/down exceeds a threshold"
|
||||
|
||||
#: src/lib/utils.ts:292
|
||||
#: src/lib/utils.ts:312
|
||||
msgid "Triggers when CPU usage exceeds a threshold"
|
||||
msgstr "Triggers when CPU usage exceeds a threshold"
|
||||
|
||||
#: src/lib/utils.ts:298
|
||||
#: src/lib/utils.ts:318
|
||||
msgid "Triggers when memory usage exceeds a threshold"
|
||||
msgstr "Triggers when memory usage exceeds a threshold"
|
||||
|
||||
#: src/lib/utils.ts:285
|
||||
#: src/lib/utils.ts:304
|
||||
msgid "Triggers when status switches between up and down"
|
||||
msgstr "Triggers when status switches between up and down"
|
||||
|
||||
#: src/lib/utils.ts:304
|
||||
#: src/lib/utils.ts:324
|
||||
msgid "Triggers when usage of any disk exceeds a threshold"
|
||||
msgstr "Triggers when usage of any disk exceeds a threshold"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx:269
|
||||
#. Context: System is up
|
||||
#: src/components/routes/system.tsx:307
|
||||
msgid "Up"
|
||||
msgstr "Up"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx:322
|
||||
msgid "Updated in real time. Click on a system to view information."
|
||||
msgstr "Updated in real time. Click on a system to view information."
|
||||
|
||||
#: src/components/routes/system.tsx:261
|
||||
#: src/components/routes/system.tsx:263
|
||||
msgid "Uptime"
|
||||
msgstr "Uptime"
|
||||
|
||||
#: src/components/charts/area-chart.tsx:73
|
||||
#: src/components/routes/system.tsx:514
|
||||
#: src/components/routes/system.tsx:551
|
||||
#: src/components/routes/system.tsx:528
|
||||
#: src/components/routes/system.tsx:565
|
||||
msgid "Usage"
|
||||
msgstr "Usage"
|
||||
|
||||
#: src/components/routes/system.tsx:424
|
||||
#: src/components/routes/system.tsx:438
|
||||
msgid "Usage of root partition"
|
||||
msgstr "Usage of root partition"
|
||||
|
||||
@@ -782,20 +816,20 @@ msgstr "Usage of root partition"
|
||||
msgid "Used"
|
||||
msgstr "Used"
|
||||
|
||||
#: src/components/command-palette.tsx:142
|
||||
#: src/components/navbar.tsx:69
|
||||
#: src/components/command-palette.tsx:143
|
||||
#: src/components/navbar.tsx:70
|
||||
msgid "Users"
|
||||
msgstr "Users"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx:278
|
||||
#: src/components/systems-table/systems-table.tsx:331
|
||||
msgid "View"
|
||||
msgstr "View"
|
||||
|
||||
#: src/components/systems-table/systems-table.tsx:342
|
||||
#: src/components/systems-table/systems-table.tsx:395
|
||||
msgid "Visible Fields"
|
||||
msgstr "Visible Fields"
|
||||
|
||||
#: src/components/routes/system.tsx:662
|
||||
#: src/components/routes/system.tsx:670
|
||||
msgid "Waiting for enough records to display"
|
||||
msgstr "Waiting for enough records to display"
|
||||
|
||||
@@ -803,7 +837,7 @@ msgstr "Waiting for enough records to display"
|
||||
msgid "Want to help us make our translations even better? Check out <0>Crowdin</0> for more details."
|
||||
msgstr "Want to help us make our translations even better? Check out <0>Crowdin</0> for more details."
|
||||
|
||||
#: src/components/routes/settings/notifications.tsx:124
|
||||
#: src/components/routes/settings/notifications.tsx:125
|
||||
msgid "Webhook / Push notifications"
|
||||
msgstr "Webhook / Push notifications"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1114
beszel/site/src/locales/hu/hu.po
Normal file
1114
beszel/site/src/locales/hu/hu.po
Normal file
File diff suppressed because it is too large
Load Diff
1114
beszel/site/src/locales/is/is.po
Normal file
1114
beszel/site/src/locales/is/is.po
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1114
beszel/site/src/locales/no/no.po
Normal file
1114
beszel/site/src/locales/no/no.po
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1114
beszel/site/src/locales/sl/sl.po
Normal file
1114
beszel/site/src/locales/sl/sl.po
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1114
beszel/site/src/locales/zh/zh.po
Normal file
1114
beszel/site/src/locales/zh/zh.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -63,9 +63,9 @@ const App = () => {
|
||||
|
||||
if (!page) {
|
||||
return <h1 className="text-3xl text-center my-14">404</h1>
|
||||
} else if (page.path === "/") {
|
||||
} else if (page.route === "home") {
|
||||
return <Home />
|
||||
} else if (page.route === "server") {
|
||||
} else if (page.route === "system") {
|
||||
return <SystemDetail name={page.params.name} />
|
||||
} else if (page.route === "settings") {
|
||||
return (
|
||||
|
||||
14
beszel/site/src/types.d.ts
vendored
14
beszel/site/src/types.d.ts
vendored
@@ -1,5 +1,12 @@
|
||||
import { RecordModel } from "pocketbase"
|
||||
|
||||
// global window properties
|
||||
declare global {
|
||||
interface Window {
|
||||
BASE_PATH: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SystemRecord extends RecordModel {
|
||||
name: string
|
||||
host: string
|
||||
@@ -36,6 +43,10 @@ export interface SystemInfo {
|
||||
v: string
|
||||
/** system is using podman */
|
||||
p?: boolean
|
||||
/** highest gpu utilization */
|
||||
g?: number
|
||||
/** dashboard display temperature */
|
||||
dt?: number
|
||||
}
|
||||
|
||||
export interface SystemStats {
|
||||
@@ -189,6 +200,7 @@ interface AlertInfo {
|
||||
unit: string
|
||||
icon: any
|
||||
desc: () => string
|
||||
single?: boolean
|
||||
max?: number
|
||||
/** Single value description (when there's only one value, like status) */
|
||||
singleDesc?: () => string
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import react from "@vitejs/plugin-react-swc"
|
||||
import { lingui } from "@lingui/vite-plugin"
|
||||
|
||||
export default defineConfig({
|
||||
base: "./",
|
||||
plugins: [
|
||||
react({
|
||||
plugins: [["@lingui/swc-plugin", {}]],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package beszel
|
||||
|
||||
const (
|
||||
Version = "0.9.0"
|
||||
Version = "0.9.1"
|
||||
AppName = "beszel"
|
||||
)
|
||||
|
||||
330
readme.md
330
readme.md
@@ -1,329 +1,53 @@
|
||||
# Beszel
|
||||
|
||||
A lightweight server resource monitoring hub with historical data, docker stats, and alerts.
|
||||
Beszel is a lightweight server monitoring platform that includes Docker statistics, historical data, and alert functions.
|
||||
|
||||
[](https://hub.docker.com/r/henrygd/beszel-agent)
|
||||
[](https://hub.docker.com/r/henrygd/beszel)
|
||||
It has a friendly web interface, simple configuration, and is ready to use out of the box. It supports automatic backup, multi-user, OAuth authentication, and API access.
|
||||
|
||||
[](https://hub.docker.com/r/henrygd/beszel-agent)
|
||||
[](https://hub.docker.com/r/henrygd/beszel)
|
||||
[](https://github.com/henrygd/beszel/blob/main/LICENSE)
|
||||
[](https://crowdin.com/project/beszel)
|
||||
|
||||

|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- **Lightweight**: Smaller and less resource-intensive than leading solutions.
|
||||
- **Simple**: Easy setup, no need for public internet exposure.
|
||||
- **Docker stats**: Tracks CPU, memory, and network usage history for each container.
|
||||
- **Alerts**: Configurable alerts for CPU, memory, disk, bandwidth, temperature, and system status.
|
||||
- **Multi-user**: Each user manages their own systems. Admins can share systems across users.
|
||||
- **OAuth / OIDC**: Supports multiple OAuth2 providers. Password authentication can be disabled.
|
||||
- **Alerts**: Configurable alerts for CPU, memory, disk, bandwidth, temperature, and status.
|
||||
- **Multi-user**: Users manage their own systems. Admins can share systems across users.
|
||||
- **OAuth / OIDC**: Supports many OAuth2 providers. Password auth can be disabled.
|
||||
- **Automatic backups**: Save and restore data from disk or S3-compatible storage.
|
||||
- **REST API**: Use or update your data in your own scripts and applications.
|
||||
|
||||
## Introduction
|
||||
## Architecture
|
||||
|
||||
Beszel consists of two main components: the hub and the agent.
|
||||
Beszel consists of two main components: the **hub** and the **agent**.
|
||||
|
||||
- **Hub:** A web application that provides a dashboard for viewing and managing connected systems. Built on [PocketBase](https://pocketbase.io/).
|
||||
|
||||
- **Agent:** Runs on each system you want to monitor, creating a minimal SSH server to communicate system metrics to the hub.
|
||||
- **Hub**: A web application built on [PocketBase](https://pocketbase.io/) that provides a dashboard for viewing and managing connected systems.
|
||||
- **Agent**: Runs on each system you want to monitor, creating a minimal SSH server to communicate system metrics to the hub.
|
||||
|
||||
## Getting started
|
||||
|
||||
If not using docker, skip steps 4-5 and run the agent using the binary.
|
||||
The [quick start guide](https://beszel.dev/guide/getting-started) and other documentation is available on our website, [beszel.dev](https://beszel.dev). You'll be up and running in a few minutes.
|
||||
|
||||
1. Start the hub (see [installation](#installation)).
|
||||
2. Open <http://localhost:8090> and create an admin user.
|
||||
3. Click "Add system." Enter the name and host of the system you want to monitor.
|
||||
4. Click "Copy docker compose" to copy the agent's docker-compose.yml file to your clipboard.
|
||||
5. On the agent system, create the compose file and run `docker compose up` to start the agent.
|
||||
6. Back in the hub, click the "Add system" button in the dialog to finish adding the system.
|
||||
## Screenshots
|
||||
|
||||
If all goes well, the system should flip to green. If it turns red, check the Logs page and refer to [troubleshooting tips](#faq--troubleshooting).
|
||||

|
||||

|
||||

|
||||
|
||||
### Tutoriel en français
|
||||
## Supported metrics
|
||||
|
||||
Pour le tutoriel en français, consultez <https://belginux.com/installer-beszel-avec-docker/>
|
||||
|
||||
## Installation
|
||||
|
||||
You can install the hub and agent as single binaries or using Docker.
|
||||
|
||||
### Docker
|
||||
|
||||
**Hub**: See the example [docker-compose.yml](/supplemental/docker/hub/docker-compose.yml) file.
|
||||
|
||||
**Agent**: The hub provides compose content for the agent, but you can also reference the example [docker-compose.yml](/supplemental/docker/agent/docker-compose.yml) file.
|
||||
|
||||
The agent uses host network mode to access network interface stats, which automatically exposes the port. Change the port using an environment variable if needed.
|
||||
|
||||
If you don't need network stats, remove that line from the compose file and map the port manually.
|
||||
|
||||
### Binary
|
||||
|
||||
> [!TIP]
|
||||
> If using Linux, see [guides/systemd.md](/supplemental/guides/systemd.md) for a script to install the hub or agent as a system service. This is also built into the web UI.
|
||||
|
||||
Download and run the latest binaries from the [releases page](https://github.com/henrygd/beszel/releases) or use the commands below.
|
||||
|
||||
#### Hub
|
||||
|
||||
```bash
|
||||
curl -sL "https://github.com/henrygd/beszel/releases/latest/download/beszel_$(uname -s)_$(uname -m | sed 's/x86_64/amd64/' | sed 's/armv7l/arm/' | sed 's/aarch64/arm64/').tar.gz" | tar -xz -O beszel | tee ./beszel >/dev/null && chmod +x beszel && ls beszel
|
||||
```
|
||||
|
||||
Running the hub directly:
|
||||
|
||||
```bash
|
||||
./beszel serve
|
||||
```
|
||||
|
||||
#### Agent
|
||||
|
||||
```bash
|
||||
curl -sL "https://github.com/henrygd/beszel/releases/latest/download/beszel-agent_$(uname -s)_$(uname -m | sed 's/x86_64/amd64/' | sed 's/armv7l/arm/' | sed 's/aarch64/arm64/').tar.gz" | tar -xz -O beszel-agent | tee ./beszel-agent >/dev/null && chmod +x beszel-agent && ls beszel-agent
|
||||
```
|
||||
|
||||
Running the agent directly:
|
||||
|
||||
```bash
|
||||
PORT=45876 KEY="{PASTE_YOUR_KEY}" ./beszel-agent
|
||||
```
|
||||
|
||||
#### Updating
|
||||
|
||||
Use `./beszel update` and `./beszel-agent update` to update to the latest version.
|
||||
|
||||
## Environment variables
|
||||
|
||||
### Hub
|
||||
|
||||
| Name | Default | Description |
|
||||
| ----------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `CSP` | unset | Adds a [Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) header with this value. |
|
||||
| `DISABLE_PASSWORD_AUTH` | false | Disables password authentication. |
|
||||
|
||||
### Agent
|
||||
|
||||
| Name | Default | Description |
|
||||
| ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `DOCKER_HOST` | unset | Overrides the docker host (docker.sock) if using a proxy.[^socket] |
|
||||
| `EXTRA_FILESYSTEMS` | unset | See [Monitoring additional disks, partitions, or remote mounts](#monitoring-additional-disks-partitions-or-remote-mounts) |
|
||||
| `FILESYSTEM` | unset | Device, partition, or mount point to use for root disk stats. |
|
||||
| `KEY` | unset | Public SSH key to use for authentication. Provided in hub. |
|
||||
| `LOG_LEVEL` | info | Logging level. Valid values: "debug", "info", "warn", "error". |
|
||||
| `MEM_CALC` | unset | Overrides the default memory calculation.[^memcalc] |
|
||||
| `NICS` | unset | Whitelist of network interfaces to monitor for bandwidth chart. |
|
||||
| `PORT` | 45876 | Port or address:port to listen on. |
|
||||
| `SENSORS` | unset | Whitelist of temperature sensors to monitor. |
|
||||
| `SYS_SENSORS` | unset | Overrides sys path for sensors. See [#160](https://github.com/henrygd/beszel/discussions/160). |
|
||||
|
||||
[^socket]: Beszel only needs access to read container information. For [linuxserver/docker-socket-proxy](https://github.com/linuxserver/docker-socket-proxy) you would set `CONTAINERS=1`.
|
||||
[^memcalc]: The default value for used memory is based on gopsutil's [Used](https://pkg.go.dev/github.com/shirou/gopsutil/v4@v4.24.6/mem#VirtualMemoryStat) calculation, which should align fairly closely with `free`. Set `MEM_CALC` to `htop` to align with htop's calculation.
|
||||
|
||||
## OAuth / OIDC Setup
|
||||
|
||||
Beszel supports OpenID Connect and many OAuth2 authentication providers (see list below).
|
||||
|
||||
Visit the "Auth providers" page to enable your provider. The redirect / callback URL should be `<your-beszel-url>/api/oauth2-redirect`.
|
||||
|
||||
<details>
|
||||
<summary>Supported provider list</summary>
|
||||
|
||||
- Apple
|
||||
- Bitbucket
|
||||
- Discord
|
||||
- Facebook
|
||||
- Gitea
|
||||
- Gitee
|
||||
- GitHub
|
||||
- GitLab
|
||||
- Google
|
||||
- Instagram
|
||||
- Kakao
|
||||
- LiveChat
|
||||
- mailcow
|
||||
- Microsoft
|
||||
- OpenID Connect
|
||||
- Patreon (v2)
|
||||
- Spotify
|
||||
- Strava
|
||||
- Twitch
|
||||
- Twitter
|
||||
- VK
|
||||
- Yandex
|
||||
|
||||
</details>
|
||||
|
||||
## Monitoring additional disks, partitions, or remote mounts
|
||||
|
||||
The method for adding additional disks differs depending on your deployment method.
|
||||
|
||||
Use `lsblk` to find the names and mount points of your partitions. If you have trouble, check the agent logs.
|
||||
|
||||
> Note: The charts will use the name of the device or partition if available, and fall back to the folder name. You will not get I/O stats for network mounted drives.
|
||||
|
||||
### Docker
|
||||
|
||||
Mount a folder from the target filesystem in the container's `/extra-filesystems` directory. For example:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- /mnt/disk1/.beszel:/extra-filesystems/sdb1:ro
|
||||
- /dev/mmcblk0/.beszel:/extra-filesystems/mmcblk0:ro
|
||||
```
|
||||
|
||||
### Binary
|
||||
|
||||
Set the `EXTRA_FILESYSTEMS` environment variable to a comma-separated list of devices, partitions, or mount points to monitor. For example:
|
||||
|
||||
```bash
|
||||
EXTRA_FILESYSTEMS="sdb,sdc1,mmcblk0,/mnt/network-share"
|
||||
```
|
||||
|
||||
## REST API
|
||||
|
||||
Because Beszel is built on PocketBase, you can use the PocketBase [web APIs](https://pocketbase.io/docs/api-records/) and [client-side SDKs](https://pocketbase.io/docs/client-side-sdks/) to read or update data from outside Beszel itself.
|
||||
|
||||
## Security
|
||||
|
||||
The hub and agent communicate over SSH, so they don't need to be exposed to the internet. Even if you place an external auth gateway, such as Authelia, in front of the hub, it won't disrupt or break the connection between the hub and agent.
|
||||
|
||||
When the hub is started for the first time, it generates an ED25519 key pair.
|
||||
|
||||
The agent's SSH server is configured to accept connections using this key only. It does not provide a pseudo-terminal or accept input, so it's impossible to execute commands on the agent even if your private key is compromised.
|
||||
|
||||
## User roles
|
||||
|
||||
### Admin
|
||||
|
||||
Admins have access to additional links in the hub, such as backups, SMTP settings, etc. The first user created is automatically an admin and can log into PocketBase.
|
||||
|
||||
Changing a user's role does not create a PocketBase admin account for them. To do that, go to Settings > Admins in PocketBase and add them manually.
|
||||
|
||||
### User
|
||||
|
||||
Users can create their own systems and alerts. Links to PocketBase settings are not shown in the hub.
|
||||
|
||||
### Read only
|
||||
|
||||
Read-only users cannot create systems but can view any system shared with them by an admin and create alerts.
|
||||
|
||||
## FAQ / Troubleshooting
|
||||
|
||||
### Agent is not connecting
|
||||
|
||||
Assuming the agent is running, the connection is probably being blocked by a firewall. You have two options:
|
||||
|
||||
1. Add an inbound rule to the agent system's firewall(s) to allow TCP connections to the port. Check any active firewalls, like iptables, and your cloud provider's firewall settings if applicable.
|
||||
2. Alternatively, use software like [Cloudflare Tunnel](https://www.cloudflare.com/products/tunnel/), [WireGuard](https://www.wireguard.com/), or [Tailscale](https://tailscale.com/) to securely bypass your firewall.
|
||||
|
||||
You can test connectivity by running `telnet <agent-ip> <port>`.
|
||||
|
||||
### Connecting the hub and agent on the same system using Docker
|
||||
|
||||
If using host network mode for the agent but not the hub, add your system using the hostname `host.docker.internal`, which resolves to the internal IP address used by the host. See the [example docker-compose.yml](/supplemental/docker/same-system/docker-compose.yml).
|
||||
|
||||
If using host network mode for both, you can use `localhost` as the hostname.
|
||||
|
||||
Otherwise, use the agent's `container_name` as the hostname if both are in the same Docker network.
|
||||
|
||||
### Finding the correct filesystem
|
||||
|
||||
Specify the filesystem/device/partition for root disk stats using the `FILESYSTEM` environment variable.
|
||||
|
||||
If not set, the agent will try to find the partition mounted on `/` and use that. This may not work correctly in a container, so it's recommended to set this value. Use one of the following methods to find the correct filesystem:
|
||||
|
||||
- Run `lsblk` and choose an option under "NAME."
|
||||
- Run `df -h` and choose an option under "Filesystem."
|
||||
- Run `sudo fdisk -l` and choose an option under "Device."
|
||||
|
||||
### Docker container charts are empty or missing
|
||||
|
||||
If container charts show empty data or don't appear at all, you may need to enable cgroup memory accounting. To verify, run `docker stats`. If that shows zero memory usage, follow this guide to fix the issue:
|
||||
|
||||
<https://akashrajpurohit.com/blog/resolving-missing-memory-stats-in-docker-stats-on-raspberry-pi/>
|
||||
|
||||
### Docker Containers Are Not Populating Reliably
|
||||
|
||||
Try upgrading your Docker version on the agent system. This issue was observed on a machine running version 24 and was resolved by upgrading to version 27.
|
||||
|
||||
### Month / week records are not populating reliably
|
||||
|
||||
Records for longer time periods are created by averaging stats from shorter periods. The agent must run uninterrupted for a full set of data to populate these records.
|
||||
|
||||
Pausing/unpausing the agent for longer than one minute will result in incomplete data, resetting the timing for the current interval.
|
||||
|
||||
## Compiling
|
||||
|
||||
Both the hub and agent are written in Go, so you can easily build them yourself, or cross-compile for different platforms. Please [install Go](https://go.dev/doc/install) first if you haven't already.
|
||||
|
||||
### Using Makefile
|
||||
|
||||
Run `make` in `/beszel`. This creates a `build` directory containing the binaries.
|
||||
|
||||
```bash
|
||||
cd beszel && make
|
||||
```
|
||||
|
||||
You can also build for different platforms:
|
||||
|
||||
```bash
|
||||
make OS=freebsd ARCH=arm64
|
||||
```
|
||||
|
||||
See a list of valid options by running `go tool dist list`.
|
||||
|
||||
### Manual compilation
|
||||
|
||||
#### Prepare dependencies
|
||||
|
||||
```bash
|
||||
cd beszel && go mod tidy
|
||||
```
|
||||
|
||||
#### Agent
|
||||
|
||||
Go to `beszel/cmd/agent` and run the following command to create a binary in the current directory:
|
||||
|
||||
```bash
|
||||
CGO_ENABLED=0 go build -ldflags "-w -s" .
|
||||
```
|
||||
|
||||
#### Hub
|
||||
|
||||
The hub embeds the web UI in the binary, so you must build the website first. I use [Bun](https://bun.sh/), but you may use Node.js if you prefer:
|
||||
|
||||
```bash
|
||||
cd beszel/site
|
||||
bun install
|
||||
bun run build
|
||||
```
|
||||
|
||||
Then in `beszel/cmd/hub`:
|
||||
|
||||
```bash
|
||||
CGO_ENABLED=0 go build -ldflags "-w -s" .
|
||||
```
|
||||
|
||||
#### Cross-compiling
|
||||
|
||||
You can cross-compile for different platforms using the `GOOS` and `GOARCH` environment variables.
|
||||
|
||||
For example, to build for FreeBSD ARM64:
|
||||
|
||||
```bash
|
||||
GOOS=freebsd GOARCH=arm64 CGO_ENABLED=0 go build -ldflags "-w -s" .
|
||||
```
|
||||
|
||||
See a list of valid options by running `go tool dist list`.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome, but it's a good idea to check with us first in a discussion / issue if you plan on doing anything significant.
|
||||
|
||||
We use [Crowdin](https://crowdin.com/project/beszel) to manage translations. New languages or improvements to existing translations are appreciated!
|
||||
|
||||
We'll have more helpful information about contributing to Beszel in the near future.
|
||||
- **CPU usage** - Host system and Docker / Podman containers.
|
||||
- **Memory usage** - Host system and containers. Includes swap and ZFS ARC.
|
||||
- **Disk usage** - Host system. Supports multiple partitions and devices.
|
||||
- **Disk I/O** - Host system. Supports multiple partitions and devices.
|
||||
- **Network usage** - Host system and containers.
|
||||
- **Temperature** - Host system sensors.
|
||||
- **GPU usage / temperature / power draw** - Nvidia and AMD only. Must use binary agent.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
31
supplemental/debian/beszel-agent.service
Normal file
31
supplemental/debian/beszel-agent.service
Normal file
@@ -0,0 +1,31 @@
|
||||
[Unit]
|
||||
Description=Beszel Agent Service
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Environment="PORT=45876"
|
||||
# Port number can be overridden in beszel-agent.conf if needed
|
||||
EnvironmentFile=/etc/beszel-agent.conf
|
||||
ExecStart=/usr/bin/beszel-agent
|
||||
User=beszel
|
||||
Restart=on-failure
|
||||
StateDirectory=beszel-agent
|
||||
|
||||
# Security/sandboxing settings
|
||||
KeyringMode=private
|
||||
LockPersonality=yes
|
||||
NoNewPrivileges=yes
|
||||
PrivateTmp=yes
|
||||
ProtectClock=yes
|
||||
ProtectHome=read-only
|
||||
ProtectHostname=yes
|
||||
ProtectKernelLogs=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectSystem=strict
|
||||
RemoveIPC=yes
|
||||
RestrictSUIDSGID=true
|
||||
SystemCallArchitectures=native
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
8
supplemental/debian/config.sh
Normal file
8
supplemental/debian/config.sh
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
. /usr/share/debconf/confmodule
|
||||
db_version 2.0
|
||||
|
||||
db_input high beszel-agent/key || true
|
||||
db_go
|
||||
8
supplemental/debian/copyright
Normal file
8
supplemental/debian/copyright
Normal file
@@ -0,0 +1,8 @@
|
||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: Beszel
|
||||
Upstream-Contact: henrygd <hank@henrygd.me>
|
||||
Source: https://beszel.dev/
|
||||
|
||||
Files: *
|
||||
Copyright: 2024 henrygd
|
||||
License: MIT
|
||||
11
supplemental/debian/lintian-overrides
Normal file
11
supplemental/debian/lintian-overrides
Normal file
@@ -0,0 +1,11 @@
|
||||
# No changelog in the repo at the moment. This would be good to fix
|
||||
beszel-agent: no-changelog
|
||||
# Current unable to fix these due to Goreleaser bug
|
||||
# https://github.com/goreleaser/goreleaser/issues/5487
|
||||
beszel-agent: no-debconf-config
|
||||
beszel-agent: postinst-uses-db-input
|
||||
# Needs to be fixed in Beszel build
|
||||
beszel-agent: hardening-no-pie
|
||||
beszel-agent: hardening-no-relro
|
||||
# Maybe one day
|
||||
beszel-agent: no-manual-page
|
||||
49
supplemental/debian/postinstall.sh
Normal file
49
supplemental/debian/postinstall.sh
Normal file
@@ -0,0 +1,49 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
[ "$1" = "configure" ] || exit 0
|
||||
|
||||
CONFIG_FILE=/etc/beszel-agent.conf
|
||||
SERVICE=beszel-agent
|
||||
SERVICE_USER=beszel
|
||||
|
||||
. /usr/share/debconf/confmodule
|
||||
|
||||
# This would normally be in the config control file, however this is currently
|
||||
# broken in goreleaser. Temporarily do it here.
|
||||
# https://github.com/goreleaser/goreleaser/issues/5487
|
||||
db_version 2.0
|
||||
db_input high beszel-agent/key || true
|
||||
db_go
|
||||
|
||||
# Create group and user
|
||||
if ! getent group "$SERVICE_USER" >/dev/null; then
|
||||
echo "Creating $SERVICE_USER group"
|
||||
addgroup --quiet --system "$SERVICE_USER"
|
||||
fi
|
||||
|
||||
if ! getent passwd "$SERVICE_USER" >/dev/null; then
|
||||
echo "Creating $SERVICE_USER user"
|
||||
adduser --quiet --system "$SERVICE_USER" \
|
||||
--ingroup "$SERVICE_USER" \
|
||||
--no-create-home \
|
||||
--home /nonexistent \
|
||||
--gecos "System user for $SERVICE"
|
||||
fi
|
||||
|
||||
# Create config file if it doesn't already exist
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
touch "$CONFIG_FILE"
|
||||
chmod 0600 "$CONFIG_FILE"
|
||||
chown "$SERVICE_USER":"$SERVICE_USER" "$CONFIG_FILE"
|
||||
fi;
|
||||
|
||||
# Only add key to config if it's not already present
|
||||
if ! grep -q "^KEY=" "$CONFIG_FILE"; then
|
||||
db_get beszel-agent/key
|
||||
echo "KEY=$RET" > "$CONFIG_FILE"
|
||||
fi;
|
||||
|
||||
deb-systemd-helper enable "$SERVICE".service
|
||||
systemctl daemon-reload
|
||||
deb-systemd-invoke start "$SERVICE".service || echo "could not start $SERVICE.service!"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user